@godxjp/ui 33.22.2 → 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.
- package/DESIGN.md +1 -1
- package/README.md +4 -4
- package/agent/DESIGN.md +1 -1
- package/agent/START-HERE.md +7 -7
- package/agent/components/AgentBadge.json +43 -0
- package/agent/components/AppShell.json +6 -0
- package/agent/components/Avatar.json +7 -0
- package/agent/components/PageContainer.json +5 -0
- package/agent/components/PageErrorBoundary.json +48 -0
- package/agent/components/Toaster.json +1 -1
- package/agent/components/Toolbar.json +1 -1
- package/agent/components/Topbar.json +1 -1
- package/agent/components-index.json +10 -0
- package/agent/components.json +112 -3
- package/agent/index.json +7 -7
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +84 -0
- package/dist/components/data-display/avatar.d.ts +1 -1
- package/dist/components/data-display/avatar.js +7 -1
- package/dist/components/data-display/badge.d.ts +11 -0
- package/dist/components/data-display/badge.js +20 -0
- package/dist/components/data-display/image.js +1 -1
- package/dist/components/data-display/index.d.ts +2 -2
- package/dist/components/data-display/index.js +2 -1
- package/dist/components/data-entry/record-picker.js +122 -98
- package/dist/components/data-entry/select.js +13 -3
- package/dist/components/layout/app-shell.d.ts +1 -1
- package/dist/components/layout/app-shell.js +3 -1
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +6 -3
- package/dist/components/layout/page-error-boundary.d.ts +32 -0
- package/dist/components/layout/page-error-boundary.js +57 -0
- package/dist/components/layout/sidebar.d.ts +1 -1
- package/dist/components/layout/sidebar.js +100 -49
- package/dist/components/layout/topbar.js +108 -21
- package/dist/components/navigation/toolbar.js +2 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +6 -0
- package/dist/i18n/messages/ja.json +6 -0
- package/dist/i18n/messages/vi.json +6 -0
- package/dist/props/components/data-display.prop.d.ts +27 -0
- package/dist/props/components/layout.prop.d.ts +45 -0
- package/dist/props/components/navigation.prop.d.ts +12 -1
- package/dist/props/registry.d.ts +50 -2
- package/dist/props/registry.js +72 -3
- package/dist/styles/alert-layout.css +4 -0
- package/dist/styles/badge-layout.css +14 -0
- package/dist/styles/data-display-layout.css +44 -0
- package/dist/styles/icon-layout.css +3 -0
- package/dist/styles/layers.json +22 -2
- package/dist/styles/layout.css +11 -0
- package/dist/styles/shell-layout.css +17 -1
- package/dist/styles/table-layout.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/tokens/components/badge.css +3 -0
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/components/shell.css +2 -0
- package/dist/tokens/foundation.css +8 -0
- package/docs/CONSUMER-RULES.md +1 -1
- package/package.json +2 -2
- package/scripts/consumer-rule.md +1 -1
- package/skills/godxjp-ui-navigation/SKILL.md +14 -9
- 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(
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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({
|
|
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
|
|
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
|
|
30
|
-
const
|
|
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
|
-
|
|
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:
|
|
52
|
-
/* @__PURE__ */
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
};
|