@godxjp/ui 33.22.2 → 33.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +16 -5
  3. package/agent/DESIGN.md +1 -1
  4. package/agent/START-HERE.md +8 -8
  5. package/agent/components/AgentBadge.json +43 -0
  6. package/agent/components/AppShell.json +6 -0
  7. package/agent/components/Avatar.json +7 -0
  8. package/agent/components/CatalogDetail.json +7 -3
  9. package/agent/components/DatePicker.json +1 -1
  10. package/agent/components/DropdownMenu.json +1 -0
  11. package/agent/components/EditorialCard.json +2 -1
  12. package/agent/components/Image.json +6 -1
  13. package/agent/components/PageContainer.json +5 -0
  14. package/agent/components/PageErrorBoundary.json +48 -0
  15. package/agent/components/Pagination.json +5 -0
  16. package/agent/components/Popover.json +1 -0
  17. package/agent/components/Select.json +2 -1
  18. package/agent/components/Toaster.json +2 -2
  19. package/agent/components/Toolbar.json +1 -1
  20. package/agent/components/Topbar.json +1 -1
  21. package/agent/components-index.json +10 -0
  22. package/agent/components.json +138 -11
  23. package/agent/index.json +8 -8
  24. package/agent/llms.txt +7 -7
  25. package/agent/patterns/notice-bar.json +16 -0
  26. package/agent/patterns-index.json +15 -0
  27. package/agent/patterns.json +16 -0
  28. package/agent/rules.json +2 -2
  29. package/agent/tokens.json +121 -7
  30. package/dist/components/charts/chart-cartesian.js +5 -1
  31. package/dist/components/charts/chart-summary.d.ts +1 -1
  32. package/dist/components/charts/chart-summary.js +17 -6
  33. package/dist/components/charts/compact-bar-trend.js +4 -3
  34. package/dist/components/charts/pie-chart.js +2 -1
  35. package/dist/components/data-display/avatar.d.ts +1 -1
  36. package/dist/components/data-display/avatar.js +7 -1
  37. package/dist/components/data-display/badge.d.ts +11 -0
  38. package/dist/components/data-display/badge.js +20 -0
  39. package/dist/components/data-display/image.d.ts +1 -0
  40. package/dist/components/data-display/image.js +10 -2
  41. package/dist/components/data-display/index.d.ts +2 -2
  42. package/dist/components/data-display/index.js +2 -1
  43. package/dist/components/data-display/popover.js +11 -0
  44. package/dist/components/data-entry/calendar.js +1 -1
  45. package/dist/components/data-entry/date-picker.js +22 -5
  46. package/dist/components/data-entry/record-picker.js +122 -98
  47. package/dist/components/data-entry/select.js +13 -3
  48. package/dist/components/general/button.js +2 -2
  49. package/dist/components/layout/affix.js +1 -1
  50. package/dist/components/layout/app-shell.d.ts +1 -1
  51. package/dist/components/layout/app-shell.js +3 -1
  52. package/dist/components/layout/editorial-catalog.d.ts +1 -1
  53. package/dist/components/layout/editorial-catalog.js +53 -15
  54. package/dist/components/layout/index.d.ts +2 -0
  55. package/dist/components/layout/index.js +2 -0
  56. package/dist/components/layout/page-container.d.ts +1 -1
  57. package/dist/components/layout/page-container.js +6 -3
  58. package/dist/components/layout/page-error-boundary.d.ts +32 -0
  59. package/dist/components/layout/page-error-boundary.js +57 -0
  60. package/dist/components/layout/sidebar.d.ts +1 -1
  61. package/dist/components/layout/sidebar.js +100 -49
  62. package/dist/components/layout/topbar.js +108 -21
  63. package/dist/components/navigation/dropdown-menu.d.ts +13 -3
  64. package/dist/components/navigation/dropdown-menu.js +36 -6
  65. package/dist/components/navigation/pagination.d.ts +1 -1
  66. package/dist/components/navigation/pagination.js +13 -1
  67. package/dist/components/navigation/toolbar.js +2 -1
  68. package/dist/contracts/measurement.json +1 -1
  69. package/dist/i18n/messages/en.json +7 -0
  70. package/dist/i18n/messages/ja.json +7 -0
  71. package/dist/i18n/messages/vi.json +7 -0
  72. package/dist/props/components/data-display.prop.d.ts +29 -0
  73. package/dist/props/components/layout.prop.d.ts +52 -2
  74. package/dist/props/components/navigation.prop.d.ts +15 -1
  75. package/dist/props/registry.d.ts +65 -5
  76. package/dist/props/registry.js +90 -4
  77. package/dist/styles/alert-layout.css +4 -0
  78. package/dist/styles/application.css +24 -0
  79. package/dist/styles/badge-layout.css +14 -0
  80. package/dist/styles/base.css +11 -0
  81. package/dist/styles/control.css +32 -19
  82. package/dist/styles/core.css +1 -0
  83. package/dist/styles/data-display-layout.css +51 -0
  84. package/dist/styles/dialog-layout.css +9 -0
  85. package/dist/styles/editorial-layout.css +2076 -0
  86. package/dist/styles/editorial.css +3 -0
  87. package/dist/styles/icon-layout.css +3 -0
  88. package/dist/styles/index.css +1 -0
  89. package/dist/styles/layers.json +60 -9
  90. package/dist/styles/layout.css +9 -1985
  91. package/dist/styles/precompiled/alert-layout.css +2 -0
  92. package/dist/styles/precompiled/badge-layout.css +2 -0
  93. package/dist/styles/precompiled/base.css +2 -0
  94. package/dist/styles/precompiled/card-layout.css +2 -0
  95. package/dist/styles/precompiled/chart-layout.css +2 -0
  96. package/dist/styles/precompiled/control.css +2 -0
  97. package/dist/styles/precompiled/data-display-layout.css +2 -0
  98. package/dist/styles/precompiled/data-entry-layout.css +2 -0
  99. package/dist/styles/precompiled/dialog-layout.css +2 -0
  100. package/dist/styles/precompiled/editorial-account-layout.css +2 -0
  101. package/dist/styles/precompiled/editorial-layout.css +2 -0
  102. package/dist/styles/precompiled/float-button-layout.css +2 -0
  103. package/dist/styles/precompiled/form-layout.css +2 -0
  104. package/dist/styles/precompiled/icon-layout.css +2 -0
  105. package/dist/styles/precompiled/layout.css +2 -0
  106. package/dist/styles/precompiled/logo-layout.css +2 -0
  107. package/dist/styles/precompiled/motion.css +2 -0
  108. package/dist/styles/precompiled/navigation-layout.css +2 -0
  109. package/dist/styles/precompiled/shell-layout.css +2 -0
  110. package/dist/styles/precompiled/table-layout.css +2 -0
  111. package/dist/styles/precompiled/text-layout.css +2 -0
  112. package/dist/styles/precompiled/toggle.css +2 -0
  113. package/dist/styles/precompiled/vendor-day-picker.css +2 -0
  114. package/dist/styles/precompiled/vendor-sonner.css +2 -0
  115. package/dist/styles/shell-layout.css +17 -1
  116. package/dist/styles/table-layout.css +1 -1
  117. package/dist/styles.css +1 -1
  118. package/dist/tokens/components/badge.css +3 -0
  119. package/dist/tokens/components/control.css +3 -0
  120. package/dist/tokens/components/data-display.css +9 -0
  121. package/dist/tokens/components/editorial.css +9 -6
  122. package/dist/tokens/components/shell.css +2 -0
  123. package/dist/tokens/foundation.css +8 -0
  124. package/docs/CONSUMER-RULES.md +36 -1
  125. package/package.json +4 -2
  126. package/scripts/consumer-rule.md +1 -1
  127. package/scripts/prune-css.mjs +56 -8
  128. package/scripts/ui-audit.mjs +36 -0
  129. package/skills/godxjp-ui-navigation/SKILL.md +14 -9
  130. package/skills/godxjp-ui-navigation/references/principles.md +2 -2
