@fayz-ai/ui 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/analytics/AnalyticsCard.d.ts.map +1 -1
  2. package/dist/analytics/AnalyticsControls.d.ts.map +1 -1
  3. package/dist/analytics/batch.d.ts +19 -0
  4. package/dist/analytics/batch.d.ts.map +1 -0
  5. package/dist/analytics/context.d.ts.map +1 -1
  6. package/dist/analytics/derive.d.ts.map +1 -1
  7. package/dist/analytics/index.d.ts +2 -0
  8. package/dist/analytics/index.d.ts.map +1 -1
  9. package/dist/{chunk-SEIY2F5F.js → chunk-7BHG3XGJ.js} +317 -86
  10. package/dist/chunk-7BHG3XGJ.js.map +1 -0
  11. package/dist/{chunk-I6CPKAOD.js → chunk-7BMXN7MP.js} +215 -99
  12. package/dist/chunk-7BMXN7MP.js.map +1 -0
  13. package/dist/{chunk-6V5MGYXH.js → chunk-JGJNRNQW.js} +142 -31
  14. package/dist/chunk-JGJNRNQW.js.map +1 -0
  15. package/dist/chunk-ZBQ772DU.js +17 -0
  16. package/dist/chunk-ZBQ772DU.js.map +1 -0
  17. package/dist/{chunk-H63TOSQ7.js → chunk-ZDRHCKRK.js} +499 -142
  18. package/dist/chunk-ZDRHCKRK.js.map +1 -0
  19. package/dist/dashboard/ChartWidget.d.ts.map +1 -1
  20. package/dist/dashboard/KpiCard.d.ts.map +1 -1
  21. package/dist/dashboard/SidebarOnboarding.d.ts +16 -11
  22. package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -1
  23. package/dist/dashboard/Sparkline.d.ts +13 -0
  24. package/dist/dashboard/Sparkline.d.ts.map +1 -0
  25. package/dist/dashboard/index.d.ts +1 -0
  26. package/dist/dashboard/index.d.ts.map +1 -1
  27. package/dist/dashboard/index.js +3 -2
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +237 -117
  31. package/dist/index.js.map +1 -1
  32. package/dist/layout/AppShell.d.ts +4 -1
  33. package/dist/layout/AppShell.d.ts.map +1 -1
  34. package/dist/layout/AppShellSkeleton.d.ts +23 -0
  35. package/dist/layout/AppShellSkeleton.d.ts.map +1 -0
  36. package/dist/layout/ModulePage.d.ts +5 -1
  37. package/dist/layout/ModulePage.d.ts.map +1 -1
  38. package/dist/layout/Sidebar.d.ts +19 -1
  39. package/dist/layout/Sidebar.d.ts.map +1 -1
  40. package/dist/layout/Topbar.d.ts +4 -1
  41. package/dist/layout/Topbar.d.ts.map +1 -1
  42. package/dist/layout/index.d.ts +2 -0
  43. package/dist/layout/index.d.ts.map +1 -1
  44. package/dist/layout/index.js +2 -1
  45. package/dist/layout/page-trail.d.ts +23 -0
  46. package/dist/layout/page-trail.d.ts.map +1 -0
  47. package/dist/layout/rail-flyout.d.ts +39 -0
  48. package/dist/layout/rail-flyout.d.ts.map +1 -0
  49. package/dist/primitives/bulk-action-bar.d.ts +30 -0
  50. package/dist/primitives/bulk-action-bar.d.ts.map +1 -0
  51. package/dist/primitives/checkbox.d.ts +5 -0
  52. package/dist/primitives/checkbox.d.ts.map +1 -1
  53. package/dist/primitives/data-table.d.ts +11 -1
  54. package/dist/primitives/data-table.d.ts.map +1 -1
  55. package/dist/primitives/dropdown.d.ts.map +1 -1
  56. package/dist/primitives/index.d.ts +5 -0
  57. package/dist/primitives/index.d.ts.map +1 -1
  58. package/dist/primitives/index.js +2 -2
  59. package/dist/primitives/list-chrome.d.ts +20 -0
  60. package/dist/primitives/list-chrome.d.ts.map +1 -0
  61. package/dist/primitives/modal.d.ts.map +1 -1
  62. package/dist/primitives/phone-field.d.ts +19 -0
  63. package/dist/primitives/phone-field.d.ts.map +1 -0
  64. package/dist/primitives/refresh-button.d.ts +18 -0
  65. package/dist/primitives/refresh-button.d.ts.map +1 -0
  66. package/dist/primitives/search-select.d.ts +6 -1
  67. package/dist/primitives/search-select.d.ts.map +1 -1
  68. package/dist/primitives/thumbnail.d.ts +19 -0
  69. package/dist/primitives/thumbnail.d.ts.map +1 -0
  70. package/dist/styles.css +298 -1
  71. package/package.json +2 -2
  72. package/dist/chunk-6V5MGYXH.js.map +0 -1
  73. package/dist/chunk-H63TOSQ7.js.map +0 -1
  74. package/dist/chunk-I6CPKAOD.js.map +0 -1
  75. package/dist/chunk-SEIY2F5F.js.map +0 -1
@@ -1,40 +1,262 @@
1
- import { useHasPageHeaderHost, useCrossModuleBack, useBackHandler, usePageHeader, PageHeaderCrumb, PageHeaderOutlet, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, HeaderTabs, usePageHeaderSlot, useHasPageHeaderOutlet, usePageHeaderHost, SaveBarProvider, SaveBar } from './chunk-VAKWUUVC.js';
1
+ import { useHasPageHeaderHost, useCrossModuleBack, useBackHandler, PageHeaderCrumb, usePageHeader, PageHeaderOutlet, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, HeaderTabs, usePageHeaderSlot, useHasPageHeaderOutlet, usePageHeaderHost, SaveBarProvider, SaveBar } from './chunk-VAKWUUVC.js';
2
+ import { useLayoutStore } from './chunk-ZBQ772DU.js';
2
3
  import { cn, Button } from './chunk-OKZ6WQB3.js';
3
4
  import { ICON_MAP } from './chunk-22EVFMS3.js';
4
- import * as React3 from 'react';
5
+ import * as React from 'react';
5
6
  import { PanelLeftClose, PanelLeft, Search, Bell, Menu, Crown, Repeat, GraduationCap, Newspaper, Clapperboard, Zap, Star, UserCheck, Inbox, Clock3, Ban, Map, ListPlus, CalendarX, CalendarCheck2, Mic, UsersRound, Disc3, ListMusic, Eye, Music, Layers, Banknote, BadgeDollarSign, CalendarDays, Radio, PartyPopper, FileCheck2, Box, Boxes, TreePalm, UserX, UserPlus, ArrowDownRight, ArrowUpRight, Ruler, Warehouse, Wallet, Sparkles, AlertTriangle, Clock, CircleDollarSign, TrendingUp, Receipt, Landmark, TableProperties, SlidersHorizontal, Plug, ArrowLeftRight, ArrowUpCircle, ArrowDownCircle, Wheat, Egg, Apple, LeafyGreen, LayoutTemplate, Heart, PawPrint, Cat, Dog, FolderOpen, ListChecks, List, Shield, Plus, Filter, ChevronDown, Building2, Contact, Handshake, MapPin, UtensilsCrossed, Camera, Tags, Tag, Percent, BookOpenCheck, Globe, MessageCircle, BookOpen, UserCog, Briefcase, ClipboardCheck, ClipboardList, Wrench, Target, ShoppingBag, ShoppingCart, Megaphone, DollarSign, Mail, FileText, BarChart3, Activity, Package, CalendarClock, Calendar, CreditCard, Settings, Users, Home, ChevronRight, ChevronLeft, PanelRightOpen, PanelRightClose, PanelRight, ExternalLink, PiggyBank, LogOut } from 'lucide-react';
