@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
@@ -33,6 +33,30 @@ function SidebarTrailingIcon({
33
33
  }) {
34
34
  return /* @__PURE__ */ jsx("span", { className: "sb-trailing-icon", children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" }) });
35
35
  }
36
+ function SidebarLabel({ children }) {
37
+ const ref = React.useRef(null);
38
+ React.useLayoutEffect(() => {
39
+ const label = ref.current;
40
+ if (!label) return;
41
+ const sync = () => {
42
+ const cut = label.scrollWidth - label.clientWidth > 1;
43
+ if (cut) {
44
+ const text = (label.textContent ?? "").replace(/\s+/g, " ").trim();
45
+ if (label.title !== text) label.title = text;
46
+ label.dataset.truncated = "";
47
+ } else if (label.dataset.truncated !== void 0) {
48
+ label.removeAttribute("title");
49
+ delete label.dataset.truncated;
50
+ }
51
+ };
52
+ sync();
53
+ if (typeof ResizeObserver === "undefined") return;
54
+ const observer = new ResizeObserver(sync);
55
+ observer.observe(label);
56
+ return () => observer.disconnect();
57
+ });
58
+ return /* @__PURE__ */ jsx("span", { ref, className: "sb-label", children });
59
+ }
36
60
  function SidebarRowContent({
37
61
  item,
38
62
  sub = false,
@@ -42,7 +66,7 @@ function SidebarRowContent({
42
66
  const showBadge = item.badge !== void 0 && item.badge !== "";
43
67
  return /* @__PURE__ */ jsxs(Fragment, { children: [
44
68
  !sub ? /* @__PURE__ */ jsx(SidebarIcon, { icon: item.icon }) : null,
45
- /* @__PURE__ */ jsx("span", { className: "sb-label", children: item.label }),
69
+ /* @__PURE__ */ jsx(SidebarLabel, { children: item.label }),
46
70
  showBadge ? (
47
71
  // absent-when-off), so a rail that never sets `badgeTone` renders the exact same node it
48
72
  // always did and no consumer selector has to out-specify a marker meaning "unchanged".
@@ -147,20 +171,40 @@ function NavLeafsInGroup({
147
171
  linkComponent,
148
172
  renderItem
149
173
  }) {
150
- return children.map((child) => /* @__PURE__ */ jsx(
151
- NavLeaf,
152
- {
153
- item: child,
154
- activeId,
155
- onSelect,
156
- sub: true,
157
- linkComponent,
158
- renderItem
159
- },
160
- child.id
161
- ));
174
+ return children.map(
175
+ (child) => child.children && child.children.length > 0 ? /* @__PURE__ */ jsx(
176
+ NavGroup,
177
+ {
178
+ item: child,
179
+ activeId,
180
+ onSelect,
181
+ sub: true,
182
+ linkComponent,
183
+ renderItem
184
+ },
185
+ child.id
186
+ ) : /* @__PURE__ */ jsx(
187
+ NavLeaf,
188
+ {
189
+ item: child,
190
+ activeId,
191
+ onSelect,
192
+ sub: true,
193
+ linkComponent,
194
+ renderItem
195
+ },
196
+ child.id
197
+ )
198
+ );
162
199
  }
163
- function NavGroup({ item, activeId, onSelect, linkComponent, renderItem }) {
200
+ function NavGroup({
201
+ item,
202
+ activeId,
203
+ onSelect,
204
+ sub = false,
205
+ linkComponent,
206
+ renderItem
207
+ }) {
164
208
  const active = isItemActive(item, activeId);
165
209
  const children = item.children ?? [];
166
210
  const [open, setOpen] = React.useState(active);
@@ -171,10 +215,10 @@ function NavGroup({ item, activeId, onSelect, linkComponent, renderItem }) {
171
215
  /* @__PURE__ */ jsxs(
172
216
  CollapsibleTrigger,
173
217
  {
174
- className: "sb-nav-item sb-nav-group-trigger",
218
+ className: cn("sb-nav-item sb-nav-group-trigger", sub && "sb-nav-item--sub"),
175
219
  "data-active": active ? "true" : void 0,
176
220
  children: [
177
- /* @__PURE__ */ jsx(SidebarRowContent, { item }),
221
+ /* @__PURE__ */ jsx(SidebarRowContent, { item, sub }),
178
222
  /* @__PURE__ */ jsx(ChevronDown, { className: "sb-chevron", "aria-hidden": "true" })
179
223
  ]
180
224
  }
@@ -232,6 +276,45 @@ function CollapsedRow({ item, activeId, onSelect, linkComponent: LinkComponent }
232
276
  children: /* @__PURE__ */ jsx(SidebarRowContent, { item, iconOnly: true })
233
277
  }
234
278
  ) : null;
279
+ const renderFlyoutEntries = (entries) => entries.map((child) => {
280
+ if (child.children && child.children.length > 0) {
281
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": child.label, className: "sb-flyout-group", children: [
282
+ /* @__PURE__ */ jsx("div", { className: "sb-flyout-title", "aria-hidden": "true", children: child.label }),
283
+ renderFlyoutEntries(child.children)
284
+ ] }, child.id);
285
+ }
286
+ const childActive = child.id === activeId;
287
+ const select = (event) => {
288
+ setMenuOpen(false);
289
+ if (child.disabled) {
290
+ event.preventDefault();
291
+ return;
292
+ }
293
+ onSelect?.(child.id);
294
+ };
295
+ const flyoutState = {
296
+ role: "menuitem",
297
+ className: "sb-nav-item",
298
+ "data-active": childActive ? "true" : void 0,
299
+ "aria-current": childActive ? "page" : void 0,
300
+ "aria-disabled": child.disabled || void 0
301
+ };
302
+ return LinkComponent && child.href ? /* @__PURE__ */ jsx(
303
+ LinkComponent,
304
+ {
305
+ ...flyoutState,
306
+ href: child.disabled ? void 0 : child.href,
307
+ onClick: select,
308
+ children: /* @__PURE__ */ jsx(SidebarRowContent, { item: child, sub: true })
309
+ },
310
+ child.id
311
+ ) : (
312
+ // A collapsed-rail flyout entry is a `menuitem`-styled nav row owning `.sb-nav-item`
313
+ // composition, not a <Button> — same contract as the leaf row above.
314
+ // ui-audit-disable-next-line no-raw-button
315
+ /* @__PURE__ */ jsx("button", { type: "button", ...flyoutState, onClick: select, children: /* @__PURE__ */ jsx(SidebarRowContent, { item: child, sub: true }) }, child.id)
316
+ );
317
+ });
235
318
  if (!hasChildren) {
236
319
  return /* @__PURE__ */ jsxs(Tooltip, { children: [
237
320
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: iconLink ?? iconButton }),
@@ -253,39 +336,7 @@ function CollapsedRow({ item, activeId, onSelect, linkComponent: LinkComponent }
253
336
  className: "sb-flyout-pop",
254
337
  children: [
255
338
  /* @__PURE__ */ jsx("div", { className: "sb-flyout-title", children: item.label }),
256
- children.map((child) => {
257
- const childActive = child.id === activeId;
258
- const select = (event) => {
259
- setMenuOpen(false);
260
- if (child.disabled) {
261
- event.preventDefault();
262
- return;
263
- }
264
- onSelect?.(child.id);
265
- };
266
- const flyoutState = {
267
- role: "menuitem",
268
- className: "sb-nav-item",
269
- "data-active": childActive ? "true" : void 0,
270
- "aria-current": childActive ? "page" : void 0,
271
- "aria-disabled": child.disabled || void 0
272
- };
273
- return LinkComponent && child.href ? /* @__PURE__ */ jsx(
274
- LinkComponent,
275
- {
276
- ...flyoutState,
277
- href: child.disabled ? void 0 : child.href,
278
- onClick: select,
279
- children: /* @__PURE__ */ jsx(SidebarRowContent, { item: child, sub: true })
280
- },
281
- child.id
282
- ) : (
283
- // A collapsed-rail flyout entry is a `menuitem`-styled nav row owning `.sb-nav-item`
284
- // composition, not a <Button> — same contract as the leaf row above.
285
- // ui-audit-disable-next-line no-raw-button
286
- /* @__PURE__ */ jsx("button", { type: "button", ...flyoutState, onClick: select, children: /* @__PURE__ */ jsx(SidebarRowContent, { item: child, sub: true }) }, child.id)
287
- );
288
- })
339
+ renderFlyoutEntries(children)
289
340
  ]
290
341
  }
291
342
  )
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { revealKeyboardFocus } from "../../lib/focus-reveal.js";
4
4
  import { MoreHorizontal } from "lucide-react";
5
5
  import * as React from "react";
6
+ import { createPortal } from "react-dom";
6
7
  import { useTranslation } from "../../i18n/use-translation.js";
7
8
  import { padStyle } from "../../lib/variants.js";
8
9
  import { cn } from "../../lib/utils.js";
@@ -26,13 +27,54 @@ function Topbar({
26
27
  const controlsRef = React.useRef(null);
27
28
  const hasIdentity = identity != null && children == null;
28
29
  const foldable = overflow === "menu" && children == null;
29
- const collapsed = useCollapsesToMenu(hasIdentity ? controlsRef : barRef, foldable);
30
- const clusters = /* @__PURE__ */ jsxs(Fragment, { children: [
30
+ const hosts = useFoldHosts(foldable);
31
+ const present = { start: start != null, center: center != null, end: end != null };
32
+ const holderRef = React.useRef(null);
33
+ const overflowRef = React.useRef(null);
34
+ const placeClusters = React.useCallback(
35
+ (isCollapsed) => {
36
+ if (!hosts) return;
37
+ const target = !isCollapsed ? hasIdentity ? controlsRef.current : barRef.current : overflowRef.current ?? holderRef.current;
38
+ if (!target) return;
39
+ for (const key of CLUSTER_ORDER) {
40
+ const node = hosts[key];
41
+ if (!present[key]) node.remove();
42
+ else if (node.parentNode !== target || node !== target.lastElementChild) {
43
+ target.appendChild(node);
44
+ }
45
+ }
46
+ },
47
+ // `present` is derived from the three slots on every render; its booleans are what matter.
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ [hosts, hasIdentity, present.start, present.center, present.end]
50
+ );
51
+ const collapsed = useCollapsesToMenu(
52
+ hasIdentity ? controlsRef : barRef,
53
+ foldable,
54
+ hosts ? placeClusters : void 0
55
+ );
56
+ const overflowNode = React.useCallback(
57
+ (node) => {
58
+ overflowRef.current = node;
59
+ placeClusters(true);
60
+ },
61
+ [placeClusters]
62
+ );
63
+ const clusters = hosts ? (
64
+ // Kept mounted in ONE place, whatever the bar is showing; `placeClusters` moves the elements.
65
+ /* @__PURE__ */ jsx(TopbarClusterHolder, { ref: holderRef, visible: collapsed })
66
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
31
67
  start != null ? /* @__PURE__ */ jsx("div", { "data-slot": "topbar-start", className: "ui-topbar-start", children: start }) : null,
32
68
  center != null ? /* @__PURE__ */ jsx("div", { "data-slot": "topbar-center", className: "ui-topbar-center", children: center }) : null,
33
69
  end != null ? /* @__PURE__ */ jsx("div", { "data-slot": "topbar-end", className: "ui-topbar-end", children: end }) : null
34
70
  ] });
35
- return /* @__PURE__ */ jsx(
71
+ const portals = hosts ? /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ start != null ? createPortal(start, hosts.start) : null,
73
+ center != null ? createPortal(center, hosts.center) : null,
74
+ end != null ? createPortal(end, hosts.end) : null
75
+ ] }) : null;
76
+ const overflowMenu = /* @__PURE__ */ jsx(TopbarOverflowMenu, { contentRef: hosts ? overflowNode : void 0, children: hosts ? null : clusters });
77
+ return /* @__PURE__ */ jsxs(
36
78
  "div",
37
79
  {
38
80
  ref: barRef,
@@ -48,36 +90,81 @@ function Topbar({
48
90
  "data-slot": "topbar",
49
91
  className: cn("ui-topbar", className),
50
92
  ...props,
51
- children: children ?? (hasIdentity ? /* @__PURE__ */ jsxs(Fragment, { children: [
52
- /* @__PURE__ */ jsx(
53
- "div",
54
- {
55
- ref: controlsRef,
56
- onFocusCapture: revealKeyboardFocus,
57
- "data-slot": "topbar-controls",
58
- "data-overflow": overflow,
59
- className: "ui-topbar ui-topbar-controls",
60
- children: collapsed ? /* @__PURE__ */ jsx(TopbarOverflowMenu, { children: clusters }) : clusters
61
- }
62
- ),
63
- /* @__PURE__ */ jsx("div", { "data-slot": "topbar-identity", className: "ui-topbar-identity", children: identity })
64
- ] }) : collapsed ? /* @__PURE__ */ jsx(TopbarOverflowMenu, { children: clusters }) : clusters)
93
+ children: [
94
+ children ?? (hasIdentity ? /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ /* @__PURE__ */ jsxs(
96
+ "div",
97
+ {
98
+ ref: controlsRef,
99
+ onFocusCapture: revealKeyboardFocus,
100
+ "data-slot": "topbar-controls",
101
+ "data-overflow": overflow,
102
+ className: "ui-topbar ui-topbar-controls",
103
+ children: [
104
+ collapsed ? overflowMenu : null,
105
+ collapsed && !hosts ? null : clusters
106
+ ]
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsx("div", { "data-slot": "topbar-identity", className: "ui-topbar-identity", children: identity })
110
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
111
+ collapsed ? overflowMenu : null,
112
+ collapsed && !hosts ? null : clusters
113
+ ] })),
114
+ portals
115
+ ]
65
116
  }
66
117
  );
67
118
  }
68
- function TopbarOverflowMenu({ children }) {
119
+ const CLUSTER_ORDER = ["start", "center", "end"];
120
+ const subscribeNothing = () => () => {
121
+ };
122
+ function useFoldHosts(enabled) {
123
+ const isClient = React.useSyncExternalStore(
124
+ subscribeNothing,
125
+ () => true,
126
+ () => false
127
+ );
128
+ const [hosts] = React.useState(() => {
129
+ if (typeof document === "undefined") return null;
130
+ const make = (key) => {
131
+ const node = document.createElement("div");
132
+ node.dataset.slot = `topbar-${key}`;
133
+ node.className = `ui-topbar-${key}`;
134
+ return node;
135
+ };
136
+ return { start: make("start"), center: make("center"), end: make("end") };
137
+ });
138
+ React.useEffect(
139
+ () => () => {
140
+ if (hosts) for (const key of CLUSTER_ORDER) hosts[key].remove();
141
+ },
142
+ [hosts]
143
+ );
144
+ return enabled && isClient ? hosts : null;
145
+ }
146
+ const TopbarClusterHolder = React.forwardRef(
147
+ function TopbarClusterHolder2({ visible }, ref) {
148
+ return visible ? /* @__PURE__ */ jsx("div", { ref, hidden: true, "data-slot": "topbar-fold-holder" }) : null;
149
+ }
150
+ );
151
+ function TopbarOverflowMenu({
152
+ children,
153
+ contentRef
154
+ }) {
69
155
  const { t } = useTranslation();
70
156
  return /* @__PURE__ */ jsxs(Popover, { children: [
71
157
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(TopbarItem, { "data-topbar-overflow-trigger": "", "aria-label": t("layout.topbar.more"), children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true" }) }) }),
72
- /* @__PURE__ */ jsx(PopoverContent, { align: "end", width: "auto", children: /* @__PURE__ */ jsx("div", { "data-slot": "topbar-overflow", className: "ui-topbar-overflow", children }) })
158
+ /* @__PURE__ */ jsx(PopoverContent, { align: "end", width: "auto", children: /* @__PURE__ */ jsx("div", { ref: contentRef, "data-slot": "topbar-overflow", className: "ui-topbar-overflow", children }) })
73
159
  ] });
74
160
  }
75
- function useCollapsesToMenu(ref, enabled) {
161
+ function useCollapsesToMenu(ref, enabled, place) {
76
162
  const [collapsed, setCollapsed] = React.useState(false);
77
163
  const needed = React.useRef(0);
78
164
  React.useLayoutEffect(() => {
79
165
  const bar = ref.current;
80
166
  if (!enabled || !bar) return void 0;
167
+ place?.(collapsed);
81
168
  const update = () => {
82
169
  if (!collapsed) {
83
170
  if (bar.clientWidth > 0 && bar.scrollWidth - bar.clientWidth > 1) {
@@ -100,7 +187,7 @@ function useCollapsesToMenu(ref, enabled) {
100
187
  }
101
188
  }
102
189
  return () => observer.disconnect();
103
- }, [ref, enabled, collapsed]);
190
+ }, [ref, enabled, collapsed, place]);
104
191
  return enabled && collapsed;
105
192
  }
106
193
  export {
@@ -169,6 +169,14 @@ interface DropdownMenuItemPropsOwn {
169
169
  description?: React.ReactNode;
170
170
  /** Mượn thẻ của con thay vì dựng `<div>` riêng — chỗ để nhét một `<Link>` vào một mục menu. */
171
171
  asChild?: boolean;
172
+ /**
173
+ * The item is a link to this URL. With `asChild` it is read from the borrowed child's own `href`
174
+ * when there is one; pass it here when the child is a router link that takes something else
175
+ * (TanStack/React Router `<Link to>`), so react-aria renders its LINK item — an `<a>` — instead of
176
+ * an action `<div>` (and stops warning "Expected <div>, got <a>"). Without a react-aria
177
+ * `RouterProvider` the click stays the child's own: navigation is whatever the child does.
178
+ */
179
+ href?: string;
172
180
  /**
173
181
  * `id` và mọi `data-*` ĐI XUỐNG phần tử gốc của item (gh#631).
174
182
  *
@@ -183,7 +191,7 @@ interface DropdownMenuItemPropsOwn {
183
191
  id?: string;
184
192
  }
185
193
  type DropdownMenuItemProps = React.PropsWithChildren<DropdownMenuItemPropsOwn> & Record<`data-${string}`, unknown>;
186
- export declare function DropdownMenuItem({ children, className, inset, variant, disabled, onSelect, textValue, description, asChild, ...forwarded }: DropdownMenuItemProps): React.JSX.Element;
194
+ export declare function DropdownMenuItem({ children, className, inset, variant, disabled, onSelect, textValue, description, asChild, href, ...forwarded }: DropdownMenuItemProps): React.JSX.Element;
187
195
  interface DropdownMenuLabelPropsOwn {
188
196
  className?: string;
189
197
  inset?: boolean;
@@ -197,10 +205,12 @@ type DropdownMenuLabelProps = React.PropsWithChildren<DropdownMenuLabelPropsOwn>
197
205
  * `Header` không đi qua `useRenderProps`, nên `props.children` trong `render` CHÍNH là children của
198
206
  * consumer — không có `Provider` chen vào như ở `MenuItem`. `Slot` vì thế nhận thẳng gói props.
199
207
  *
200
- * NHƯNG PHẢI RENDER RA `<div>`, KHÔNG PHẢI `<header>`. `<header>` mang vai trò ngầm `banner`, và
208
+ * `<header>` TRẦN thì không được: nó mang vai trò ngầm `banner`, và
201
209
  * `banner` không nằm trong danh sách con hợp lệ của `role="menu"` (axe: aria-required-children,
202
210
  * ARIA 1.2 — menu chỉ nhận menuitem / menuitemcheckbox / menuitemradio / group / separator). Radix
203
- * dựng `<div>`, vai trò `generic`, nên nó không bao giờ vấp lỗi này.
211
+ * dựng `<div>`, vai trò `generic`, nên nó không bao giờ vấp lỗi này. Còn `<div>` thì RAC cảnh báo
212
+ * "Expected <header>, got <div>" (gh#1452). Nên: `<header role="presentation">` — đúng thẻ RAC chờ,
213
+ * không có landmark.
204
214
  *
205
215
  * `Header` vẫn được dùng chứ không thay bằng `<div>` trần: bộ dựng collection của RAC chỉ nhận các
206
216
  * nút nó biết, một phần tử lạ đặt thẳng trong `Menu` sẽ bị nó bỏ. Nên giữ nút, đổi thẻ.
@@ -22,6 +22,7 @@ import {
22
22
  import { Check, ChevronRight } from "lucide-react";
23
23
  import { ariaKeyShortcuts, parseShortcut, shouldHandleShortcut } from "../../lib/shortcut.js";
24
24
  import { cn } from "../../lib/utils.js";
25
+ import { isDevelopment } from "../../lib/dev.js";
25
26
  import { Slot } from "../../lib/slot.js";
26
27
  const DROPDOWN_MENU_PLACEMENT = {
27
28
  top: { side: "top", align: "center" },
@@ -40,7 +41,7 @@ class MenuCollectionBoundary extends React.Component {
40
41
  // of gh#637 that cost the reporter a night of instrumenting `Object.prototype`.
41
42
  componentDidCatch(error) {
42
43
  console.error(
43
- "[@godxjp/ui] DropdownMenu: the menu could not be built and was left empty; the rest of the application is unaffected. A menu's children must be collection nodes (DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuSub) or components returning one \u2014 an icon or a text label belongs INSIDE an item, not beside it. Underlying error:",
44
+ "[@godxjp/ui] DropdownMenu: the menu could not be built and was left empty; the rest of the application is unaffected. A menu's children must be collection nodes (DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuSub) or components returning one \u2014 an icon or a text label belongs INSIDE an item, not beside it. Use Popover + ListRow for arbitrary panel content. Underlying error:",
44
45
  error
45
46
  );
46
47
  }
@@ -51,6 +52,10 @@ class MenuCollectionBoundary extends React.Component {
51
52
  function borrowedTag(children, asChild) {
52
53
  return asChild ? React.Children.only(children) : null;
53
54
  }
55
+ function borrowedHref(tag) {
56
+ const href = tag?.props?.href;
57
+ return typeof href === "string" ? href : void 0;
58
+ }
54
59
  function renderItemTag(tag, domProps, content) {
55
60
  if (!tag) {
56
61
  return /* @__PURE__ */ jsx("div", { ...domProps, children: content });
@@ -331,6 +336,16 @@ function DropdownMenuSub({ children }) {
331
336
  const parts = React.Children.toArray(children).filter(React.isValidElement);
332
337
  return /* @__PURE__ */ jsx(SubmenuTrigger, { children: parts });
333
338
  }
339
+ function hasNonMenuChildren(children) {
340
+ return React.Children.toArray(children).some((child) => {
341
+ if (!React.isValidElement(child)) return true;
342
+ if (typeof child.type === "string") return true;
343
+ if (child.type === React.Fragment || child.type === DropdownMenuGroup || child.type === DropdownMenuRadioGroup) {
344
+ return hasNonMenuChildren(child.props.children);
345
+ }
346
+ return false;
347
+ });
348
+ }
334
349
  function DropdownMenuContent({
335
350
  children,
336
351
  className,
@@ -348,6 +363,13 @@ function DropdownMenuContent({
348
363
  sticky,
349
364
  forceMount
350
365
  }) {
366
+ React.useEffect(() => {
367
+ if (isDevelopment() && hasNonMenuChildren(children)) {
368
+ console.error(
369
+ "[@godxjp/ui] DropdownMenuContent accepts menu collection children only. Use DropdownMenuItem for actions; use Popover + ListRow for notifications, headers, lists and links."
370
+ );
371
+ }
372
+ }, [children]);
351
373
  void hideWhenDetached;
352
374
  void sticky;
353
375
  void forceMount;
@@ -417,6 +439,7 @@ function DropdownMenuItem({
417
439
  textValue,
418
440
  description,
419
441
  asChild,
442
+ href,
420
443
  ...forwarded
421
444
  }) {
422
445
  const tag = borrowedTag(children, asChild);
@@ -426,6 +449,7 @@ function DropdownMenuItem({
426
449
  MenuItem,
427
450
  {
428
451
  isDisabled: disabled,
452
+ href: href ?? borrowedHref(tag),
429
453
  textValue: textValue ?? (typeof label === "string" ? label : void 0),
430
454
  onAction: onSelect ? () => onSelect(selectEvent("dropdownmenu.itemSelect")) : void 0,
431
455
  className: cn(
@@ -477,11 +501,17 @@ function DropdownMenuLabel({ children, className, inset, asChild }) {
477
501
  {
478
502
  ...props
479
503
  }
480
- ) : (props) => /* @__PURE__ */ jsx(
481
- "div",
482
- {
483
- ...props
484
- }
504
+ ) : (props) => (
505
+ // RAC's `Header` element (it warns in dev when `render` returns another one, gh#1452),
506
+ // with the landmark dropped: `role="presentation"` is allowed on `header` and keeps
507
+ // the menu owning no `banner`.
508
+ /* @__PURE__ */ jsx(
509
+ "header",
510
+ {
511
+ role: "presentation",
512
+ ...props
513
+ }
514
+ )
485
515
  ),
486
516
  children
487
517
  }
@@ -22,5 +22,5 @@ declare const PaginationNext: React.ForwardRefExoticComponent<Omit<Omit<React.De
22
22
  disabled?: boolean;
23
23
  href?: string;
24
24
  } & React.RefAttributes<HTMLButtonElement>>;
25
- export declare function Pagination({ ariaLabel, value, total, pageSize, pageSizeOptions, showSizeChanger, showTotal, hideOnSinglePage, simple, showQuickJumper, size, align, responsive, disabled, className, onValueChange, }: PaginationProp): React.JSX.Element | null;
25
+ export declare function Pagination({ ariaLabel, value, total, pageSize, pageSizeOptions, showSizeChanger, showTotal, hideOnSinglePage, simple, showQuickJumper, size, align, responsive, disabled, className, onValueChange, onPageIntent, }: PaginationProp): React.JSX.Element | null;
26
26
  export { PaginationContent, PaginationItem, PaginationLink, PaginationEllipsis, PaginationPrevious, PaginationNext, };
@@ -122,7 +122,8 @@ function Pagination({
122
122
  responsive = true,
123
123
  disabled,
124
124
  className,
125
- onValueChange
125
+ onValueChange,
126
+ onPageIntent
126
127
  }) {
127
128
  const { t } = useTranslation();
128
129
  const jumperId = React.useId();
@@ -138,6 +139,12 @@ function Pagination({
138
139
  const nextPage = Math.min(Math.max(1, page), Math.max(1, Math.ceil(total / size2)));
139
140
  onValueChange?.(nextPage, size2);
140
141
  };
142
+ const intent = (page) => {
143
+ const notify = () => {
144
+ if (!disabled && page >= 1 && page <= totalPages) onPageIntent?.(page);
145
+ };
146
+ return { onPointerEnter: notify, onFocus: notify, onTouchStart: notify };
147
+ };
141
148
  const commitJump = () => {
142
149
  const parsed = Number.parseInt(jumperDraft, 10);
143
150
  setJumperDraft("");
@@ -204,6 +211,7 @@ function Pagination({
204
211
  size: "sm",
205
212
  disabled: Boolean(disabled) || safeCurrent <= 1,
206
213
  "aria-label": t("navigation.pagination.prev"),
214
+ ...intent(safeCurrent - 1),
207
215
  onClick: () => go(safeCurrent - 1),
208
216
  children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
209
217
  }
@@ -221,6 +229,7 @@ function Pagination({
221
229
  size: "sm",
222
230
  disabled: Boolean(disabled) || safeCurrent >= totalPages,
223
231
  "aria-label": t("navigation.pagination.next"),
232
+ ...intent(safeCurrent + 1),
224
233
  onClick: () => go(safeCurrent + 1),
225
234
  children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
226
235
  }
@@ -264,6 +273,7 @@ function Pagination({
264
273
  PaginationPrevious,
265
274
  {
266
275
  "aria-label": t("navigation.pagination.prev"),
276
+ ...intent(safeCurrent - 1),
267
277
  onClick: () => go(safeCurrent - 1),
268
278
  disabled: Boolean(disabled) || safeCurrent <= 1,
269
279
  children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
@@ -274,6 +284,7 @@ function Pagination({
274
284
  PaginationLink,
275
285
  {
276
286
  isActive: page === safeCurrent,
287
+ ...intent(page),
277
288
  "aria-label": t("navigation.pagination.page", { page }),
278
289
  onClick: () => {
279
290
  if (!disabled) go(page);
@@ -287,6 +298,7 @@ function Pagination({
287
298
  PaginationNext,
288
299
  {
289
300
  "aria-label": t("navigation.pagination.next"),
301
+ ...intent(safeCurrent + 1),
290
302
  onClick: () => go(safeCurrent + 1),
291
303
  disabled: Boolean(disabled) || safeCurrent >= totalPages,
292
304
  children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
@@ -132,7 +132,8 @@ function Toolbar({
132
132
  onValueChange: (selected) => filter.onSelectedChange?.(selected),
133
133
  placeholder: filter.placeholder,
134
134
  disabled: disabled || filter.disabled,
135
- allowClear: true,
135
+ allowClear: filter.required ? false : filter.allowClear ?? true,
136
+ "aria-required": filter.required || void 0,
136
137
  clearLabel: typeof filter.label === "string" ? t("navigation.filterBar.clearFilter", { label: filter.label }) : void 0
137
138
  }
138
139
  )
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "33.22.2",
3
+ "version": "33.25.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -382,10 +382,16 @@
382
382
  "position": "Cover position",
383
383
  "hint": "Drag the image, or use the arrow keys, to reposition it",
384
384
  "valueText": "{value}% from the top"
385
+ },
386
+ "pageErrorBoundary": {
387
+ "title": "This page could not be displayed",
388
+ "description": "Something went wrong while showing this page. The rest of the app still works: reload the page, or go to another page from the menu.",
389
+ "reload": "Reload page"
385
390
  }
386
391
  },
387
392
  "dataDisplay": {
388
393
  "avatar": {
394
+ "agent": "AI agent",
389
395
  "presence": {
390
396
  "online": "Online",
391
397
  "away": "Away",
@@ -805,6 +811,7 @@
805
811
  "trendCurrent": "current",
806
812
  "heatmapSummary": "Heatmap, {rows} × {columns}",
807
813
  "heatmapHighest": "Highest: {value} at {row}, {column}",
814
+ "noData": "No data",
808
815
  "heatmapNoData": "No data",
809
816
  "heatmapTop": "Top {rank}",
810
817
  "heatmapLegend": "Color scale",
@@ -374,10 +374,16 @@
374
374
  "position": "カバー画像の位置",
375
375
  "hint": "画像をドラッグするか矢印キーで位置を調整",
376
376
  "valueText": "上から {value}%"
377
+ },
378
+ "pageErrorBoundary": {
379
+ "title": "このページを表示できませんでした",
380
+ "description": "ページの表示中に問題が発生しました。アプリの他の部分は使えます。ページを再読み込みするか、メニューから別のページに移動してください。",
381
+ "reload": "ページを再読み込み"
377
382
  }
378
383
  },
379
384
  "dataDisplay": {
380
385
  "avatar": {
386
+ "agent": "AIエージェント",
381
387
  "presence": {
382
388
  "online": "オンライン",
383
389
  "away": "離席中",
@@ -779,6 +785,7 @@
779
785
  "trendCurrent": "現在",
780
786
  "heatmapSummary": "ヒートマップ({rows}行×{columns}列)",
781
787
  "heatmapHighest": "最大 {value}({row}・{column})",
788
+ "noData": "データなし",
782
789
  "heatmapNoData": "データなし",
783
790
  "heatmapTop": "上位{rank}位",
784
791
  "heatmapLegend": "色の濃さの凡例",