@@ -21,6 +21,7 @@ import {
21
21
  import { Command, CommandGroup } from "./command.js";
22
22
  import { Input } from "./input.js";
23
23
  import { isImeComposing } from "../../lib/ime.js";
24
+ import { FieldNameContext } from "../../lib/field-a11y.js";
24
25
  import { Select } from "./select.js";
25
26
  function selectLoadOptionsFor(loadOptions) {
26
27
  if (!loadOptions) return void 0;
@@ -35,103 +36,53 @@ function selectLoadOptionsFor(loadOptions) {
35
36
  return { options: r.options, hasMore: r.nextCursor !== void 0 };
36
37
  };
37
38
  }
38
- const RecordPicker = React.forwardRef(
39
- function RecordPicker2({
40
- mode = "single",
41
- shape = "auto",
42
- value,
43
- defaultValue,
44
- onValueChange,
45
- options,
46
- loadOptions,
47
- count,
48
- threshold = 10,
49
- filters,
50
- selectedOptions,
51
- emptyOption,
52
- placeholder,
53
- dialogTitle,
54
- notFoundContent,
55
- loadMoreLabel,
56
- disabled = false,
57
- size,
58
- className,
59
- "data-field": fieldName,
60
- ...props
61
- }, ref) {
62
- const { t } = useTranslation();
63
- const isMultiple = mode === "multiple";
64
- const controlled = value !== void 0;
65
- const [internal, setInternal] = React.useState(
66
- () => toArray(defaultValue ?? (isMultiple ? [] : null))
67
- );
68
- const selected = controlled ? toArray(value) : internal;
69
- const commit = React.useCallback(
70
- (next) => {
71
- if (!controlled) setInternal(next);
72
- onValueChange?.(isMultiple ? next : next[0] ?? null);
73
- },
74
- [controlled, isMultiple, onValueChange]
75
- );
76
- const selectLoadOptions = React.useMemo(() => selectLoadOptionsFor(loadOptions), [loadOptions]);
77
- const staticOptions = React.useMemo(
78
- () => options ? normalizeSelectOptions(options) : [],
79
- [options]
80
- );
81
- if (shape === "inline") {
82
- return /* @__PURE__ */ jsx(
83
- InlinePicker,
84
- {
85
- ref,
86
- isMultiple,
87
- selected,
88
- commit,
89
- staticOptions,
90
- loadOptions,
91
- filters,
92
- selectedOptions,
93
- emptyOption,
94
- placeholder,
95
- dialogTitle,
96
- notFoundContent,
97
- loadMoreLabel,
98
- disabled,
99
- size,
100
- className,
101
- "data-field": fieldName,
102
- ...props
103
- }
104
- );
105
- }
106
- const total = count ?? (options ? staticOptions.length : void 0);
107
- const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
108
- if (!asDialog) {
109
- return /* @__PURE__ */ jsx(
110
- Select,
111
- {
112
- mode: isMultiple ? "multiple" : void 0,
113
- loadOptions: selectLoadOptions,
114
- id: props.id,
115
- "data-field": fieldName,
116
- "aria-describedby": props["aria-describedby"],
117
- labelInValue: false,
118
- showSearch: true,
119
- options: withEmptyOption(options, emptyOption),
120
- value: isMultiple ? selected : selected[0] ?? void 0,
121
- onValueChange: ((next) => commit(toArray(next))),
122
- placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
123
- notFoundContent,
124
- disabled,
125
- size,
126
- className
127
- }
128
- );
129
- }
39
+ const RecordPicker = React.forwardRef(function RecordPicker2({
40
+ mode = "single",
41
+ shape = "auto",
42
+ value,
43
+ defaultValue,
44
+ onValueChange,
45
+ options,
46
+ loadOptions,
47
+ count,
48
+ threshold = 10,
49
+ filters,
50
+ selectedOptions,
51
+ emptyOption,
52
+ placeholder,
53
+ dialogTitle,
54
+ notFoundContent,
55
+ loadMoreLabel,
56
+ disabled = false,
57
+ size,
58
+ className,
59
+ "data-field": fieldName,
60
+ ...props
61
+ }, ref) {
62
+ const { t } = useTranslation();
63
+ const isMultiple = mode === "multiple";
64
+ const controlled = value !== void 0;
65
+ const [internal, setInternal] = React.useState(
66
+ () => toArray(defaultValue ?? (isMultiple ? [] : null))
67
+ );
68
+ const selected = controlled ? toArray(value) : internal;
69
+ const commit = React.useCallback(
70
+ (next) => {
71
+ if (!controlled) setInternal(next);
72
+ onValueChange?.(isMultiple ? next : next[0] ?? null);
73
+ },
74
+ [controlled, isMultiple, onValueChange]
75
+ );
76
+ const selectLoadOptions = React.useMemo(() => selectLoadOptionsFor(loadOptions), [loadOptions]);
77
+ const staticOptions = React.useMemo(
78
+ () => options ? normalizeSelectOptions(options) : [],
79
+ [options]
80
+ );
81
+ if (shape === "inline") {
130
82
  return /* @__PURE__ */ jsx(
131
- DialogPicker,
83
+ InlinePicker,
132
84
  {
133
85
  ref,
134
- "data-field": fieldName,
135
86
  isMultiple,
136
87
  selected,
137
88
  commit,
@@ -147,11 +98,59 @@ const RecordPicker = React.forwardRef(
147
98
  disabled,
148
99
  size,
149
100
  className,
101
+ "data-field": fieldName,
150
102
  ...props
151
103
  }
152
104
  );
153
105
  }
154
- );
106
+ const total = count ?? (options ? staticOptions.length : void 0);
107
+ const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
108
+ if (!asDialog) {
109
+ return /* @__PURE__ */ jsx(
110
+ Select,
111
+ {
112
+ mode: isMultiple ? "multiple" : void 0,
113
+ loadOptions: selectLoadOptions,
114
+ id: props.id,
115
+ "data-field": fieldName,
116
+ "aria-describedby": props["aria-describedby"],
117
+ labelInValue: false,
118
+ showSearch: true,
119
+ options: withEmptyOption(options, emptyOption),
120
+ value: isMultiple ? selected : selected[0] ?? void 0,
121
+ onValueChange: ((next) => commit(toArray(next))),
122
+ placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
123
+ notFoundContent,
124
+ disabled,
125
+ size,
126
+ className
127
+ }
128
+ );
129
+ }
130
+ return /* @__PURE__ */ jsx(
131
+ DialogPicker,
132
+ {
133
+ ref,
134
+ "data-field": fieldName,
135
+ isMultiple,
136
+ selected,
137
+ commit,
138
+ staticOptions,
139
+ loadOptions,
140
+ filters,
141
+ selectedOptions,
142
+ emptyOption,
143
+ placeholder,
144
+ dialogTitle,
145
+ notFoundContent,
146
+ loadMoreLabel,
147
+ disabled,
148
+ size,
149
+ className,
150
+ ...props
151
+ }
152
+ );
153
+ });
155
154
  const INLINE_SUGGESTION_LIMIT = 8;
156
155
  function toArray(v) {
157
156
  if (v === null || v === void 0) return [];
@@ -179,10 +178,30 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
179
178
  className,
180
179
  id,
181
180
  "data-field": fieldName,
182
- "aria-describedby": describedBy
181
+ "aria-describedby": describedBy,
182
+ "aria-label": ariaLabel,
183
+ "aria-labelledby": ariaLabelledBy,
184
+ "aria-errormessage": ariaErrorMessage,
185
+ "aria-invalid": ariaInvalid,
186
+ "aria-required": ariaRequired
183
187
  }, ref) {
184
188
  const { t } = useTranslation();
185
189
  const [query, setQuery] = React.useState("");
190
+ const inputRef = React.useRef(null);
191
+ const [hasNativeLabel, setHasNativeLabel] = React.useState(false);
192
+ React.useLayoutEffect(() => {
193
+ setHasNativeLabel((inputRef.current?.labels?.length ?? 0) > 0);
194
+ }, [id]);
195
+ const enclosingField = React.useContext(FieldNameContext);
196
+ const fallbackName = ariaLabel === void 0 && ariaLabelledBy === void 0 && !enclosingField && !hasNativeLabel ? placeholder ?? t("dataEntry.recordPicker.search") : void 0;
197
+ const setInputRef = React.useCallback(
198
+ (node) => {
199
+ inputRef.current = node;
200
+ if (typeof ref === "function") ref(node);
201
+ else if (ref) ref.current = node;
202
+ },
203
+ [ref]
204
+ );
186
205
  const [rows, setRows] = React.useState(staticOptions);
187
206
  const [openSuggest, setOpenSuggest] = React.useState(false);
188
207
  const [dialogQuery, setDialogQuery] = React.useState(null);
@@ -214,7 +233,7 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
214
233
  /* @__PURE__ */ jsx(
215
234
  Input,
216
235
  {
217
- ref,
236
+ ref: setInputRef,
218
237
  type: "search",
219
238
  value: query,
220
239
  disabled,
@@ -232,7 +251,12 @@ const InlinePicker = React.forwardRef(function InlinePicker2({
232
251
  className: "ui-record-picker-inline-input",
233
252
  id,
234
253
  "data-field": fieldName,
235
- "aria-describedby": describedBy
254
+ "aria-describedby": describedBy,
255
+ "aria-label": ariaLabel ?? fallbackName,
256
+ "aria-labelledby": ariaLabelledBy,
257
+ "aria-errormessage": ariaErrorMessage,
258
+ "aria-invalid": ariaInvalid,
259
+ "aria-required": ariaRequired
236
260
  }
237
261
  ),
238
262
  /* @__PURE__ */ jsxs(
@@ -13,7 +13,8 @@ import {
13
13
  Select as AriaSelect,
14
14
  SelectStateContext,
15
15
  SelectValue as AriaSelectValue,
16
- Separator as AriaSeparator
16
+ Separator as AriaSeparator,
17
+ Text as AriaText
17
18
  } from "react-aria-components";
18
19
  import { ChevronDown, Loader2, X } from "lucide-react";
19
20
  import { cn } from "../../lib/utils.js";
@@ -613,6 +614,14 @@ function withOptionIcon(option, label) {
613
614
  /* @__PURE__ */ jsx("span", { className: "ui-select-option-label", children: label })
614
615
  ] });
615
616
  }
617
+ function optionRow(option) {
618
+ if (!option.sublabel) return withOptionIcon(option, option.label);
619
+ return /* @__PURE__ */ jsxs("span", { className: "ui-select-option-body", children: [
620
+ option.icon ? /* @__PURE__ */ jsx("span", { className: "ui-select-option-icon", "aria-hidden": "true", children: option.icon }) : null,
621
+ /* @__PURE__ */ jsx(AriaText, { slot: "label", className: "ui-select-option-label", children: option.label }),
622
+ /* @__PURE__ */ jsx(AriaText, { slot: "description", className: "ui-search-select-option-sublabel", children: option.sublabel })
623
+ ] });
624
+ }
616
625
  function DataSelectLabel({
617
626
  labelRender,
618
627
  options
@@ -729,7 +738,7 @@ function DataSelect(props) {
729
738
  disabled: option.disabled,
730
739
  "data-testid": optionTestId(option.value),
731
740
  selectedIcon: menuItemSelectedIcon,
732
- children: optionRender ? optionRender(option, { index }) : renderOption ? renderOption(option) : withOptionIcon(option, option.label)
741
+ children: optionRender ? optionRender(option, { index }) : renderOption ? renderOption(option) : optionRow(option)
733
742
  },
734
743
  option.value
735
744
  );
@@ -784,8 +793,9 @@ function DataSelect(props) {
784
793
  ...ariaProps,
785
794
  children: [
786
795
  prefix === void 0 || prefix === null ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-prefix", className: "ui-select-prefix", children: prefix }),
787
- /* @__PURE__ */ jsx(SelectValue, { placeholder: placeholder ?? t("dataEntry.searchSelect.placeholder"), children: labelRender ? /* @__PURE__ */ jsx(DataSelectLabel, { labelRender, options }) : options.some((option) => option.icon) ? (
796
+ /* @__PURE__ */ jsx(SelectValue, { placeholder: placeholder ?? t("dataEntry.searchSelect.placeholder"), children: labelRender ? /* @__PURE__ */ jsx(DataSelectLabel, { labelRender, options }) : options.some((option) => option.icon || option.sublabel) ? (
788
797
  // gh#1444 — the selected option's icon leads the trigger too, as in the search panel.
798
+ // gh#1495 — and a sublabel stays in the row: the trigger names the pick by its label.
789
799
  /* @__PURE__ */ jsx(
790
800
  DataSelectLabel,
791
801
  {
@@ -54,11 +54,11 @@ const buttonVariants = cva("ui-button", {
54
54
  md: "ui-button--default-size",
55
55
  // utilities after components, so only a utility can out-rank a child's own `size-*`.
56
56
  // They read the token, so the value is still themeable.
57
- xs: "ui-button--xs [&_svg:not([class*='size-'])]:size-[var(--button-xs-icon-size)]",
57
+ xs: "ui-button--xs [&_svg:not([class*='size-'])]:size-[var(--button-icon-size,var(--button-xs-icon-size))]",
58
58
  sm: "ui-button--sm",
59
59
  lg: "ui-button--lg",
60
60
  icon: "ui-button--icon",
61
- "icon-xs": "ui-button--icon-xs [&_svg]:size-[var(--button-xs-icon-size)] [&_svg]:shrink-0",
61
+ "icon-xs": "ui-button--icon-xs [&_svg]:size-[var(--button-icon-size,var(--button-xs-icon-size))] [&_svg]:shrink-0",
62
62
  "icon-sm": "ui-button--icon-sm",
63
63
  "icon-lg": "ui-button--icon-lg",
64
64
  // The glove step (gh#1304): ≥56×56, `--control-height-touch`.
@@ -106,7 +106,7 @@ const Affix = React.forwardRef(function Affix2({
106
106
  };
107
107
  }, [measure, targetElement]);
108
108
  React.useEffect(() => {
109
- if (!affixed || !targetElement || typeof window === "undefined") return void 0;
109
+ if (!affixed || typeof window === "undefined") return void 0;
110
110
  let frame = null;
111
111
  const onScroll = () => {
112
112
  if (frame !== null) return;
@@ -3,4 +3,4 @@ import type { AppShellProp } from "../../props/components/layout.prop.js";
3
3
  export type { AppShellProp, AppShellProp as AppShellProps, } from "../../props/components/layout.prop.js";
4
4
  /** Same rail boundary as shell-layout.css; SSR starts in docked mode. */
5
5
  export declare function useAppShellNavigationMode(): "drawer" | "docked";
6
- export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, logoCompact, logoCompactBelow, breadcrumb, footer, children, sidebarCollapsed, responsiveNavigation, topbarSpan, navRail, navRailPosition, navRailEnd, navRailLabel, mobileTabBar, mobileTabBarLabel, mobileNav, mobileNavLabel, mobileNavTriggerLabel, mobileNavOnCloseAutoFocus, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
6
+ export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, logoCompact, logoCompactBelow, breadcrumb, footer, children, errorBoundary, sidebarCollapsed, responsiveNavigation, topbarSpan, navRail, navRailPosition, navRailEnd, navRailLabel, mobileTabBar, mobileTabBarLabel, mobileNav, mobileNavLabel, mobileNavTriggerLabel, mobileNavOnCloseAutoFocus, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
@@ -10,6 +10,7 @@ import { ShellTabBar } from "./mobile-shell.js";
10
10
  import { useToastStickyBarClearance } from "../../lib/toast-clearance.js";
11
11
  import { NavSurfaceProvider } from "./nav-surface.js";
12
12
  import { TopbarItem } from "./topbar-item.js";
13
+ import { PageErrorBoundary } from "./page-error-boundary.js";
13
14
  import { isDevelopment } from "../../lib/dev.js";
14
15
  function useAppShellNavigationMode() {
15
16
  return useMediaQuery("(width <= 56.25rem)") ? "drawer" : "docked";
@@ -25,6 +26,7 @@ function AppShell({
25
26
  breadcrumb,
26
27
  footer,
27
28
  children,
29
+ errorBoundary = true,
28
30
  sidebarCollapsed = false,
29
31
  responsiveNavigation = "drawer",
30
32
  topbarSpan = "content",
@@ -201,7 +203,7 @@ function AppShell({
201
203
  tabIndex: 0,
202
204
  children: [
203
205
  breadcrumb !== void 0 && /* @__PURE__ */ jsx("div", { className: "app-breadcrumb", children: breadcrumb }),
204
- children,
206
+ errorBoundary === false ? children : /* @__PURE__ */ jsx(PageErrorBoundary, { ...errorBoundary === true ? {} : errorBoundary, children }),
205
207
  showTabBar && footerRegion
206
208
  ]
207
209
  }
@@ -7,7 +7,7 @@ export declare function EditorialFilterPanel({ variant, children, action, classN
7
7
  /** One field of an `EditorialFilterPanel`: a visible label over one control. */
8
8
  export declare function EditorialFilterField({ label, htmlFor, children, className, }: EditorialFilterFieldProp): React.JSX.Element;
9
9
  /** The head of a catalog item's page: framed picture beside kicker, h1, tags, summary, facts. */
10
- export declare function CatalogDetail({ variant, media, favorite, mediaCaption, overline, title, meta, summary, actions, facts, className, }: CatalogDetailProp): React.JSX.Element;
10
+ export declare function CatalogDetail({ variant, media, image, favorite, mediaCaption, overline, title, meta, summary, actions, facts, className, }: CatalogDetailProp): React.JSX.Element;
11
11
  /** One outlined tag in a `CatalogDetail` meta row. */
12
12
  export declare function CatalogDetailTag({ children, className }: CatalogDetailTagProp): React.JSX.Element;
13
13
  /** One band of an item page under its head: hairline, h2, muted paragraph, then the rest. */
@@ -1,5 +1,8 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
2
4
  import { Bookmark } from "lucide-react";
5
+ import { Image } from "../data-display/image.js";
3
6
  import { cn } from "../../lib/utils.js";
4
7
  function EditorialFilterPanel({
5
8
  variant = "panel",
@@ -37,6 +40,7 @@ function EditorialFilterField({
37
40
  function CatalogDetail({
38
41
  variant = "card",
39
42
  media,
43
+ image,
40
44
  favorite,
41
45
  mediaCaption,
42
46
  overline,
@@ -47,6 +51,18 @@ function CatalogDetail({
47
51
  facts,
48
52
  className
49
53
  }) {
54
+ const [load, setLoad] = React.useState(null);
55
+ const src = image?.src;
56
+ const status = image ? load && load.src === src ? load.status : "loading" : void 0;
57
+ const onStatusChange = React.useCallback(
58
+ (next) => {
59
+ if (src !== void 0)
60
+ setLoad(
61
+ (previous) => previous?.src === src && previous.status === next ? previous : { src, status: next }
62
+ );
63
+ },
64
+ [src]
65
+ );
50
66
  return /* @__PURE__ */ jsxs(
51
67
  "div",
52
68
  {
@@ -55,21 +71,43 @@ function CatalogDetail({
55
71
  className: cn("ui-catalog-detail", className),
56
72
  children: [
57
73
  /* @__PURE__ */ jsxs("div", { className: "ui-catalog-detail-visual", children: [
58
- /* @__PURE__ */ jsxs("div", { className: "ui-catalog-detail-art", children: [
59
- favorite && /* @__PURE__ */ jsx(
60
- "button",
61
- {
62
- type: "button",
63
- "aria-pressed": favorite.pressed,
64
- "aria-label": favorite.label,
65
- disabled: favorite.disabled,
66
- className: "ui-catalog-card-favorite ui-focus-ring",
67
- onClick: () => favorite.onPressedChange(!favorite.pressed),
68
- children: /* @__PURE__ */ jsx(Bookmark, { "aria-hidden": true, className: "ui-catalog-card-favorite-icon" })
69
- }
70
- ),
71
- media
72
- ] }),
74
+ /* @__PURE__ */ jsxs(
75
+ "div",
76
+ {
77
+ className: "ui-catalog-detail-art",
78
+ "data-status": status,
79
+ "aria-busy": status === "loading" ? true : void 0,
80
+ children: [
81
+ favorite && /* @__PURE__ */ jsx(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ "aria-pressed": favorite.pressed,
86
+ "aria-label": favorite.label,
87
+ disabled: favorite.disabled,
88
+ className: "ui-catalog-card-favorite ui-focus-ring",
89
+ onClick: () => favorite.onPressedChange(!favorite.pressed),
90
+ children: /* @__PURE__ */ jsx(Bookmark, { "aria-hidden": true, className: "ui-catalog-card-favorite-icon" })
91
+ }
92
+ ),
93
+ image ? /* @__PURE__ */ jsx(
94
+ Image,
95
+ {
96
+ src: image.src,
97
+ alt: image.alt,
98
+ width: image.width,
99
+ height: image.height,
100
+ fit: "contain",
101
+ fallback: image.fallback,
102
+ fetchPriority: image.priority ? "high" : void 0,
103
+ preview: false,
104
+ onStatusChange
105
+ },
106
+ image.src
107
+ ) : media
108
+ ]
109
+ }
110
+ ),
73
111
  mediaCaption != null && /* @__PURE__ */ jsx("p", { className: "ui-catalog-detail-caption", children: mediaCaption })
74
112
  ] }),
75
113
  /* @__PURE__ */ jsxs("div", { className: "ui-catalog-detail-main", children: [
@@ -31,6 +31,8 @@ export { CenteredShell } from "./centered-shell.js";
31
31
  export type { CenteredShellProp, CenteredShellProps, CenteredShellAuthProp, CenteredShellPageProp, } from "./centered-shell.js";
32
32
  export type { CenteredShellWidthProp, CenteredShellAlignProp } from "../../props/vocabulary/index.js";
33
33
  export { ErrorSurface } from "./error-surface.js";
34
+ export { PageErrorBoundary } from "./page-error-boundary.js";
35
+ export type { PageErrorBoundaryFallbackProp, PageErrorBoundaryProp, PageErrorBoundaryProps, } from "./page-error-boundary.js";
34
36
  export type { ErrorSurfaceMaintenanceProp, ErrorSurfaceProp, ErrorSurfaceProps, } from "./error-surface.js";
35
37
  export type { ErrorSurfaceModeProp, ErrorSurfaceStatusProp } from "../../props/vocabulary/index.js";
36
38
  export { Breadcrumb } from "./breadcrumb.js";
@@ -14,6 +14,7 @@ import { AuthStack } from "./auth-stack.js";
14
14
  import { MobileShell } from "./mobile-shell.js";
15
15
  import { CenteredShell } from "./centered-shell.js";
16
16
  import { ErrorSurface } from "./error-surface.js";
17
+ import { PageErrorBoundary } from "./page-error-boundary.js";
17
18
  import { Breadcrumb } from "./breadcrumb.js";
18
19
  import { createSidebarLink, Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
19
20
  import { Topbar } from "./topbar.js";
@@ -141,6 +142,7 @@ export {
141
142
  NavList,
142
143
  OrgSwitcher,
143
144
  PageContainer,
145
+ PageErrorBoundary,
144
146
  PriceSummary,
145
147
  ResizableHandle,
146
148
  ResizablePanel,
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
2
2
  export type { PageContainerProp, PageContainerProp as PageContainerProps, PageContainerExtraProp, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
- declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbCurrentItem, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, icon, cover, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
5
+ declare function PageContainerRoot({ title, subtitle, status, extra, help, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbCurrentItem, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, titleRef, density, variant, preset, headerLayout, headerScale, icon, cover, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
6
6
  export declare const PageContainer: typeof PageContainerRoot & {
7
7
  Inset: typeof PageContainerInset;
8
8
  };
@@ -37,6 +37,7 @@ function PageContainerRoot({
37
37
  subtitle,
38
38
  status,
39
39
  extra,
40
+ help,
40
41
  toolbar,
41
42
  footer,
42
43
  toolbarPad,
@@ -68,7 +69,8 @@ function PageContainerRoot({
68
69
  const hasHeading = headerLoading || hasTitle || hasIcon || status != null || !!subtitle;
69
70
  const hasBreadcrumb = !!breadcrumb && breadcrumb.length > 0;
70
71
  const hasEyecatch = isValidElement(cover) && cover.props.variant === "eyecatch";
71
- const hasHeader = hasHeading || hasBreadcrumb || hasEyecatch || extraStart != null || extraEnd != null;
72
+ const hasHelp = help != null && help !== false;
73
+ const hasHeader = hasHeading || hasBreadcrumb || hasEyecatch || extraStart != null || extraEnd != null || hasHelp;
72
74
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll" && hasHeader;
73
75
  const { headerRef, revealed } = useFooterReveal(reveal);
74
76
  const footerRef = useRef(null);
@@ -177,9 +179,10 @@ function PageContainerRoot({
177
179
  )
178
180
  ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
179
181
  ] }),
180
- (extraStart != null || extraEnd != null) && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-extra", children: [
182
+ (extraStart != null || extraEnd != null || hasHelp) && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-extra", children: [
181
183
  extraStart,
182
- extraEnd
184
+ extraEnd,
185
+ hasHelp ? /* @__PURE__ */ jsx("div", { className: "ui-page-header-help", "data-slot": "page-header-help", children: help }) : null
183
186
  ] })
184
187
  ] })
185
188
  ]
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import type { PageErrorBoundaryProp } from "../../props/components/layout.prop.js";
3
+ export type { PageErrorBoundaryFallbackProp, PageErrorBoundaryProp, PageErrorBoundaryProp as PageErrorBoundaryProps, } from "../../props/components/layout.prop.js";
4
+ type BoundaryState = {
5
+ error: unknown;
6
+ failedAt: string | undefined;
7
+ } | null;
8
+ /**
9
+ * PageErrorBoundary — catches a render error thrown by ONE page and draws the failure in its
10
+ * place, so the shell around it (sidebar, topbar, breadcrumb) stays up and the user can go
11
+ * somewhere else (gh#1494). `AppShell` wraps its `children` in one by default.
12
+ *
13
+ * It RESETS ON NAVIGATION without knowing the router: a failed boundary records the URL it failed
14
+ * on, and the next render under a different URL (the router re-renders the shell's children on
15
+ * every visit) draws the page again. `resetKey` is the explicit form, for a change the URL does
16
+ * not show. Without a reset, Back through history stayed on the error surface — the symptom a
17
+ * consumer measured before it wrote its own boundary.
18
+ */
19
+ export declare class PageErrorBoundary extends React.Component<PageErrorBoundaryProp, {
20
+ failure: BoundaryState;
21
+ }> {
22
+ state: {
23
+ failure: BoundaryState;
24
+ };
25
+ static getDerivedStateFromError(error: unknown): {
26
+ failure: BoundaryState;
27
+ };
28
+ componentDidCatch(error: unknown, info: React.ErrorInfo): void;
29
+ componentDidUpdate(previous: PageErrorBoundaryProp): void;
30
+ reset: () => void;
31
+ render(): React.ReactNode;
32
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useTranslation } from "../../i18n/use-translation.js";
5
+ import { isDevelopment } from "../../lib/dev.js";
6
+ import { Button } from "../general/button.js";
7
+ import { ErrorSurface } from "./error-surface.js";
8
+ function DefaultPageErrorFallback() {
9
+ const { t } = useTranslation();
10
+ return /* @__PURE__ */ jsx(
11
+ ErrorSurface,
12
+ {
13
+ mode: "application",
14
+ status: 500,
15
+ titleLevel: 1,
16
+ title: t("layout.pageErrorBoundary.title"),
17
+ description: t("layout.pageErrorBoundary.description"),
18
+ action: /* @__PURE__ */ jsx(Button, { type: "button", onClick: () => window.location.reload(), children: t("layout.pageErrorBoundary.reload") })
19
+ }
20
+ );
21
+ }
22
+ const currentLocation = () => typeof window === "undefined" ? void 0 : window.location.href;
23
+ class PageErrorBoundary extends React.Component {
24
+ state = { failure: null };
25
+ static getDerivedStateFromError(error) {
26
+ return { failure: { error, failedAt: currentLocation() } };
27
+ }
28
+ componentDidCatch(error, info) {
29
+ if (this.props.onError) {
30
+ this.props.onError(error, info);
31
+ } else if (isDevelopment()) {
32
+ console.error("[@godxjp/ui] PageErrorBoundary caught a page render error:", error);
33
+ }
34
+ }
35
+ componentDidUpdate(previous) {
36
+ const { failure } = this.state;
37
+ if (!failure) return;
38
+ const keyChanged = !Object.is(previous.resetKey, this.props.resetKey);
39
+ const moved = failure.failedAt !== void 0 && failure.failedAt !== currentLocation();
40
+ if (keyChanged || moved) this.reset();
41
+ }
42
+ reset = () => {
43
+ this.setState({ failure: null });
44
+ };
45
+ render() {
46
+ const { failure } = this.state;
47
+ if (!failure) return this.props.children;
48
+ const { fallback } = this.props;
49
+ if (typeof fallback === "function")
50
+ return fallback({ error: failure.error, reset: this.reset });
51
+ if (fallback !== void 0) return fallback;
52
+ return /* @__PURE__ */ jsx(DefaultPageErrorFallback, {});
53
+ }
54
+ }
55
+ export {
56
+ PageErrorBoundary
57
+ };
@@ -41,7 +41,7 @@ type RowProps = {
41
41
  * only within `src/components/layout`) so `NavList` renders nested items through the SAME group as
42
42
  * the rail instead of dropping `item.children` on the floor (gh#815).
43
43
  */
44
- export declare function NavGroup({ item, activeId, onSelect, linkComponent, renderItem }: RowProps): React.JSX.Element;
44
+ export declare function NavGroup({ item, activeId, onSelect, sub, linkComponent, renderItem, }: RowProps): React.JSX.Element;
45
45
  export { createSidebarLink } from "./sidebar-link.js";
46
46
  /**
47
47
  * Sidebar — data-driven vertical nav rail. Use {@link createSidebarLink} to adapt a router `Link`,