@godxjp/ui 33.22.1 → 33.24.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 (65) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +4 -4
  3. package/agent/DESIGN.md +1 -1
  4. package/agent/START-HERE.md +7 -7
  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/PageContainer.json +5 -0
  9. package/agent/components/PageErrorBoundary.json +48 -0
  10. package/agent/components/Toaster.json +1 -1
  11. package/agent/components/Toolbar.json +1 -1
  12. package/agent/components/Topbar.json +1 -1
  13. package/agent/components-index.json +10 -0
  14. package/agent/components.json +112 -3
  15. package/agent/index.json +7 -7
  16. package/agent/llms.txt +6 -6
  17. package/agent/tokens.json +84 -0
  18. package/dist/components/data-display/avatar.d.ts +1 -1
  19. package/dist/components/data-display/avatar.js +7 -1
  20. package/dist/components/data-display/badge.d.ts +11 -0
  21. package/dist/components/data-display/badge.js +20 -0
  22. package/dist/components/data-display/image.js +1 -1
  23. package/dist/components/data-display/index.d.ts +2 -2
  24. package/dist/components/data-display/index.js +2 -1
  25. package/dist/components/data-entry/record-picker.js +122 -98
  26. package/dist/components/data-entry/select.js +13 -3
  27. package/dist/components/layout/app-shell.d.ts +1 -1
  28. package/dist/components/layout/app-shell.js +3 -1
  29. package/dist/components/layout/index.d.ts +2 -0
  30. package/dist/components/layout/index.js +2 -0
  31. package/dist/components/layout/page-container.d.ts +1 -1
  32. package/dist/components/layout/page-container.js +6 -3
  33. package/dist/components/layout/page-error-boundary.d.ts +32 -0
  34. package/dist/components/layout/page-error-boundary.js +57 -0
  35. package/dist/components/layout/sidebar.d.ts +1 -1
  36. package/dist/components/layout/sidebar.js +100 -49
  37. package/dist/components/layout/topbar.js +108 -21
  38. package/dist/components/navigation/toolbar.js +2 -1
  39. package/dist/contracts/measurement.json +1 -1
  40. package/dist/i18n/messages/en.json +6 -0
  41. package/dist/i18n/messages/ja.json +6 -0
  42. package/dist/i18n/messages/vi.json +6 -0
  43. package/dist/props/components/data-display.prop.d.ts +27 -0
  44. package/dist/props/components/layout.prop.d.ts +45 -0
  45. package/dist/props/components/navigation.prop.d.ts +12 -1
  46. package/dist/props/registry.d.ts +50 -2
  47. package/dist/props/registry.js +72 -3
  48. package/dist/styles/alert-layout.css +4 -0
  49. package/dist/styles/badge-layout.css +14 -0
  50. package/dist/styles/data-display-layout.css +44 -0
  51. package/dist/styles/icon-layout.css +3 -0
  52. package/dist/styles/layers.json +22 -2
  53. package/dist/styles/layout.css +11 -0
  54. package/dist/styles/shell-layout.css +17 -1
  55. package/dist/styles/table-layout.css +1 -1
  56. package/dist/styles.css +1 -1
  57. package/dist/tokens/components/badge.css +3 -0
  58. package/dist/tokens/components/data-display.css +9 -0
  59. package/dist/tokens/components/shell.css +2 -0
  60. package/dist/tokens/foundation.css +8 -0
  61. package/docs/CONSUMER-RULES.md +1 -1
  62. package/package.json +3 -2
  63. package/scripts/consumer-rule.md +1 -1
  64. package/skills/godxjp-ui-navigation/SKILL.md +14 -9
  65. package/skills/godxjp-ui-navigation/references/principles.md +2 -2
