@arcfusionz/arc-primitive-ui 0.2.1 → 0.2.3
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/dist/assets/brand/arcfusion-logo.svg +44 -0
- package/dist/assets/images.d.ts +9 -0
- package/dist/assets/system-states/access-denied.png +0 -0
- package/dist/assets/system-states/completed.png +0 -0
- package/dist/assets/system-states/empty.png +0 -0
- package/dist/assets/system-states/feature-locked.png +0 -0
- package/dist/assets/system-states/first-use.png +0 -0
- package/dist/assets/system-states/no-results.png +0 -0
- package/dist/assets/system-states/setup-required.png +0 -0
- package/dist/assets/system-states/sign-in-required.png +0 -0
- package/dist/components/AILoader/AILoader.js +2 -2
- package/dist/components/Avatar/Avatar.js +4 -4
- package/dist/components/Badge/Badge.js +2 -2
- package/dist/components/Button/Button.js +3 -3
- package/dist/components/Calendar/Calendar.js +2 -2
- package/dist/components/Field/Field.js +2 -2
- package/dist/components/Shell/Shell.d.ts +232 -0
- package/dist/components/Shell/Shell.js +639 -0
- package/dist/components/Shell/index.d.ts +2 -0
- package/dist/components/Shell/index.js +2 -0
- package/dist/components/Stepper/Stepper.js +2 -2
- package/dist/components/Switch/Switch.js +2 -2
- package/dist/components/SystemState/SystemState.d.ts +70 -0
- package/dist/components/SystemState/SystemState.js +186 -0
- package/dist/components/SystemState/index.d.ts +2 -0
- package/dist/components/SystemState/index.js +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { AILoader } from "../AILoader/AILoader.js";
|
|
3
|
+
import { Badge } from "../Badge/Badge.js";
|
|
4
|
+
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, breadcrumbLinkVariants } from "../Breadcrumb/Breadcrumb.js";
|
|
5
|
+
import { Button } from "../Button/Button.js";
|
|
6
|
+
import { Menu, MenuItem, MenuLinkItem, MenuPopup, MenuTrigger } from "../Menu/Menu.js";
|
|
7
|
+
import { Tabs, TabsList, TabsTab } from "../Tabs/Tabs.js";
|
|
8
|
+
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../Tooltip/Tooltip.js";
|
|
9
|
+
import { Fragment, createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import arcfusionLogo from "../../assets/brand/arcfusion-logo.svg";
|
|
12
|
+
//#region src/components/Shell/Shell.tsx
|
|
13
|
+
function LayoutGridIcon(props) {
|
|
14
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
15
|
+
viewBox: "0 0 24 24",
|
|
16
|
+
fill: "none",
|
|
17
|
+
stroke: "currentColor",
|
|
18
|
+
strokeWidth: 2,
|
|
19
|
+
strokeLinecap: "round",
|
|
20
|
+
strokeLinejoin: "round",
|
|
21
|
+
"aria-hidden": "true",
|
|
22
|
+
...props,
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx("rect", {
|
|
25
|
+
width: "7",
|
|
26
|
+
height: "7",
|
|
27
|
+
x: "3",
|
|
28
|
+
y: "3",
|
|
29
|
+
rx: "1"
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx("rect", {
|
|
32
|
+
width: "7",
|
|
33
|
+
height: "7",
|
|
34
|
+
x: "14",
|
|
35
|
+
y: "3",
|
|
36
|
+
rx: "1"
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ jsx("rect", {
|
|
39
|
+
width: "7",
|
|
40
|
+
height: "7",
|
|
41
|
+
x: "14",
|
|
42
|
+
y: "14",
|
|
43
|
+
rx: "1"
|
|
44
|
+
}),
|
|
45
|
+
/* @__PURE__ */ jsx("rect", {
|
|
46
|
+
width: "7",
|
|
47
|
+
height: "7",
|
|
48
|
+
x: "3",
|
|
49
|
+
y: "14",
|
|
50
|
+
rx: "1"
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function XIcon(props) {
|
|
56
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
57
|
+
viewBox: "0 0 24 24",
|
|
58
|
+
fill: "none",
|
|
59
|
+
stroke: "currentColor",
|
|
60
|
+
strokeWidth: 2,
|
|
61
|
+
strokeLinecap: "round",
|
|
62
|
+
strokeLinejoin: "round",
|
|
63
|
+
"aria-hidden": "true",
|
|
64
|
+
...props,
|
|
65
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Compact count for badges: everything past 99 reads `99+`. */
|
|
69
|
+
function formatBadgeCount(count) {
|
|
70
|
+
return count > 99 ? "99+" : String(count);
|
|
71
|
+
}
|
|
72
|
+
function useControllableState(controlled, defaultValue, onChange) {
|
|
73
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
74
|
+
const isControlled = controlled !== void 0;
|
|
75
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
76
|
+
const onChangeRef = useRef(onChange);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
onChangeRef.current = onChange;
|
|
79
|
+
});
|
|
80
|
+
const valueRef = useRef(value);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
valueRef.current = value;
|
|
83
|
+
});
|
|
84
|
+
return [value, useCallback((next) => {
|
|
85
|
+
if (next === valueRef.current) return;
|
|
86
|
+
valueRef.current = next;
|
|
87
|
+
if (!isControlled) setUncontrolled(next);
|
|
88
|
+
onChangeRef.current?.(next);
|
|
89
|
+
}, [isControlled])];
|
|
90
|
+
}
|
|
91
|
+
function assignRef(ref, node) {
|
|
92
|
+
if (typeof ref === "function") ref(node);
|
|
93
|
+
else if (ref != null) ref.current = node;
|
|
94
|
+
}
|
|
95
|
+
function isKeyboardFocus(element) {
|
|
96
|
+
try {
|
|
97
|
+
return element.matches(":focus-visible");
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const ShellContext = createContext({
|
|
103
|
+
rightPanelOpen: false,
|
|
104
|
+
setRightPanelOpen: () => {},
|
|
105
|
+
rightPanelId: "",
|
|
106
|
+
rightPanelTriggerRef: { current: null },
|
|
107
|
+
focusTriggerOnMountRef: { current: false }
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Shared Shell UI state for custom triggers — e.g. an "Ask AI" entry point
|
|
111
|
+
* inside page content that opens the same right panel as the Topbar button.
|
|
112
|
+
* Outside a `Shell` it returns inert state (closed, no-op setter).
|
|
113
|
+
*/
|
|
114
|
+
function useShell() {
|
|
115
|
+
const { rightPanelOpen, setRightPanelOpen } = useContext(ShellContext);
|
|
116
|
+
return useMemo(() => ({
|
|
117
|
+
rightPanelOpen,
|
|
118
|
+
setRightPanelOpen
|
|
119
|
+
}), [rightPanelOpen, setRightPanelOpen]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Application frame that positions the sidebar rail, topbar, optional
|
|
123
|
+
* tabbar, main content, and the optional right panel, and owns the shared
|
|
124
|
+
* panel state. It fills the viewport (`h-dvh`) by default — pass
|
|
125
|
+
* `className="h-full"` to fill a parent instead — and creates no
|
|
126
|
+
* document-level overflow: `ShellMain` is the primary scroll region. The
|
|
127
|
+
* collapsed rail width stays reserved while sidebar expansion overlays the
|
|
128
|
+
* layout, so page content never shifts. Popups portal to `document.body`;
|
|
129
|
+
* the root is isolated, but consumer apps should still set
|
|
130
|
+
* `isolation: isolate` on their app root so no page stacking context
|
|
131
|
+
* outbids them.
|
|
132
|
+
*/
|
|
133
|
+
const Shell = forwardRef(({ sidebar, topbar, tabbar, rightPanel, children, rightPanelOpen: rightPanelOpenProp, defaultRightPanelOpen = false, onRightPanelOpenChange, className, ...rest }, ref) => {
|
|
134
|
+
const [rightPanelOpen, setRightPanelOpen] = useControllableState(rightPanelOpenProp, defaultRightPanelOpen, onRightPanelOpenChange);
|
|
135
|
+
const rightPanelId = useId();
|
|
136
|
+
const rightPanelTriggerRef = useRef(null);
|
|
137
|
+
const focusTriggerOnMountRef = useRef(false);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (rightPanelOpen) focusTriggerOnMountRef.current = false;
|
|
140
|
+
}, [rightPanelOpen]);
|
|
141
|
+
const context = useMemo(() => ({
|
|
142
|
+
rightPanelOpen,
|
|
143
|
+
setRightPanelOpen,
|
|
144
|
+
rightPanelId,
|
|
145
|
+
rightPanelTriggerRef,
|
|
146
|
+
focusTriggerOnMountRef
|
|
147
|
+
}), [
|
|
148
|
+
rightPanelOpen,
|
|
149
|
+
setRightPanelOpen,
|
|
150
|
+
rightPanelId
|
|
151
|
+
]);
|
|
152
|
+
return /* @__PURE__ */ jsx(ShellContext.Provider, {
|
|
153
|
+
value: context,
|
|
154
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
155
|
+
ref,
|
|
156
|
+
className: cn("relative isolate flex h-dvh w-full overflow-hidden bg-background font-sans text-foreground", className),
|
|
157
|
+
...rest,
|
|
158
|
+
children: [
|
|
159
|
+
sidebar != null && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
160
|
+
"aria-hidden": "true",
|
|
161
|
+
className: "w-16 shrink-0"
|
|
162
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
163
|
+
className: "absolute inset-y-0 left-0 z-40 flex",
|
|
164
|
+
children: sidebar
|
|
165
|
+
})] }),
|
|
166
|
+
/* @__PURE__ */ jsxs("div", {
|
|
167
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
168
|
+
children: [
|
|
169
|
+
topbar,
|
|
170
|
+
tabbar,
|
|
171
|
+
/* @__PURE__ */ jsx("div", {
|
|
172
|
+
className: "flex min-h-0 flex-1",
|
|
173
|
+
children
|
|
174
|
+
})
|
|
175
|
+
]
|
|
176
|
+
}),
|
|
177
|
+
rightPanel != null && rightPanelOpen && /* @__PURE__ */ jsx("div", {
|
|
178
|
+
className: "absolute inset-y-0 right-0 z-30 flex max-w-full lg:static lg:z-auto",
|
|
179
|
+
children: rightPanel
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
Shell.displayName = "Shell";
|
|
186
|
+
const sidebarRootClasses = "group/sidebar relative flex h-full w-16 flex-col overflow-hidden bg-background shadow-[6px_0_24px_-4px_color-mix(in_oklab,var(--color-slate-400)_25%,transparent)] data-expanded:w-64 data-expanded:shadow-[12px_0_48px_-8px_color-mix(in_oklab,var(--color-slate-400)_25%,transparent)] motion-safe:transition-[width,box-shadow] motion-safe:duration-200 motion-safe:ease-out";
|
|
187
|
+
const showWhenExpandedClasses = "opacity-0 motion-safe:transition-opacity motion-safe:duration-200 group-data-expanded/sidebar:opacity-100";
|
|
188
|
+
const showWhenCollapsedClasses = "motion-safe:transition-opacity motion-safe:duration-200 group-data-expanded/sidebar:opacity-0";
|
|
189
|
+
const itemRowClasses = "relative flex h-10 w-full items-center gap-3 overflow-hidden rounded-md font-sans text-sm font-medium text-slate-600 transition-colors duration-150 hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&_svg]:pointer-events-none";
|
|
190
|
+
const itemActiveClasses = "bg-primary-50 text-primary hover:bg-primary-100 hover:text-primary";
|
|
191
|
+
const itemDisabledClasses = "pointer-events-none opacity-50";
|
|
192
|
+
function SidebarItemRow({ item, active, expanded }) {
|
|
193
|
+
const className = cn(itemRowClasses, active && itemActiveClasses, item.disabled && itemDisabledClasses);
|
|
194
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
195
|
+
/* @__PURE__ */ jsx("span", {
|
|
196
|
+
className: "flex size-10 shrink-0 items-center justify-center [&_svg:not([class*='size-'])]:size-5",
|
|
197
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
198
|
+
"aria-hidden": "true",
|
|
199
|
+
className: "contents",
|
|
200
|
+
children: item.icon
|
|
201
|
+
})
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ jsx("span", {
|
|
204
|
+
className: cn("min-w-0 flex-1 truncate", showWhenExpandedClasses),
|
|
205
|
+
children: item.name
|
|
206
|
+
}),
|
|
207
|
+
item.badge && /* @__PURE__ */ jsx(Badge, {
|
|
208
|
+
size: "sm",
|
|
209
|
+
pill: true,
|
|
210
|
+
variant: item.badge.tone ?? "primary",
|
|
211
|
+
appearance: "soft",
|
|
212
|
+
"aria-hidden": "true",
|
|
213
|
+
className: cn("mr-2", showWhenExpandedClasses),
|
|
214
|
+
children: formatBadgeCount(item.badge.count)
|
|
215
|
+
}),
|
|
216
|
+
item.badge && /* @__PURE__ */ jsx(Badge, {
|
|
217
|
+
dot: true,
|
|
218
|
+
variant: item.badge.tone ?? "primary",
|
|
219
|
+
"aria-label": item.badge.ariaLabel ?? formatBadgeCount(item.badge.count),
|
|
220
|
+
className: cn("absolute top-1 left-6.5", showWhenCollapsedClasses)
|
|
221
|
+
})
|
|
222
|
+
] });
|
|
223
|
+
const ariaCurrent = active ? "page" : void 0;
|
|
224
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Tooltip, {
|
|
225
|
+
disabled: expanded,
|
|
226
|
+
children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
227
|
+
render: item.href != null && !item.disabled ? /* @__PURE__ */ jsx("a", {
|
|
228
|
+
href: item.href,
|
|
229
|
+
"aria-current": ariaCurrent,
|
|
230
|
+
className
|
|
231
|
+
}) : item.href != null ? /* @__PURE__ */ jsx("a", {
|
|
232
|
+
"aria-disabled": "true",
|
|
233
|
+
"aria-current": ariaCurrent,
|
|
234
|
+
className
|
|
235
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
236
|
+
type: "button",
|
|
237
|
+
onClick: item.action,
|
|
238
|
+
disabled: item.disabled,
|
|
239
|
+
"aria-current": ariaCurrent,
|
|
240
|
+
className: cn(className, "cursor-pointer")
|
|
241
|
+
}),
|
|
242
|
+
children: content
|
|
243
|
+
}), /* @__PURE__ */ jsx(TooltipPopup, {
|
|
244
|
+
side: "right",
|
|
245
|
+
children: item.name
|
|
246
|
+
})]
|
|
247
|
+
}) });
|
|
248
|
+
}
|
|
249
|
+
function SidebarGroupSection({ group, activeItemId, expanded, isFirst }) {
|
|
250
|
+
const titleId = useId();
|
|
251
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
252
|
+
className: "mt-2 first:mt-0",
|
|
253
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
254
|
+
className: "relative flex h-8 w-full items-center overflow-hidden",
|
|
255
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
256
|
+
id: titleId,
|
|
257
|
+
className: cn("px-2 font-sans text-sm font-light capitalize whitespace-nowrap text-muted-foreground", showWhenExpandedClasses),
|
|
258
|
+
children: group.groupTitle
|
|
259
|
+
}), !isFirst && /* @__PURE__ */ jsx("span", {
|
|
260
|
+
"aria-hidden": "true",
|
|
261
|
+
className: cn("absolute inset-x-1 top-1/2 h-px -translate-y-1/2 bg-border", showWhenCollapsedClasses)
|
|
262
|
+
})]
|
|
263
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
264
|
+
"aria-labelledby": titleId,
|
|
265
|
+
className: "flex flex-col gap-1",
|
|
266
|
+
children: group.pages.map((item) => /* @__PURE__ */ jsx(SidebarItemRow, {
|
|
267
|
+
item,
|
|
268
|
+
active: item.id === activeItemId,
|
|
269
|
+
expanded
|
|
270
|
+
}, item.id))
|
|
271
|
+
})]
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Collapsible navigation rail. Collapsed it shows the brand mark, item
|
|
276
|
+
* icons, on-icon badge dots, and group dividers; hovering it for
|
|
277
|
+
* `expandDelay` (or focusing it with the keyboard, immediately) expands it
|
|
278
|
+
* over the layout — never pushing it — revealing the brand name, group
|
|
279
|
+
* titles, labels, and count badges in the same geometry, so nothing jumps.
|
|
280
|
+
* It collapses `collapseDelay` after pointer and focus have both left,
|
|
281
|
+
* unless it is pinned (the brand button, which is also the touch toggle),
|
|
282
|
+
* controlled expanded, or a popup inside it is open. Items are real links
|
|
283
|
+
* (`href`) or buttons (`action`) inside a labelled `nav`; the active item
|
|
284
|
+
* carries `aria-current="page"`, and collapsed items show their name in a
|
|
285
|
+
* tooltip.
|
|
286
|
+
*/
|
|
287
|
+
const ShellSidebar = forwardRef(({ brandLogo, brandName = "ArcFusion", groups, activeItemId, expanded: expandedProp, defaultExpanded = false, onExpandedChange, expandDelay = 175, collapseDelay = 175, navLabel = "Main", toggleLabel = "Toggle navigation", onPointerEnter, onPointerLeave, onFocus, onBlur, className, ...rest }, ref) => {
|
|
288
|
+
const [expanded, setExpanded] = useControllableState(expandedProp, defaultExpanded, onExpandedChange);
|
|
289
|
+
const rootRef = useRef(null);
|
|
290
|
+
const navId = useId();
|
|
291
|
+
const expandedRef = useRef(expanded);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
expandedRef.current = expanded;
|
|
294
|
+
});
|
|
295
|
+
const pinnedRef = useRef(false);
|
|
296
|
+
const pointerInsideRef = useRef(false);
|
|
297
|
+
const focusInsideRef = useRef(false);
|
|
298
|
+
const expandTimer = useRef(void 0);
|
|
299
|
+
const collapseTimer = useRef(void 0);
|
|
300
|
+
const cancelExpandTimer = useCallback(() => {
|
|
301
|
+
window.clearTimeout(expandTimer.current);
|
|
302
|
+
expandTimer.current = void 0;
|
|
303
|
+
}, []);
|
|
304
|
+
const cancelCollapseTimer = useCallback(() => {
|
|
305
|
+
window.clearTimeout(collapseTimer.current);
|
|
306
|
+
collapseTimer.current = void 0;
|
|
307
|
+
}, []);
|
|
308
|
+
useEffect(() => () => {
|
|
309
|
+
window.clearTimeout(expandTimer.current);
|
|
310
|
+
window.clearTimeout(collapseTimer.current);
|
|
311
|
+
}, []);
|
|
312
|
+
const scheduleExpand = useCallback(() => {
|
|
313
|
+
cancelCollapseTimer();
|
|
314
|
+
if (expandedRef.current || expandTimer.current !== void 0) return;
|
|
315
|
+
expandTimer.current = window.setTimeout(() => {
|
|
316
|
+
expandTimer.current = void 0;
|
|
317
|
+
setExpanded(true);
|
|
318
|
+
}, expandDelay);
|
|
319
|
+
}, [
|
|
320
|
+
cancelCollapseTimer,
|
|
321
|
+
expandDelay,
|
|
322
|
+
setExpanded
|
|
323
|
+
]);
|
|
324
|
+
const scheduleCollapse = useCallback(() => {
|
|
325
|
+
cancelExpandTimer();
|
|
326
|
+
if (!expandedRef.current || pinnedRef.current) return;
|
|
327
|
+
const attempt = () => {
|
|
328
|
+
collapseTimer.current = void 0;
|
|
329
|
+
if (!expandedRef.current || pinnedRef.current) return;
|
|
330
|
+
if (pointerInsideRef.current || focusInsideRef.current) return;
|
|
331
|
+
if (rootRef.current?.querySelector("[data-popup-open]")) {
|
|
332
|
+
collapseTimer.current = window.setTimeout(attempt, collapseDelay);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
setExpanded(false);
|
|
336
|
+
};
|
|
337
|
+
window.clearTimeout(collapseTimer.current);
|
|
338
|
+
collapseTimer.current = window.setTimeout(attempt, collapseDelay);
|
|
339
|
+
}, [
|
|
340
|
+
cancelExpandTimer,
|
|
341
|
+
collapseDelay,
|
|
342
|
+
setExpanded
|
|
343
|
+
]);
|
|
344
|
+
const handlePointerEnter = (event) => {
|
|
345
|
+
onPointerEnter?.(event);
|
|
346
|
+
if (event.pointerType !== "touch") {
|
|
347
|
+
pointerInsideRef.current = true;
|
|
348
|
+
cancelCollapseTimer();
|
|
349
|
+
if (!expanded) scheduleExpand();
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const handlePointerLeave = (event) => {
|
|
353
|
+
onPointerLeave?.(event);
|
|
354
|
+
if (event.pointerType !== "touch") {
|
|
355
|
+
pointerInsideRef.current = false;
|
|
356
|
+
cancelExpandTimer();
|
|
357
|
+
if (!focusInsideRef.current) scheduleCollapse();
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const handleFocus = (event) => {
|
|
361
|
+
onFocus?.(event);
|
|
362
|
+
focusInsideRef.current = true;
|
|
363
|
+
cancelCollapseTimer();
|
|
364
|
+
if (!expanded && isKeyboardFocus(event.target)) {
|
|
365
|
+
cancelExpandTimer();
|
|
366
|
+
setExpanded(true);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const handleBlur = (event) => {
|
|
370
|
+
onBlur?.(event);
|
|
371
|
+
if (rootRef.current?.contains(event.relatedTarget)) return;
|
|
372
|
+
focusInsideRef.current = false;
|
|
373
|
+
if (!pointerInsideRef.current) scheduleCollapse();
|
|
374
|
+
};
|
|
375
|
+
const handleToggle = () => {
|
|
376
|
+
cancelExpandTimer();
|
|
377
|
+
cancelCollapseTimer();
|
|
378
|
+
if (expanded) {
|
|
379
|
+
pinnedRef.current = false;
|
|
380
|
+
setExpanded(false);
|
|
381
|
+
} else {
|
|
382
|
+
pinnedRef.current = true;
|
|
383
|
+
setExpanded(true);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
387
|
+
ref: (node) => {
|
|
388
|
+
rootRef.current = node;
|
|
389
|
+
assignRef(ref, node);
|
|
390
|
+
},
|
|
391
|
+
"data-expanded": expanded ? "" : void 0,
|
|
392
|
+
onPointerEnter: handlePointerEnter,
|
|
393
|
+
onPointerLeave: handlePointerLeave,
|
|
394
|
+
onFocus: handleFocus,
|
|
395
|
+
onBlur: handleBlur,
|
|
396
|
+
className: cn(sidebarRootClasses, className),
|
|
397
|
+
...rest,
|
|
398
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
399
|
+
className: "flex h-16 w-full shrink-0 items-center gap-3 overflow-hidden px-3",
|
|
400
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
401
|
+
type: "button",
|
|
402
|
+
"aria-expanded": expanded,
|
|
403
|
+
"aria-controls": navId,
|
|
404
|
+
"aria-label": toggleLabel,
|
|
405
|
+
onClick: handleToggle,
|
|
406
|
+
className: cn("flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors duration-150", "hover:bg-muted active:bg-slate-200", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", "[&_img]:pointer-events-none [&_svg]:pointer-events-none"),
|
|
407
|
+
children: brandLogo ?? /* @__PURE__ */ jsx("img", {
|
|
408
|
+
src: arcfusionLogo,
|
|
409
|
+
alt: "",
|
|
410
|
+
className: "size-8"
|
|
411
|
+
})
|
|
412
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
413
|
+
className: cn("min-w-0 truncate font-heading text-lg font-semibold text-foreground", showWhenExpandedClasses),
|
|
414
|
+
children: brandName
|
|
415
|
+
})]
|
|
416
|
+
}), /* @__PURE__ */ jsx(TooltipProvider, {
|
|
417
|
+
delay: 300,
|
|
418
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
419
|
+
id: navId,
|
|
420
|
+
"aria-label": navLabel,
|
|
421
|
+
className: "min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-3 pb-3",
|
|
422
|
+
children: groups.map((group, index) => /* @__PURE__ */ jsx(SidebarGroupSection, {
|
|
423
|
+
group,
|
|
424
|
+
activeItemId,
|
|
425
|
+
expanded,
|
|
426
|
+
isFirst: index === 0
|
|
427
|
+
}, group.id))
|
|
428
|
+
})
|
|
429
|
+
})]
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
ShellSidebar.displayName = "ShellSidebar";
|
|
433
|
+
/**
|
|
434
|
+
* Page header row: a title (or breadcrumb trail) on the left; page-owned
|
|
435
|
+
* actions, the optional Ask AI toggle, and the global application menu on
|
|
436
|
+
* the right. The Ask AI button only flips the Shell's panel state
|
|
437
|
+
* (`aria-expanded` reflects it) — the panel itself belongs to Shell layout.
|
|
438
|
+
* The global menu renders through the Menu primitive, so arrow keys,
|
|
439
|
+
* typeahead, and Escape come built in.
|
|
440
|
+
*/
|
|
441
|
+
const ShellTopbar = forwardRef(({ title, breadcrumbs, pageActions, showAskAi = false, askAiLabel = "Ask AI", globalMenuItems, globalMenuLabel = "Open application menu", className, ...rest }, ref) => {
|
|
442
|
+
const { rightPanelOpen, setRightPanelOpen, rightPanelId, rightPanelTriggerRef, focusTriggerOnMountRef } = useContext(ShellContext);
|
|
443
|
+
const askAiRef = useCallback((node) => {
|
|
444
|
+
rightPanelTriggerRef.current = node;
|
|
445
|
+
if (node != null && focusTriggerOnMountRef.current) {
|
|
446
|
+
focusTriggerOnMountRef.current = false;
|
|
447
|
+
node.focus();
|
|
448
|
+
}
|
|
449
|
+
}, [rightPanelTriggerRef, focusTriggerOnMountRef]);
|
|
450
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
451
|
+
ref,
|
|
452
|
+
className: cn("flex pt-4 shrink-0 items-center gap-3 bg-background px-6", className),
|
|
453
|
+
...rest,
|
|
454
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
455
|
+
className: "min-w-0 flex-1",
|
|
456
|
+
children: breadcrumbs != null && breadcrumbs.length > 0 ? /* @__PURE__ */ jsx(Breadcrumb, { children: /* @__PURE__ */ jsx(BreadcrumbList, {
|
|
457
|
+
className: "flex-nowrap",
|
|
458
|
+
children: breadcrumbs.map((crumb, index) => {
|
|
459
|
+
const isLast = index === breadcrumbs.length - 1;
|
|
460
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 && /* @__PURE__ */ jsx(BreadcrumbSeparator, {}), /* @__PURE__ */ jsx(BreadcrumbItem, {
|
|
461
|
+
className: "min-w-0",
|
|
462
|
+
children: isLast ? crumb.href != null ? /* @__PURE__ */ jsx(BreadcrumbLink, {
|
|
463
|
+
href: crumb.href,
|
|
464
|
+
"aria-current": "page",
|
|
465
|
+
className: "block truncate",
|
|
466
|
+
children: crumb.label
|
|
467
|
+
}) : /* @__PURE__ */ jsx(BreadcrumbPage, {
|
|
468
|
+
className: "block truncate",
|
|
469
|
+
children: crumb.label
|
|
470
|
+
}) : crumb.href != null ? /* @__PURE__ */ jsx(BreadcrumbLink, {
|
|
471
|
+
href: crumb.href,
|
|
472
|
+
className: "block truncate",
|
|
473
|
+
children: crumb.label
|
|
474
|
+
}) : crumb.action != null ? /* @__PURE__ */ jsx("button", {
|
|
475
|
+
type: "button",
|
|
476
|
+
onClick: crumb.action,
|
|
477
|
+
className: breadcrumbLinkVariants({ className: "block cursor-pointer truncate" }),
|
|
478
|
+
children: crumb.label
|
|
479
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
480
|
+
className: "block truncate",
|
|
481
|
+
children: crumb.label
|
|
482
|
+
})
|
|
483
|
+
})] }, crumb.id);
|
|
484
|
+
})
|
|
485
|
+
}) }) : title != null ? /* @__PURE__ */ jsx("h1", {
|
|
486
|
+
className: "truncate font-heading text-xl font-semibold text-foreground",
|
|
487
|
+
children: title
|
|
488
|
+
}) : null
|
|
489
|
+
}), (pageActions != null || showAskAi || (globalMenuItems?.length ?? 0) > 0) && /* @__PURE__ */ jsxs("div", {
|
|
490
|
+
className: "flex shrink-0 items-center gap-2",
|
|
491
|
+
children: [
|
|
492
|
+
pageActions,
|
|
493
|
+
(globalMenuItems?.length ?? 0) > 0 && /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
|
|
494
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
495
|
+
variant: "outline",
|
|
496
|
+
size: "sm",
|
|
497
|
+
iconOnly: true,
|
|
498
|
+
"aria-label": globalMenuLabel
|
|
499
|
+
}),
|
|
500
|
+
children: /* @__PURE__ */ jsx(LayoutGridIcon, {})
|
|
501
|
+
}), /* @__PURE__ */ jsx(MenuPopup, {
|
|
502
|
+
align: "end",
|
|
503
|
+
className: "min-w-48",
|
|
504
|
+
children: globalMenuItems?.map((item) => item.href != null && !item.disabled ? /* @__PURE__ */ jsxs(MenuLinkItem, {
|
|
505
|
+
href: item.href,
|
|
506
|
+
children: [item.menuIcon != null && /* @__PURE__ */ jsx("span", {
|
|
507
|
+
"aria-hidden": "true",
|
|
508
|
+
className: "contents",
|
|
509
|
+
children: item.menuIcon
|
|
510
|
+
}), item.menuName]
|
|
511
|
+
}, item.id) : /* @__PURE__ */ jsxs(MenuItem, {
|
|
512
|
+
onClick: item.action,
|
|
513
|
+
disabled: item.disabled,
|
|
514
|
+
children: [item.menuIcon != null && /* @__PURE__ */ jsx("span", {
|
|
515
|
+
"aria-hidden": "true",
|
|
516
|
+
className: "contents",
|
|
517
|
+
children: item.menuIcon
|
|
518
|
+
}), item.menuName]
|
|
519
|
+
}, item.id))
|
|
520
|
+
})] }),
|
|
521
|
+
showAskAi && !rightPanelOpen && /* @__PURE__ */ jsx(Button, {
|
|
522
|
+
ref: askAiRef,
|
|
523
|
+
variant: "outline",
|
|
524
|
+
size: "sm",
|
|
525
|
+
iconOnly: true,
|
|
526
|
+
"aria-label": askAiLabel,
|
|
527
|
+
"aria-expanded": false,
|
|
528
|
+
"aria-controls": rightPanelId !== "" ? rightPanelId : void 0,
|
|
529
|
+
onClick: () => setRightPanelOpen(true),
|
|
530
|
+
children: /* @__PURE__ */ jsx(AILoader, {
|
|
531
|
+
size: "sm",
|
|
532
|
+
tone: "current"
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
]
|
|
536
|
+
})]
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
ShellTopbar.displayName = "ShellTopbar";
|
|
540
|
+
/**
|
|
541
|
+
* Optional row under the Topbar: page-view tabs on the left (the Tabs
|
|
542
|
+
* primitive, so arrow-key navigation comes built in) and page-owned filter
|
|
543
|
+
* controls on the right. It only emits `onTabChange` — it never touches page
|
|
544
|
+
* data. When the tabs and filters can't share a row, the filters drop to
|
|
545
|
+
* their own row below and stay right-aligned; if the tabs alone still
|
|
546
|
+
* overflow, they scroll horizontally.
|
|
547
|
+
*/
|
|
548
|
+
const ShellTabbar = forwardRef(({ tabs, activeTabId, onTabChange, filters, className, ...rest }, ref) => /* @__PURE__ */ jsxs("div", {
|
|
549
|
+
ref,
|
|
550
|
+
className: cn("flex min-h-12 shrink-0 flex-wrap items-end gap-x-4 gap-y-1 bg-background px-6", className),
|
|
551
|
+
...rest,
|
|
552
|
+
children: [/* @__PURE__ */ jsx(Tabs, {
|
|
553
|
+
value: activeTabId,
|
|
554
|
+
onValueChange: (value) => {
|
|
555
|
+
if (typeof value === "string") onTabChange(value);
|
|
556
|
+
},
|
|
557
|
+
showDivider: false,
|
|
558
|
+
className: "min-w-0",
|
|
559
|
+
children: /* @__PURE__ */ jsx(TabsList, {
|
|
560
|
+
className: "overflow-x-auto",
|
|
561
|
+
children: tabs.map((tab) => /* @__PURE__ */ jsxs(TabsTab, {
|
|
562
|
+
value: tab.id,
|
|
563
|
+
disabled: tab.disabled,
|
|
564
|
+
children: [tab.label, tab.badge != null && /* @__PURE__ */ jsx(Badge, {
|
|
565
|
+
size: "sm",
|
|
566
|
+
pill: true,
|
|
567
|
+
children: formatBadgeCount(tab.badge)
|
|
568
|
+
})]
|
|
569
|
+
}, tab.id))
|
|
570
|
+
})
|
|
571
|
+
}), filters != null && /* @__PURE__ */ jsx("div", {
|
|
572
|
+
className: "ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2 self-center py-2",
|
|
573
|
+
children: filters
|
|
574
|
+
})]
|
|
575
|
+
}));
|
|
576
|
+
ShellTabbar.displayName = "ShellTabbar";
|
|
577
|
+
/**
|
|
578
|
+
* The page content region and the Shell's primary scroll area — a `main`
|
|
579
|
+
* landmark that fills the remaining space. Content is free-form; it carries
|
|
580
|
+
* no page logic.
|
|
581
|
+
*/
|
|
582
|
+
const ShellMain = forwardRef(({ padded = true, className, ...rest }, ref) => /* @__PURE__ */ jsx("main", {
|
|
583
|
+
ref,
|
|
584
|
+
className: cn("min-w-0 flex-1 overflow-y-auto bg-background", padded && "p-6", className),
|
|
585
|
+
...rest
|
|
586
|
+
}));
|
|
587
|
+
ShellMain.displayName = "ShellMain";
|
|
588
|
+
/**
|
|
589
|
+
* Right-side panel chrome — the Shell mounts it while the panel is open,
|
|
590
|
+
* docked at full Shell height beside the topbar/tabbar/main column on `lg`+
|
|
591
|
+
* viewports (the column narrows) and overlaying it below. A labelled
|
|
592
|
+
* `aside` with its own scroll area and a close control;
|
|
593
|
+
* Escape from inside also closes it, returning focus to the Ask AI trigger.
|
|
594
|
+
* It is not modal and traps no focus. Content is a free slot — Ask AI
|
|
595
|
+
* functionality lives with the app, not here.
|
|
596
|
+
*/
|
|
597
|
+
const ShellRightPanel = forwardRef(({ title, closeLabel = "Close panel", onKeyDown, className, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }, ref) => {
|
|
598
|
+
const { setRightPanelOpen, rightPanelId, rightPanelTriggerRef, focusTriggerOnMountRef } = useContext(ShellContext);
|
|
599
|
+
const titleId = useId();
|
|
600
|
+
const close = () => {
|
|
601
|
+
if (rightPanelTriggerRef.current == null) focusTriggerOnMountRef.current = true;
|
|
602
|
+
setRightPanelOpen(false);
|
|
603
|
+
rightPanelTriggerRef.current?.focus();
|
|
604
|
+
};
|
|
605
|
+
const labelledBy = ariaLabelledBy ?? (title != null ? titleId : void 0);
|
|
606
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
607
|
+
ref,
|
|
608
|
+
id: rightPanelId === "" ? void 0 : rightPanelId,
|
|
609
|
+
"aria-label": ariaLabel ?? (labelledBy == null ? "Ask AI" : void 0),
|
|
610
|
+
"aria-labelledby": labelledBy,
|
|
611
|
+
onKeyDown: (event) => {
|
|
612
|
+
onKeyDown?.(event);
|
|
613
|
+
if (event.key === "Escape" && !event.defaultPrevented) close();
|
|
614
|
+
},
|
|
615
|
+
className: cn("flex h-full w-90 max-w-full flex-col bg-background shadow-[-6px_0_24px_-4px_color-mix(in_oklab,var(--color-slate-400)_25%,transparent)]", className),
|
|
616
|
+
...rest,
|
|
617
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
618
|
+
className: "flex h-14 shrink-0 items-center justify-between gap-2 border-b border-border py-2 pr-3 pl-4",
|
|
619
|
+
children: [title != null ? /* @__PURE__ */ jsx("h2", {
|
|
620
|
+
id: titleId,
|
|
621
|
+
className: "min-w-0 truncate font-heading text-base font-semibold text-foreground",
|
|
622
|
+
children: title
|
|
623
|
+
}) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }), /* @__PURE__ */ jsx(Button, {
|
|
624
|
+
variant: "ghost",
|
|
625
|
+
size: "sm",
|
|
626
|
+
iconOnly: true,
|
|
627
|
+
"aria-label": closeLabel,
|
|
628
|
+
onClick: close,
|
|
629
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
630
|
+
})]
|
|
631
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
632
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
633
|
+
children
|
|
634
|
+
})]
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
ShellRightPanel.displayName = "ShellRightPanel";
|
|
638
|
+
//#endregion
|
|
639
|
+
export { Shell, ShellMain, ShellRightPanel, ShellSidebar, ShellTabbar, ShellTopbar, useShell };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { Shell, ShellBreadcrumbItem, ShellGlobalMenuItem, ShellMain, ShellMainProps, ShellNavTarget, ShellProps, ShellRightPanel, ShellRightPanelProps, ShellSidebar, ShellSidebarProps, ShellTabItem, ShellTabbar, ShellTabbarProps, ShellTopbar, ShellTopbarProps, SidebarMenuGroup, SidebarMenuItem, SidebarMenuItemBadge, useShell } from "./Shell.js";
|
|
2
|
+
export { Shell, type ShellBreadcrumbItem, type ShellGlobalMenuItem, ShellMain, type ShellMainProps, type ShellNavTarget, type ShellProps, ShellRightPanel, type ShellRightPanelProps, ShellSidebar, type ShellSidebarProps, type ShellTabItem, ShellTabbar, type ShellTabbarProps, ShellTopbar, type ShellTopbarProps, type SidebarMenuGroup, type SidebarMenuItem, type SidebarMenuItemBadge, useShell };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { createContext, forwardRef, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { useRender } from "@base-ui/react/use-render";
|
|
5
5
|
//#region src/components/Stepper/Stepper.tsx
|
|
6
6
|
function CheckIcon(props) {
|
|
@@ -272,7 +272,7 @@ const StepperIndicator = forwardRef(({ children, render, className, ...rest }, r
|
|
|
272
272
|
props: {
|
|
273
273
|
...stepStateAttributes(item, orientation),
|
|
274
274
|
...rest,
|
|
275
|
-
children: /* @__PURE__ */ jsxs(Fragment, { children: [children ?? glyph, statusLabel != null && /* @__PURE__ */ jsx("span", {
|
|
275
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [children ?? glyph, statusLabel != null && /* @__PURE__ */ jsx("span", {
|
|
276
276
|
className: "sr-only",
|
|
277
277
|
children: statusLabel
|
|
278
278
|
})] }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
2
|
import { forwardRef, useId } from "react";
|
|
3
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { Switch } from "@base-ui/react/switch";
|
|
5
5
|
//#region src/components/Switch/Switch.tsx
|
|
6
6
|
const trackClasses = "inline-flex shrink-0 cursor-pointer select-none items-center rounded-full border border-border bg-muted p-px transition-colors duration-150 hover:border-slate-300 hover:bg-slate-200 data-checked:border-primary data-checked:bg-primary hover:data-checked:border-primary-700 hover:data-checked:bg-primary-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-invalid:border-destructive data-invalid:border-destructive aria-invalid:focus-visible:outline-destructive data-invalid:focus-visible:outline-destructive data-readonly:cursor-default disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50";
|
|
@@ -74,7 +74,7 @@ const Switch$1 = forwardRef(({ size = "md", label, description, labelPosition =
|
|
|
74
74
|
className: cn("inline-flex max-w-full flex-col gap-1 font-sans", "has-data-disabled:pointer-events-none has-data-disabled:opacity-50"),
|
|
75
75
|
children: [hasLabel ? /* @__PURE__ */ jsx("label", {
|
|
76
76
|
className: cn("inline-flex cursor-pointer items-start gap-2", "has-data-readonly:cursor-default", labelPosition === "start" && "justify-between"),
|
|
77
|
-
children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment, { children: [labelText, control] }) : /* @__PURE__ */ jsxs(Fragment, { children: [control, labelText] })
|
|
77
|
+
children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [labelText, control] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [control, labelText] })
|
|
78
78
|
}) : control, description != null && /* @__PURE__ */ jsx("span", {
|
|
79
79
|
id: descriptionId,
|
|
80
80
|
className: cn("text-muted-foreground", descriptionTextClasses[size], labelPosition === "end" && descriptionIndentClasses[size]),
|