6
- import * as Tooltip from '@radix-ui/react-tooltip';
7
- import { create } from 'zustand';
8
7
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
9
8
  import * as Popover from '@radix-ui/react-popover';
10
9
  import { createPortal } from 'react-dom';
11
10
  import { useTranslation } from '@fayz-ai/core';
12
11
 
13
- var useLayoutStore = create((set) => ({
14
- sidebarOpen: true,
15
- sidebarCollapsed: false,
16
- mobileDrawerOpen: false,
17
- commandPaletteOpen: false,
18
- toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
19
- setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
20
- toggleMobileDrawer: () => set((state) => ({ mobileDrawerOpen: !state.mobileDrawerOpen })),
21
- setCommandPaletteOpen: (open) => set({ commandPaletteOpen: open })
22
- }));
12
+ var GUTTER_PX = 8;
13
+ var CLOSE_DELAY_MS = 140;
14
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
15
+ function iconOf(name) {
16
+ if (!name) return null;
17
+ return ICON_MAP[name] ?? null;
18
+ }
19
+ function useRailFlyout({
20
+ enabled,
21
+ railRef,
22
+ activeRoute,
23
+ onNavigate
24
+ }) {
25
+ const [state, setState] = React.useState(null);
26
+ const [pos, setPos] = React.useState({ top: 0, arrow: 20 });
27
+ const [animate, setAnimate] = React.useState(false);
28
+ const panelRef = React.useRef(null);
29
+ const closeTimer = React.useRef(null);
30
+ const lastContent = React.useRef(null);
31
+ const cancelClose = React.useCallback(() => {
32
+ if (closeTimer.current) {
33
+ clearTimeout(closeTimer.current);
34
+ closeTimer.current = null;
35
+ }
36
+ }, []);
37
+ const show = React.useCallback(
38
+ (anchor, content2) => {
39
+ if (!enabled) return;
40
+ cancelClose();
41
+ setState({ anchor, content: content2 });
42
+ },
43
+ [enabled, cancelClose]
44
+ );
45
+ const hide = React.useCallback(() => {
46
+ cancelClose();
47
+ closeTimer.current = setTimeout(() => setState(null), CLOSE_DELAY_MS);
48
+ }, [cancelClose]);
49
+ const dismiss = React.useCallback(() => {
50
+ cancelClose();
51
+ setState(null);
52
+ }, [cancelClose]);
53
+ React.useEffect(() => () => cancelClose(), [cancelClose]);
54
+ React.useEffect(() => {
55
+ if (!enabled) dismiss();
56
+ }, [enabled, dismiss]);
57
+ const reposition = React.useCallback(() => {
58
+ const rail = railRef.current;
59
+ const anchor = state?.anchor;
60
+ if (!rail || !anchor) return;
61
+ const railRect = rail.getBoundingClientRect();
62
+ const rect = anchor.getBoundingClientRect();
63
+ const height = panelRef.current?.offsetHeight ?? 0;
64
+ const rowTop = rect.top - railRect.top;
65
+ const rowCenter = rowTop + rect.height / 2;
66
+ const max = Math.max(GUTTER_PX, railRect.height - height - GUTTER_PX);
67
+ const top = Math.min(Math.max(GUTTER_PX, rowTop - 6), max);
68
+ const arrow = Math.min(Math.max(rowCenter - top, 14), Math.max(14, height - 14));
69
+ setPos({ top, arrow });
70
+ }, [railRef, state]);
71
+ useIsomorphicLayoutEffect(reposition, [reposition, state?.content.key]);
72
+ React.useEffect(() => {
73
+ if (!state) return;
74
+ const rail = railRef.current;
75
+ const onScroll = () => reposition();
76
+ rail?.addEventListener("scroll", onScroll, true);
77
+ window.addEventListener("resize", onScroll);
78
+ return () => {
79
+ rail?.removeEventListener("scroll", onScroll, true);
80
+ window.removeEventListener("resize", onScroll);
81
+ };
82
+ }, [state, railRef, reposition]);
83
+ const visible = !!state;
84
+ React.useEffect(() => {
85
+ if (!visible) {
86
+ setAnimate(false);
87
+ return;
88
+ }
89
+ const id = requestAnimationFrame(() => setAnimate(true));
90
+ return () => cancelAnimationFrame(id);
91
+ }, [visible]);
92
+ React.useEffect(() => {
93
+ if (!visible) return;
94
+ const onKey = (event) => {
95
+ if (event.key === "Escape") dismiss();
96
+ };
97
+ window.addEventListener("keydown", onKey);
98
+ return () => window.removeEventListener("keydown", onKey);
99
+ }, [visible, dismiss]);
100
+ const rowProps = React.useCallback(
101
+ (content2) => ({
102
+ onMouseEnter: (event) => show(event.currentTarget, content2),
103
+ onFocus: (event) => show(event.currentTarget, content2),
104
+ onMouseLeave: hide
105
+ }),
106
+ [show, hide]
107
+ );
108
+ if (state) lastContent.current = state.content;
109
+ const content = state?.content ?? lastContent.current;
110
+ const go = (entry) => {
111
+ dismiss();
112
+ if (entry.href) {
113
+ window.open(entry.href, "_blank", "noopener,noreferrer");
114
+ return;
115
+ }
116
+ if (entry.route) onNavigate(entry.route);
117
+ };
118
+ const entries = content?.entries ?? [];
119
+ const hasEntries = entries.length > 0;
120
+ const panel = enabled ? /* @__PURE__ */ jsx(
121
+ "div",
122
+ {
123
+ style: { transform: `translateY(${pos.top}px)`, paddingLeft: GUTTER_PX },
124
+ onMouseEnter: cancelClose,
125
+ onMouseLeave: hide,
126
+ className: cn(
127
+ "absolute left-full top-0 z-50 hidden md:block",
128
+ animate && "motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-out",
129
+ visible ? "opacity-100" : "pointer-events-none opacity-0"
130
+ ),
131
+ "aria-hidden": !visible,
132
+ children: /* @__PURE__ */ jsxs(
133
+ "div",
134
+ {
135
+ ref: panelRef,
136
+ className: cn(
137
+ // Sidebar tone, sidebar border: the panel is the menu leaning out,
138
+ // not a popover from the page underneath it.
139
+ "relative rounded-lg border border-sidebar-border bg-sidebar p-1 text-sidebar-foreground shadow-xl shadow-black/25",
140
+ hasEntries ? "min-w-[12.5rem] max-w-[16rem]" : "max-w-[15rem]"
141
+ ),
142
+ children: [
143
+ /* @__PURE__ */ jsx(
144
+ "span",
145
+ {
146
+ "aria-hidden": true,
147
+ style: { top: pos.arrow },
148
+ className: "absolute -left-[5px] h-2.5 w-2.5 -translate-y-1/2 rotate-45 border-b border-l border-sidebar-border bg-sidebar"
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx("div", { className: "saas-flyout-swap", children: content && /* @__PURE__ */ jsx(FlyoutBody, { content, entries, activeRoute, onGo: go }) }, content?.key)
152
+ ]
153
+ }
154
+ )
155
+ }
156
+ ) : null;
157
+ return { rowProps, panel, dismiss };
158
+ }
159
+ function FlyoutBody({
160
+ content,
161
+ entries,
162
+ activeRoute,
163
+ onGo
164
+ }) {
165
+ const hasEntries = entries.length > 0;
166
+ const headerActive = !!content.route && content.route === activeRoute;
167
+ const headerClickable = !!content.route || !!content.href;
168
+ const header = /* @__PURE__ */ jsxs(
169
+ "div",
170
+ {
171
+ className: cn(
172
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] font-semibold",
173
+ hasEntries ? "text-sidebar-foreground" : "whitespace-nowrap",
174
+ headerActive && "text-sidebar-accent-foreground",
175
+ headerClickable && "transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
176
+ ),
177
+ children: [
178
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: content.label }),
179
+ content.href && /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 shrink-0 opacity-50", "aria-hidden": true }),
180
+ content.premium && /* @__PURE__ */ jsx(Crown, { className: "h-3 w-3 shrink-0 text-amber-500", "aria-hidden": true }),
181
+ content.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground", children: content.badge }),
182
+ hasEntries && headerClickable && /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-sidebar-muted", "aria-hidden": true })
183
+ ]
184
+ }
185
+ );
186
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
187
+ headerClickable ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onGo(content), className: "w-full text-left", children: header }) : header,
188
+ hasEntries && /* @__PURE__ */ jsxs(Fragment, { children: [
189
+ /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-sidebar-border" }),
190
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: entries.map((entry) => {
191
+ const EntryIcon = iconOf(entry.icon);
192
+ const isActive = !!entry.route && entry.route === activeRoute;
193
+ return /* @__PURE__ */ jsxs(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ onClick: () => onGo(entry),
198
+ className: cn(
199
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-[12.5px] transition-colors",
200
+ isActive ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "text-sidebar-foreground/75 hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground"
201
+ ),
202
+ children: [
203
+ EntryIcon && /* @__PURE__ */ jsx(EntryIcon, { className: "h-3.5 w-3.5 shrink-0 opacity-70" }),
204
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: entry.label }),
205
+ entry.href && /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 shrink-0 opacity-50", "aria-hidden": true }),
206
+ entry.premium && /* @__PURE__ */ jsx(Crown, { className: "h-3 w-3 shrink-0 text-amber-500", "aria-hidden": true }),
207
+ entry.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground", children: entry.badge })
208
+ ]
209
+ },
210
+ entry.id
211
+ );
212
+ }) })
213
+ ] })
214
+ ] });
215
+ }
23
216
  var RAIL_PUSH_MS = 260;
