@cboxdk/cbox-ui 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3671 @@
1
+ import { createContext, useContext, useEffect, useState, useRef, useMemo, useCallback, useId } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import clsx from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { X, Menu, PanelLeftOpen, PanelLeftClose } from 'lucide-react';
6
+
7
+ // src/context/cbox-id.tsx
8
+ var CboxIdContext = createContext(null);
9
+ function CboxIdProvider({
10
+ value,
11
+ children
12
+ }) {
13
+ return /* @__PURE__ */ jsx(CboxIdContext.Provider, { value, children });
14
+ }
15
+ function useCboxId() {
16
+ const ctx = useContext(CboxIdContext);
17
+ if (ctx === null) {
18
+ throw new Error(
19
+ "@cboxdk/cbox-ui: useCboxId() must be called inside <CboxIdProvider>. Wrap your app root and populate it from the id-client SDK (or, in id itself, from Inertia shared props)."
20
+ );
21
+ }
22
+ return ctx;
23
+ }
24
+ function useCboxIdOptional() {
25
+ return useContext(CboxIdContext);
26
+ }
27
+ function cn(...inputs) {
28
+ return twMerge(clsx(inputs));
29
+ }
30
+ function AppContent({ children, className, ...props }) {
31
+ return /* @__PURE__ */ jsx(
32
+ "main",
33
+ {
34
+ ...props,
35
+ className: cn(
36
+ "canvas-gradient relative flex h-svh flex-1 flex-col overflow-y-auto",
37
+ className
38
+ ),
39
+ children
40
+ }
41
+ );
42
+ }
43
+ function AppShell({ children }) {
44
+ return /* @__PURE__ */ jsx("div", { className: "flex h-svh w-full bg-canvas text-foreground", children });
45
+ }
46
+ function CboxBrand({
47
+ appKey,
48
+ submark,
49
+ assetBase = "/brand",
50
+ tone = "dark",
51
+ collapsed = false,
52
+ className
53
+ }) {
54
+ if (collapsed) {
55
+ return /* @__PURE__ */ jsx(
56
+ "img",
57
+ {
58
+ src: `${assetBase}/cbox-icon.png`,
59
+ srcSet: `${assetBase}/cbox-icon.png 1x, ${assetBase}/cbox-icon@2x.png 2x`,
60
+ alt: "Cbox",
61
+ width: 36,
62
+ height: 36,
63
+ className: cn("h-9 w-9 rounded-md", className)
64
+ }
65
+ );
66
+ }
67
+ const wordmark = tone === "light" ? `${assetBase}/cbox-logo-white.png` : `${assetBase}/cbox-logo.png`;
68
+ const wordmarkRetina = tone === "light" ? `${assetBase}/cbox-logo-white@2x.png` : `${assetBase}/cbox-logo@2x.png`;
69
+ const submarkText = submark === void 0 ? appKey : submark;
70
+ return /* @__PURE__ */ jsxs(
71
+ "div",
72
+ {
73
+ className: cn("inline-flex items-center gap-2", className),
74
+ "aria-label": `Cbox \xB7 ${appKey}`,
75
+ children: [
76
+ /* @__PURE__ */ jsx(
77
+ "img",
78
+ {
79
+ src: wordmark,
80
+ srcSet: `${wordmark} 1x, ${wordmarkRetina} 2x`,
81
+ alt: "Cbox",
82
+ height: 22,
83
+ className: "h-[22px] w-auto select-none",
84
+ draggable: false
85
+ }
86
+ ),
87
+ submarkText !== null && submarkText !== "" ? /* @__PURE__ */ jsx(
88
+ "span",
89
+ {
90
+ className: cn(
91
+ "text-mono text-[10px] font-semibold tracking-[0.22em] uppercase",
92
+ tone === "light" ? "text-white/60" : "text-muted-foreground/80"
93
+ ),
94
+ children: submarkText
95
+ }
96
+ ) : null
97
+ ]
98
+ }
99
+ );
100
+ }
101
+ function CboxAppSidebar({
102
+ appKey,
103
+ brandSubmark,
104
+ assetBase,
105
+ brandHref = "/",
106
+ sections,
107
+ currentPath,
108
+ LinkComponent,
109
+ collapsed = false,
110
+ className
111
+ }) {
112
+ return /* @__PURE__ */ jsxs(
113
+ "aside",
114
+ {
115
+ "data-slot": "cbox-app-sidebar",
116
+ "data-collapsed": collapsed ? "true" : "false",
117
+ className: cn(
118
+ "hidden h-svh shrink-0 flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-150 ease-out lg:flex",
119
+ collapsed ? "w-[60px]" : "w-[240px]",
120
+ className
121
+ ),
122
+ children: [
123
+ /* @__PURE__ */ jsx(
124
+ LinkComponent,
125
+ {
126
+ href: brandHref,
127
+ "aria-label": "Home",
128
+ className: cn(
129
+ "flex h-14 shrink-0 items-center border-b border-sidebar-border/70",
130
+ collapsed ? "justify-center px-0" : "px-4"
131
+ ),
132
+ children: /* @__PURE__ */ jsx(
133
+ CboxBrand,
134
+ {
135
+ appKey,
136
+ submark: brandSubmark,
137
+ assetBase,
138
+ collapsed
139
+ }
140
+ )
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsx(
144
+ "nav",
145
+ {
146
+ className: cn(
147
+ "flex-1 overflow-y-auto pt-5 pb-3",
148
+ collapsed ? "space-y-3 px-2" : "space-y-5 px-3"
149
+ ),
150
+ children: sections.map((section) => /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
151
+ collapsed ? null : /* @__PURE__ */ jsx("p", { className: "text-eyebrow px-3 pb-1", children: section.label }),
152
+ /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: section.items.map((item) => {
153
+ const active = item.matchPrefix === true ? currentPath === item.href || currentPath.startsWith(item.href + "/") : currentPath === item.href;
154
+ const Icon = item.icon;
155
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
156
+ LinkComponent,
157
+ {
158
+ href: item.href,
159
+ prefetch: true,
160
+ "aria-label": collapsed ? item.label : void 0,
161
+ title: collapsed ? item.label : void 0,
162
+ className: cn(
163
+ collapsed ? "group mx-auto flex h-9 w-9 items-center justify-center rounded-md transition-colors" : "group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] transition-colors",
164
+ active ? "bg-sidebar-accent font-semibold text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-foreground"
165
+ ),
166
+ children: [
167
+ /* @__PURE__ */ jsx(
168
+ Icon,
169
+ {
170
+ className: cn(
171
+ "h-4 w-4 shrink-0",
172
+ active ? "text-foreground" : "text-sidebar-muted"
173
+ ),
174
+ strokeWidth: 1.7
175
+ }
176
+ ),
177
+ collapsed ? null : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label })
178
+ ]
179
+ }
180
+ ) }, item.label);
181
+ }) })
182
+ ] }, section.label))
183
+ }
184
+ )
185
+ ]
186
+ }
187
+ );
188
+ }
189
+ function CboxMobileNavDrawer({
190
+ open,
191
+ onClose,
192
+ appKey,
193
+ brandSubmark,
194
+ assetBase,
195
+ brandHref = "/",
196
+ sections,
197
+ currentPath,
198
+ LinkComponent
199
+ }) {
200
+ useEffect(() => {
201
+ if (!open) {
202
+ return;
203
+ }
204
+ const onKey = (e) => {
205
+ if (e.key === "Escape") {
206
+ onClose();
207
+ }
208
+ };
209
+ document.addEventListener("keydown", onKey);
210
+ const prevOverflow = document.body.style.overflow;
211
+ document.body.style.overflow = "hidden";
212
+ return () => {
213
+ document.removeEventListener("keydown", onKey);
214
+ document.body.style.overflow = prevOverflow;
215
+ };
216
+ }, [open, onClose]);
217
+ return /* @__PURE__ */ jsxs(
218
+ "div",
219
+ {
220
+ className: cn(
221
+ "fixed inset-0 z-40 lg:hidden",
222
+ open ? "" : "pointer-events-none"
223
+ ),
224
+ "aria-hidden": open ? "false" : "true",
225
+ children: [
226
+ /* @__PURE__ */ jsx(
227
+ "div",
228
+ {
229
+ onClick: onClose,
230
+ className: cn(
231
+ "absolute inset-0 bg-black/40 transition-opacity duration-200",
232
+ open ? "opacity-100" : "opacity-0"
233
+ )
234
+ }
235
+ ),
236
+ /* @__PURE__ */ jsxs(
237
+ "aside",
238
+ {
239
+ role: "dialog",
240
+ "aria-modal": "true",
241
+ "aria-label": "Navigation",
242
+ className: cn(
243
+ "absolute inset-y-0 left-0 flex w-[280px] max-w-[80vw] flex-col border-r border-sidebar-border bg-sidebar shadow-xl transition-transform duration-200 ease-out",
244
+ open ? "translate-x-0" : "-translate-x-full"
245
+ ),
246
+ children: [
247
+ /* @__PURE__ */ jsxs("div", { className: "flex h-14 shrink-0 items-center justify-between gap-2 border-b border-sidebar-border/70 px-4", children: [
248
+ /* @__PURE__ */ jsx(
249
+ LinkComponent,
250
+ {
251
+ href: brandHref,
252
+ "aria-label": "Home",
253
+ className: "flex items-center",
254
+ children: /* @__PURE__ */ jsx(
255
+ CboxBrand,
256
+ {
257
+ appKey,
258
+ submark: brandSubmark,
259
+ assetBase,
260
+ collapsed: false
261
+ }
262
+ )
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsx(
266
+ "button",
267
+ {
268
+ type: "button",
269
+ onClick: onClose,
270
+ "aria-label": "Close navigation",
271
+ className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground",
272
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", strokeWidth: 1.8 })
273
+ }
274
+ )
275
+ ] }),
276
+ /* @__PURE__ */ jsx("nav", { className: "flex-1 space-y-5 overflow-y-auto px-3 pt-5 pb-3", children: sections.map((section) => /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
277
+ /* @__PURE__ */ jsx("p", { className: "text-eyebrow px-3 pb-1", children: section.label }),
278
+ /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: section.items.map((item) => {
279
+ const active = item.matchPrefix === true ? currentPath === item.href || currentPath.startsWith(
280
+ item.href + "/"
281
+ ) : currentPath === item.href;
282
+ const Icon = item.icon;
283
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
284
+ LinkComponent,
285
+ {
286
+ href: item.href,
287
+ prefetch: true,
288
+ className: cn(
289
+ "group flex items-center gap-2.5 rounded-md px-3 py-2 text-[14px] transition-colors",
290
+ active ? "bg-sidebar-accent font-semibold text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-foreground"
291
+ ),
292
+ children: [
293
+ /* @__PURE__ */ jsx(
294
+ Icon,
295
+ {
296
+ className: cn(
297
+ "h-4 w-4 shrink-0",
298
+ active ? "text-foreground" : "text-sidebar-muted"
299
+ ),
300
+ strokeWidth: 1.7
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label })
304
+ ]
305
+ }
306
+ ) }, item.label);
307
+ }) })
308
+ ] }, section.label)) })
309
+ ]
310
+ }
311
+ )
312
+ ]
313
+ }
314
+ );
315
+ }
316
+ function AppSwitcher({ className, onLockedAppClick }) {
317
+ const cboxId = useCboxIdOptional();
318
+ const [open, setOpen] = useState(false);
319
+ const [query, setQuery] = useState("");
320
+ const [activeIndex, setActiveIndex] = useState(0);
321
+ const containerRef = useRef(null);
322
+ const searchRef = useRef(null);
323
+ useEffect(() => {
324
+ if (open) {
325
+ setQuery("");
326
+ setActiveIndex(0);
327
+ const id = requestAnimationFrame(() => searchRef.current?.focus());
328
+ return () => cancelAnimationFrame(id);
329
+ }
330
+ }, [open]);
331
+ useEffect(() => {
332
+ if (!open) {
333
+ return;
334
+ }
335
+ function onPointerDown(event) {
336
+ if (containerRef.current !== null && !containerRef.current.contains(event.target)) {
337
+ setOpen(false);
338
+ }
339
+ }
340
+ function onKey(event) {
341
+ if (event.key === "Escape") {
342
+ setOpen(false);
343
+ }
344
+ }
345
+ window.addEventListener("pointerdown", onPointerDown);
346
+ window.addEventListener("keydown", onKey);
347
+ return () => {
348
+ window.removeEventListener("pointerdown", onPointerDown);
349
+ window.removeEventListener("keydown", onKey);
350
+ };
351
+ }, [open]);
352
+ if (cboxId === null || cboxId.apps.length === 0) {
353
+ return null;
354
+ }
355
+ const apps = cboxId.apps;
356
+ const orgSlug = cboxId.currentOrganization?.slug;
357
+ const currentApp = apps.find((a) => a.current === true) ?? null;
358
+ const { entitled, locked, navigableHrefs } = useMemo(() => {
359
+ const q = query.trim().toLowerCase();
360
+ const matches = (app) => {
361
+ if (q === "") {
362
+ return true;
363
+ }
364
+ return app.name.toLowerCase().includes(q) || app.key.toLowerCase().includes(q) || (app.description?.toLowerCase().includes(q) ?? false);
365
+ };
366
+ const entitledList = [];
367
+ const lockedList = [];
368
+ for (const app of apps) {
369
+ if (!matches(app)) {
370
+ continue;
371
+ }
372
+ const isEntitled = app.entitled !== false && app.comingSoon !== true;
373
+ (isEntitled ? entitledList : lockedList).push(app);
374
+ }
375
+ const navigable = [];
376
+ for (const app of entitledList) {
377
+ if (app.current !== true) {
378
+ navigable.push({ app, isLocked: false });
379
+ }
380
+ }
381
+ for (const app of lockedList) {
382
+ if (app.comingSoon !== true) {
383
+ navigable.push({ app, isLocked: true });
384
+ }
385
+ }
386
+ return {
387
+ entitled: entitledList,
388
+ locked: lockedList,
389
+ navigableHrefs: navigable
390
+ };
391
+ }, [apps, query]);
392
+ const totalMatches = entitled.length + locked.length;
393
+ useEffect(() => {
394
+ if (activeIndex >= navigableHrefs.length) {
395
+ setActiveIndex(0);
396
+ }
397
+ }, [navigableHrefs.length, activeIndex]);
398
+ const triggerMonogram = currentApp?.monogram ?? currentApp?.name.slice(0, 1).toUpperCase() ?? "\u2B1B";
399
+ const triggerName = currentApp?.name ?? "Apps";
400
+ const triggerAccent = currentApp?.accentColor;
401
+ function onSearchKeyDown(event) {
402
+ if (event.key === "ArrowDown") {
403
+ event.preventDefault();
404
+ if (navigableHrefs.length === 0) {
405
+ return;
406
+ }
407
+ setActiveIndex((i) => (i + 1) % navigableHrefs.length);
408
+ return;
409
+ }
410
+ if (event.key === "ArrowUp") {
411
+ event.preventDefault();
412
+ if (navigableHrefs.length === 0) {
413
+ return;
414
+ }
415
+ setActiveIndex((i) => (i - 1 + navigableHrefs.length) % navigableHrefs.length);
416
+ return;
417
+ }
418
+ if (event.key === "Enter") {
419
+ event.preventDefault();
420
+ const target = navigableHrefs[activeIndex];
421
+ if (target === void 0) {
422
+ return;
423
+ }
424
+ if (target.isLocked) {
425
+ onLockedAppClick?.(target.app);
426
+ setOpen(false);
427
+ return;
428
+ }
429
+ const href = orgSlug !== void 0 && target.app.current !== true ? buildOrgUrl(target.app.url, orgSlug) : target.app.url;
430
+ window.location.href = href;
431
+ setOpen(false);
432
+ }
433
+ }
434
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: cn("relative", className), children: [
435
+ /* @__PURE__ */ jsxs(
436
+ "button",
437
+ {
438
+ type: "button",
439
+ "aria-haspopup": "true",
440
+ "aria-expanded": open,
441
+ onClick: () => setOpen((v) => !v),
442
+ className: "flex items-center gap-2 rounded-md border border-border bg-card px-2.5 py-1.5 text-sm font-medium text-foreground hover:bg-secondary",
443
+ children: [
444
+ /* @__PURE__ */ jsx(
445
+ "span",
446
+ {
447
+ style: triggerAccent !== void 0 ? {
448
+ backgroundColor: triggerAccent,
449
+ color: "#fff",
450
+ borderColor: triggerAccent
451
+ } : void 0,
452
+ className: "flex h-5 w-5 shrink-0 items-center justify-center rounded-sm border border-border bg-secondary text-[10px] font-bold",
453
+ children: currentApp?.iconUrl ? /* @__PURE__ */ jsx(
454
+ "img",
455
+ {
456
+ src: currentApp.iconUrl,
457
+ alt: "",
458
+ className: "h-3.5 w-3.5",
459
+ loading: "eager"
460
+ }
461
+ ) : triggerMonogram
462
+ }
463
+ ),
464
+ /* @__PURE__ */ jsx("span", { className: "max-w-[140px] truncate", children: triggerName }),
465
+ /* @__PURE__ */ jsx(Chevron, {})
466
+ ]
467
+ }
468
+ ),
469
+ open ? /* @__PURE__ */ jsxs(
470
+ "div",
471
+ {
472
+ role: "dialog",
473
+ "aria-label": "Switch app",
474
+ className: "absolute left-0 top-full z-50 mt-1 w-[min(22rem,calc(100vw-1.5rem))] overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
475
+ children: [
476
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 px-3 pt-2.5 pb-1.5", children: /* @__PURE__ */ jsx("p", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] text-muted-foreground uppercase", children: "Switch app" }) }),
477
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 px-2 py-1.5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md px-2 py-1 focus-within:bg-secondary", children: [
478
+ /* @__PURE__ */ jsx(SearchIcon, {}),
479
+ /* @__PURE__ */ jsx(
480
+ "input",
481
+ {
482
+ ref: searchRef,
483
+ type: "text",
484
+ value: query,
485
+ onChange: (e) => setQuery(e.target.value),
486
+ onKeyDown: onSearchKeyDown,
487
+ placeholder: "Filter apps\u2026",
488
+ "aria-label": "Filter apps",
489
+ autoComplete: "off",
490
+ spellCheck: false,
491
+ className: "w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
492
+ }
493
+ )
494
+ ] }) }),
495
+ /* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-y-auto py-1", children: [
496
+ totalMatches === 0 ? /* @__PURE__ */ jsxs("p", { className: "px-3 py-6 text-center text-[12px] text-muted-foreground", children: [
497
+ "No apps match",
498
+ " ",
499
+ /* @__PURE__ */ jsxs("span", { className: "text-mono text-foreground", children: [
500
+ "\u201C",
501
+ query,
502
+ "\u201D"
503
+ ] })
504
+ ] }) : null,
505
+ entitled.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1", children: entitled.map((app) => {
506
+ const navIndex = findNavIndex(navigableHrefs, app);
507
+ return /* @__PURE__ */ jsx(
508
+ AppRow,
509
+ {
510
+ app,
511
+ orgSlug,
512
+ highlighted: navIndex === activeIndex,
513
+ onMouseEnter: () => {
514
+ if (navIndex !== -1) {
515
+ setActiveIndex(navIndex);
516
+ }
517
+ },
518
+ onLockedAppClick,
519
+ onNavigate: () => setOpen(false)
520
+ },
521
+ app.key
522
+ );
523
+ }) }) : null,
524
+ locked.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
525
+ entitled.length > 0 ? /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border/50" }) : null,
526
+ /* @__PURE__ */ jsx("p", { className: "text-mono px-3 py-1.5 text-[10px] font-semibold tracking-[0.18em] text-muted-foreground/70 uppercase", children: "Available" }),
527
+ /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1 pt-0", children: locked.map((app) => {
528
+ const navIndex = findNavIndex(navigableHrefs, app);
529
+ return /* @__PURE__ */ jsx(
530
+ AppRow,
531
+ {
532
+ app,
533
+ orgSlug,
534
+ highlighted: navIndex === activeIndex,
535
+ onMouseEnter: () => {
536
+ if (navIndex !== -1) {
537
+ setActiveIndex(navIndex);
538
+ }
539
+ },
540
+ onLockedAppClick,
541
+ onNavigate: () => setOpen(false)
542
+ },
543
+ app.key
544
+ );
545
+ }) })
546
+ ] }) : null
547
+ ] })
548
+ ]
549
+ }
550
+ ) : null
551
+ ] });
552
+ }
553
+ function findNavIndex(navigable, app) {
554
+ return navigable.findIndex((n) => n.app.key === app.key);
555
+ }
556
+ function AppRow({
557
+ app,
558
+ orgSlug,
559
+ highlighted,
560
+ onMouseEnter,
561
+ onLockedAppClick,
562
+ onNavigate
563
+ }) {
564
+ const isComingSoon = app.comingSoon === true;
565
+ const isLocked = app.entitled === false || isComingSoon;
566
+ const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
567
+ const accent = app.accentColor;
568
+ const tileStyle = accent !== void 0 && app.iconUrl === void 0 ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
569
+ const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl(app.url, orgSlug) : app.url;
570
+ const tile = /* @__PURE__ */ jsx(
571
+ "div",
572
+ {
573
+ style: tileStyle,
574
+ className: cn(
575
+ "flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border bg-card text-[13px] font-semibold text-foreground",
576
+ app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
577
+ isLocked && "opacity-60 grayscale"
578
+ ),
579
+ children: app.iconUrl ? /* @__PURE__ */ jsx("img", { src: app.iconUrl, alt: "", className: "h-5 w-5", loading: "lazy" }) : /* @__PURE__ */ jsx("span", { children: monogram })
580
+ }
581
+ );
582
+ const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
583
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
584
+ /* @__PURE__ */ jsx(
585
+ "span",
586
+ {
587
+ className: cn(
588
+ "truncate text-sm font-medium text-foreground",
589
+ isLocked && "text-muted-foreground"
590
+ ),
591
+ children: app.name
592
+ }
593
+ ),
594
+ app.current ? /* @__PURE__ */ jsx("span", { className: "text-mono shrink-0 rounded-sm border border-accent-edge/40 bg-accent-soft px-1 text-[9px] font-semibold tracking-[0.14em] text-accent uppercase", children: "current" }) : null
595
+ ] }),
596
+ app.description ? /* @__PURE__ */ jsx(
597
+ "span",
598
+ {
599
+ className: cn(
600
+ "truncate text-[11px]",
601
+ isLocked ? "text-muted-foreground/70" : "text-muted-foreground"
602
+ ),
603
+ children: app.description
604
+ }
605
+ ) : null
606
+ ] });
607
+ const trailing = isComingSoon ? /* @__PURE__ */ jsx("span", { className: "text-mono shrink-0 rounded-sm border border-border bg-secondary px-1.5 py-0.5 text-[9px] font-semibold tracking-[0.14em] text-muted-foreground uppercase", children: "soon" }) : isLocked ? /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1 text-[10px] font-medium text-muted-foreground", children: [
608
+ /* @__PURE__ */ jsx(Lock, {}),
609
+ "Add"
610
+ ] }) : null;
611
+ if (isComingSoon) {
612
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
613
+ "div",
614
+ {
615
+ "aria-disabled": true,
616
+ className: "flex w-full cursor-not-allowed items-center gap-3 rounded-md px-2.5 py-2",
617
+ title: `${app.name} \u2014 coming soon`,
618
+ children: [
619
+ tile,
620
+ body,
621
+ trailing
622
+ ]
623
+ }
624
+ ) });
625
+ }
626
+ if (isLocked) {
627
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
628
+ "button",
629
+ {
630
+ type: "button",
631
+ onMouseEnter,
632
+ onClick: () => {
633
+ onLockedAppClick?.(app);
634
+ onNavigate();
635
+ },
636
+ className: cn(
637
+ "group flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left transition-colors",
638
+ highlighted && "bg-secondary",
639
+ onLockedAppClick !== void 0 ? "hover:bg-secondary" : "cursor-default"
640
+ ),
641
+ title: `Add ${app.name} to your plan`,
642
+ children: [
643
+ tile,
644
+ body,
645
+ trailing
646
+ ]
647
+ }
648
+ ) });
649
+ }
650
+ if (app.current === true) {
651
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
652
+ "div",
653
+ {
654
+ onMouseEnter,
655
+ className: "flex w-full items-center gap-3 rounded-md bg-accent-soft px-2.5 py-2",
656
+ children: [
657
+ tile,
658
+ body,
659
+ trailing
660
+ ]
661
+ }
662
+ ) });
663
+ }
664
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
665
+ "a",
666
+ {
667
+ href,
668
+ onMouseEnter,
669
+ onClick: onNavigate,
670
+ className: cn(
671
+ "group flex w-full items-center gap-3 rounded-md px-2.5 py-2 transition-colors hover:bg-secondary",
672
+ highlighted && "bg-secondary"
673
+ ),
674
+ children: [
675
+ tile,
676
+ body,
677
+ trailing
678
+ ]
679
+ }
680
+ ) });
681
+ }
682
+ function buildOrgUrl(url, orgSlug) {
683
+ if (url === "" || orgSlug === "") {
684
+ return url;
685
+ }
686
+ try {
687
+ const u = new URL(url);
688
+ u.pathname = `/org/${encodeURIComponent(orgSlug)}`;
689
+ return u.toString();
690
+ } catch {
691
+ return `${url.replace(/\/$/, "")}/org/${encodeURIComponent(orgSlug)}`;
692
+ }
693
+ }
694
+ function Chevron() {
695
+ return /* @__PURE__ */ jsx(
696
+ "svg",
697
+ {
698
+ width: "10",
699
+ height: "10",
700
+ viewBox: "0 0 10 10",
701
+ fill: "none",
702
+ xmlns: "http://www.w3.org/2000/svg",
703
+ "aria-hidden": true,
704
+ children: /* @__PURE__ */ jsx(
705
+ "path",
706
+ {
707
+ d: "M2 4l3 3 3-3",
708
+ stroke: "currentColor",
709
+ strokeWidth: "1.4",
710
+ strokeLinecap: "round",
711
+ strokeLinejoin: "round"
712
+ }
713
+ )
714
+ }
715
+ );
716
+ }
717
+ function SearchIcon() {
718
+ return /* @__PURE__ */ jsxs(
719
+ "svg",
720
+ {
721
+ width: "12",
722
+ height: "12",
723
+ viewBox: "0 0 12 12",
724
+ fill: "none",
725
+ stroke: "currentColor",
726
+ strokeWidth: "1.4",
727
+ strokeLinecap: "round",
728
+ strokeLinejoin: "round",
729
+ "aria-hidden": true,
730
+ className: "shrink-0 text-muted-foreground",
731
+ children: [
732
+ /* @__PURE__ */ jsx("circle", { cx: "5.25", cy: "5.25", r: "3.25" }),
733
+ /* @__PURE__ */ jsx("path", { d: "M8 8l2 2" })
734
+ ]
735
+ }
736
+ );
737
+ }
738
+ function Lock() {
739
+ return /* @__PURE__ */ jsxs(
740
+ "svg",
741
+ {
742
+ width: "10",
743
+ height: "10",
744
+ viewBox: "0 0 12 12",
745
+ fill: "none",
746
+ stroke: "currentColor",
747
+ strokeWidth: "1.6",
748
+ strokeLinecap: "round",
749
+ strokeLinejoin: "round",
750
+ "aria-hidden": true,
751
+ children: [
752
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "5", width: "7", height: "5", rx: "1" }),
753
+ /* @__PURE__ */ jsx("path", { d: "M4 5V3.5a2 2 0 1 1 4 0V5" })
754
+ ]
755
+ }
756
+ );
757
+ }
758
+ function EmptyState({
759
+ icon: Icon,
760
+ title,
761
+ description,
762
+ action,
763
+ className
764
+ }) {
765
+ return /* @__PURE__ */ jsxs(
766
+ "div",
767
+ {
768
+ className: cn(
769
+ "flex flex-col items-center justify-center gap-3 px-6 py-12 text-center",
770
+ className
771
+ ),
772
+ children: [
773
+ Icon ? /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-md bg-secondary text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4", strokeWidth: 1.6 }) }) : null,
774
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
775
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-foreground", children: title }),
776
+ description ? /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }) : null
777
+ ] }),
778
+ action ? /* @__PURE__ */ jsx("div", { className: "pt-1", children: action }) : null
779
+ ]
780
+ }
781
+ );
782
+ }
783
+ var toneStyles = {
784
+ neutral: "border-border bg-secondary text-secondary-foreground",
785
+ info: "border-info/20 bg-info-soft text-info",
786
+ success: "border-success/20 bg-success-soft text-success",
787
+ warning: "border-warning/20 bg-warning-soft text-warning",
788
+ destructive: "border-destructive/20 bg-destructive-soft text-destructive",
789
+ muted: "border-border bg-card text-muted-foreground"
790
+ };
791
+ var dotStyles = {
792
+ neutral: "bg-muted-foreground",
793
+ info: "bg-info",
794
+ success: "bg-success",
795
+ warning: "bg-warning",
796
+ destructive: "bg-destructive",
797
+ muted: "bg-muted-foreground/60"
798
+ };
799
+ function StatusPill({
800
+ children,
801
+ tone = "neutral",
802
+ className,
803
+ dot = false
804
+ }) {
805
+ return /* @__PURE__ */ jsxs(
806
+ "span",
807
+ {
808
+ className: cn(
809
+ "inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] leading-none font-medium",
810
+ toneStyles[tone],
811
+ className
812
+ ),
813
+ children: [
814
+ dot ? /* @__PURE__ */ jsx(
815
+ "span",
816
+ {
817
+ className: cn("h-1.5 w-1.5 rounded-full", dotStyles[tone])
818
+ }
819
+ ) : null,
820
+ children
821
+ ]
822
+ }
823
+ );
824
+ }
825
+ function SurfaceModal({
826
+ open,
827
+ onClose,
828
+ title,
829
+ eyebrow,
830
+ headerActions,
831
+ footer,
832
+ size = "wide",
833
+ children
834
+ }) {
835
+ const dialogRef = useRef(null);
836
+ const previousFocusRef = useRef(null);
837
+ useEffect(() => {
838
+ if (!open) return;
839
+ const original = document.body.style.overflow;
840
+ const gutter = window.innerWidth - document.documentElement.clientWidth;
841
+ document.body.style.overflow = "hidden";
842
+ if (gutter > 0) {
843
+ document.body.style.paddingRight = `${gutter}px`;
844
+ }
845
+ return () => {
846
+ document.body.style.overflow = original;
847
+ document.body.style.paddingRight = "";
848
+ };
849
+ }, [open]);
850
+ useEffect(() => {
851
+ if (!open) return;
852
+ previousFocusRef.current = document.activeElement;
853
+ const onKey = (e) => {
854
+ if (e.key === "Escape") {
855
+ e.preventDefault();
856
+ onClose();
857
+ return;
858
+ }
859
+ if (e.key === "Tab" && dialogRef.current !== null) {
860
+ const focusable = dialogRef.current.querySelectorAll(
861
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
862
+ );
863
+ if (focusable.length === 0) return;
864
+ const first = focusable[0];
865
+ const last = focusable[focusable.length - 1];
866
+ if (first === void 0 || last === void 0) return;
867
+ if (e.shiftKey && document.activeElement === first) {
868
+ e.preventDefault();
869
+ last.focus();
870
+ } else if (!e.shiftKey && document.activeElement === last) {
871
+ e.preventDefault();
872
+ first.focus();
873
+ }
874
+ }
875
+ };
876
+ document.addEventListener("keydown", onKey);
877
+ const id = window.setTimeout(() => {
878
+ const focusable = dialogRef.current?.querySelector(
879
+ 'input, button, [tabindex]:not([tabindex="-1"])'
880
+ );
881
+ focusable?.focus();
882
+ }, 50);
883
+ return () => {
884
+ document.removeEventListener("keydown", onKey);
885
+ window.clearTimeout(id);
886
+ previousFocusRef.current?.focus?.();
887
+ };
888
+ }, [open, onClose]);
889
+ const onBackdrop = useCallback(
890
+ (e) => {
891
+ if (e.target === e.currentTarget) onClose();
892
+ },
893
+ [onClose]
894
+ );
895
+ if (!open) return null;
896
+ return /* @__PURE__ */ jsx(
897
+ "div",
898
+ {
899
+ role: "presentation",
900
+ onMouseDown: onBackdrop,
901
+ className: cn(
902
+ "fixed inset-0 z-50 flex animate-in fade-in duration-150",
903
+ size === "spotlight" ? "items-start justify-center pt-[10vh]" : "items-center justify-center px-4 py-[5vh]",
904
+ "bg-foreground/40 backdrop-blur-sm"
905
+ ),
906
+ children: /* @__PURE__ */ jsxs(
907
+ "div",
908
+ {
909
+ ref: dialogRef,
910
+ role: "dialog",
911
+ "aria-modal": "true",
912
+ "aria-label": typeof title === "string" ? title : void 0,
913
+ className: cn(
914
+ "flex max-h-full w-full flex-col overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-2xl",
915
+ "animate-in zoom-in-95 slide-in-from-bottom-2 duration-150",
916
+ size === "spotlight" ? "max-w-[640px]" : "max-w-[960px]"
917
+ ),
918
+ children: [
919
+ /* @__PURE__ */ jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b border-border/70 bg-background/80 px-5 py-4 backdrop-blur-md", children: [
920
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
921
+ eyebrow !== void 0 ? /* @__PURE__ */ jsx("p", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] uppercase text-muted-foreground", children: eyebrow }) : null,
922
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold tracking-tight text-foreground", children: title })
923
+ ] }),
924
+ headerActions !== void 0 ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: headerActions }) : null,
925
+ /* @__PURE__ */ jsx(
926
+ "button",
927
+ {
928
+ type: "button",
929
+ onClick: onClose,
930
+ className: "ml-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground",
931
+ "aria-label": "Close",
932
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", strokeWidth: 1.8 })
933
+ }
934
+ )
935
+ ] }),
936
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children }),
937
+ footer !== void 0 ? /* @__PURE__ */ jsx("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border/70 bg-background/80 px-5 py-3 text-xs text-muted-foreground backdrop-blur-md", children: footer }) : null
938
+ ]
939
+ }
940
+ )
941
+ }
942
+ );
943
+ }
944
+ var TONES = {
945
+ info: "info",
946
+ warning: "warning",
947
+ critical: "destructive"
948
+ };
949
+ function InboxSurface({ open, onClose }) {
950
+ const transport = useTransport();
951
+ const cboxId = useCboxIdOptional();
952
+ const [inbox, setInbox] = useState(null);
953
+ const [loading, setLoading] = useState(false);
954
+ const [error, setError] = useState(null);
955
+ const [filter, setFilter] = useState("all");
956
+ const userId = cboxId?.user.id ?? null;
957
+ const baseUrl = transport.urls.notifications ?? null;
958
+ useEffect(() => {
959
+ if (!open || userId === null || baseUrl === null) return;
960
+ let cancelled = false;
961
+ const load = async () => {
962
+ setLoading(true);
963
+ setError(null);
964
+ try {
965
+ const headers = await authHeaders(transport, "notifications");
966
+ const response = await fetch(
967
+ `${baseUrl}/api/v1/recipients/${userId}/inbox?limit=100`,
968
+ { headers, credentials: "include" }
969
+ );
970
+ if (!response.ok) {
971
+ throw new Error(`HTTP ${response.status}`);
972
+ }
973
+ const body = await response.json();
974
+ if (!cancelled) setInbox(body);
975
+ } catch (e) {
976
+ if (!cancelled) {
977
+ setError(e instanceof Error ? e.message : "Load failed");
978
+ }
979
+ } finally {
980
+ if (!cancelled) setLoading(false);
981
+ }
982
+ };
983
+ void load();
984
+ return () => {
985
+ cancelled = true;
986
+ };
987
+ }, [open, userId, baseUrl, transport]);
988
+ const markRead = async (id) => {
989
+ if (inbox === null || userId === null || baseUrl === null) return;
990
+ const prev = inbox;
991
+ setInbox({
992
+ ...inbox,
993
+ items: inbox.items.map(
994
+ (it) => it.id === id ? { ...it, read_at: (/* @__PURE__ */ new Date()).toISOString() } : it
995
+ ),
996
+ unread_count: Math.max(0, inbox.unread_count - 1)
997
+ });
998
+ try {
999
+ const headers = await authHeaders(transport, "notifications");
1000
+ const csrf = transport.csrfToken?.();
1001
+ if (csrf !== void 0 && csrf !== "") {
1002
+ headers["X-CSRF-TOKEN"] = csrf;
1003
+ headers["X-Requested-With"] = "XMLHttpRequest";
1004
+ }
1005
+ const response = await fetch(
1006
+ `${baseUrl}/api/v1/recipients/${userId}/notifications/${id}/read`,
1007
+ {
1008
+ method: "POST",
1009
+ headers,
1010
+ credentials: "include"
1011
+ }
1012
+ );
1013
+ if (!response.ok) {
1014
+ throw new Error(`HTTP ${response.status}`);
1015
+ }
1016
+ } catch {
1017
+ setInbox(prev);
1018
+ }
1019
+ };
1020
+ const items = inbox?.items ?? [];
1021
+ const filtered = filter === "unread" ? items.filter((i) => i.read_at === null) : items;
1022
+ const unread = inbox?.unread_count ?? 0;
1023
+ return /* @__PURE__ */ jsx(
1024
+ SurfaceModal,
1025
+ {
1026
+ open,
1027
+ onClose,
1028
+ eyebrow: "Notifications",
1029
+ title: unread > 0 ? `Inbox \xB7 ${unread} unread` : "Inbox \xB7 all caught up",
1030
+ headerActions: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1031
+ /* @__PURE__ */ jsx(
1032
+ FilterTab,
1033
+ {
1034
+ active: filter === "all",
1035
+ onClick: () => setFilter("all"),
1036
+ children: "All"
1037
+ }
1038
+ ),
1039
+ /* @__PURE__ */ jsx(
1040
+ FilterTab,
1041
+ {
1042
+ active: filter === "unread",
1043
+ onClick: () => setFilter("unread"),
1044
+ children: "Unread"
1045
+ }
1046
+ )
1047
+ ] }),
1048
+ footer: inbox !== null && baseUrl !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [
1049
+ /* @__PURE__ */ jsxs("span", { children: [
1050
+ "Showing ",
1051
+ filtered.length,
1052
+ " of ",
1053
+ items.length,
1054
+ "."
1055
+ ] }),
1056
+ /* @__PURE__ */ jsx(
1057
+ "a",
1058
+ {
1059
+ href: `${baseUrl}/inbox`,
1060
+ target: "_blank",
1061
+ rel: "noreferrer",
1062
+ className: "font-medium text-foreground hover:underline",
1063
+ children: "Open in new tab \u2197"
1064
+ }
1065
+ )
1066
+ ] }) : /* @__PURE__ */ jsx("span", { children: "\u2014" }),
1067
+ children: error !== null ? /* @__PURE__ */ jsxs("div", { className: "px-5 py-8 text-center", children: [
1068
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-destructive", children: "Couldn't load inbox" }),
1069
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: error })
1070
+ ] }) : loading && inbox === null ? /* @__PURE__ */ jsx("div", { className: "px-5 py-12 text-center text-sm text-muted-foreground", children: "Loading\u2026" }) : filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-5 py-12", children: /* @__PURE__ */ jsx(
1071
+ EmptyState,
1072
+ {
1073
+ title: filter === "unread" ? "No unread notifications" : "No notifications yet",
1074
+ description: "When a Cbox app sends you something \u2014 invitation accepted, billing event, security alert \u2014 it lands here."
1075
+ }
1076
+ ) }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/70", children: filtered.map((item) => {
1077
+ const data = item.data ?? {};
1078
+ const ctaUrl = data.cta_url;
1079
+ const InnerWrapper = ({
1080
+ children
1081
+ }) => ctaUrl !== void 0 && ctaUrl !== "" ? /* @__PURE__ */ jsx(
1082
+ "a",
1083
+ {
1084
+ href: ctaUrl,
1085
+ className: "block transition-colors hover:bg-secondary/40",
1086
+ children
1087
+ }
1088
+ ) : /* @__PURE__ */ jsx("div", { children });
1089
+ return /* @__PURE__ */ jsx(
1090
+ "li",
1091
+ {
1092
+ className: item.read_at === null ? "bg-secondary/20" : "",
1093
+ children: /* @__PURE__ */ jsx(InnerWrapper, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 px-5 py-3", children: [
1094
+ /* @__PURE__ */ jsx(
1095
+ StatusPill,
1096
+ {
1097
+ tone: TONES[item.severity],
1098
+ dot: true,
1099
+ children: item.severity
1100
+ }
1101
+ ),
1102
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1103
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: data.title ?? item.event_type }),
1104
+ (data.body ?? "") !== "" ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-2 text-xs text-muted-foreground", children: data.body }) : null,
1105
+ /* @__PURE__ */ jsxs("p", { className: "text-mono mt-1 text-[10px] text-muted-foreground", children: [
1106
+ item.event_type,
1107
+ " \xB7 ",
1108
+ new Date(
1109
+ item.created_at
1110
+ ).toLocaleString()
1111
+ ] })
1112
+ ] }),
1113
+ item.read_at === null ? /* @__PURE__ */ jsx(
1114
+ "button",
1115
+ {
1116
+ type: "button",
1117
+ onClick: (e) => {
1118
+ e.preventDefault();
1119
+ e.stopPropagation();
1120
+ void markRead(item.id);
1121
+ },
1122
+ className: "shrink-0 rounded-md border border-border bg-card px-2 py-1 text-xs font-medium text-foreground transition-colors hover:border-border-strong",
1123
+ "aria-label": "Mark as read",
1124
+ children: "Mark read"
1125
+ }
1126
+ ) : null
1127
+ ] }) })
1128
+ },
1129
+ item.id
1130
+ );
1131
+ }) })
1132
+ }
1133
+ );
1134
+ }
1135
+ async function authHeaders(transport, audience) {
1136
+ const headers = {
1137
+ Accept: "application/json"
1138
+ };
1139
+ try {
1140
+ const { token } = await transport.getToken(audience);
1141
+ if (token !== "") {
1142
+ headers.Authorization = `Bearer ${token}`;
1143
+ }
1144
+ } catch {
1145
+ }
1146
+ return headers;
1147
+ }
1148
+ function FilterTab({
1149
+ active,
1150
+ onClick,
1151
+ children
1152
+ }) {
1153
+ return /* @__PURE__ */ jsx(
1154
+ "button",
1155
+ {
1156
+ type: "button",
1157
+ onClick,
1158
+ className: active ? "rounded-md bg-secondary px-2 py-1 text-xs font-medium text-foreground" : "rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-secondary/60 hover:text-foreground",
1159
+ "aria-pressed": active,
1160
+ children
1161
+ }
1162
+ );
1163
+ }
1164
+ var TransportContext = createContext(null);
1165
+ var SurfaceContext = createContext(null);
1166
+ function CboxSurfaces({
1167
+ transport,
1168
+ surfaces,
1169
+ config,
1170
+ children
1171
+ }) {
1172
+ const enabled = useMemo(() => new Set(surfaces), [surfaces]);
1173
+ const [active, setActive] = useState(null);
1174
+ const open = useCallback(
1175
+ (id, args) => {
1176
+ if (!enabled.has(id)) {
1177
+ console.warn(
1178
+ `[cbox-ui] surface "${id}" is not enabled in this host. Add it to <CboxSurfaces surfaces={[\u2026]}>.`
1179
+ );
1180
+ return;
1181
+ }
1182
+ setActive({ id, args });
1183
+ },
1184
+ [enabled]
1185
+ );
1186
+ const close = useCallback(() => setActive(null), []);
1187
+ const controller = useMemo(
1188
+ () => ({
1189
+ activeSurface: active?.id ?? null,
1190
+ open,
1191
+ close
1192
+ }),
1193
+ [active, open, close]
1194
+ );
1195
+ return /* @__PURE__ */ jsx(TransportContext.Provider, { value: transport, children: /* @__PURE__ */ jsxs(SurfaceContext.Provider, { value: controller, children: [
1196
+ children,
1197
+ /* @__PURE__ */ jsx(
1198
+ InboxSurface,
1199
+ {
1200
+ open: active?.id === "inbox",
1201
+ onClose: close
1202
+ }
1203
+ )
1204
+ ] }) });
1205
+ }
1206
+ function useCboxSurfaces() {
1207
+ const ctx = useContext(SurfaceContext);
1208
+ if (ctx === null) {
1209
+ throw new Error(
1210
+ "[cbox-ui] useCboxSurfaces must be used inside <CboxSurfaces>. Did the host forget to mount it?"
1211
+ );
1212
+ }
1213
+ return ctx;
1214
+ }
1215
+ function useCboxSurfacesOptional() {
1216
+ return useContext(SurfaceContext);
1217
+ }
1218
+ function useTransport() {
1219
+ const t = useContext(TransportContext);
1220
+ if (t === null) {
1221
+ throw new Error(
1222
+ "[cbox-ui] useTransport must be used inside <CboxSurfaces>."
1223
+ );
1224
+ }
1225
+ return t;
1226
+ }
1227
+ function NotificationsBell({ className }) {
1228
+ const ctx = useCboxIdOptional();
1229
+ const surfaces = useCboxSurfacesOptional();
1230
+ const [open, setOpen] = useState(false);
1231
+ const ref = useRef(null);
1232
+ useEffect(() => {
1233
+ if (!open) return;
1234
+ const onDocClick = (e) => {
1235
+ const target = e.target;
1236
+ if (target instanceof Node && ref.current !== null && !ref.current.contains(target)) {
1237
+ setOpen(false);
1238
+ }
1239
+ };
1240
+ const onEsc = (e) => {
1241
+ if (e.key === "Escape") setOpen(false);
1242
+ };
1243
+ document.addEventListener("mousedown", onDocClick);
1244
+ document.addEventListener("keydown", onEsc);
1245
+ return () => {
1246
+ document.removeEventListener("mousedown", onDocClick);
1247
+ document.removeEventListener("keydown", onEsc);
1248
+ };
1249
+ }, [open]);
1250
+ const notificationsData = ctx === null ? null : ctx.notifications ?? null;
1251
+ if (ctx === null) {
1252
+ return null;
1253
+ }
1254
+ if (notificationsData === null) {
1255
+ return /* @__PURE__ */ jsx("div", { className: cn("relative", className), children: /* @__PURE__ */ jsx(
1256
+ "button",
1257
+ {
1258
+ type: "button",
1259
+ "aria-label": "Notifications",
1260
+ disabled: true,
1261
+ className: "relative flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground opacity-60 cursor-default",
1262
+ children: /* @__PURE__ */ jsx(Bell, {})
1263
+ }
1264
+ ) });
1265
+ }
1266
+ const { unreadCount, inboxUrl, previewItems, markReadUrl } = notificationsData;
1267
+ const display = unreadCount > 99 ? "99+" : unreadCount.toString();
1268
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("relative", className), children: [
1269
+ /* @__PURE__ */ jsxs(
1270
+ "button",
1271
+ {
1272
+ type: "button",
1273
+ onClick: () => setOpen((v) => !v),
1274
+ "aria-label": unreadCount > 0 ? `${unreadCount} unread notifications` : "Notifications",
1275
+ "aria-expanded": open,
1276
+ "aria-haspopup": "menu",
1277
+ className: "relative flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground data-[open=true]:bg-secondary data-[open=true]:text-foreground",
1278
+ "data-open": open ? "true" : "false",
1279
+ children: [
1280
+ /* @__PURE__ */ jsx(Bell, {}),
1281
+ unreadCount > 0 ? /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 flex min-w-[16px] items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold leading-4 text-destructive-foreground", children: display }) : null
1282
+ ]
1283
+ }
1284
+ ),
1285
+ open ? /* @__PURE__ */ jsxs(
1286
+ "div",
1287
+ {
1288
+ role: "menu",
1289
+ className: "absolute right-0 z-30 mt-2 w-[360px] origin-top-right rounded-md border border-border bg-popover text-popover-foreground shadow-lg",
1290
+ children: [
1291
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-border/70 px-3 py-2", children: [
1292
+ /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] uppercase text-muted-foreground", children: "Notifications" }),
1293
+ unreadCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "text-mono text-[10px] tabular-nums text-muted-foreground", children: [
1294
+ unreadCount,
1295
+ " unread"
1296
+ ] }) : null
1297
+ ] }),
1298
+ /* @__PURE__ */ jsx("div", { className: "max-h-[420px] overflow-y-auto", children: previewItems === void 0 || previewItems.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "px-3 py-8 text-center", children: [
1299
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: "All caught up" }),
1300
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "No recent notifications." })
1301
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/70", children: previewItems.map((item) => /* @__PURE__ */ jsx(
1302
+ PreviewRow,
1303
+ {
1304
+ item,
1305
+ markReadUrl,
1306
+ onMarkedRead: () => setOpen(false)
1307
+ },
1308
+ item.id
1309
+ )) }) }),
1310
+ surfaces !== null ? /* @__PURE__ */ jsx(
1311
+ "button",
1312
+ {
1313
+ type: "button",
1314
+ onClick: () => {
1315
+ setOpen(false);
1316
+ surfaces.open("inbox");
1317
+ },
1318
+ className: "flex w-full items-center justify-center border-t border-border/70 px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary",
1319
+ children: "See all notifications \u2192"
1320
+ }
1321
+ ) : /* @__PURE__ */ jsx(
1322
+ "a",
1323
+ {
1324
+ href: inboxUrl,
1325
+ className: "flex items-center justify-center border-t border-border/70 px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary",
1326
+ children: "See all notifications \u2192"
1327
+ }
1328
+ )
1329
+ ]
1330
+ }
1331
+ ) : null
1332
+ ] });
1333
+ }
1334
+ function PreviewRow({
1335
+ item,
1336
+ markReadUrl,
1337
+ onMarkedRead
1338
+ }) {
1339
+ const tone = item.severity === "critical" ? "bg-destructive" : item.severity === "warning" ? "bg-warning" : "bg-info";
1340
+ const ts = (() => {
1341
+ try {
1342
+ return new Date(item.createdAt).toLocaleString();
1343
+ } catch {
1344
+ return item.createdAt;
1345
+ }
1346
+ })();
1347
+ const markRead = async (e) => {
1348
+ e.preventDefault();
1349
+ e.stopPropagation();
1350
+ if (markReadUrl === void 0 || markReadUrl === "") return;
1351
+ const url = markReadUrl.replace("{id}", item.id);
1352
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
1353
+ try {
1354
+ await fetch(url, {
1355
+ method: "POST",
1356
+ headers: {
1357
+ Accept: "application/json",
1358
+ "X-CSRF-TOKEN": csrf,
1359
+ "X-Requested-With": "XMLHttpRequest"
1360
+ }
1361
+ });
1362
+ } catch {
1363
+ }
1364
+ onMarkedRead();
1365
+ };
1366
+ const Wrapper = ({ children }) => item.ctaUrl !== null && item.ctaUrl !== void 0 && item.ctaUrl !== "" ? /* @__PURE__ */ jsx(
1367
+ "a",
1368
+ {
1369
+ href: item.ctaUrl,
1370
+ className: "block transition-colors hover:bg-secondary/50",
1371
+ children
1372
+ }
1373
+ ) : /* @__PURE__ */ jsx("div", { children });
1374
+ return /* @__PURE__ */ jsx("li", { className: item.readAt === null ? "bg-secondary/20" : "", children: /* @__PURE__ */ jsx(Wrapper, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 px-3 py-2.5", children: [
1375
+ /* @__PURE__ */ jsx(
1376
+ "span",
1377
+ {
1378
+ "aria-hidden": true,
1379
+ className: cn(
1380
+ "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full",
1381
+ tone
1382
+ )
1383
+ }
1384
+ ),
1385
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1386
+ /* @__PURE__ */ jsx("p", { className: "truncate text-[13px] font-medium text-foreground", children: item.title }),
1387
+ item.body !== void 0 && item.body !== null && item.body !== "" ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 line-clamp-2 text-xs text-muted-foreground", children: item.body }) : null,
1388
+ /* @__PURE__ */ jsx("p", { className: "text-mono mt-1 text-[10px] text-muted-foreground", children: ts })
1389
+ ] }),
1390
+ item.readAt === null ? /* @__PURE__ */ jsx(
1391
+ "button",
1392
+ {
1393
+ type: "button",
1394
+ onClick: markRead,
1395
+ className: "shrink-0 rounded-md px-2 py-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground",
1396
+ "aria-label": "Mark as read",
1397
+ title: "Mark as read",
1398
+ children: "\u2713"
1399
+ }
1400
+ ) : null
1401
+ ] }) }) });
1402
+ }
1403
+ function Bell() {
1404
+ return /* @__PURE__ */ jsxs(
1405
+ "svg",
1406
+ {
1407
+ width: "16",
1408
+ height: "16",
1409
+ viewBox: "0 0 16 16",
1410
+ fill: "none",
1411
+ "aria-hidden": true,
1412
+ children: [
1413
+ /* @__PURE__ */ jsx(
1414
+ "path",
1415
+ {
1416
+ d: "M4 6.5a4 4 0 1 1 8 0v2.4l1.2 2H2.8l1.2-2V6.5z",
1417
+ stroke: "currentColor",
1418
+ strokeWidth: "1.4",
1419
+ strokeLinejoin: "round"
1420
+ }
1421
+ ),
1422
+ /* @__PURE__ */ jsx(
1423
+ "path",
1424
+ {
1425
+ d: "M6.5 12.5a1.5 1.5 0 0 0 3 0",
1426
+ stroke: "currentColor",
1427
+ strokeWidth: "1.4",
1428
+ strokeLinecap: "round"
1429
+ }
1430
+ )
1431
+ ]
1432
+ }
1433
+ );
1434
+ }
1435
+ function SearchTrigger({ onOpen, className, label = "Search" }) {
1436
+ const [shortcut, setShortcut] = useState("\u2318K");
1437
+ useEffect(() => {
1438
+ if (typeof navigator === "undefined") return;
1439
+ setShortcut(/Mac/.test(navigator.platform) ? "\u2318K" : "Ctrl K");
1440
+ }, []);
1441
+ return /* @__PURE__ */ jsxs(
1442
+ "button",
1443
+ {
1444
+ type: "button",
1445
+ onClick: onOpen,
1446
+ className: cn(
1447
+ "hidden h-8 w-[180px] items-center gap-2 rounded-md border border-border bg-card pr-1.5 pl-2.5 text-xs text-muted-foreground transition-colors hover:border-border-strong hover:text-foreground sm:inline-flex",
1448
+ className
1449
+ ),
1450
+ "aria-label": "Open command palette",
1451
+ children: [
1452
+ /* @__PURE__ */ jsx(SearchIcon2, {}),
1453
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: label }),
1454
+ /* @__PURE__ */ jsx("kbd", { className: "text-mono ml-1 rounded-sm border border-border bg-secondary px-1 py-0.5 text-[10px] font-medium tracking-tight text-muted-foreground", children: shortcut })
1455
+ ]
1456
+ }
1457
+ );
1458
+ }
1459
+ function SearchIcon2() {
1460
+ return /* @__PURE__ */ jsxs(
1461
+ "svg",
1462
+ {
1463
+ width: "14",
1464
+ height: "14",
1465
+ viewBox: "0 0 24 24",
1466
+ fill: "none",
1467
+ stroke: "currentColor",
1468
+ strokeWidth: "1.8",
1469
+ strokeLinecap: "round",
1470
+ strokeLinejoin: "round",
1471
+ "aria-hidden": true,
1472
+ children: [
1473
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
1474
+ /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
1475
+ ]
1476
+ }
1477
+ );
1478
+ }
1479
+ function UserMenu({ extras, className }) {
1480
+ const { user, currentOrganization, urls } = useCboxId();
1481
+ const [open, setOpen] = useState(false);
1482
+ const initials = user.name.split(/\s+/).filter(Boolean).slice(0, 2).map((n) => n[0]?.toUpperCase() ?? "").join("");
1483
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
1484
+ /* @__PURE__ */ jsx(
1485
+ "button",
1486
+ {
1487
+ type: "button",
1488
+ onClick: () => setOpen((v) => !v),
1489
+ className: "flex h-9 items-center gap-2 rounded-md px-1.5 hover:bg-secondary",
1490
+ "aria-label": "Open user menu",
1491
+ children: user.avatar_url ? /* @__PURE__ */ jsx(
1492
+ "img",
1493
+ {
1494
+ src: user.avatar_url,
1495
+ alt: "",
1496
+ className: "h-7 w-7 rounded-full object-cover"
1497
+ }
1498
+ ) : /* @__PURE__ */ jsx("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-accent-soft text-[11px] font-semibold text-foreground", children: initials || "?" })
1499
+ }
1500
+ ),
1501
+ open ? /* @__PURE__ */ jsxs("div", { className: "absolute right-0 top-full z-50 mt-1 w-64 rounded-md border border-border bg-popover p-1 shadow-md", children: [
1502
+ /* @__PURE__ */ jsxs("div", { className: "px-2 py-2", children: [
1503
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: user.name }),
1504
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: user.email })
1505
+ ] }),
1506
+ extras ? /* @__PURE__ */ jsxs(Fragment, { children: [
1507
+ /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
1508
+ extras
1509
+ ] }) : null,
1510
+ /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
1511
+ /* @__PURE__ */ jsxs(
1512
+ "a",
1513
+ {
1514
+ href: urls.accountUrl,
1515
+ className: "flex items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-secondary",
1516
+ children: [
1517
+ /* @__PURE__ */ jsx("span", { children: "Manage account" }),
1518
+ /* @__PURE__ */ jsx(CrossAppArrow, {})
1519
+ ]
1520
+ }
1521
+ ),
1522
+ urls.orgSettingsUrl && currentOrganization ? /* @__PURE__ */ jsxs(
1523
+ "a",
1524
+ {
1525
+ href: urls.orgSettingsUrl,
1526
+ className: "flex items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-secondary",
1527
+ children: [
1528
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
1529
+ "Team & billing",
1530
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1531
+ " \xB7 ",
1532
+ currentOrganization.name
1533
+ ] })
1534
+ ] }),
1535
+ /* @__PURE__ */ jsx(CrossAppArrow, {})
1536
+ ]
1537
+ }
1538
+ ) : null,
1539
+ /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
1540
+ /* @__PURE__ */ jsxs("form", { action: urls.signOutUrl, method: "POST", children: [
1541
+ /* @__PURE__ */ jsx(SignOutCsrf, {}),
1542
+ /* @__PURE__ */ jsx(
1543
+ "button",
1544
+ {
1545
+ type: "submit",
1546
+ className: "flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-secondary",
1547
+ children: "Sign out"
1548
+ }
1549
+ )
1550
+ ] })
1551
+ ] }) : null
1552
+ ] });
1553
+ }
1554
+ function CrossAppArrow() {
1555
+ return /* @__PURE__ */ jsx(
1556
+ "svg",
1557
+ {
1558
+ width: "10",
1559
+ height: "10",
1560
+ viewBox: "0 0 10 10",
1561
+ fill: "none",
1562
+ "aria-hidden": true,
1563
+ children: /* @__PURE__ */ jsx(
1564
+ "path",
1565
+ {
1566
+ d: "M3 7l4-4M3 3h4v4",
1567
+ stroke: "currentColor",
1568
+ strokeWidth: "1.4",
1569
+ strokeLinecap: "round",
1570
+ strokeLinejoin: "round"
1571
+ }
1572
+ )
1573
+ }
1574
+ );
1575
+ }
1576
+ function SignOutCsrf() {
1577
+ if (typeof document === "undefined") return null;
1578
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
1579
+ if (!csrf) return null;
1580
+ return /* @__PURE__ */ jsx("input", { type: "hidden", name: "_token", value: csrf });
1581
+ }
1582
+ function CboxAppShell({
1583
+ appKey,
1584
+ brandSubmark,
1585
+ assetBase,
1586
+ brandHref,
1587
+ sections,
1588
+ currentPath,
1589
+ LinkComponent,
1590
+ sidebarCollapsed = false,
1591
+ onToggleSidebar,
1592
+ topbarCentre,
1593
+ onSearchOpen,
1594
+ topbarRightCluster,
1595
+ children
1596
+ }) {
1597
+ const ToggleIcon = sidebarCollapsed ? PanelLeftOpen : PanelLeftClose;
1598
+ const [mobileNavOpen, setMobileNavOpen] = useState(false);
1599
+ useEffect(() => {
1600
+ setMobileNavOpen(false);
1601
+ }, [currentPath]);
1602
+ return /* @__PURE__ */ jsxs(AppShell, { children: [
1603
+ /* @__PURE__ */ jsx(
1604
+ CboxAppSidebar,
1605
+ {
1606
+ appKey,
1607
+ brandSubmark,
1608
+ assetBase,
1609
+ brandHref,
1610
+ sections,
1611
+ currentPath,
1612
+ LinkComponent,
1613
+ collapsed: sidebarCollapsed
1614
+ }
1615
+ ),
1616
+ /* @__PURE__ */ jsx(
1617
+ CboxMobileNavDrawer,
1618
+ {
1619
+ open: mobileNavOpen,
1620
+ onClose: () => setMobileNavOpen(false),
1621
+ appKey,
1622
+ brandSubmark,
1623
+ assetBase,
1624
+ brandHref,
1625
+ sections,
1626
+ currentPath,
1627
+ LinkComponent
1628
+ }
1629
+ ),
1630
+ /* @__PURE__ */ jsxs(AppContent, { children: [
1631
+ /* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-20 flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-4 backdrop-blur-md", children: [
1632
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
1633
+ /* @__PURE__ */ jsx(
1634
+ "button",
1635
+ {
1636
+ type: "button",
1637
+ onClick: () => setMobileNavOpen(true),
1638
+ className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground lg:hidden",
1639
+ "aria-label": "Open navigation",
1640
+ title: "Open navigation",
1641
+ children: /* @__PURE__ */ jsx(Menu, { className: "h-4 w-4", strokeWidth: 1.8 })
1642
+ }
1643
+ ),
1644
+ onToggleSidebar !== void 0 ? /* @__PURE__ */ jsx(
1645
+ "button",
1646
+ {
1647
+ type: "button",
1648
+ onClick: onToggleSidebar,
1649
+ className: "hidden h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground lg:inline-flex",
1650
+ "aria-label": sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
1651
+ title: sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar",
1652
+ children: /* @__PURE__ */ jsx(
1653
+ ToggleIcon,
1654
+ {
1655
+ className: "h-4 w-4",
1656
+ strokeWidth: 1.8
1657
+ }
1658
+ )
1659
+ }
1660
+ ) : null,
1661
+ /* @__PURE__ */ jsx(AppSwitcher, {}),
1662
+ topbarCentre !== void 0 ? topbarCentre : null
1663
+ ] }),
1664
+ /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: topbarRightCluster !== void 0 ? topbarRightCluster : /* @__PURE__ */ jsxs(Fragment, { children: [
1665
+ /* @__PURE__ */ jsx(SearchTrigger, { onOpen: onSearchOpen ?? (() => {
1666
+ }) }),
1667
+ /* @__PURE__ */ jsx(NotificationsBell, {}),
1668
+ /* @__PURE__ */ jsx(UserMenu, {})
1669
+ ] }) })
1670
+ ] }),
1671
+ children
1672
+ ] })
1673
+ ] });
1674
+ }
1675
+ var DefaultLink = ({ href, className, children }) => /* @__PURE__ */ jsx("a", { href, className, children });
1676
+ function ContextBar({
1677
+ crumbs,
1678
+ LinkComponent = DefaultLink,
1679
+ className
1680
+ }) {
1681
+ if (crumbs.length === 0) {
1682
+ return null;
1683
+ }
1684
+ return /* @__PURE__ */ jsx(
1685
+ "nav",
1686
+ {
1687
+ "data-component": "context-bar",
1688
+ "aria-label": "Context",
1689
+ className: cn("flex min-w-0 items-center gap-1", className),
1690
+ children: crumbs.map((crumb, index) => /* @__PURE__ */ jsxs(
1691
+ "div",
1692
+ {
1693
+ className: "flex min-w-0 items-center gap-1",
1694
+ children: [
1695
+ index > 0 ? /* @__PURE__ */ jsx(Separator, {}) : null,
1696
+ /* @__PURE__ */ jsx(
1697
+ ContextCrumbChip,
1698
+ {
1699
+ crumb,
1700
+ LinkComponent
1701
+ }
1702
+ )
1703
+ ]
1704
+ },
1705
+ crumb.id
1706
+ ))
1707
+ }
1708
+ );
1709
+ }
1710
+ function Separator() {
1711
+ return /* @__PURE__ */ jsx(
1712
+ "span",
1713
+ {
1714
+ "aria-hidden": true,
1715
+ className: "text-mono shrink-0 text-[14px] font-light text-muted-foreground/50 select-none",
1716
+ children: "/"
1717
+ }
1718
+ );
1719
+ }
1720
+ function ContextCrumbChip({
1721
+ crumb,
1722
+ LinkComponent
1723
+ }) {
1724
+ const [open, setOpen] = useState(false);
1725
+ const ref = useRef(null);
1726
+ const listId = useId();
1727
+ useEffect(() => {
1728
+ if (!open) return;
1729
+ function onPointerDown(event) {
1730
+ if (ref.current !== null && !ref.current.contains(event.target)) {
1731
+ setOpen(false);
1732
+ }
1733
+ }
1734
+ function onKey(event) {
1735
+ if (event.key === "Escape") setOpen(false);
1736
+ }
1737
+ window.addEventListener("pointerdown", onPointerDown);
1738
+ window.addEventListener("keydown", onKey);
1739
+ return () => {
1740
+ window.removeEventListener("pointerdown", onPointerDown);
1741
+ window.removeEventListener("keydown", onKey);
1742
+ };
1743
+ }, [open]);
1744
+ const hasMenu = crumb.items.length > 0 || crumb.footerLink !== void 0;
1745
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
1746
+ /* @__PURE__ */ jsxs(
1747
+ "button",
1748
+ {
1749
+ type: "button",
1750
+ onClick: () => setOpen((v) => !v),
1751
+ "aria-expanded": hasMenu ? open : void 0,
1752
+ "aria-haspopup": hasMenu ? "listbox" : void 0,
1753
+ "aria-controls": hasMenu ? listId : void 0,
1754
+ "aria-label": crumb.ariaLabel ?? `Switch ${crumb.label}`,
1755
+ disabled: !hasMenu,
1756
+ className: cn(
1757
+ "flex h-8 min-w-0 items-center gap-2 rounded-md border border-transparent px-2 text-[13px] text-foreground transition-colors",
1758
+ hasMenu && "hover:border-border hover:bg-secondary/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
1759
+ open && "border-border bg-secondary/60"
1760
+ ),
1761
+ children: [
1762
+ crumb.avatar !== void 0 ? /* @__PURE__ */ jsx(
1763
+ ContextAvatarChip,
1764
+ {
1765
+ avatar: crumb.avatar,
1766
+ fallbackLabel: crumb.label,
1767
+ size: "sm"
1768
+ }
1769
+ ) : null,
1770
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: crumb.label }),
1771
+ hasMenu ? /* @__PURE__ */ jsx(Chevron2, { open }) : null
1772
+ ]
1773
+ }
1774
+ ),
1775
+ open && hasMenu ? /* @__PURE__ */ jsxs(
1776
+ "div",
1777
+ {
1778
+ id: listId,
1779
+ role: "listbox",
1780
+ "aria-label": crumb.ariaLabel ?? crumb.label,
1781
+ className: "absolute left-0 top-full z-30 mt-1 w-64 overflow-hidden rounded-md border border-border bg-popover shadow-lg",
1782
+ children: [
1783
+ crumb.items.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "max-h-72 overflow-y-auto py-1", children: crumb.items.map((item) => /* @__PURE__ */ jsx(
1784
+ ContextItemRow,
1785
+ {
1786
+ item,
1787
+ onSelect: crumb.onSelect,
1788
+ LinkComponent,
1789
+ onAfterClick: () => setOpen(false)
1790
+ },
1791
+ item.id
1792
+ )) }) : null,
1793
+ crumb.footerLink !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1794
+ crumb.items.length > 0 ? /* @__PURE__ */ jsx("div", { className: "border-t border-border/60" }) : null,
1795
+ /* @__PURE__ */ jsx(
1796
+ "div",
1797
+ {
1798
+ onClickCapture: () => setOpen(false),
1799
+ children: /* @__PURE__ */ jsxs(
1800
+ LinkComponent,
1801
+ {
1802
+ href: crumb.footerLink.href,
1803
+ prefetch: true,
1804
+ className: "flex items-center gap-2 px-3 py-2.5 text-[13px] font-medium text-info transition-colors hover:bg-info-soft",
1805
+ children: [
1806
+ /* @__PURE__ */ jsx(Plus, {}),
1807
+ /* @__PURE__ */ jsx("span", { children: crumb.footerLink.label })
1808
+ ]
1809
+ }
1810
+ )
1811
+ }
1812
+ )
1813
+ ] }) : null
1814
+ ]
1815
+ }
1816
+ ) : null
1817
+ ] });
1818
+ }
1819
+ function ContextItemRow({
1820
+ item,
1821
+ onSelect,
1822
+ LinkComponent,
1823
+ onAfterClick
1824
+ }) {
1825
+ const body = /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-2.5 px-2.5 py-2", children: [
1826
+ item.avatar !== void 0 ? /* @__PURE__ */ jsx(
1827
+ ContextAvatarChip,
1828
+ {
1829
+ avatar: item.avatar,
1830
+ fallbackLabel: item.label,
1831
+ size: "md"
1832
+ }
1833
+ ) : null,
1834
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
1835
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-foreground", children: item.label }),
1836
+ item.secondary !== void 0 ? /* @__PURE__ */ jsx("span", { className: "text-mono truncate text-[10px] text-muted-foreground", children: item.secondary }) : null
1837
+ ] }),
1838
+ item.current === true && item.settingsHref === void 0 ? /* @__PURE__ */ jsx(Check, {}) : null
1839
+ ] });
1840
+ const baseClassName = cn(
1841
+ "group/item block w-full rounded-sm transition-colors",
1842
+ item.current === true ? "bg-accent-soft hover:bg-accent-soft/80" : "hover:bg-secondary"
1843
+ );
1844
+ const settingsCog = item.settingsHref !== void 0 ? /* @__PURE__ */ jsx(
1845
+ LinkComponent,
1846
+ {
1847
+ href: item.settingsHref,
1848
+ prefetch: true,
1849
+ className: "invisible absolute right-1 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground transition-colors group-hover/item:visible hover:bg-card hover:text-foreground focus-visible:visible focus-visible:bg-card focus-visible:text-foreground focus-visible:outline-none",
1850
+ children: /* @__PURE__ */ jsx(
1851
+ "span",
1852
+ {
1853
+ "aria-label": item.settingsAriaLabel ?? `Settings for ${item.label}`,
1854
+ onClickCapture: (event) => {
1855
+ event.stopPropagation();
1856
+ onAfterClick();
1857
+ },
1858
+ className: "contents",
1859
+ children: /* @__PURE__ */ jsx(SettingsIcon, {})
1860
+ }
1861
+ )
1862
+ }
1863
+ ) : null;
1864
+ if (onSelect !== void 0) {
1865
+ return /* @__PURE__ */ jsxs("li", { className: "relative", children: [
1866
+ /* @__PURE__ */ jsx(
1867
+ "button",
1868
+ {
1869
+ type: "button",
1870
+ onClick: () => {
1871
+ onSelect(item.id);
1872
+ onAfterClick();
1873
+ },
1874
+ className: cn(baseClassName, "text-left"),
1875
+ children: body
1876
+ }
1877
+ ),
1878
+ settingsCog
1879
+ ] });
1880
+ }
1881
+ if (item.href !== void 0) {
1882
+ return /* @__PURE__ */ jsxs("li", { className: "relative", children: [
1883
+ /* @__PURE__ */ jsx(
1884
+ LinkComponent,
1885
+ {
1886
+ href: item.href,
1887
+ prefetch: true,
1888
+ className: baseClassName,
1889
+ children: body
1890
+ }
1891
+ ),
1892
+ settingsCog
1893
+ ] });
1894
+ }
1895
+ return /* @__PURE__ */ jsxs("li", { className: "relative", children: [
1896
+ /* @__PURE__ */ jsx("div", { className: baseClassName, children: body }),
1897
+ settingsCog
1898
+ ] });
1899
+ }
1900
+ function SettingsIcon() {
1901
+ return /* @__PURE__ */ jsxs(
1902
+ "svg",
1903
+ {
1904
+ width: "14",
1905
+ height: "14",
1906
+ viewBox: "0 0 14 14",
1907
+ fill: "none",
1908
+ stroke: "currentColor",
1909
+ strokeWidth: "1.5",
1910
+ strokeLinecap: "round",
1911
+ strokeLinejoin: "round",
1912
+ "aria-hidden": true,
1913
+ children: [
1914
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "2" }),
1915
+ /* @__PURE__ */ jsx("path", { d: "M11.66 8.42a.79.79 0 0 0 .16.86l.03.03a.96.96 0 1 1-1.36 1.36l-.03-.03a.79.79 0 0 0-.86-.16.79.79 0 0 0-.48.72v.08a.96.96 0 1 1-1.92 0v-.04a.79.79 0 0 0-.52-.72.79.79 0 0 0-.86.16l-.03.03a.96.96 0 1 1-1.36-1.36l.03-.03a.79.79 0 0 0 .16-.86.79.79 0 0 0-.72-.48h-.08a.96.96 0 1 1 0-1.92h.04a.79.79 0 0 0 .72-.52.79.79 0 0 0-.16-.86l-.03-.03a.96.96 0 1 1 1.36-1.36l.03.03a.79.79 0 0 0 .86.16h.04a.79.79 0 0 0 .48-.72v-.08a.96.96 0 1 1 1.92 0v.04a.79.79 0 0 0 .48.72.79.79 0 0 0 .86-.16l.03-.03a.96.96 0 1 1 1.36 1.36l-.03.03a.79.79 0 0 0-.16.86v.04a.79.79 0 0 0 .72.48h.08a.96.96 0 1 1 0 1.92h-.04a.79.79 0 0 0-.72.48Z" })
1916
+ ]
1917
+ }
1918
+ );
1919
+ }
1920
+ function ContextAvatarChip({
1921
+ avatar,
1922
+ fallbackLabel,
1923
+ size = "md"
1924
+ }) {
1925
+ const initials = avatar.initials ?? deriveInitials(fallbackLabel);
1926
+ const sizeClass = size === "sm" ? "h-5 w-5 text-[9px]" : "h-7 w-7 text-[11px]";
1927
+ const style = avatar.src === void 0 && avatar.accent !== void 0 ? {
1928
+ backgroundColor: avatar.accent,
1929
+ color: "#fff"
1930
+ } : void 0;
1931
+ if (avatar.src !== void 0) {
1932
+ return /* @__PURE__ */ jsx(
1933
+ "img",
1934
+ {
1935
+ src: avatar.src,
1936
+ alt: "",
1937
+ className: cn(
1938
+ "shrink-0 rounded-sm bg-secondary object-cover",
1939
+ sizeClass
1940
+ ),
1941
+ loading: "lazy"
1942
+ }
1943
+ );
1944
+ }
1945
+ return /* @__PURE__ */ jsx(
1946
+ "span",
1947
+ {
1948
+ style,
1949
+ className: cn(
1950
+ "flex shrink-0 items-center justify-center rounded-sm font-semibold tracking-tight",
1951
+ avatar.accent === void 0 && "bg-info-soft text-info",
1952
+ sizeClass
1953
+ ),
1954
+ children: initials
1955
+ }
1956
+ );
1957
+ }
1958
+ function deriveInitials(label) {
1959
+ const parts = label.trim().split(/\s+/);
1960
+ if (parts.length >= 2) {
1961
+ return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase();
1962
+ }
1963
+ return (label.slice(0, 2) || "?").toUpperCase();
1964
+ }
1965
+ function Chevron2({ open }) {
1966
+ return /* @__PURE__ */ jsx(
1967
+ "svg",
1968
+ {
1969
+ width: "10",
1970
+ height: "10",
1971
+ viewBox: "0 0 10 10",
1972
+ fill: "none",
1973
+ stroke: "currentColor",
1974
+ strokeWidth: "1.5",
1975
+ strokeLinecap: "round",
1976
+ strokeLinejoin: "round",
1977
+ className: cn(
1978
+ "shrink-0 text-muted-foreground transition-transform duration-150",
1979
+ open && "rotate-180"
1980
+ ),
1981
+ "aria-hidden": true,
1982
+ children: /* @__PURE__ */ jsx("path", { d: "m2.5 4 2.5 2.5L7.5 4" })
1983
+ }
1984
+ );
1985
+ }
1986
+ function Check() {
1987
+ return /* @__PURE__ */ jsx(
1988
+ "svg",
1989
+ {
1990
+ width: "14",
1991
+ height: "14",
1992
+ viewBox: "0 0 14 14",
1993
+ fill: "none",
1994
+ stroke: "currentColor",
1995
+ strokeWidth: "1.6",
1996
+ strokeLinecap: "round",
1997
+ strokeLinejoin: "round",
1998
+ className: "shrink-0 text-accent",
1999
+ "aria-hidden": true,
2000
+ children: /* @__PURE__ */ jsx("path", { d: "m3 7.5 2.5 2.5L11 4.5" })
2001
+ }
2002
+ );
2003
+ }
2004
+ function Plus() {
2005
+ return /* @__PURE__ */ jsx(
2006
+ "svg",
2007
+ {
2008
+ width: "12",
2009
+ height: "12",
2010
+ viewBox: "0 0 12 12",
2011
+ fill: "none",
2012
+ stroke: "currentColor",
2013
+ strokeWidth: "1.8",
2014
+ strokeLinecap: "round",
2015
+ strokeLinejoin: "round",
2016
+ className: "shrink-0",
2017
+ "aria-hidden": true,
2018
+ children: /* @__PURE__ */ jsx("path", { d: "M6 2v8M2 6h8" })
2019
+ }
2020
+ );
2021
+ }
2022
+ function CboxOrgContextBar({
2023
+ LinkComponent,
2024
+ onNavigate
2025
+ }) {
2026
+ const cboxId = useCboxIdOptional();
2027
+ if (cboxId === null) {
2028
+ return null;
2029
+ }
2030
+ const { currentOrganization, organizations, urls } = cboxId;
2031
+ const activeOrg = currentOrganization ?? organizations[0] ?? null;
2032
+ if (activeOrg === null) {
2033
+ return null;
2034
+ }
2035
+ const idBase = urls.idBaseUrl.replace(/\/$/, "");
2036
+ const items = organizations.map((org) => ({
2037
+ id: org.slug,
2038
+ label: org.name,
2039
+ avatar: { initials: deriveInitials2(org.name) },
2040
+ current: org.slug === activeOrg.slug,
2041
+ // Cogwheel shortcut → id's org-settings page. Cross-app
2042
+ // deep link; id handles the auth and rendering.
2043
+ settingsHref: `${idBase}/org/${org.slug}/settings`,
2044
+ settingsAriaLabel: `Settings for ${org.name}`
2045
+ }));
2046
+ const onSelect = (slug) => {
2047
+ const org = organizations.find((o) => o.slug === slug);
2048
+ if (org === void 0) return;
2049
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
2050
+ const next = path.startsWith("/org/") ? path.replace(/^\/org\/[^/]+/, `/org/${org.slug}`) : `/org/${org.slug}`;
2051
+ const search = typeof window !== "undefined" ? window.location.search : "";
2052
+ onNavigate(next + search);
2053
+ };
2054
+ return /* @__PURE__ */ jsx(
2055
+ ContextBar,
2056
+ {
2057
+ LinkComponent,
2058
+ crumbs: [
2059
+ {
2060
+ id: "org",
2061
+ label: activeOrg.name,
2062
+ avatar: { initials: deriveInitials2(activeOrg.name) },
2063
+ items,
2064
+ onSelect,
2065
+ footerLink: {
2066
+ label: "Manage organizations",
2067
+ href: `${idBase}/settings/organizations`
2068
+ },
2069
+ ariaLabel: "Switch organization"
2070
+ }
2071
+ ]
2072
+ }
2073
+ );
2074
+ }
2075
+ function deriveInitials2(label) {
2076
+ const parts = label.trim().split(/\s+/);
2077
+ if (parts.length >= 2) {
2078
+ return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase();
2079
+ }
2080
+ return (label.slice(0, 2) || "?").toUpperCase();
2081
+ }
2082
+ function OrgSwitcher({ Trigger = DefaultTrigger, className }) {
2083
+ const { currentOrganization, organizations, onSwitchOrganization } = useCboxId();
2084
+ const [open, setOpen] = useState(false);
2085
+ const [query, setQuery] = useState("");
2086
+ const [activeIndex, setActiveIndex] = useState(0);
2087
+ const containerRef = useRef(null);
2088
+ const searchRef = useRef(null);
2089
+ useEffect(() => {
2090
+ if (open) {
2091
+ setQuery("");
2092
+ setActiveIndex(0);
2093
+ const id = requestAnimationFrame(() => searchRef.current?.focus());
2094
+ return () => cancelAnimationFrame(id);
2095
+ }
2096
+ }, [open]);
2097
+ useEffect(() => {
2098
+ if (!open) {
2099
+ return;
2100
+ }
2101
+ function onPointerDown(event) {
2102
+ if (containerRef.current !== null && !containerRef.current.contains(event.target)) {
2103
+ setOpen(false);
2104
+ }
2105
+ }
2106
+ function onKey(event) {
2107
+ if (event.key === "Escape") {
2108
+ setOpen(false);
2109
+ }
2110
+ }
2111
+ window.addEventListener("pointerdown", onPointerDown);
2112
+ window.addEventListener("keydown", onKey);
2113
+ return () => {
2114
+ window.removeEventListener("pointerdown", onPointerDown);
2115
+ window.removeEventListener("keydown", onKey);
2116
+ };
2117
+ }, [open]);
2118
+ const filtered = useMemo(() => {
2119
+ const q = query.trim().toLowerCase();
2120
+ if (q === "") {
2121
+ return organizations;
2122
+ }
2123
+ return organizations.filter(
2124
+ (org) => org.name.toLowerCase().includes(q) || org.slug.toLowerCase().includes(q)
2125
+ );
2126
+ }, [organizations, query]);
2127
+ useEffect(() => {
2128
+ if (activeIndex >= filtered.length) {
2129
+ setActiveIndex(0);
2130
+ }
2131
+ }, [filtered.length, activeIndex]);
2132
+ const onPick = (org) => {
2133
+ setOpen(false);
2134
+ if (onSwitchOrganization) {
2135
+ onSwitchOrganization(org);
2136
+ return;
2137
+ }
2138
+ if (typeof console !== "undefined") {
2139
+ console.warn(
2140
+ "[cbox-ui] OrgSwitcher: no onSwitchOrganization handler provided. Apps must wire one (router.visit) \u2014 there is no server endpoint to fall back to."
2141
+ );
2142
+ }
2143
+ };
2144
+ function onSearchKeyDown(event) {
2145
+ if (event.key === "ArrowDown") {
2146
+ event.preventDefault();
2147
+ if (filtered.length === 0) {
2148
+ return;
2149
+ }
2150
+ setActiveIndex((i) => (i + 1) % filtered.length);
2151
+ return;
2152
+ }
2153
+ if (event.key === "ArrowUp") {
2154
+ event.preventDefault();
2155
+ if (filtered.length === 0) {
2156
+ return;
2157
+ }
2158
+ setActiveIndex((i) => (i - 1 + filtered.length) % filtered.length);
2159
+ return;
2160
+ }
2161
+ if (event.key === "Enter") {
2162
+ event.preventDefault();
2163
+ const target = filtered[activeIndex];
2164
+ if (target !== void 0) {
2165
+ onPick(target);
2166
+ }
2167
+ }
2168
+ }
2169
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: cn("relative", className), children: [
2170
+ /* @__PURE__ */ jsx(
2171
+ Trigger,
2172
+ {
2173
+ organization: currentOrganization,
2174
+ onClick: () => setOpen((v) => !v),
2175
+ open
2176
+ }
2177
+ ),
2178
+ open ? /* @__PURE__ */ jsxs(
2179
+ "div",
2180
+ {
2181
+ role: "dialog",
2182
+ "aria-label": "Switch organization",
2183
+ className: "absolute left-0 top-full z-50 mt-1 w-64 overflow-hidden rounded-md border border-border bg-popover shadow-md",
2184
+ children: [
2185
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 px-3 pt-2 pb-1", children: /* @__PURE__ */ jsx("p", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] text-muted-foreground uppercase", children: "Switch organization" }) }),
2186
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 px-2 py-1.5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md px-2 py-1 focus-within:bg-secondary", children: [
2187
+ /* @__PURE__ */ jsx(SearchIcon3, {}),
2188
+ /* @__PURE__ */ jsx(
2189
+ "input",
2190
+ {
2191
+ ref: searchRef,
2192
+ type: "text",
2193
+ value: query,
2194
+ onChange: (e) => setQuery(e.target.value),
2195
+ onKeyDown: onSearchKeyDown,
2196
+ placeholder: "Filter\u2026",
2197
+ "aria-label": "Filter organizations",
2198
+ autoComplete: "off",
2199
+ spellCheck: false,
2200
+ className: "w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
2201
+ }
2202
+ )
2203
+ ] }) }),
2204
+ /* @__PURE__ */ jsx("div", { className: "max-h-[50vh] overflow-y-auto p-1", children: filtered.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "px-3 py-6 text-center text-[12px] text-muted-foreground", children: [
2205
+ "No organizations match",
2206
+ " ",
2207
+ /* @__PURE__ */ jsxs("span", { className: "text-mono text-foreground", children: [
2208
+ "\u201C",
2209
+ query,
2210
+ "\u201D"
2211
+ ] })
2212
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: filtered.map((org, index) => {
2213
+ const active = currentOrganization?.id === org.id;
2214
+ const highlighted = index === activeIndex;
2215
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2216
+ "button",
2217
+ {
2218
+ type: "button",
2219
+ onMouseEnter: () => setActiveIndex(index),
2220
+ onClick: () => onPick(org),
2221
+ className: cn(
2222
+ "flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm transition-colors",
2223
+ active ? "bg-accent-soft text-foreground" : highlighted ? "bg-secondary" : "hover:bg-secondary"
2224
+ ),
2225
+ children: [
2226
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: org.name }),
2227
+ active ? /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] text-muted-foreground", children: "current" }) : null
2228
+ ]
2229
+ }
2230
+ ) }, org.id);
2231
+ }) }) })
2232
+ ]
2233
+ }
2234
+ ) : null
2235
+ ] });
2236
+ }
2237
+ var DefaultTrigger = ({ organization, onClick }) => /* @__PURE__ */ jsxs(
2238
+ "button",
2239
+ {
2240
+ type: "button",
2241
+ onClick,
2242
+ className: "flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm font-medium text-foreground hover:bg-secondary",
2243
+ children: [
2244
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[180px]", children: organization?.name ?? "Pick an organization" }),
2245
+ /* @__PURE__ */ jsx(Chevron3, {})
2246
+ ]
2247
+ }
2248
+ );
2249
+ function Chevron3() {
2250
+ return /* @__PURE__ */ jsx(
2251
+ "svg",
2252
+ {
2253
+ width: "10",
2254
+ height: "10",
2255
+ viewBox: "0 0 10 10",
2256
+ fill: "none",
2257
+ xmlns: "http://www.w3.org/2000/svg",
2258
+ "aria-hidden": true,
2259
+ children: /* @__PURE__ */ jsx(
2260
+ "path",
2261
+ {
2262
+ d: "M2 4l3 3 3-3",
2263
+ stroke: "currentColor",
2264
+ strokeWidth: "1.4",
2265
+ strokeLinecap: "round",
2266
+ strokeLinejoin: "round"
2267
+ }
2268
+ )
2269
+ }
2270
+ );
2271
+ }
2272
+ function SearchIcon3() {
2273
+ return /* @__PURE__ */ jsxs(
2274
+ "svg",
2275
+ {
2276
+ width: "12",
2277
+ height: "12",
2278
+ viewBox: "0 0 12 12",
2279
+ fill: "none",
2280
+ stroke: "currentColor",
2281
+ strokeWidth: "1.4",
2282
+ strokeLinecap: "round",
2283
+ strokeLinejoin: "round",
2284
+ "aria-hidden": true,
2285
+ className: "shrink-0 text-muted-foreground",
2286
+ children: [
2287
+ /* @__PURE__ */ jsx("circle", { cx: "5.25", cy: "5.25", r: "3.25" }),
2288
+ /* @__PURE__ */ jsx("path", { d: "M8 8l2 2" })
2289
+ ]
2290
+ }
2291
+ );
2292
+ }
2293
+ function TopBar({
2294
+ brand,
2295
+ centre,
2296
+ rightCluster,
2297
+ showOrgSwitcher = true,
2298
+ className
2299
+ }) {
2300
+ return /* @__PURE__ */ jsxs(
2301
+ "header",
2302
+ {
2303
+ className: cn(
2304
+ "flex h-14 shrink-0 items-center gap-3 border-b border-border bg-background px-4",
2305
+ className
2306
+ ),
2307
+ children: [
2308
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2309
+ brand,
2310
+ /* @__PURE__ */ jsx(AppSwitcher, {}),
2311
+ showOrgSwitcher ? /* @__PURE__ */ jsx(OrgSwitcher, {}) : null
2312
+ ] }),
2313
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: centre }),
2314
+ /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1", children: rightCluster ?? /* @__PURE__ */ jsxs(Fragment, { children: [
2315
+ /* @__PURE__ */ jsx(NotificationsBell, {}),
2316
+ /* @__PURE__ */ jsx(UserMenu, {})
2317
+ ] }) })
2318
+ ]
2319
+ }
2320
+ );
2321
+ }
2322
+ function AppLauncher({ className, onLockedAppClick }) {
2323
+ const cboxId = useCboxIdOptional();
2324
+ const [open, setOpen] = useState(false);
2325
+ const containerRef = useRef(null);
2326
+ useEffect(() => {
2327
+ if (!open) {
2328
+ return;
2329
+ }
2330
+ function onPointerDown(event) {
2331
+ if (containerRef.current !== null && !containerRef.current.contains(event.target)) {
2332
+ setOpen(false);
2333
+ }
2334
+ }
2335
+ function onKey(event) {
2336
+ if (event.key === "Escape") {
2337
+ setOpen(false);
2338
+ }
2339
+ }
2340
+ window.addEventListener("pointerdown", onPointerDown);
2341
+ window.addEventListener("keydown", onKey);
2342
+ return () => {
2343
+ window.removeEventListener("pointerdown", onPointerDown);
2344
+ window.removeEventListener("keydown", onKey);
2345
+ };
2346
+ }, [open]);
2347
+ if (cboxId === null || cboxId.apps.length === 0) {
2348
+ return null;
2349
+ }
2350
+ const apps = cboxId.apps;
2351
+ const orgSlug = cboxId.currentOrganization?.slug;
2352
+ const entitled = [];
2353
+ const locked = [];
2354
+ for (const app of apps) {
2355
+ const isEntitled = app.entitled !== false && app.comingSoon !== true;
2356
+ (isEntitled ? entitled : locked).push(app);
2357
+ }
2358
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: cn("relative", className), children: [
2359
+ /* @__PURE__ */ jsx(
2360
+ "button",
2361
+ {
2362
+ type: "button",
2363
+ "aria-label": "Open app launcher",
2364
+ "aria-expanded": open,
2365
+ onClick: () => setOpen((v) => !v),
2366
+ className: cn(
2367
+ "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground",
2368
+ open && "bg-secondary text-foreground"
2369
+ ),
2370
+ children: /* @__PURE__ */ jsx(Grid, {})
2371
+ }
2372
+ ),
2373
+ open ? /* @__PURE__ */ jsxs(
2374
+ "div",
2375
+ {
2376
+ role: "dialog",
2377
+ "aria-label": "Cbox app launcher",
2378
+ className: "absolute right-0 top-full z-50 mt-1.5 w-[min(20rem,calc(100vw-1.5rem))] overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
2379
+ children: [
2380
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 px-3 py-2.5", children: /* @__PURE__ */ jsx("p", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] text-muted-foreground uppercase", children: "Cbox suite" }) }),
2381
+ /* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-y-auto py-1", children: [
2382
+ entitled.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1", children: entitled.map((app) => /* @__PURE__ */ jsx(
2383
+ AppRow2,
2384
+ {
2385
+ app,
2386
+ orgSlug,
2387
+ onLockedAppClick
2388
+ },
2389
+ app.key
2390
+ )) }) : null,
2391
+ locked.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2392
+ entitled.length > 0 ? /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border/50" }) : null,
2393
+ /* @__PURE__ */ jsx("p", { className: "text-mono px-3 py-1.5 text-[10px] font-semibold tracking-[0.18em] text-muted-foreground/70 uppercase", children: "Available" }),
2394
+ /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1 pt-0", children: locked.map((app) => /* @__PURE__ */ jsx(
2395
+ AppRow2,
2396
+ {
2397
+ app,
2398
+ orgSlug,
2399
+ onLockedAppClick
2400
+ },
2401
+ app.key
2402
+ )) })
2403
+ ] }) : null
2404
+ ] })
2405
+ ]
2406
+ }
2407
+ ) : null
2408
+ ] });
2409
+ }
2410
+ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2411
+ const isComingSoon = app.comingSoon === true;
2412
+ const isLocked = app.entitled === false || isComingSoon;
2413
+ const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
2414
+ const accent = app.accentColor;
2415
+ const tileStyle = accent !== void 0 && app.iconUrl === void 0 ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
2416
+ const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl2(app.url, orgSlug) : app.url;
2417
+ const tile = /* @__PURE__ */ jsx(
2418
+ "div",
2419
+ {
2420
+ style: tileStyle,
2421
+ className: cn(
2422
+ "flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border bg-card text-[13px] font-semibold text-foreground",
2423
+ app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
2424
+ isLocked && "opacity-60 grayscale"
2425
+ ),
2426
+ children: app.iconUrl ? /* @__PURE__ */ jsx("img", { src: app.iconUrl, alt: "", className: "h-5 w-5", loading: "lazy" }) : /* @__PURE__ */ jsx("span", { children: monogram })
2427
+ }
2428
+ );
2429
+ const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
2430
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2431
+ /* @__PURE__ */ jsx(
2432
+ "span",
2433
+ {
2434
+ className: cn(
2435
+ "truncate text-sm font-medium text-foreground",
2436
+ isLocked && "text-muted-foreground"
2437
+ ),
2438
+ children: app.name
2439
+ }
2440
+ ),
2441
+ app.current ? /* @__PURE__ */ jsx("span", { className: "text-mono shrink-0 rounded-sm border border-accent-edge/40 bg-accent-soft px-1 text-[9px] font-semibold tracking-[0.14em] text-accent uppercase", children: "current" }) : null
2442
+ ] }),
2443
+ app.description ? /* @__PURE__ */ jsx(
2444
+ "span",
2445
+ {
2446
+ className: cn(
2447
+ "truncate text-[11px]",
2448
+ isLocked ? "text-muted-foreground/70" : "text-muted-foreground"
2449
+ ),
2450
+ children: app.description
2451
+ }
2452
+ ) : null
2453
+ ] });
2454
+ const trailing = isComingSoon ? /* @__PURE__ */ jsx("span", { className: "text-mono shrink-0 rounded-sm border border-border bg-secondary px-1.5 py-0.5 text-[9px] font-semibold tracking-[0.14em] text-muted-foreground uppercase", children: "soon" }) : isLocked ? /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1 text-[10px] font-medium text-muted-foreground", children: [
2455
+ /* @__PURE__ */ jsx(Lock2, {}),
2456
+ "Add"
2457
+ ] }) : null;
2458
+ if (isComingSoon) {
2459
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2460
+ "div",
2461
+ {
2462
+ "aria-disabled": true,
2463
+ className: "flex w-full cursor-not-allowed items-center gap-3 rounded-md px-2.5 py-2",
2464
+ title: `${app.name} \u2014 coming soon`,
2465
+ children: [
2466
+ tile,
2467
+ body,
2468
+ trailing
2469
+ ]
2470
+ }
2471
+ ) });
2472
+ }
2473
+ if (isLocked) {
2474
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2475
+ "button",
2476
+ {
2477
+ type: "button",
2478
+ onClick: () => onLockedAppClick?.(app),
2479
+ className: cn(
2480
+ "group flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left transition-colors",
2481
+ onLockedAppClick !== void 0 ? "hover:bg-secondary" : "cursor-default"
2482
+ ),
2483
+ title: `Add ${app.name} to your plan`,
2484
+ children: [
2485
+ tile,
2486
+ body,
2487
+ trailing
2488
+ ]
2489
+ }
2490
+ ) });
2491
+ }
2492
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
2493
+ "a",
2494
+ {
2495
+ href,
2496
+ className: cn(
2497
+ "group flex w-full items-center gap-3 rounded-md px-2.5 py-2 transition-colors hover:bg-secondary",
2498
+ app.current && "bg-accent-soft hover:bg-accent-soft"
2499
+ ),
2500
+ children: [
2501
+ tile,
2502
+ body,
2503
+ trailing
2504
+ ]
2505
+ }
2506
+ ) });
2507
+ }
2508
+ function buildOrgUrl2(url, orgSlug) {
2509
+ if (url === "" || orgSlug === "") {
2510
+ return url;
2511
+ }
2512
+ try {
2513
+ const u = new URL(url);
2514
+ u.pathname = `/org/${encodeURIComponent(orgSlug)}`;
2515
+ return u.toString();
2516
+ } catch {
2517
+ return `${url.replace(/\/$/, "")}/org/${encodeURIComponent(orgSlug)}`;
2518
+ }
2519
+ }
2520
+ function Grid() {
2521
+ return /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", "aria-hidden": true, children: [1, 8, 15].flatMap(
2522
+ (y) => [1, 8, 15].map((x) => /* @__PURE__ */ jsx(
2523
+ "rect",
2524
+ {
2525
+ x,
2526
+ y,
2527
+ width: "2.5",
2528
+ height: "2.5",
2529
+ rx: "0.6",
2530
+ fill: "currentColor"
2531
+ },
2532
+ `${x}-${y}`
2533
+ ))
2534
+ ) });
2535
+ }
2536
+ function Lock2() {
2537
+ return /* @__PURE__ */ jsxs(
2538
+ "svg",
2539
+ {
2540
+ width: "10",
2541
+ height: "10",
2542
+ viewBox: "0 0 12 12",
2543
+ fill: "none",
2544
+ stroke: "currentColor",
2545
+ strokeWidth: "1.6",
2546
+ strokeLinecap: "round",
2547
+ strokeLinejoin: "round",
2548
+ "aria-hidden": true,
2549
+ children: [
2550
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "5", width: "7", height: "5", rx: "1" }),
2551
+ /* @__PURE__ */ jsx("path", { d: "M4 5V3.5a2 2 0 1 1 4 0V5" })
2552
+ ]
2553
+ }
2554
+ );
2555
+ }
2556
+ function Combobox(props) {
2557
+ const {
2558
+ items,
2559
+ placeholder = "Select\u2026",
2560
+ searchPlaceholder = "Search\u2026",
2561
+ emptyText = "Nothing matches.",
2562
+ footerLink,
2563
+ disabled = false,
2564
+ required = false,
2565
+ ariaLabel,
2566
+ triggerClassName,
2567
+ panelClassName,
2568
+ triggerWidth = "auto",
2569
+ LinkComponent,
2570
+ id: idProp,
2571
+ className,
2572
+ value
2573
+ } = props;
2574
+ const groups = useMemo(() => normaliseGroups(items), [items]);
2575
+ const flatOptions = useMemo(
2576
+ () => groups.flatMap((g) => g.options),
2577
+ [groups]
2578
+ );
2579
+ const activeOption = useMemo(
2580
+ () => value !== null && value !== void 0 ? flatOptions.find((opt) => opt.value === value) : null,
2581
+ [flatOptions, value]
2582
+ );
2583
+ const [open, setOpen] = useState(false);
2584
+ const [query, setQuery] = useState("");
2585
+ const [highlightIndex, setHighlightIndex] = useState(-1);
2586
+ const containerRef = useRef(null);
2587
+ const inputRef = useRef(null);
2588
+ const listRef = useRef(null);
2589
+ const reactId = useId();
2590
+ const id = idProp ?? reactId;
2591
+ const listId = `${id}-listbox`;
2592
+ const filteredGroups = useMemo(
2593
+ () => filterGroups(groups, query),
2594
+ [groups, query]
2595
+ );
2596
+ const filteredFlat = useMemo(
2597
+ () => filteredGroups.flatMap((g) => g.options),
2598
+ [filteredGroups]
2599
+ );
2600
+ useEffect(() => {
2601
+ if (!open) return;
2602
+ const firstEnabled = filteredFlat.findIndex(
2603
+ (opt) => opt.disabled !== true
2604
+ );
2605
+ setHighlightIndex(firstEnabled);
2606
+ }, [filteredFlat, open]);
2607
+ useEffect(() => {
2608
+ if (open) {
2609
+ const id2 = window.setTimeout(() => inputRef.current?.focus(), 0);
2610
+ return () => window.clearTimeout(id2);
2611
+ }
2612
+ setQuery("");
2613
+ return void 0;
2614
+ }, [open]);
2615
+ useEffect(() => {
2616
+ if (!open) return;
2617
+ function onPointerDown(event) {
2618
+ if (containerRef.current !== null && !containerRef.current.contains(event.target)) {
2619
+ setOpen(false);
2620
+ }
2621
+ }
2622
+ window.addEventListener("pointerdown", onPointerDown);
2623
+ return () => window.removeEventListener("pointerdown", onPointerDown);
2624
+ }, [open]);
2625
+ const onSelectOption = useCallback(
2626
+ (opt) => {
2627
+ if (opt.disabled === true) return;
2628
+ if (opt.href !== void 0 && LinkComponent !== void 0 && props.onChange === void 0) {
2629
+ setOpen(false);
2630
+ return;
2631
+ }
2632
+ if (props.onChange !== void 0) {
2633
+ props.onChange(opt.value);
2634
+ }
2635
+ setOpen(false);
2636
+ },
2637
+ [LinkComponent, props]
2638
+ );
2639
+ const onKeyDown = useCallback(
2640
+ (event) => {
2641
+ if (event.key === "ArrowDown") {
2642
+ event.preventDefault();
2643
+ setHighlightIndex(
2644
+ (current) => nextEnabledIndex(filteredFlat, current, 1)
2645
+ );
2646
+ return;
2647
+ }
2648
+ if (event.key === "ArrowUp") {
2649
+ event.preventDefault();
2650
+ setHighlightIndex(
2651
+ (current) => nextEnabledIndex(filteredFlat, current, -1)
2652
+ );
2653
+ return;
2654
+ }
2655
+ if (event.key === "Home") {
2656
+ event.preventDefault();
2657
+ setHighlightIndex(
2658
+ filteredFlat.findIndex((o) => o.disabled !== true)
2659
+ );
2660
+ return;
2661
+ }
2662
+ if (event.key === "End") {
2663
+ event.preventDefault();
2664
+ let last = -1;
2665
+ for (let i = 0; i < filteredFlat.length; i++) {
2666
+ const opt = filteredFlat[i];
2667
+ if (opt !== void 0 && opt.disabled !== true) {
2668
+ last = i;
2669
+ }
2670
+ }
2671
+ setHighlightIndex(last);
2672
+ return;
2673
+ }
2674
+ if (event.key === "Enter") {
2675
+ if (highlightIndex >= 0) {
2676
+ event.preventDefault();
2677
+ const opt = filteredFlat[highlightIndex];
2678
+ if (opt !== void 0) {
2679
+ onSelectOption(opt);
2680
+ }
2681
+ }
2682
+ return;
2683
+ }
2684
+ if (event.key === "Escape") {
2685
+ event.preventDefault();
2686
+ setOpen(false);
2687
+ return;
2688
+ }
2689
+ if (event.key === "Tab") {
2690
+ setOpen(false);
2691
+ return;
2692
+ }
2693
+ },
2694
+ [filteredFlat, highlightIndex, onSelectOption]
2695
+ );
2696
+ return /* @__PURE__ */ jsxs(
2697
+ "div",
2698
+ {
2699
+ ref: containerRef,
2700
+ className: cn("relative", triggerWidth === "full" && "w-full", className),
2701
+ children: [
2702
+ /* @__PURE__ */ jsxs(
2703
+ "button",
2704
+ {
2705
+ id,
2706
+ type: "button",
2707
+ role: "combobox",
2708
+ disabled,
2709
+ "aria-expanded": open,
2710
+ "aria-haspopup": "listbox",
2711
+ "aria-controls": listId,
2712
+ "aria-required": required,
2713
+ "aria-label": ariaLabel,
2714
+ onClick: () => setOpen((v) => !v),
2715
+ className: cn(
2716
+ "flex h-9 items-center gap-2 rounded-md border border-input bg-card px-3 text-left text-[13px] text-foreground transition-colors",
2717
+ triggerWidth === "full" && "w-full",
2718
+ "hover:border-border-strong focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
2719
+ open && "border-border-strong ring-2 ring-ring/30",
2720
+ disabled && "cursor-not-allowed opacity-60",
2721
+ triggerClassName
2722
+ ),
2723
+ children: [
2724
+ activeOption?.avatar !== void 0 ? /* @__PURE__ */ jsx(
2725
+ ComboboxAvatarChip,
2726
+ {
2727
+ avatar: activeOption.avatar,
2728
+ fallbackLabel: activeOption.label
2729
+ }
2730
+ ) : activeOption?.icon !== void 0 ? /* @__PURE__ */ jsx(
2731
+ activeOption.icon,
2732
+ {
2733
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
2734
+ strokeWidth: 1.7
2735
+ }
2736
+ ) : null,
2737
+ /* @__PURE__ */ jsx(
2738
+ "span",
2739
+ {
2740
+ className: cn(
2741
+ "flex-1 truncate",
2742
+ activeOption === null || activeOption === void 0 ? "text-muted-foreground" : ""
2743
+ ),
2744
+ children: activeOption !== null && activeOption !== void 0 ? activeOption.label : placeholder
2745
+ }
2746
+ ),
2747
+ /* @__PURE__ */ jsx(Chevron4, { open })
2748
+ ]
2749
+ }
2750
+ ),
2751
+ open ? /* @__PURE__ */ jsxs(
2752
+ "div",
2753
+ {
2754
+ className: cn(
2755
+ "absolute left-0 right-0 top-full z-30 mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-lg",
2756
+ panelClassName
2757
+ ),
2758
+ children: [
2759
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/60 px-2 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2760
+ /* @__PURE__ */ jsx(SearchIcon4, {}),
2761
+ /* @__PURE__ */ jsx(
2762
+ "input",
2763
+ {
2764
+ ref: inputRef,
2765
+ type: "text",
2766
+ value: query,
2767
+ onChange: (e) => setQuery(e.target.value),
2768
+ onKeyDown,
2769
+ placeholder: searchPlaceholder,
2770
+ className: "h-8 w-full rounded-sm border-0 bg-transparent pl-7 pr-2 text-[13px] outline-none placeholder:text-muted-foreground/70",
2771
+ "aria-controls": listId,
2772
+ "aria-autocomplete": "list",
2773
+ "aria-activedescendant": highlightIndex >= 0 ? `${listId}-${highlightIndex}` : void 0
2774
+ }
2775
+ )
2776
+ ] }) }),
2777
+ /* @__PURE__ */ jsx(
2778
+ "ul",
2779
+ {
2780
+ id: listId,
2781
+ ref: listRef,
2782
+ role: "listbox",
2783
+ className: "max-h-72 overflow-y-auto py-1",
2784
+ children: filteredFlat.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-3 py-6 text-center text-[13px] text-muted-foreground", children: emptyText }) : filteredGroups.map((group, groupIndex) => /* @__PURE__ */ jsx(
2785
+ ComboboxGroupRows,
2786
+ {
2787
+ group,
2788
+ flatOptions: filteredFlat,
2789
+ listId,
2790
+ highlightIndex,
2791
+ activeValue: value ?? null,
2792
+ LinkComponent,
2793
+ onSelect: onSelectOption,
2794
+ onHover: (idx) => setHighlightIndex(idx)
2795
+ },
2796
+ group.label || `group-${groupIndex}`
2797
+ ))
2798
+ }
2799
+ ),
2800
+ footerLink !== void 0 ? /* @__PURE__ */ jsx(
2801
+ FooterLink,
2802
+ {
2803
+ label: footerLink.label,
2804
+ href: footerLink.href,
2805
+ LinkComponent,
2806
+ onAfterClick: () => setOpen(false)
2807
+ }
2808
+ ) : null
2809
+ ]
2810
+ }
2811
+ ) : null
2812
+ ]
2813
+ }
2814
+ );
2815
+ }
2816
+ function ComboboxGroupRows({
2817
+ group,
2818
+ flatOptions,
2819
+ listId,
2820
+ highlightIndex,
2821
+ activeValue,
2822
+ LinkComponent,
2823
+ onSelect,
2824
+ onHover
2825
+ }) {
2826
+ if (group.options.length === 0) return null;
2827
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2828
+ group.label !== "" ? /* @__PURE__ */ jsx(
2829
+ "li",
2830
+ {
2831
+ role: "presentation",
2832
+ className: "text-mono px-3 pt-2 pb-1 text-[10px] font-semibold tracking-[0.18em] text-muted-foreground/70 uppercase",
2833
+ children: group.label
2834
+ }
2835
+ ) : null,
2836
+ group.options.map((opt) => {
2837
+ const flatIndex = flatOptions.indexOf(opt);
2838
+ const isHighlighted = flatIndex === highlightIndex;
2839
+ const isActive = opt.value === activeValue;
2840
+ const inner = /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-2.5 px-2.5 py-2", children: [
2841
+ opt.avatar !== void 0 ? /* @__PURE__ */ jsx(
2842
+ ComboboxAvatarChip,
2843
+ {
2844
+ avatar: opt.avatar,
2845
+ fallbackLabel: opt.label
2846
+ }
2847
+ ) : opt.icon !== void 0 ? /* @__PURE__ */ jsx(
2848
+ opt.icon,
2849
+ {
2850
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
2851
+ strokeWidth: 1.7
2852
+ }
2853
+ ) : null,
2854
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
2855
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-foreground", children: opt.label }),
2856
+ opt.secondary !== void 0 ? /* @__PURE__ */ jsx("span", { className: "text-mono truncate text-[10px] text-muted-foreground", children: opt.secondary }) : null
2857
+ ] }),
2858
+ isActive ? /* @__PURE__ */ jsx(Check2, {}) : null
2859
+ ] });
2860
+ const className = cn(
2861
+ "group block w-full rounded-sm transition-colors text-left",
2862
+ opt.disabled === true && "cursor-not-allowed opacity-50",
2863
+ isHighlighted && opt.disabled !== true && "bg-secondary",
2864
+ isActive && "bg-accent-soft hover:bg-accent-soft/80"
2865
+ );
2866
+ if (opt.href !== void 0 && LinkComponent !== void 0) {
2867
+ return /* @__PURE__ */ jsx(
2868
+ "li",
2869
+ {
2870
+ id: `${listId}-${flatIndex}`,
2871
+ role: "option",
2872
+ "aria-selected": isActive,
2873
+ "aria-disabled": opt.disabled === true,
2874
+ onMouseEnter: () => onHover(flatIndex),
2875
+ children: /* @__PURE__ */ jsx(
2876
+ LinkComponent,
2877
+ {
2878
+ href: opt.href,
2879
+ prefetch: true,
2880
+ className,
2881
+ children: inner
2882
+ }
2883
+ )
2884
+ },
2885
+ opt.value
2886
+ );
2887
+ }
2888
+ return /* @__PURE__ */ jsx(
2889
+ "li",
2890
+ {
2891
+ id: `${listId}-${flatIndex}`,
2892
+ role: "option",
2893
+ "aria-selected": isActive,
2894
+ "aria-disabled": opt.disabled === true,
2895
+ onMouseEnter: () => onHover(flatIndex),
2896
+ children: /* @__PURE__ */ jsx(
2897
+ "button",
2898
+ {
2899
+ type: "button",
2900
+ disabled: opt.disabled === true,
2901
+ onClick: () => onSelect(opt),
2902
+ className,
2903
+ children: inner
2904
+ }
2905
+ )
2906
+ },
2907
+ opt.value
2908
+ );
2909
+ })
2910
+ ] });
2911
+ }
2912
+ function FooterLink({
2913
+ label,
2914
+ href,
2915
+ LinkComponent,
2916
+ onAfterClick
2917
+ }) {
2918
+ const className = "flex items-center gap-2 border-t border-border/60 px-3 py-2.5 text-[13px] font-medium text-info transition-colors hover:bg-info-soft";
2919
+ if (LinkComponent !== void 0) {
2920
+ return /* @__PURE__ */ jsxs(
2921
+ LinkComponent,
2922
+ {
2923
+ href,
2924
+ prefetch: true,
2925
+ className,
2926
+ children: [
2927
+ /* @__PURE__ */ jsx(Plus2, {}),
2928
+ /* @__PURE__ */ jsx("span", { children: label })
2929
+ ]
2930
+ }
2931
+ );
2932
+ }
2933
+ return /* @__PURE__ */ jsxs(
2934
+ "a",
2935
+ {
2936
+ href,
2937
+ className,
2938
+ onClick: onAfterClick,
2939
+ children: [
2940
+ /* @__PURE__ */ jsx(Plus2, {}),
2941
+ /* @__PURE__ */ jsx("span", { children: label })
2942
+ ]
2943
+ }
2944
+ );
2945
+ }
2946
+ function ComboboxAvatarChip({
2947
+ avatar,
2948
+ fallbackLabel
2949
+ }) {
2950
+ const initials = avatar.initials ?? deriveInitials3(fallbackLabel);
2951
+ const style = avatar.src === void 0 && avatar.accent !== void 0 ? { backgroundColor: avatar.accent, color: "#fff" } : void 0;
2952
+ if (avatar.src !== void 0) {
2953
+ return /* @__PURE__ */ jsx(
2954
+ "img",
2955
+ {
2956
+ src: avatar.src,
2957
+ alt: "",
2958
+ className: "h-5 w-5 shrink-0 rounded-sm bg-secondary object-cover",
2959
+ loading: "lazy"
2960
+ }
2961
+ );
2962
+ }
2963
+ return /* @__PURE__ */ jsx(
2964
+ "span",
2965
+ {
2966
+ style,
2967
+ className: cn(
2968
+ "flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-[9px] font-semibold tracking-tight",
2969
+ avatar.accent === void 0 && "bg-info-soft text-info"
2970
+ ),
2971
+ children: initials
2972
+ }
2973
+ );
2974
+ }
2975
+ function deriveInitials3(label) {
2976
+ const parts = label.trim().split(/\s+/);
2977
+ if (parts.length >= 2) {
2978
+ return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase();
2979
+ }
2980
+ return (label.slice(0, 2) || "?").toUpperCase();
2981
+ }
2982
+ function normaliseGroups(items) {
2983
+ if (items.length === 0) return [];
2984
+ const first = items[0];
2985
+ if (first !== void 0 && "options" in first) {
2986
+ return items;
2987
+ }
2988
+ return [{ label: "", options: items }];
2989
+ }
2990
+ function filterGroups(groups, query) {
2991
+ const q = query.trim().toLowerCase();
2992
+ if (q === "") return groups;
2993
+ return groups.map((group) => ({
2994
+ label: group.label,
2995
+ options: group.options.filter((opt) => matchesQuery(opt, q))
2996
+ })).filter((group) => group.options.length > 0);
2997
+ }
2998
+ function matchesQuery(option, q) {
2999
+ const haystack = [
3000
+ option.label,
3001
+ option.secondary ?? "",
3002
+ ...option.keywords ?? []
3003
+ ].join(" ").toLowerCase();
3004
+ return haystack.includes(q);
3005
+ }
3006
+ function nextEnabledIndex(options, current, direction) {
3007
+ const total = options.length;
3008
+ if (total === 0) return -1;
3009
+ let i = current;
3010
+ for (let step = 0; step < total; step++) {
3011
+ i = (i + direction + total) % total;
3012
+ const opt = options[i];
3013
+ if (opt !== void 0 && opt.disabled !== true) {
3014
+ return i;
3015
+ }
3016
+ }
3017
+ return -1;
3018
+ }
3019
+ function Chevron4({ open }) {
3020
+ return /* @__PURE__ */ jsx(
3021
+ "svg",
3022
+ {
3023
+ width: "14",
3024
+ height: "14",
3025
+ viewBox: "0 0 14 14",
3026
+ fill: "none",
3027
+ stroke: "currentColor",
3028
+ strokeWidth: "1.6",
3029
+ strokeLinecap: "round",
3030
+ strokeLinejoin: "round",
3031
+ "aria-hidden": true,
3032
+ className: cn(
3033
+ "shrink-0 text-muted-foreground transition-transform duration-150",
3034
+ open && "rotate-180"
3035
+ ),
3036
+ children: /* @__PURE__ */ jsx("path", { d: "m3.5 5.5 3.5 3.5 3.5-3.5" })
3037
+ }
3038
+ );
3039
+ }
3040
+ function Check2() {
3041
+ return /* @__PURE__ */ jsx(
3042
+ "svg",
3043
+ {
3044
+ width: "14",
3045
+ height: "14",
3046
+ viewBox: "0 0 14 14",
3047
+ fill: "none",
3048
+ stroke: "currentColor",
3049
+ strokeWidth: "1.6",
3050
+ strokeLinecap: "round",
3051
+ strokeLinejoin: "round",
3052
+ className: "shrink-0 text-accent",
3053
+ "aria-hidden": true,
3054
+ children: /* @__PURE__ */ jsx("path", { d: "m3 7.5 2.5 2.5L11 4.5" })
3055
+ }
3056
+ );
3057
+ }
3058
+ function Plus2() {
3059
+ return /* @__PURE__ */ jsx(
3060
+ "svg",
3061
+ {
3062
+ width: "12",
3063
+ height: "12",
3064
+ viewBox: "0 0 12 12",
3065
+ fill: "none",
3066
+ stroke: "currentColor",
3067
+ strokeWidth: "1.8",
3068
+ strokeLinecap: "round",
3069
+ strokeLinejoin: "round",
3070
+ className: "shrink-0",
3071
+ "aria-hidden": true,
3072
+ children: /* @__PURE__ */ jsx("path", { d: "M6 2v8M2 6h8" })
3073
+ }
3074
+ );
3075
+ }
3076
+ function SearchIcon4() {
3077
+ return /* @__PURE__ */ jsxs(
3078
+ "svg",
3079
+ {
3080
+ width: "14",
3081
+ height: "14",
3082
+ viewBox: "0 0 14 14",
3083
+ fill: "none",
3084
+ stroke: "currentColor",
3085
+ strokeWidth: "1.6",
3086
+ strokeLinecap: "round",
3087
+ strokeLinejoin: "round",
3088
+ className: "pointer-events-none absolute left-1.5 top-1/2 -translate-y-1/2 text-muted-foreground",
3089
+ "aria-hidden": true,
3090
+ children: [
3091
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "4.25" }),
3092
+ /* @__PURE__ */ jsx("path", { d: "m11.5 11.5-2.5-2.5" })
3093
+ ]
3094
+ }
3095
+ );
3096
+ }
3097
+ function DataPanel({
3098
+ className,
3099
+ children,
3100
+ ...rest
3101
+ }) {
3102
+ return /* @__PURE__ */ jsx(
3103
+ "section",
3104
+ {
3105
+ className: cn(
3106
+ "rounded-lg border border-border bg-card text-card-foreground shadow-[0_1px_2px_rgba(0,0,0,0.02)]",
3107
+ className
3108
+ ),
3109
+ ...rest,
3110
+ children
3111
+ }
3112
+ );
3113
+ }
3114
+ function SectionHeader({
3115
+ title,
3116
+ description,
3117
+ actions,
3118
+ className
3119
+ }) {
3120
+ return /* @__PURE__ */ jsxs(
3121
+ "header",
3122
+ {
3123
+ className: cn(
3124
+ "flex flex-col gap-3 border-b border-border px-5 py-4 sm:flex-row sm:items-center sm:justify-between",
3125
+ className
3126
+ ),
3127
+ children: [
3128
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
3129
+ /* @__PURE__ */ jsx("h2", { className: "text-[15px] leading-none font-semibold text-foreground", children: title }),
3130
+ description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
3131
+ ] }),
3132
+ actions ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions }) : null
3133
+ ]
3134
+ }
3135
+ );
3136
+ }
3137
+ function PanelBody({
3138
+ className,
3139
+ children,
3140
+ ...rest
3141
+ }) {
3142
+ return /* @__PURE__ */ jsx("div", { className: cn("px-5 py-4", className), ...rest, children });
3143
+ }
3144
+ function Heading({
3145
+ title,
3146
+ description,
3147
+ variant = "default"
3148
+ }) {
3149
+ return /* @__PURE__ */ jsxs("header", { className: variant === "small" ? "" : "mb-8 space-y-0.5", children: [
3150
+ /* @__PURE__ */ jsx(
3151
+ "h2",
3152
+ {
3153
+ className: variant === "small" ? "mb-0.5 text-base font-medium" : "text-xl font-semibold tracking-tight",
3154
+ children: title
3155
+ }
3156
+ ),
3157
+ description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description })
3158
+ ] });
3159
+ }
3160
+ function KeyValue({
3161
+ label,
3162
+ children,
3163
+ mono = true,
3164
+ className
3165
+ }) {
3166
+ return /* @__PURE__ */ jsxs(
3167
+ "div",
3168
+ {
3169
+ className: cn(
3170
+ "grid grid-cols-[140px_1fr] items-baseline gap-x-6 border-b border-border py-3 last:border-b-0",
3171
+ className
3172
+ ),
3173
+ children: [
3174
+ /* @__PURE__ */ jsx("dt", { className: "text-eyebrow text-muted-foreground", children: label }),
3175
+ /* @__PURE__ */ jsx(
3176
+ "dd",
3177
+ {
3178
+ className: cn(
3179
+ "text-sm break-all text-foreground",
3180
+ mono && "text-mono text-[13px]"
3181
+ ),
3182
+ children
3183
+ }
3184
+ )
3185
+ ]
3186
+ }
3187
+ );
3188
+ }
3189
+ function KeyValueList({
3190
+ children,
3191
+ className
3192
+ }) {
3193
+ return /* @__PURE__ */ jsx("dl", { className: cn("px-6", className), children });
3194
+ }
3195
+ function PageHeader({
3196
+ title,
3197
+ eyebrow,
3198
+ description,
3199
+ actions,
3200
+ className
3201
+ }) {
3202
+ return /* @__PURE__ */ jsxs(
3203
+ "header",
3204
+ {
3205
+ className: cn(
3206
+ "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between",
3207
+ className
3208
+ ),
3209
+ children: [
3210
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3211
+ eyebrow !== void 0 ? /* @__PURE__ */ jsx("p", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] text-muted-foreground uppercase", children: eyebrow }) : null,
3212
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl leading-tight font-semibold tracking-tight text-foreground", children: title }),
3213
+ description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
3214
+ ] }),
3215
+ actions ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: actions }) : null
3216
+ ]
3217
+ }
3218
+ );
3219
+ }
3220
+ var widthClass = {
3221
+ comfortable: "max-w-6xl px-6 py-8 lg:px-10 lg:py-10",
3222
+ wide: "max-w-7xl px-6 py-8 lg:px-10 lg:py-10",
3223
+ full: "max-w-none px-6 py-8 lg:px-10 lg:py-10",
3224
+ bleed: "max-w-none p-0"
3225
+ };
3226
+ function PageShell({
3227
+ children,
3228
+ width = "comfortable",
3229
+ className
3230
+ }) {
3231
+ return /* @__PURE__ */ jsx(
3232
+ "div",
3233
+ {
3234
+ className: cn(
3235
+ "mx-auto w-full space-y-6",
3236
+ widthClass[width],
3237
+ className
3238
+ ),
3239
+ children
3240
+ }
3241
+ );
3242
+ }
3243
+ var DefaultLink2 = ({ href, className, children, "aria-current": ariaCurrent }) => /* @__PURE__ */ jsx("a", { href, className, "aria-current": ariaCurrent, children });
3244
+ function PageTabs({
3245
+ items,
3246
+ currentPath,
3247
+ LinkComponent = DefaultLink2,
3248
+ actions,
3249
+ className
3250
+ }) {
3251
+ const activeHref = resolveActiveHref(items, currentPath);
3252
+ return /* @__PURE__ */ jsxs(
3253
+ "div",
3254
+ {
3255
+ "data-component": "page-tabs",
3256
+ className: cn(
3257
+ // Card-style chrome — same border, radius, bg as DataPanel
3258
+ // so the tab strip reads as a peer of the content panels
3259
+ // beneath it instead of floating on the page background.
3260
+ // Width matches PageShell content; no sticky positioning.
3261
+ "rounded-lg border border-border bg-card",
3262
+ className
3263
+ ),
3264
+ children: [
3265
+ /* @__PURE__ */ jsxs("div", { className: "hidden items-end gap-3 px-3 sm:flex", children: [
3266
+ /* @__PURE__ */ jsx(
3267
+ DesktopStrip,
3268
+ {
3269
+ items,
3270
+ activeHref,
3271
+ LinkComponent
3272
+ }
3273
+ ),
3274
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-12 shrink-0 items-center gap-2", children: actions })
3275
+ ] }),
3276
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 p-3 sm:hidden", children: [
3277
+ /* @__PURE__ */ jsx(
3278
+ MobileDropdown,
3279
+ {
3280
+ items,
3281
+ activeHref,
3282
+ LinkComponent
3283
+ }
3284
+ ),
3285
+ actions !== void 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions }) : null
3286
+ ] })
3287
+ ]
3288
+ }
3289
+ );
3290
+ }
3291
+ function DesktopStrip({ items, activeHref, LinkComponent }) {
3292
+ return /* @__PURE__ */ jsx(
3293
+ "nav",
3294
+ {
3295
+ className: "flex min-h-12 flex-1 items-center gap-1 overflow-x-auto overflow-y-hidden py-1.5",
3296
+ role: "tablist",
3297
+ "aria-label": "Section navigation",
3298
+ children: items.map((item) => {
3299
+ const Icon = item.icon;
3300
+ const active = item.href === activeHref;
3301
+ return /* @__PURE__ */ jsxs(
3302
+ LinkComponent,
3303
+ {
3304
+ href: item.href,
3305
+ prefetch: true,
3306
+ "aria-current": active ? "page" : void 0,
3307
+ className: cn(
3308
+ "inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] whitespace-nowrap transition-colors",
3309
+ active ? "bg-secondary font-semibold text-foreground shadow-sm ring-1 ring-border-strong/60" : "text-muted-foreground hover:bg-secondary/60 hover:text-foreground"
3310
+ ),
3311
+ children: [
3312
+ Icon !== void 0 ? /* @__PURE__ */ jsx(
3313
+ Icon,
3314
+ {
3315
+ className: cn(
3316
+ "h-3.5 w-3.5 shrink-0",
3317
+ active ? "text-foreground" : "text-muted-foreground"
3318
+ ),
3319
+ strokeWidth: 1.7
3320
+ }
3321
+ ) : null,
3322
+ /* @__PURE__ */ jsx("span", { children: item.label }),
3323
+ /* @__PURE__ */ jsx(ItemTrailing, { item, active })
3324
+ ]
3325
+ },
3326
+ item.href
3327
+ );
3328
+ })
3329
+ }
3330
+ );
3331
+ }
3332
+ function MobileDropdown({ items, activeHref, LinkComponent }) {
3333
+ const [open, setOpen] = useState(false);
3334
+ const ref = useRef(null);
3335
+ const listId = useId();
3336
+ const active = items.find((item) => item.href === activeHref) ?? items[0];
3337
+ useEffect(() => {
3338
+ if (!open) return;
3339
+ function onPointerDown(event) {
3340
+ if (ref.current !== null && !ref.current.contains(event.target)) {
3341
+ setOpen(false);
3342
+ }
3343
+ }
3344
+ function onKey(event) {
3345
+ if (event.key === "Escape") setOpen(false);
3346
+ }
3347
+ window.addEventListener("pointerdown", onPointerDown);
3348
+ window.addEventListener("keydown", onKey);
3349
+ return () => {
3350
+ window.removeEventListener("pointerdown", onPointerDown);
3351
+ window.removeEventListener("keydown", onKey);
3352
+ };
3353
+ }, [open]);
3354
+ if (active === void 0) {
3355
+ return null;
3356
+ }
3357
+ const ActiveIcon = active.icon;
3358
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
3359
+ /* @__PURE__ */ jsxs(
3360
+ "button",
3361
+ {
3362
+ type: "button",
3363
+ onClick: () => setOpen((v) => !v),
3364
+ "aria-expanded": open,
3365
+ "aria-haspopup": "listbox",
3366
+ "aria-controls": listId,
3367
+ className: cn(
3368
+ "flex w-full min-h-11 items-center justify-between gap-2 rounded-md border border-border bg-card px-3 text-left text-[13px] font-medium text-foreground transition-colors hover:border-border-strong",
3369
+ open && "border-border-strong ring-2 ring-ring/30"
3370
+ ),
3371
+ children: [
3372
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
3373
+ ActiveIcon !== void 0 ? /* @__PURE__ */ jsx(
3374
+ ActiveIcon,
3375
+ {
3376
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
3377
+ strokeWidth: 1.7
3378
+ }
3379
+ ) : null,
3380
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: active.label }),
3381
+ /* @__PURE__ */ jsx(ItemTrailing, { item: active, active: true })
3382
+ ] }),
3383
+ /* @__PURE__ */ jsx(Chevron5, { open })
3384
+ ]
3385
+ }
3386
+ ),
3387
+ open ? /* @__PURE__ */ jsx(
3388
+ "ul",
3389
+ {
3390
+ id: listId,
3391
+ role: "listbox",
3392
+ "aria-label": "Section navigation",
3393
+ className: "absolute left-0 right-0 top-full z-30 mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-lg",
3394
+ children: items.map((item) => {
3395
+ const Icon = item.icon;
3396
+ const isActive = item.href === activeHref;
3397
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
3398
+ LinkComponent,
3399
+ {
3400
+ href: item.href,
3401
+ prefetch: true,
3402
+ "aria-current": isActive ? "page" : void 0,
3403
+ className: cn(
3404
+ "flex w-full items-center gap-2 px-3 py-2.5 text-[13px] transition-colors",
3405
+ isActive ? "bg-accent-soft font-semibold text-foreground" : "text-muted-foreground hover:bg-secondary hover:text-foreground"
3406
+ ),
3407
+ children: [
3408
+ Icon !== void 0 ? /* @__PURE__ */ jsx(
3409
+ Icon,
3410
+ {
3411
+ className: cn(
3412
+ "h-3.5 w-3.5 shrink-0",
3413
+ isActive ? "text-foreground" : "text-muted-foreground"
3414
+ ),
3415
+ strokeWidth: 1.7
3416
+ }
3417
+ ) : null,
3418
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label }),
3419
+ /* @__PURE__ */ jsx(
3420
+ ItemTrailing,
3421
+ {
3422
+ item,
3423
+ active: isActive
3424
+ }
3425
+ )
3426
+ ]
3427
+ }
3428
+ ) }, item.href);
3429
+ })
3430
+ }
3431
+ ) : null
3432
+ ] });
3433
+ }
3434
+ function ItemTrailing({
3435
+ item,
3436
+ active
3437
+ }) {
3438
+ if (item.badge !== void 0) {
3439
+ return /* @__PURE__ */ jsx("span", { className: "ml-1 shrink-0", children: item.badge });
3440
+ }
3441
+ if (item.count !== void 0) {
3442
+ return /* @__PURE__ */ jsx(
3443
+ "span",
3444
+ {
3445
+ className: cn(
3446
+ "ml-1 shrink-0 rounded-sm border px-1 text-[10px] font-semibold tracking-tight tabular-nums",
3447
+ active ? "border-border-strong bg-secondary text-foreground" : "border-border bg-secondary/60 text-muted-foreground"
3448
+ ),
3449
+ children: item.count
3450
+ }
3451
+ );
3452
+ }
3453
+ return null;
3454
+ }
3455
+ function Chevron5({ open }) {
3456
+ return /* @__PURE__ */ jsx(
3457
+ "svg",
3458
+ {
3459
+ width: "14",
3460
+ height: "14",
3461
+ viewBox: "0 0 14 14",
3462
+ fill: "none",
3463
+ stroke: "currentColor",
3464
+ strokeWidth: "1.6",
3465
+ strokeLinecap: "round",
3466
+ strokeLinejoin: "round",
3467
+ className: cn(
3468
+ "shrink-0 text-muted-foreground transition-transform duration-150",
3469
+ open && "rotate-180"
3470
+ ),
3471
+ "aria-hidden": true,
3472
+ children: /* @__PURE__ */ jsx("path", { d: "m3.5 5.5 3.5 3.5 3.5-3.5" })
3473
+ }
3474
+ );
3475
+ }
3476
+ function pathOnly(href) {
3477
+ const queryIndex = href.indexOf("?");
3478
+ const hashIndex = href.indexOf("#");
3479
+ let end = href.length;
3480
+ if (queryIndex !== -1) end = Math.min(end, queryIndex);
3481
+ if (hashIndex !== -1) end = Math.min(end, hashIndex);
3482
+ return href.slice(0, end);
3483
+ }
3484
+ function resolveActiveHref(items, currentPath) {
3485
+ const current = pathOnly(currentPath);
3486
+ const candidates = items.map((item) => {
3487
+ const itemPath = pathOnly(item.href);
3488
+ const matched = item.matchPrefix === true ? current === itemPath || current.startsWith(itemPath + "/") : current === itemPath;
3489
+ return matched ? item.href : null;
3490
+ }).filter((href) => href !== null);
3491
+ if (candidates.length === 0) return null;
3492
+ return candidates.reduce(
3493
+ (longest, href) => href.length > longest.length ? href : longest
3494
+ );
3495
+ }
3496
+ var DefaultLink3 = ({ href, className, children }) => /* @__PURE__ */ jsx("a", { href, className, children });
3497
+ function RowLink({
3498
+ href,
3499
+ children,
3500
+ trailing,
3501
+ LinkComponent = DefaultLink3,
3502
+ active = false,
3503
+ className
3504
+ }) {
3505
+ return /* @__PURE__ */ jsx(
3506
+ LinkComponent,
3507
+ {
3508
+ href,
3509
+ prefetch: true,
3510
+ className: cn(
3511
+ "group flex w-full items-center gap-4 px-5 py-3.5 transition-colors focus-visible:bg-secondary/60 focus-visible:outline-none",
3512
+ active ? "bg-accent-soft hover:bg-accent-soft/80" : "hover:bg-secondary/60",
3513
+ className
3514
+ ),
3515
+ children: /* @__PURE__ */ jsxs(
3516
+ "span",
3517
+ {
3518
+ "data-component": "row-link",
3519
+ "data-active": active ? "true" : void 0,
3520
+ onClickCapture: (event) => {
3521
+ const target = event.target;
3522
+ if (target.closest(
3523
+ 'button, a, input, select, textarea, [role="button"], [data-stop-row-click]'
3524
+ ) !== null && target.closest("[data-row-body]") === null) {
3525
+ event.stopPropagation();
3526
+ }
3527
+ },
3528
+ className: "flex w-full items-center gap-4",
3529
+ children: [
3530
+ /* @__PURE__ */ jsx(
3531
+ "span",
3532
+ {
3533
+ "data-row-body": true,
3534
+ className: "flex min-w-0 flex-1 items-center gap-4",
3535
+ children
3536
+ }
3537
+ ),
3538
+ trailing !== void 0 ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-2", children: trailing }) : null
3539
+ ]
3540
+ }
3541
+ )
3542
+ }
3543
+ );
3544
+ }
3545
+ var toneStyles2 = {
3546
+ info: { bg: "bg-info-soft", fg: "text-info" },
3547
+ success: { bg: "bg-success-soft", fg: "text-success" },
3548
+ warning: { bg: "bg-warning-soft", fg: "text-warning" },
3549
+ destructive: { bg: "bg-destructive-soft", fg: "text-destructive" },
3550
+ neutral: { bg: "bg-secondary", fg: "text-foreground" }
3551
+ };
3552
+ function StatTile({
3553
+ label,
3554
+ value,
3555
+ suffix,
3556
+ icon: Icon,
3557
+ tone = "info",
3558
+ className
3559
+ }) {
3560
+ const styles = toneStyles2[tone];
3561
+ return /* @__PURE__ */ jsxs(
3562
+ "div",
3563
+ {
3564
+ className: cn(
3565
+ "flex items-center gap-3 rounded-lg border border-border bg-card p-4 shadow-[0_1px_2px_rgba(0,0,0,0.02)] transition-colors hover:border-border-strong",
3566
+ className
3567
+ ),
3568
+ children: [
3569
+ /* @__PURE__ */ jsx(
3570
+ "div",
3571
+ {
3572
+ className: cn(
3573
+ "flex h-10 w-10 shrink-0 items-center justify-center rounded-md",
3574
+ styles.bg
3575
+ ),
3576
+ children: /* @__PURE__ */ jsx(Icon, { className: cn("h-5 w-5", styles.fg), strokeWidth: 1.8 })
3577
+ }
3578
+ ),
3579
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [
3580
+ /* @__PURE__ */ jsx("p", { className: "tabular text-2xl leading-none font-semibold tracking-tight text-foreground", children: value }),
3581
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: label }),
3582
+ suffix ? /* @__PURE__ */ jsx("p", { className: "text-mono truncate text-[10px] text-muted-foreground/80", children: suffix }) : null
3583
+ ] })
3584
+ ]
3585
+ }
3586
+ );
3587
+ }
3588
+
3589
+ // src/utils/url-state.ts
3590
+ function urlString(defaultValue = "") {
3591
+ return {
3592
+ parse: (raw) => raw ?? defaultValue,
3593
+ serialize: (value) => value === "" ? null : value,
3594
+ isDefault: (value) => value === defaultValue
3595
+ };
3596
+ }
3597
+ function urlNumber(defaultValue, options = {}) {
3598
+ const clamp = (n) => {
3599
+ if (options.min !== void 0 && n < options.min) return options.min;
3600
+ if (options.max !== void 0 && n > options.max) return options.max;
3601
+ return n;
3602
+ };
3603
+ return {
3604
+ parse: (raw) => {
3605
+ if (raw === null) return defaultValue;
3606
+ const parsed = Number.parseInt(raw, 10);
3607
+ if (Number.isNaN(parsed)) return defaultValue;
3608
+ return clamp(parsed);
3609
+ },
3610
+ serialize: (value) => String(value),
3611
+ isDefault: (value) => value === defaultValue
3612
+ };
3613
+ }
3614
+ function urlEnum(values, defaultValue) {
3615
+ const allowed = new Set(values);
3616
+ return {
3617
+ parse: (raw) => raw !== null && allowed.has(raw) ? raw : defaultValue,
3618
+ serialize: (value) => value,
3619
+ isDefault: (value) => value === defaultValue
3620
+ };
3621
+ }
3622
+ function urlBoolean(defaultValue = false) {
3623
+ return {
3624
+ parse: (raw) => raw === null ? defaultValue : raw === "true",
3625
+ serialize: (value) => value ? "true" : "false",
3626
+ isDefault: (value) => value === defaultValue
3627
+ };
3628
+ }
3629
+ function urlStringArray(defaultValue = []) {
3630
+ const defaultKey = [...defaultValue].sort().join(",");
3631
+ return {
3632
+ parse: (raw) => {
3633
+ if (raw === null || raw === "") return [...defaultValue];
3634
+ return raw.split(",").filter((item) => item !== "");
3635
+ },
3636
+ serialize: (value) => value.length === 0 ? null : value.join(","),
3637
+ isDefault: (value) => [...value].sort().join(",") === defaultKey
3638
+ };
3639
+ }
3640
+ function defineUrlSchema(schema) {
3641
+ return schema;
3642
+ }
3643
+ function parseUrlSearch(search, schema) {
3644
+ const normalised = search.startsWith("?") ? search.slice(1) : search;
3645
+ const params = new URLSearchParams(normalised);
3646
+ const state = {};
3647
+ for (const key of Object.keys(schema)) {
3648
+ const field = schema[key];
3649
+ if (field === void 0) continue;
3650
+ state[key] = field.parse(params.get(key));
3651
+ }
3652
+ return state;
3653
+ }
3654
+ function buildUrlSearch(state, schema) {
3655
+ const params = new URLSearchParams();
3656
+ for (const key of Object.keys(schema)) {
3657
+ const field = schema[key];
3658
+ if (field === void 0) continue;
3659
+ const value = state[key];
3660
+ if (field.isDefault(value)) continue;
3661
+ const serialized = field.serialize(value);
3662
+ if (serialized !== null) {
3663
+ params.set(key, serialized);
3664
+ }
3665
+ }
3666
+ return params.toString();
3667
+ }
3668
+
3669
+ export { AppContent, AppLauncher, AppShell, AppSwitcher, CboxAppShell, CboxAppSidebar, CboxBrand, CboxIdProvider, CboxMobileNavDrawer, CboxOrgContextBar, CboxSurfaces, Combobox, ContextAvatarChip, ContextBar, DataPanel, EmptyState, Heading, KeyValue, KeyValueList, NotificationsBell, OrgSwitcher, PageHeader, PageShell, PageTabs, PanelBody, RowLink, SearchTrigger, SectionHeader, StatTile, StatusPill, TopBar, UserMenu, buildUrlSearch, cn, defineUrlSchema, parseUrlSearch, urlBoolean, urlEnum, urlNumber, urlString, urlStringArray, useCboxId, useCboxIdOptional, useCboxSurfaces, useCboxSurfacesOptional };
3670
+ //# sourceMappingURL=index.js.map
3671
+ //# sourceMappingURL=index.js.map