@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.
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { Shell, ShellMain, ShellRightPanel, ShellSidebar, ShellTabbar, ShellTopbar, useShell } from "./Shell.js";
2
+ export { Shell, ShellMain, ShellRightPanel, ShellSidebar, ShellTabbar, ShellTopbar, 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]),