24
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React3.useLayoutEffect : React3.useEffect;
217
+ var useIsomorphicLayoutEffect2 = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
25
218
  function getIcon(name) {
26
219
  return ICON_MAP[name] ?? Home;
27
220
  }
221
+ function shortcutKey() {
222
+ if (typeof navigator === "undefined") return "\u2318";
223
+ return /Mac|iPhone|iPad/u.test(navigator.platform || navigator.userAgent) ? "\u2318" : "Ctrl ";
224
+ }
225
+ function flyoutContentOf(item) {
226
+ return {
227
+ key: item.id,
228
+ label: item.label,
229
+ route: item.route,
230
+ href: item.href,
231
+ premium: item.premium,
232
+ badge: item.badge,
233
+ entries: item.children?.map((child) => ({
234
+ id: child.id,
235
+ label: child.label,
236
+ icon: child.icon,
237
+ route: child.route,
238
+ href: child.href,
239
+ premium: child.premium,
240
+ badge: child.badge
241
+ }))
242
+ };
243
+ }
28
244
  function NavItem({
29
245
  item,
30
246
  collapsed,
31
247
  isActive,
32
- onNavigate
248
+ onNavigate,
249
+ flyout
33
250
  }) {
34
251
  const Icon = getIcon(item.icon);
35
252
  const isExternal = !!item.href;
253
+ const hoverProps = collapsed && flyout ? flyout.rowProps(flyoutContentOf(item)) : void 0;
36
254
  const className = cn(
37
- "flex w-full items-center gap-3 rounded-button px-2.5 py-1.5 text-[13px] font-medium transition-colors",
255
+ // 36px rows. The rail was condensed to 28px while it listed every
256
+ // destination flat; with the destinations folded into collapsible groups it
257
+ // shows a handful of rows at rest, and at that length the tight rhythm read
258
+ // as cramped rather than as dense.
259
+ "flex w-full items-center gap-3 rounded-button px-2.5 py-2 text-[13px] font-medium transition-colors",
38
260
  "hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground",
39
261
  isActive ? "bg-sidebar-accent font-semibold text-sidebar-accent-foreground shadow-sm" : "text-sidebar-foreground/80",
40
262
  collapsed && "justify-center px-2"
@@ -48,27 +270,18 @@ function NavItem({
48
270
  item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: cn("rounded-full bg-primary px-2 py-0.5 text-xs font-semibold text-primary-foreground", !isExternal && !item.premium && "ml-auto"), children: item.badge })
49
271
  ] })
50
272
  ] });
51
- const content = isExternal ? /* @__PURE__ */ jsx("a", { href: item.href, target: "_blank", rel: "noopener noreferrer", className, children: inner }) : /* @__PURE__ */ jsx("button", { onClick: () => onNavigate(item.route), className, children: inner });
52
- if (collapsed) {
53
- return /* @__PURE__ */ jsxs(Tooltip.Root, { delayDuration: 0, children: [
54
- /* @__PURE__ */ jsx(Tooltip.Trigger, { asChild: true, children: content }),
55
- /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsxs(
56
- Tooltip.Content,
57
- {
58
- side: "right",
59
- sideOffset: 8,
60
- className: "fayz-glass-surface z-50 rounded-md bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md",
61
- children: [
62
- item.label,
63
- item.premium && /* @__PURE__ */ jsx(Crown, { className: "ml-1.5 inline h-3 w-3 text-amber-500", "aria-hidden": true }),
64
- item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: "ml-2 rounded-full bg-primary px-1.5 py-0.5 text-xs text-primary-foreground", children: item.badge }),
65
- /* @__PURE__ */ jsx(Tooltip.Arrow, { className: "fill-popover" })
66
- ]
67
- }
68
- ) })
69
- ] });
70
- }
71
- return content;
273
+ return isExternal ? /* @__PURE__ */ jsx("a", { href: item.href, target: "_blank", rel: "noopener noreferrer", className, ...hoverProps, children: inner }) : /* @__PURE__ */ jsx(
274
+ "button",
275
+ {
276
+ onClick: () => {
277
+ flyout?.dismiss();
278
+ onNavigate(item.route);
279
+ },
280
+ className,
281
+ ...hoverProps,
282
+ children: inner
283
+ }
284
+ );
72
285
  }