@@ -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`,
@@ -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 {
@@ -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.1",
3
+ "version": "33.24.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",
@@ -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": "離席中",
@@ -375,10 +375,16 @@
375
375
  "position": "Vị trí ảnh bìa",
376
376
  "hint": "Kéo ảnh hoặc dùng phím mũi tên để chỉnh vị trí",
377
377
  "valueText": "{value}% tính từ trên"
378
+ },
379
+ "pageErrorBoundary": {
380
+ "title": "Không thể hiển thị trang này",
381
+ "description": "Đã xảy ra lỗi khi hiển thị trang này. Các phần khác của ứng dụng vẫn hoạt động: hãy tải lại trang, hoặc chuyển sang trang khác từ menu.",
382
+ "reload": "Tải lại trang"
378
383
  }
379
384
  },
380
385
  "dataDisplay": {
381
386
  "avatar": {
387
+ "agent": "Tác tử AI",
382
388
  "presence": {
383
389
  "online": "Trực tuyến",
384
390
  "away": "Vắng mặt",
@@ -261,9 +261,25 @@ export type AvatarProp = React.ComponentPropsWithoutRef<"span"> & {
261
261
  * a presence dot never carries visible text; that is `Badge status`.
262
262
  */
263
263
  presenceLabel?: LabelProp;
264
+ /**
265
+ * Who the mark stands for. `person` (default) is inert — no attribute, no node. `agent` marks an
266
+ * AI agent: a small "AI" tag at the block-start/inline-end corner (the corner `presence` does not
267
+ * use) plus a localized visually hidden "AI agent" folded into the avatar's accessible text, so the
268
+ * difference is never carried by the tag's colour alone (WCAG 1.4.1). Same vocabulary as
269
+ * `OrgChartNode.variant`.
270
+ */
271
+ kind?: AvatarKindProp;
264
272
  className?: ClassNameProp;
265
273
  children?: ChildrenProp;
266
274
  };
275
+ /**
276
+ * Who an Avatar stands for — a person or an AI agent. Deliberately the same two values as
277
+ * `OrgChartNodeVariantProp`, so an app that already knows a principal's kind passes it unchanged
278
+ * to both. Retune the agent tag with `--avatar-agent-*`.
279
+ *
280
+ * @see Avatar
281
+ */
282
+ export type AvatarKindProp = "person" | "agent";
267
283
  /**
268
284
  * Avatar presence status — a person's realtime reachability.
269
285
  *
@@ -307,6 +323,17 @@ export type AvatarAppearanceProp = "default" | "tinted";
307
323
  */
308
324
  export type AvatarImageFitProp = "cover" | "contain";
309
325
  /** @see Badge */
326
+ /**
327
+ * @see AgentBadge — the "AI agent" label beside a name (violet outline, pill, never wraps). The
328
+ * text-row half of telling an agent apart from a person; `Avatar kind="agent"` is the mark half.
329
+ */
330
+ export type AgentBadgeProp = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
331
+ /** Render element — `span` (default; the label sits in a name's text line) or `div`. */
332
+ as?: "div" | "span";
333
+ /** Overrides the localized default label (`dataDisplay.avatar.agent`). */
334
+ children?: ChildrenProp;
335
+ className?: ClassNameProp;
336
+ };
310
337
  export type BadgeProp = {
311
338
  /**
312
339
  * Render element — `div` (default) or `span` when the chip sits in a phrasing context where a
@@ -73,6 +73,13 @@ export type PageContainerProp = {
73
73
  titleRef?: React.Ref<HTMLHeadingElement>;
74
74
  /** @see PageContainerExtraProp */
75
75
  extra?: PageContainerExtraProp;
76
+ /**
77
+ * The page's Help control (gh#1494) — a `Button` that opens a help Sheet, or a link to the
78
+ * manual. It has ONE fixed place on every page: the inline end of the header row, after the
79
+ * `extra` actions, and still the last item at the inline end when those actions wrap. A page
80
+ * passing only `help` still gets a header. Pass one control; it is not a second action cluster.
81
+ */
82
+ help?: ReactNode;
76
83
  /**
77
84
  * FIXED chrome band between the page header and the scrolling body — a filter strip, a status
78
85
  * bar, a "channel workflow" rail. It is a first-class page-chrome slot precisely because the
@@ -454,6 +461,33 @@ export type PageInsetProp = React.HTMLAttributes<HTMLDivElement> & {
454
461
  children?: ChildrenProp;
455
462
  className?: ClassNameProp;
456
463
  };
464
+ /** @see PageErrorBoundary — the render-prop form of `fallback`. */
465
+ export type PageErrorBoundaryFallbackProp = (failure: {
466
+ /** What the page threw. */
467
+ error: unknown;
468
+ /** Draws the page again (e.g. a "Try again" button that re-renders without a reload). */
469
+ reset: () => void;
470
+ }) => ReactNode;
471
+ /**
472
+ * @see PageErrorBoundary — a page-level error boundary (gh#1494). A page that throws while
473
+ * rendering is replaced by an `ErrorSurface`; the shell around it stays.
474
+ */
475
+ export type PageErrorBoundaryProp = {
476
+ children?: ChildrenProp;
477
+ /**
478
+ * What to draw instead of the page. Default: an `application` `ErrorSurface` (status 500,
479
+ * localized title and description, one "Reload page" action). A function receives
480
+ * `{ error, reset }`.
481
+ */
482
+ fallback?: ReactNode | PageErrorBoundaryFallbackProp;
483
+ /**
484
+ * Draw the page again whenever this value changes (compared with `Object.is`). The boundary
485
+ * already resets when the URL changes; pass a key for a change the URL does not show.
486
+ */
487
+ resetKey?: unknown;
488
+ /** Report the error (Sentry, a log endpoint). Replaces the development-time `console.error`. */
489
+ onError?: (error: unknown, info: React.ErrorInfo) => void;
490
+ };
457
491
  /** @see AppShell */
458
492
  export type AppShellProp = {
459
493
  /** Phone destinations, using MobileShell tabBar children (3–5 links or Buttons with aria-current="page"). Only rendered in drawer navigation mode. Main/footer reserve the measured safe-area-inclusive height. */
@@ -463,6 +497,14 @@ export type AppShellProp = {
463
497
  /** Omit or pass null/false for a shell without the sidebar landmark or grid track. */
464
498
  sidebar?: ReactNode;
465
499
  children: ReactNode;
500
+ /**
501
+ * The page-level error boundary around `children` (gh#1494). Default `true`: a page that throws
502
+ * while rendering is replaced by an `ErrorSurface` inside `<main>`, and the sidebar, topbar and
503
+ * breadcrumb stay, so the user can navigate away; the boundary resets when the URL changes.
504
+ * Pass props to configure it (`{ fallback, resetKey, onError }`), or `false` when the app
505
+ * already wraps its pages in its own boundary.
506
+ */
507
+ errorBoundary?: boolean | Omit<PageErrorBoundaryProp, "children">;
466
508
  topbar?: ReactNode;
467
509
  topbarLeft?: ReactNode;
468
510
  topbarRight?: ReactNode;
@@ -1149,6 +1191,9 @@ export type SidebarItemProp = {
1149
1191
  * Nested rows — renders a collapsible submenu group (the parent reads active when any child is).
1150
1192
  * Honoured by BOTH `Sidebar` and `NavList` (gh#815): `NavList` used to map `items` straight to a
1151
1193
  * row, so a nested group type-checked, rendered as one flat row and lost its children silently.
1194
+ * A child may carry `children` of its own (gh#1493): the group nests one level further, so a
1195
+ * section can hold work area → sub-area → page (three clickable levels), and the collapsed-rail
1196
+ * flyout lists the grandchildren under a labelled group.
1152
1197
  */
1153
1198
  children?: SidebarItemProp[];
1154
1199
  };