73
286
  function routeMatchScore(route, path) {
74
287
  if (path === route) return route.length + 1;
@@ -97,30 +310,30 @@ function NavGroup({
97
310
  collapsed,
98
311
  activeRoute,
99
312
  onNavigate,
100
- childConnector
313
+ childConnector,
314
+ flyout
101
315
  }) {
102
316
  const isChildActive = item.children?.some((c) => c.route === activeRoute);
103
- const [open, setOpen] = React3.useState(() => isChildActive ?? false);
317
+ const [open, setOpen] = React.useState(() => isChildActive ?? false);
104
318
  const Icon = getIcon(item.icon);
105
319
  if (collapsed) {
106
- return /* @__PURE__ */ jsxs(Tooltip.Root, { delayDuration: 0, children: [
107
- /* @__PURE__ */ jsx(Tooltip.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(
108
- "button",
109
- {
110
- onClick: () => onNavigate(item.route),
111
- className: cn(
112
- "flex w-full items-center justify-center rounded-button px-2 py-2 text-sm font-medium transition-colors",
113
- "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
114
- isChildActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-muted"
115
- ),
116
- children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0" })
117
- }
118
- ) }),
119
- /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsxs(Tooltip.Content, { side: "right", sideOffset: 8, className: "fayz-glass-surface z-50 rounded-md bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md", children: [
120
- item.label,
121
- /* @__PURE__ */ jsx(Tooltip.Arrow, { className: "fill-popover" })
122
- ] }) })
123
- ] });
320
+ const hoverProps = flyout?.rowProps(flyoutContentOf(item));
321
+ return /* @__PURE__ */ jsx(
322
+ "button",
323
+ {
324
+ onClick: () => {
325
+ flyout?.dismiss();
326
+ onNavigate(item.route);
327
+ },
328
+ className: cn(
329
+ "flex w-full items-center justify-center rounded-button px-2 py-2 text-sm font-medium transition-colors",
330
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
331
+ isChildActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-muted"
332
+ ),
333
+ ...hoverProps,
334
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0" })
335
+ }
336
+ );
124
337
  }
125
338
  return /* @__PURE__ */ jsxs("div", { children: [
126
339
  /* @__PURE__ */ jsxs(
@@ -128,7 +341,9 @@ function NavGroup({
128
341
  {
129
342
  onClick: () => setOpen(!open),
130
343
  className: cn(
131
- "flex w-full items-center gap-3 rounded-button px-2.5 py-1.5 text-[13px] font-medium transition-colors",
344
+ // Same metrics as NavItem a group header and a plain link are peers
345
+ // in the rail and must share a baseline.
346
+ "flex w-full items-center gap-3 rounded-button px-2.5 py-2 text-[13px] font-medium transition-colors",
132
347
  "hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground",
133
348
  isChildActive ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/80"
134
349
  ),
@@ -173,7 +388,7 @@ function NavGroup({
173
388
  child.premium && /* @__PURE__ */ jsx(Crown, { className: "h-3 w-3 shrink-0 text-amber-500", "aria-hidden": true })
174
389
  ] });
175
390
  const row = childExternal ? /* @__PURE__ */ jsx("a", { href: child.href, target: "_blank", rel: "noopener noreferrer", className: childClass, children: childInner }) : /* @__PURE__ */ jsx("button", { onClick: () => onNavigate(child.route), className: childClass, children: childInner });
176
- if (!childConnector) return /* @__PURE__ */ jsx(React3.Fragment, { children: row }, child.id);
391
+ if (!childConnector) return /* @__PURE__ */ jsx(React.Fragment, { children: row }, child.id);
177
392
  const activeIndex = item.children.findIndex((c) => c.route === activeRoute);
178
393
  const onPath = activeIndex >= 0 && childIndex < activeIndex;
179
394
  return (
@@ -234,6 +449,11 @@ function NavGroup({
234
449
  }) })
235
450
  ] });
236
451
  }
452
+ function withCollapsed(slot, collapsed) {
453
+ if (!React.isValidElement(slot)) return slot;
454
+ if (typeof slot.type === "string") return slot;
455
+ return React.cloneElement(slot, { collapsed });
456
+ }
237
457
  function Sidebar({
238
458
  navigation,
239
459
  logo,
@@ -244,11 +464,13 @@ function Sidebar({
244
464
  user: _user,
245
465
  orgSwitcher,
246
466
  topContent,
467
+ navTop,
247
468
  footerContent,
248
469
  borderless,
249
470
  userMenuSlot,
250
471
  notificationSlot,
251
472
  unreadCount = 0,
473
+ searchLabel = "Search",
252
474
  sectionLabels,
253
475
  navKey,
254
476
  navDirection = "forward",
@@ -257,39 +479,61 @@ function Sidebar({
257
479
  const currentPath = currentPathProp;
258
480
  const setCommandPaletteOpen = useLayoutStore((s) => s.setCommandPaletteOpen);
259
481
  const activeRoute = resolveActiveRoute(navigation, currentPath);
482
+ const railRef = React.useRef(null);
483
+ const flyout = useRailFlyout({
484
+ enabled: collapsed,
485
+ railRef,
486
+ activeRoute,
487
+ onNavigate: (route) => onNavigate?.(route)
488
+ });
260
489
  const mainItems = navigation.filter((item) => item.section === "main");
261
490
  const secondaryItems = navigation.filter((item) => item.section === "secondary");
262
491
  const settingsItems = navigation.filter((item) => item.section === "settings");
263
492
  const handleNavigate = (route) => {
264
493
  onNavigate?.(route);
265
494
  };
266
- const sectionHeading = (label) => label && !collapsed ? /* @__PURE__ */ jsx("p", { className: "px-2.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-wider text-sidebar-muted", children: label }) : null;
267
- const renderItem = (item) => item.children && item.children.length > 0 ? /* @__PURE__ */ jsx(NavGroup, { item, collapsed, activeRoute, onNavigate: handleNavigate, childConnector: navChildConnector }, item.id) : /* @__PURE__ */ jsx(
495
+ const sectionHeading = (label) => label && !collapsed ? /* @__PURE__ */ jsx("p", { className: "px-2.5 pb-0.5 pt-1 text-[10px] font-semibold uppercase tracking-wider text-sidebar-muted", children: label }) : null;
496
+ const groupRuns = (items) => {
497
+ const runs = [];
498
+ for (const item of items) {
499
+ const last = runs[runs.length - 1];
500
+ if (last && last.group === item.group) last.items.push(item);
501
+ else runs.push({ group: item.group, items: [item] });
502
+ }
503
+ return runs;
504
+ };
505
+ const renderSection = (items, label) => {
506
+ const runs = groupRuns(items);
507
+ return runs.map((run, i) => (
508
+ // 4px between rows, 20px between runs (a run being a `group` heading's
509
+ // worth of entries — see NavigationItem.group).
510
+ /* @__PURE__ */ jsxs("div", { className: i === 0 ? "space-y-1" : "mt-5 space-y-1", children: [
511
+ sectionHeading(run.group ?? label),
512
+ run.items.map(renderItem)
513
+ ] }, run.group ?? `ungrouped-${i}`)
514
+ ));
515
+ };
516
+ const renderItem = (item) => item.children && item.children.length > 0 ? /* @__PURE__ */ jsx(NavGroup, { item, collapsed, activeRoute, onNavigate: handleNavigate, childConnector: navChildConnector, flyout }, item.id) : /* @__PURE__ */ jsx(
268
517
  NavItem,
269
518
  {
270
519
  item,
271
520
  collapsed,
272
521
  isActive: item.route === activeRoute,
273
- onNavigate: handleNavigate
522
+ onNavigate: handleNavigate,
523
+ flyout
274
524
  },
275
525
  item.id
276
526
  );
277
527
  const navBody = /* @__PURE__ */ jsxs(Fragment, { children: [
278
- topContent && /* @__PURE__ */ jsx("div", { className: "mb-4 space-y-2", children: React3.isValidElement(topContent) ? React3.cloneElement(topContent, { collapsed }) : topContent }),
279
- mainItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
280
- sectionHeading(sectionLabels?.main),
281
- mainItems.map(renderItem)
282
- ] }),
283
- secondaryItems.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-6 space-y-1", children: [
284
- sectionHeading(sectionLabels?.secondary),
285
- secondaryItems.map(renderItem)
286
- ] })
528
+ navTop && /* @__PURE__ */ jsx("div", { className: "mb-2", children: withCollapsed(navTop, collapsed) }),
529
+ mainItems.length > 0 && renderSection(mainItems, sectionLabels?.main),
530
+ secondaryItems.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-6", children: renderSection(secondaryItems, sectionLabels?.secondary) })
287
531
  ] });
288
- const [outgoing, setOutgoing] = React3.useState(null);
289
- const lastBodyRef = React3.useRef(null);
290
- const lastKeyRef = React3.useRef(navKey);
291
- const timerRef = React3.useRef(null);
292
- useIsomorphicLayoutEffect(() => {
532
+ const [outgoing, setOutgoing] = React.useState(null);
533
+ const lastBodyRef = React.useRef(null);
534
+ const lastKeyRef = React.useRef(navKey);
535
+ const timerRef = React.useRef(null);
536
+ useIsomorphicLayoutEffect2(() => {
293
537
  if (lastKeyRef.current !== navKey) {
294
538
  const previousBody = lastBodyRef.current;
295
539
  lastKeyRef.current = navKey;
@@ -301,17 +545,29 @@ function Sidebar({
301
545
  }
302
546
  lastBodyRef.current = navBody;
303
547
  });
304
- React3.useEffect(() => () => {
548
+ React.useEffect(() => () => {
305
549
  if (timerRef.current) clearTimeout(timerRef.current);
306
550
  }, []);
307
- return /* @__PURE__ */ jsx(Tooltip.Provider, { children: /* @__PURE__ */ jsxs(
551
+ return /* @__PURE__ */ jsxs(
308
552
  "aside",
309
553
  {
554
+ ref: railRef,
310
555
  "data-print": "hide",
556
+ onBlur: (event) => {
557
+ if (!event.currentTarget.contains(event.relatedTarget)) flyout.dismiss();
558
+ },
311
559
  className: cn(
312
- "flex h-full flex-col bg-sidebar text-sidebar-foreground transition-all duration-200",
560
+ // `relative` anchors the flyout, which hangs off the rail's right edge
561
+ // and has to escape the nav's own scroll box to do it.
562
+ "relative flex h-full flex-col bg-sidebar text-sidebar-foreground transition-all duration-200",
313
563
  !borderless && "border-r border-sidebar-border",
314
- collapsed ? "w-16" : "w-64"
564
+ // 14rem expanded. It was 16rem, which is the width a rail needs when
565
+ // its entries are sentences ("GlossGenius Financing", "Configure your
566
+ // payment methods"); these are one or two words, so the extra 2rem was
567
+ // paid for by the content column on every screen in every app. The
568
+ // labels still truncate, and a long one now truncates 32px earlier —
569
+ // which is the trade, taken deliberately.
570
+ collapsed ? "w-16" : "w-56"
315
571
  ),
316
572
  children: [
317
573
  /* @__PURE__ */ jsxs(
@@ -347,6 +603,36 @@ function Sidebar({
347
603
  ]
348
604
  }
349
605
  ),
606
+ topContent && /* @__PURE__ */ jsx("div", { className: "px-2 pt-2", children: withCollapsed(topContent, collapsed) }),
607
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2 pt-2", children: /* @__PURE__ */ jsxs(
608
+ "button",
609
+ {
610
+ onClick: () => setCommandPaletteOpen(true),
611
+ "aria-keyshortcuts": "Meta+K Control+K",
612
+ className: cn(
613
+ // Tonal surface, no outline. On a dark rail a 1px border is a hard
614
+ // edge competing with every other hard edge; a lighter fill of the
615
+ // same hue reads as "a field" without drawing a box around it — and
616
+ // leaves the outline treatment free to mean focus, which it must.
617
+ "fz-tap group flex items-center rounded-lg bg-sidebar-accent/40 text-sidebar-muted transition-colors",
618
+ "hover:bg-sidebar-accent/70 hover:text-sidebar-foreground",
619
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-accent-foreground/50",
620
+ collapsed ? "h-9 w-9 justify-center" : "h-9 w-full gap-2.5 px-2.5"
621
+ ),
622
+ "aria-label": searchLabel,
623
+ title: collapsed ? searchLabel : void 0,
624
+ children: [
625
+ /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0" }),
626
+ !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
627
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-left text-[13px]", children: searchLabel }),
628
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-[11px] font-medium tabular-nums text-sidebar-muted/60 transition-opacity group-hover:opacity-100 md:opacity-70", children: [
629
+ shortcutKey(),
630
+ "K"
631
+ ] })
632
+ ] })
633
+ ]
634
+ }
635
+ ) }),
350
636
  /* @__PURE__ */ jsxs(
351
637
  "nav",
352
638
  {
@@ -388,7 +674,8 @@ function Sidebar({
388
674
  item,
389
675
  collapsed,
390
676
  isActive: item.route === activeRoute,
391
- onNavigate: handleNavigate
677
+ onNavigate: handleNavigate,
678
+ flyout
392
679
  },
393
680
  item.id
394
681
  )) }),
@@ -396,26 +683,17 @@ function Sidebar({
396
683
  // SeuSiteWidget self-hides when the tenant has no project); without
397
684
  // it the padded/bordered wrapper shows as a blank block.
398
685
  /* @__PURE__ */ jsx("div", { className: cn("space-y-2 p-2 empty:hidden", !borderless && "border-t border-sidebar-border"), children: footerContent }),
399
- orgSwitcher && React3.isValidElement(orgSwitcher) ? React3.cloneElement(orgSwitcher, { collapsed }) : orgSwitcher,
686
+ withCollapsed(orgSwitcher, collapsed),
400
687
  /* @__PURE__ */ jsx("div", { className: cn("p-2", !borderless && "border-t border-sidebar-border"), children: /* @__PURE__ */ jsxs("div", { className: cn(
401
688
  "flex items-center rounded-button",
402
689
  collapsed ? "flex-col gap-1" : "gap-1 px-1"
403
690
  ), children: [
404
691
  userMenuSlot,
405
692
  !collapsed && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
406
- /* @__PURE__ */ jsx(
407
- "button",
408
- {
409
- onClick: () => setCommandPaletteOpen(true),
410
- className: "inline-flex shrink-0 items-center justify-center rounded-button p-2 text-sidebar-muted transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground",
411
- "aria-label": "Search",
412
- children: /* @__PURE__ */ jsx(Search, { className: "h-4 w-4" })
413
- }
414
- ),
415
693
  notificationSlot ?? /* @__PURE__ */ jsxs(
416
694
  "button",
417
695
  {
418
- className: "relative inline-flex shrink-0 items-center justify-center rounded-button p-2 text-sidebar-muted transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground",
696
+ className: "fz-tap relative inline-flex shrink-0 items-center justify-center rounded-button p-2 text-sidebar-muted transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground",
419
697
  "aria-label": unreadCount > 0 ? `${unreadCount} unread notifications` : "Notifications",
420
698
  children: [
421
699
  /* @__PURE__ */ jsx(Bell, { className: "h-4 w-4" }),
@@ -423,10 +701,11 @@ function Sidebar({
423
701
  ]
424
702
  }
425
703
  )
426
- ] }) })
704
+ ] }) }),
705
+ flyout.panel
427
706
  ]
428
707
  }
429
- ) });
708
+ );
430
709
  }
431
710
  function TopbarUserMenu({
432
711
  user,
@@ -438,7 +717,7 @@ function TopbarUserMenu({
438
717
  onBilling,
439
718
  userMenuExtras
440
719
  }) {
441
- const [open, setOpen] = React3.useState(false);
720
+ const [open, setOpen] = React.useState(false);
442
721
  if (!user) return null;
443
722
  const initial = (user.fullName?.trim()?.[0] ?? user.email?.[0] ?? "?").toUpperCase();
444
723
  const item = (label, onClick, Icon) => onClick ? /* @__PURE__ */ jsxs(
@@ -626,7 +905,7 @@ function getIcon2(name) {
626
905
  return ICON_MAP2[name] ?? Home;
627
906
  }
628
907
  function NavDropdown({ item, currentPath, onNavigate }) {
629
- const [open, setOpen] = React3.useState(false);
908
+ const [open, setOpen] = React.useState(false);
630
909
  const Icon = getIcon2(item.icon);
631
910
  const isChildActive = item.children?.some(
632
911
  (child) => currentPath === child.route || currentPath.startsWith(child.route + "/")
@@ -717,8 +996,7 @@ function Topbar({
717
996
  children: /* @__PURE__ */ jsx(Menu, { className: "h-5 w-5" })
718
997
  }
719
998
  ),
720
- /* @__PURE__ */ jsx("div", { className: "hidden shrink-0 items-center md:flex", children: logo ?? /* @__PURE__ */ jsx("span", { className: "text-lg font-bold", children: "App" }) }),
721
- orgSwitcher && /* @__PURE__ */ jsx("div", { className: "hidden min-w-0 items-center md:flex", children: orgSwitcher })
999
+ /* @__PURE__ */ jsx("div", { className: "hidden shrink-0 items-center md:flex", children: logo ?? /* @__PURE__ */ jsx("span", { className: "text-lg font-bold", children: "App" }) })
722
1000
  ] }),
723
1001
  /* @__PURE__ */ jsx("div", { className: "mx-8 hidden max-w-sm flex-1 md:flex", children: /* @__PURE__ */ jsxs(
724
1002
  "button",
@@ -732,7 +1010,8 @@ function Topbar({
732
1010
  ]
733
1011
  }
734
1012
  ) }),
735
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1013
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [
1014
+ orgSwitcher && /* @__PURE__ */ jsx("div", { className: "hidden min-w-0 items-center md:flex", children: orgSwitcher }),
736
1015
  leftContent,
737
1016
  rightContent,
738
1017
  /* @__PURE__ */ jsx(
@@ -792,13 +1071,13 @@ function Topbar({
792
1071
  }) }) }) })
793
1072
  ] });
794
1073
  }
795
- var ModuleHeaderSlotContext = React3.createContext(null);
1074
+ var ModuleHeaderSlotContext = React.createContext(null);
796
1075
  function useModuleHeaderSlot() {
797
- return React3.useContext(ModuleHeaderSlotContext);
1076
+ return React.useContext(ModuleHeaderSlotContext);
798
1077
  }
799
- var ModuleHeaderActionsSlotContext = React3.createContext(null);
1078
+ var ModuleHeaderActionsSlotContext = React.createContext(null);
800
1079
  function useModuleHeaderActionsSlot() {
801
- return React3.useContext(ModuleHeaderActionsSlotContext);
1080
+ return React.useContext(ModuleHeaderActionsSlotContext);
802
1081
  }
803
1082
  function ShellPageTitle({
804
1083
  title,
@@ -820,6 +1099,7 @@ function SidebarLayout({
820
1099
  userMenuExtras: _userMenuExtras,
821
1100
  orgSwitcher,
822
1101
  sidebarTopContent,
1102
+ sidebarNavTop,
823
1103
  sidebarFooterContent,
824
1104
  frame,
825
1105
  currentPath,
@@ -830,6 +1110,7 @@ function SidebarLayout({
830
1110
  userMenuSlot,
831
1111
  notificationSlot,
832
1112
  unreadCount = 0,
1113
+ searchLabel,
833
1114
  hasBottomNav = false,
834
1115
  mobileHeader = "minimal",
835
1116
  sidebarSectionLabels,
@@ -841,11 +1122,11 @@ function SidebarLayout({
841
1122
  const pageHeaderSlot = usePageHeaderSlot();
842
1123
  usePageHeaderHost();
843
1124
  const setSidebarCollapsed = useLayoutStore((s) => s.setSidebarCollapsed);
844
- const [mobileMenuOpen, setMobileMenuOpen] = React3.useState(false);
845
- const [headerSlot, setHeaderSlot] = React3.useState(null);
846
- const [actionsSlot, setActionsSlot] = React3.useState(null);
1125
+ const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
1126
+ const [headerSlot, setHeaderSlot] = React.useState(null);
1127
+ const [actionsSlot, setActionsSlot] = React.useState(null);
847
1128
  return /* @__PURE__ */ jsx(SaveBarProvider, { children: /* @__PURE__ */ jsx(ModuleHeaderSlotContext.Provider, { value: headerSlot, children: /* @__PURE__ */ jsx(ModuleHeaderActionsSlotContext.Provider, { value: actionsSlot, children: /* @__PURE__ */ jsxs("div", { className: cn(
848
- "flex h-screen overflow-hidden bg-background",
1129
+ "shell-enter flex h-screen overflow-hidden bg-background",
849
1130
  // Frame mode: the inset gap around the framed card takes the sidebar (menu)
850
1131
  // color, so the white card reads as a panel floating on the same tone as the
851
1132
  // left menu. The card's own md:m-2 margin reveals this behind it.
@@ -867,11 +1148,13 @@ function SidebarLayout({
867
1148
  onBilling,
868
1149
  orgSwitcher,
869
1150
  topContent: sidebarTopContent,
1151
+ navTop: sidebarNavTop,
870
1152
  footerContent: sidebarFooterContent,
871
- borderless: false,
1153
+ borderless: frame,
872
1154
  userMenuSlot,
873
1155
  notificationSlot,
874
1156
  unreadCount,
1157
+ searchLabel,
875
1158
  sectionLabels: sidebarSectionLabels,
876
1159
  navKey: sidebarNavKey,
877
1160
  navDirection: sidebarNavDirection,
@@ -922,10 +1205,12 @@ function SidebarLayout({
922
1205
  onBilling,
923
1206
  orgSwitcher,
924
1207
  topContent: sidebarTopContent,
1208
+ navTop: sidebarNavTop,
925
1209
  footerContent: sidebarFooterContent,
926
1210
  userMenuSlot,
927
1211
  notificationSlot,
928
1212
  unreadCount,
1213
+ searchLabel,
929
1214
  onClose: () => setMobileMenuOpen(false)
930
1215
  }
931
1216
  )
@@ -954,13 +1239,13 @@ function TopbarLayout({
954
1239
  notificationSlot,
955
1240
  hasBottomNav = false
956
1241
  }) {
957
- const [mobileMenuOpen, setMobileMenuOpen] = React3.useState(false);
1242
+ const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
958
1243
  const rawSlot = usePageHeaderSlot();
959
1244
  const hasOutlet = useHasPageHeaderOutlet();
960
1245
  usePageHeaderHost();
961
1246
  const pageHeaderSlot = hasOutlet ? null : rawSlot;
962
1247
  const showTitle = !hasOutlet && !!pageTitle;
963
- return /* @__PURE__ */ jsx(SaveBarProvider, { children: /* @__PURE__ */ jsxs("div", { className: "relative flex h-screen flex-col overflow-hidden bg-background", children: [
1248
+ return /* @__PURE__ */ jsx(SaveBarProvider, { children: /* @__PURE__ */ jsxs("div", { className: "shell-enter relative flex h-screen flex-col overflow-hidden bg-background", children: [
964
1249
  /* @__PURE__ */ jsx(
965
1250
  Topbar,
966
1251
  {
@@ -1022,7 +1307,7 @@ function MinimalLayout({
1022
1307
  children,
1023
1308
  headerEnd
1024
1309
  }) {
1025
- return /* @__PURE__ */ jsxs("div", { className: "flex h-screen flex-col", children: [
1310
+ return /* @__PURE__ */ jsxs("div", { className: "shell-enter flex h-screen flex-col", children: [
1026
1311
  /* @__PURE__ */ jsxs("header", { className: "flex h-14 shrink-0 items-center justify-between border-b border-border bg-card px-4 md:px-6", children: [
1027
1312
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: logo ?? /* @__PURE__ */ jsx("span", { className: "text-lg font-bold", children: "App" }) }),
1028
1313
  headerEnd && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: headerEnd })
@@ -1031,23 +1316,23 @@ function MinimalLayout({
1031
1316
  ] });
1032
1317
  }
1033
1318
  function MobileOverlay({ onClose, onNavigate, ...sidebarProps }) {
1034
- const [visible, setVisible] = React3.useState(false);
1035
- React3.useEffect(() => {
1319
+ const [visible, setVisible] = React.useState(false);
1320
+ React.useEffect(() => {
1036
1321
  const id = requestAnimationFrame(() => setVisible(true));
1037
1322
  return () => cancelAnimationFrame(id);
1038
1323
  }, []);
1039
- const close = React3.useCallback(() => {
1324
+ const close = React.useCallback(() => {
1040
1325
  setVisible(false);
1041
1326
  window.setTimeout(onClose, 250);
1042
1327
  }, [onClose]);
1043
- React3.useEffect(() => {
1328
+ React.useEffect(() => {
1044
1329
  const previous = document.body.style.overflow;
1045
1330
  document.body.style.overflow = "hidden";
1046
1331
  return () => {
1047
1332
  document.body.style.overflow = previous;
1048
1333
  };
1049
1334
  }, []);
1050
- React3.useEffect(() => {
1335
+ React.useEffect(() => {
1051
1336
  const onKey = (e) => {
1052
1337
  if (e.key === "Escape") close();
1053
1338
  };
@@ -1187,6 +1472,7 @@ function AppShell({
1187
1472
  topbarStart,
1188
1473
  topbarEnd,
1189
1474
  sidebarTopContent,
1475
+ sidebarNavTop,
1190
1476
  sidebarFooterContent,
1191
1477
  bottomNav,
1192
1478
  onBottomNavAction,
@@ -1194,6 +1480,7 @@ function AppShell({
1194
1480
  userMenuSlot,
1195
1481
  notificationSlot,
1196
1482
  unreadCount = 0,
1483
+ searchLabel,
1197
1484
  sidebarSectionLabels,
1198
1485
  sidebarNavKey,
1199
1486
  sidebarNavDirection,
@@ -1234,6 +1521,7 @@ function AppShell({
1234
1521
  userMenuExtras,
1235
1522
  orgSwitcher,
1236
1523
  sidebarTopContent,
1524
+ sidebarNavTop,
1237
1525
  sidebarFooterContent,
1238
1526
  frame: contentFrame,
1239
1527
  currentPath,
@@ -1244,6 +1532,7 @@ function AppShell({
1244
1532
  userMenuSlot,
1245
1533
  notificationSlot,
1246
1534
  unreadCount,
1535
+ searchLabel,
1247
1536
  hasBottomNav,
1248
1537
  mobileHeader,
1249
1538
  sidebarSectionLabels,
@@ -1319,13 +1608,13 @@ function SplitWithRightRail({
1319
1608
  maxWidth = 720,
1320
1609
  resizeLabel = "Resize panel"
1321
1610
  }) {
1322
- const [width, setWidth] = React3.useState(defaultWidth);
1323
- const [dragging, setDragging] = React3.useState(false);
1324
- const clamp = React3.useCallback(
1611
+ const [width, setWidth] = React.useState(defaultWidth);
1612
+ const [dragging, setDragging] = React.useState(false);
1613
+ const clamp = React.useCallback(
1325
1614
  (value) => Math.min(maxWidth, Math.max(minWidth, value)),
1326
1615
  [minWidth, maxWidth]
1327
1616
  );
1328
- React3.useEffect(() => {
1617
+ React.useEffect(() => {
1329
1618
  if (!dragging) return;
1330
1619
  const onMove = (event) => {
1331
1620
  event.preventDefault();
@@ -1347,7 +1636,7 @@ function SplitWithRightRail({
1347
1636
  document.body.style.cursor = previousCursor;
1348
1637
  };
1349
1638
  }, [dragging, clamp]);
1350
- React3.useEffect(() => {
1639
+ React.useEffect(() => {
1351
1640
  if (dragging) return;
1352
1641
  onWidthChange?.(width);
1353
1642
  }, [dragging]);
@@ -1426,12 +1715,40 @@ function SplitWithRightRail({
1426
1715
  ] })
1427
1716
  );
1428
1717
  }
1429
- var NavTransitionContext = React3.createContext("slide");
1718
+ function BootSpinner({ className }) {
1719
+ return /* @__PURE__ */ jsx(
1720
+ "span",
1721
+ {
1722
+ "aria-hidden": true,
1723
+ className: cn(
1724
+ "block h-7 w-7 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground/70",
1725
+ "motion-reduce:animate-none motion-reduce:border-t-foreground/20 motion-reduce:animate-pulse",
1726
+ className
1727
+ )
1728
+ }
1729
+ );
1730
+ }
1731
+ function AppShellSkeleton({ label, className }) {
1732
+ return /* @__PURE__ */ jsxs(
1733
+ "div",
1734
+ {
1735
+ role: "status",
1736
+ "aria-busy": "true",
1737
+ className: cn("flex h-screen items-center justify-center overflow-hidden bg-background", className),
1738
+ children: [
1739
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }),
1740
+ /* @__PURE__ */ jsx(BootSpinner, {})
1741
+ ]
1742
+ }
1743
+ );
1744
+ }
1745
+ AppShellSkeleton.displayName = "AppShellSkeleton";
1746
+ var NavTransitionContext = React.createContext("slide");
1430
1747
  function NavTransitionProvider({ value, children }) {
1431
1748
  return /* @__PURE__ */ jsx(NavTransitionContext.Provider, { value, children });
1432
1749
  }
1433
1750
  function useNavTransition() {
1434
- return React3.useContext(NavTransitionContext);
1751
+ return React.useContext(NavTransitionContext);
1435
1752
  }
1436
1753
  function resolveTransitionClass(transition, direction) {
1437
1754
  if (transition === "none") return "";
@@ -1460,15 +1777,15 @@ function PremiumBadge({ className }) {
1460
1777
  const tip = t("upgrade.badgeTooltip");
1461
1778
  return /* @__PURE__ */ jsx("span", { title: tip, "aria-label": tip, className: cn("inline-flex shrink-0", className), children: /* @__PURE__ */ jsx(Crown, { className: "h-3.5 w-3.5 text-amber-500", "aria-hidden": true }) });
1462
1779
  }
1463
- var ModuleLayoutContext = React3.createContext("rail");
1780
+ var ModuleLayoutContext = React.createContext("rail");
1464
1781
  function ModuleLayoutProvider({ variant, children }) {
1465
1782
  return /* @__PURE__ */ jsx(ModuleLayoutContext.Provider, { value: variant, children });
1466
1783
  }
1467
1784
  function useModuleLayout() {
1468
- return React3.useContext(ModuleLayoutContext);
1785
+ return React.useContext(ModuleLayoutContext);
1469
1786
  }
1470
1787
  function NavItem2({ item }) {
1471
- const [expanded, setExpanded] = React3.useState(true);
1788
+ const [expanded, setExpanded] = React.useState(true);
1472
1789
  const Icon = item.icon ? ICON_MAP2[item.icon] ?? null : null;
1473
1790
  const hasChildren = item.children && item.children.length > 0;
1474
1791
  const hasActiveChild = hasChildren && item.children.some((c) => c.active);
@@ -1510,9 +1827,9 @@ function NavItem2({ item }) {
1510
1827
  ] });
1511
1828
  }
1512
1829
  function MobileTabs({ nav }) {
1513
- const scrollRef = React3.useRef(null);
1514
- const [expandedId, setExpandedId] = React3.useState(null);
1515
- React3.useEffect(() => {
1830
+ const scrollRef = React.useRef(null);
1831
+ const [expandedId, setExpandedId] = React.useState(null);
1832
+ React.useEffect(() => {
1516
1833
  const container = scrollRef.current;
1517
1834
  if (!container) return;
1518
1835
  const activeEl = container.querySelector('[data-active="true"]');
@@ -1579,20 +1896,31 @@ function MobileTabs({ nav }) {
1579
1896
  })()
1580
1897
  ] });
1581
1898
  }
1582
- var BackStyleContext = React3.createContext("link");
1899
+ var BackStyleContext = React.createContext("link");
1583
1900
  function BackStyleProvider({ style, children }) {
1584
1901
  return /* @__PURE__ */ jsx(BackStyleContext.Provider, { value: style, children });
1585
1902
  }
1586
1903
  function useBackStyle() {
1587
- return React3.useContext(BackStyleContext);
1904
+ return React.useContext(BackStyleContext);
1588
1905
  }
1589
- function SubpageHeader({ title, subtitle, icon, onBack, parentLabel, actions, backStyle }) {
1906
+ function SubpageHeader({ title, subtitle, icon, onBack, parentLabel, actions, backStyle, inline }) {
1590
1907
  const t = useTranslation();
1591
1908
  const appStyle = useBackStyle();
1592
- const hasShellBar = useHasPageHeaderHost();
1593
- const { onBack: effectiveOnBack, parentLabel: effectiveParentLabel } = useCrossModuleBack(onBack, parentLabel);
1909
+ const hasShellBar = useHasPageHeaderHost() && !inline;
1910
+ const cross = useCrossModuleBack(onBack, parentLabel);
1911
+ const effectiveOnBack = inline ? onBack : cross.onBack;
1912
+ const effectiveParentLabel = inline ? parentLabel : cross.parentLabel;
1594
1913
  useBackHandler(effectiveOnBack);
1595
1914
  const Icon = icon ? ICON_MAP2[icon] ?? null : null;
1915
+ if (inline && effectiveOnBack) {
1916
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1917
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
1918
+ /* @__PURE__ */ jsx(PageHeaderCrumb, { title, parentLabel: effectiveParentLabel, onBack: effectiveOnBack }),
1919
+ actions && /* @__PURE__ */ jsx("div", { className: "no-print", children: actions })
1920
+ ] }),
1921
+ subtitle && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: subtitle })
1922
+ ] });
1923
+ }
1596
1924
  if (hasShellBar) {
1597
1925
  return /* @__PURE__ */ jsx(
1598
1926
  ShellSubpageHeader,
@@ -1739,7 +2067,36 @@ function ModulePage({ title, subtitle, nav, children, className, headerAction, s
1739
2067
  ] })
1740
2068
  ] });
1741
2069
  }
1742
- var ContentSplitContext = React3.createContext(null);
2070
+ var EMPTY = [];
2071
+ var PageTrailContext = React.createContext(EMPTY);
2072
+ function PageTrail({
2073
+ label,
2074
+ href,
2075
+ onNavigate,
2076
+ children
2077
+ }) {
2078
+ const parent = React.useContext(PageTrailContext);
2079
+ const trail = React.useMemo(
2080
+ () => [...parent, { label, href, onNavigate }],
2081
+ [parent, label, href, onNavigate]
2082
+ );
2083
+ return /* @__PURE__ */ jsx(PageTrailContext.Provider, { value: trail, children });
2084
+ }
2085
+ function usePageTrail() {
2086
+ return React.useContext(PageTrailContext);
2087
+ }
2088
+ function usePageTrailParent(href) {
2089
+ const trail = React.useContext(PageTrailContext);
2090
+ return React.useMemo(() => {
2091
+ const above = href ? trail.filter((c) => c.href !== href) : trail;
2092
+ return above[above.length - 1];
2093
+ }, [trail, href]);
2094
+ }
2095
+ function navigateToCrumb(crumb) {
2096
+ if (crumb.onNavigate) crumb.onNavigate();
2097
+ else window.location.hash = crumb.href;
2098
+ }
2099
+ var ContentSplitContext = React.createContext(null);
1743
2100
  function ContentSplit({
1744
2101
  children,
1745
2102
  aside,
@@ -1752,10 +2109,10 @@ function ContentSplit({
1752
2109
  defaultCollapsed = false,
1753
2110
  className
1754
2111
  }) {
1755
- const [internalOpen, setInternalOpen] = React3.useState(false);
2112
+ const [internalOpen, setInternalOpen] = React.useState(false);
1756
2113
  const open = asideOpen ?? internalOpen;
1757
2114
  const setOpen = onAsideOpenChange ?? setInternalOpen;
1758
- const [collapsed, setCollapsed] = React3.useState(collapsible && defaultCollapsed);
2115
+ const [collapsed, setCollapsed] = React.useState(collapsible && defaultCollapsed);
1759
2116
  return /* @__PURE__ */ jsxs(ContentSplitContext.Provider, { value: { open, setOpen }, children: [
1760
2117
  /* @__PURE__ */ jsxs(
1761
2118
  "div",
@@ -1807,7 +2164,7 @@ function ContentSplit({
1807
2164
  ] });
1808
2165
  }
1809
2166
  function ContentSplitTrigger({ label, iconOnly, className }) {
1810
- const ctx = React3.useContext(ContentSplitContext);
2167
+ const ctx = React.useContext(ContentSplitContext);
1811
2168
  if (!ctx) return null;
1812
2169
  return /* @__PURE__ */ jsxs(
1813
2170
  Button,
@@ -1825,6 +2182,6 @@ function ContentSplitTrigger({ label, iconOnly, className }) {
1825
2182
  );
1826
2183
  }
1827
2184
 
1828
- export { AppShell, BackStyleProvider, ContentSplit, ContentSplitTrigger, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTransition, Sidebar, SubpageHeader, Topbar, useBackStyle, useLayoutStore, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition };
1829
- //# sourceMappingURL=chunk-H63TOSQ7.js.map
1830
- //# sourceMappingURL=chunk-H63TOSQ7.js.map
2185
+ export { AppShell, AppShellSkeleton, BackStyleProvider, ContentSplit, ContentSplitTrigger, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTrail, PageTransition, Sidebar, SubpageHeader, Topbar, navigateToCrumb, useBackStyle, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition, usePageTrail, usePageTrailParent };
2186
+ //# sourceMappingURL=chunk-ZDRHCKRK.js.map
2187
+ //# sourceMappingURL=chunk-ZDRHCKRK.js.map