@cboxdk/cbox-ui 0.6.0 → 0.8.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 CHANGED
@@ -1,10 +1,9 @@
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';
1
+ import { cn } from './chunk-OFTNLF7H.js';
2
+ export { cn, niceTicks, timeTicks } from './chunk-OFTNLF7H.js';
3
+ import { createContext, useContext, useMemo, useSyncExternalStore, useState, useEffect, useRef, useCallback, useId } from 'react';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
+ import { X, Menu, PanelLeftOpen, PanelLeftClose, PinOff, Pin } from 'lucide-react';
6
6
 
7
- // src/context/cbox-id.tsx
8
7
  var CboxIdContext = createContext(null);
9
8
  function CboxIdProvider({
10
9
  value,
@@ -24,9 +23,6 @@ function useCboxId() {
24
23
  function useCboxIdOptional() {
25
24
  return useContext(CboxIdContext);
26
25
  }
27
- function cn(...inputs) {
28
- return twMerge(clsx(inputs));
29
- }
30
26
  function AppContent({ children, className, ...props }) {
31
27
  return /* @__PURE__ */ jsx(
32
28
  "main",
@@ -43,6 +39,118 @@ function AppContent({ children, className, ...props }) {
43
39
  function AppShell({ children }) {
44
40
  return /* @__PURE__ */ jsx("div", { className: "flex h-svh w-full bg-canvas text-foreground", children });
45
41
  }
42
+ var Localization = createContext({
43
+ locale: "en",
44
+ t: interpolate,
45
+ formatDate: (value) => new Date(value).toLocaleString()
46
+ });
47
+ function interpolate(message, values = {}) {
48
+ return message.replace(
49
+ /\{([a-zA-Z][a-zA-Z0-9_]*)\}/g,
50
+ (match, key) => Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match
51
+ );
52
+ }
53
+ function CboxLocalizationProvider({ locale, translate, formatDate = defaultDate, children }) {
54
+ const value = useMemo(() => ({ locale, t: translate, formatDate }), [locale, translate, formatDate]);
55
+ return /* @__PURE__ */ jsx(Localization.Provider, { value, children });
56
+ }
57
+ function useCboxLocalization() {
58
+ return useContext(Localization);
59
+ }
60
+ function defaultDate(value) {
61
+ return new Date(value).toLocaleString();
62
+ }
63
+ var KEPT = "cbox-app-catalog";
64
+ var EVENT = "cbox-app-catalog";
65
+ var EMPTY = [];
66
+ var cache = { raw: null, apps: EMPTY };
67
+ function storage() {
68
+ try {
69
+ return typeof window === "undefined" ? null : window.localStorage;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ function readKnownAppCatalog() {
75
+ let raw = null;
76
+ try {
77
+ raw = storage()?.getItem(KEPT) ?? null;
78
+ } catch {
79
+ raw = null;
80
+ }
81
+ if (raw === cache.raw) return cache.apps;
82
+ let apps = EMPTY;
83
+ try {
84
+ const value = raw === null ? [] : JSON.parse(raw);
85
+ if (Array.isArray(value)) {
86
+ apps = value.flatMap((entry) => {
87
+ if (typeof entry !== "object" || entry === null) return [];
88
+ const app = entry;
89
+ if (typeof app.key !== "string" || typeof app.name !== "string") return [];
90
+ return [{ key: app.key, name: app.name, icon_url: secureIconUrl(app.icon_url), icon_dark_url: secureIconUrl(app.icon_dark_url) }];
91
+ });
92
+ }
93
+ } catch {
94
+ apps = EMPTY;
95
+ }
96
+ cache = { raw, apps };
97
+ return apps;
98
+ }
99
+ function subscribe(change) {
100
+ if (typeof window === "undefined") return () => void 0;
101
+ const storageChange = (event) => {
102
+ if (event.key === KEPT) change();
103
+ };
104
+ document.addEventListener(EVENT, change);
105
+ window.addEventListener("storage", storageChange);
106
+ return () => {
107
+ document.removeEventListener(EVENT, change);
108
+ window.removeEventListener("storage", storageChange);
109
+ };
110
+ }
111
+ function rememberAppCatalog(apps) {
112
+ const kept = apps.map(({ key, name, icon_url, icon_dark_url }) => ({ key, name, icon_url: icon_url ?? null, icon_dark_url: icon_dark_url ?? null }));
113
+ try {
114
+ storage()?.setItem(KEPT, JSON.stringify(kept));
115
+ } catch {
116
+ }
117
+ if (typeof document !== "undefined") document.dispatchEvent(new CustomEvent(EVENT));
118
+ }
119
+ function useKnownAppCatalog() {
120
+ return useSyncExternalStore(subscribe, readKnownAppCatalog, () => EMPTY);
121
+ }
122
+ function secureIconUrl(url) {
123
+ if (typeof url !== "string" || url === "") return null;
124
+ try {
125
+ const parsed = new URL(url);
126
+ return parsed.protocol === "https:" ? parsed.href : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function initials(name) {
132
+ return name.replace(/^Cbox\s*·\s*/i, "").slice(0, 2);
133
+ }
134
+ function CboxAppIcon({ name, iconUrl, iconDarkUrl, className = "h-6 w-6", fallback }) {
135
+ const light = secureIconUrl(iconUrl);
136
+ const dark = secureIconUrl(iconDarkUrl);
137
+ const [failed, setFailed] = useState(null);
138
+ const key = `${light ?? ""}|${dark ?? ""}`;
139
+ if (light === null || failed === key) {
140
+ if (fallback !== void 0) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
141
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("grid shrink-0 place-items-center rounded bg-secondary text-[11px] font-semibold text-foreground", className), children: initials(name) });
142
+ }
143
+ const lost = () => setFailed(key);
144
+ return /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: cn("cbx-app-icon-box inline-flex shrink-0 items-center justify-center", className), children: [
145
+ /* @__PURE__ */ jsx("img", { src: light, alt: "", decoding: "async", onError: lost, className: cn("cbx-app-icon", dark === null ? "cbx-app-icon-single" : "cbx-app-icon-light", "h-full w-full") }),
146
+ dark !== null && /* @__PURE__ */ jsx("img", { src: dark, alt: "", decoding: "async", onError: lost, className: "cbx-app-icon cbx-app-icon-dark h-full w-full" })
147
+ ] });
148
+ }
149
+ function CboxServiceMark({ appKey, className = "h-6 w-6", fallback = null }) {
150
+ const app = useKnownAppCatalog().find((entry) => entry.key === appKey);
151
+ if (app === void 0 || app.icon_url === null) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
152
+ return /* @__PURE__ */ jsx(CboxAppIcon, { name: app.name, iconUrl: app.icon_url, iconDarkUrl: app.icon_dark_url, className, fallback: fallback ?? void 0 });
153
+ }
46
154
  function CboxBrand({
47
155
  appKey,
48
156
  submark,
@@ -52,7 +160,7 @@ function CboxBrand({
52
160
  className
53
161
  }) {
54
162
  if (collapsed) {
55
- return /* @__PURE__ */ jsx(
163
+ const cboxIcon = /* @__PURE__ */ jsx(
56
164
  "img",
57
165
  {
58
166
  src: `${assetBase}/cbox-icon.png`,
@@ -63,6 +171,7 @@ function CboxBrand({
63
171
  className: cn("h-9 w-9 rounded-md", className)
64
172
  }
65
173
  );
174
+ return /* @__PURE__ */ jsx(CboxServiceMark, { appKey, className: cn("h-9 w-9", className), fallback: cboxIcon });
66
175
  }
67
176
  const wordmark = tone === "light" ? `${assetBase}/cbox-logo-white.png` : `${assetBase}/cbox-logo.png`;
68
177
  const wordmarkRetina = tone === "light" ? `${assetBase}/cbox-logo-white@2x.png` : `${assetBase}/cbox-logo@2x.png`;
@@ -84,14 +193,17 @@ function CboxBrand({
84
193
  draggable: false
85
194
  }
86
195
  ),
87
- submarkText !== null && submarkText !== "" ? /* @__PURE__ */ jsx(
196
+ submarkText !== null && submarkText !== "" ? /* @__PURE__ */ jsxs(
88
197
  "span",
89
198
  {
90
199
  className: cn(
91
- "text-mono text-[10px] font-semibold tracking-[0.22em] uppercase",
200
+ "text-[13px] font-semibold whitespace-nowrap",
92
201
  tone === "light" ? "text-white/60" : "text-muted-foreground/80"
93
202
  ),
94
- children: submarkText
203
+ children: [
204
+ "\xB7 ",
205
+ submarkText
206
+ ]
95
207
  }
96
208
  ) : null
97
209
  ]
@@ -109,6 +221,7 @@ function CboxAppSidebar({
109
221
  collapsed = false,
110
222
  className
111
223
  }) {
224
+ const { t } = useCboxLocalization();
112
225
  return /* @__PURE__ */ jsxs(
113
226
  "aside",
114
227
  {
@@ -124,7 +237,7 @@ function CboxAppSidebar({
124
237
  LinkComponent,
125
238
  {
126
239
  href: brandHref,
127
- "aria-label": "Home",
240
+ "aria-label": t("Home"),
128
241
  className: cn(
129
242
  "flex h-14 shrink-0 items-center border-b border-sidebar-border/70",
130
243
  collapsed ? "justify-center px-0" : "px-4"
@@ -197,6 +310,7 @@ function CboxMobileNavDrawer({
197
310
  currentPath,
198
311
  LinkComponent
199
312
  }) {
313
+ const { t } = useCboxLocalization();
200
314
  useEffect(() => {
201
315
  if (!open) {
202
316
  return;
@@ -238,7 +352,7 @@ function CboxMobileNavDrawer({
238
352
  {
239
353
  role: "dialog",
240
354
  "aria-modal": "true",
241
- "aria-label": "Navigation",
355
+ "aria-label": t("Navigation"),
242
356
  className: cn(
243
357
  "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
358
  open ? "translate-x-0" : "-translate-x-full"
@@ -249,7 +363,7 @@ function CboxMobileNavDrawer({
249
363
  LinkComponent,
250
364
  {
251
365
  href: brandHref,
252
- "aria-label": "Home",
366
+ "aria-label": t("Home"),
253
367
  className: "flex items-center",
254
368
  children: /* @__PURE__ */ jsx(
255
369
  CboxBrand,
@@ -267,7 +381,7 @@ function CboxMobileNavDrawer({
267
381
  {
268
382
  type: "button",
269
383
  onClick: onClose,
270
- "aria-label": "Close navigation",
384
+ "aria-label": t("Close navigation"),
271
385
  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
386
  children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", strokeWidth: 1.8 })
273
387
  }
@@ -314,6 +428,7 @@ function CboxMobileNavDrawer({
314
428
  );
315
429
  }
316
430
  function AppSwitcher({ className, onLockedAppClick }) {
431
+ const { t } = useCboxLocalization();
317
432
  const cboxId = useCboxIdOptional();
318
433
  const [open, setOpen] = useState(false);
319
434
  const [query, setQuery] = useState("");
@@ -396,7 +511,7 @@ function AppSwitcher({ className, onLockedAppClick }) {
396
511
  }
397
512
  }, [navigableHrefs.length, activeIndex]);
398
513
  const triggerMonogram = currentApp?.monogram ?? currentApp?.name.slice(0, 1).toUpperCase() ?? "\u2B1B";
399
- const triggerName = currentApp?.name ?? "Apps";
514
+ const triggerName = currentApp?.name ?? t("Apps");
400
515
  const triggerAccent = currentApp?.accentColor;
401
516
  function onSearchKeyDown(event) {
402
517
  if (event.key === "ArrowDown") {
@@ -444,21 +559,13 @@ function AppSwitcher({ className, onLockedAppClick }) {
444
559
  /* @__PURE__ */ jsx(
445
560
  "span",
446
561
  {
447
- style: triggerAccent !== void 0 ? {
562
+ style: triggerAccent !== void 0 && secureIconUrl(currentApp?.iconUrl) === null ? {
448
563
  backgroundColor: triggerAccent,
449
564
  color: "#fff",
450
565
  borderColor: triggerAccent
451
566
  } : void 0,
452
567
  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
568
+ children: currentApp ? /* @__PURE__ */ jsx(CboxAppIcon, { name: currentApp.name, iconUrl: currentApp.iconUrl, iconDarkUrl: currentApp.iconDarkUrl, className: "h-3.5 w-3.5", fallback: /* @__PURE__ */ jsx(Fragment, { children: triggerMonogram }) }) : triggerMonogram
462
569
  }
463
570
  ),
464
571
  /* @__PURE__ */ jsx("span", { className: "max-w-[140px] truncate", children: triggerName }),
@@ -470,10 +577,10 @@ function AppSwitcher({ className, onLockedAppClick }) {
470
577
  "div",
471
578
  {
472
579
  role: "dialog",
473
- "aria-label": "Switch app",
580
+ "aria-label": t("Switch app"),
474
581
  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
582
  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" }) }),
583
+ /* @__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: t("Switch app") }) }),
477
584
  /* @__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
585
  /* @__PURE__ */ jsx(SearchIcon, {}),
479
586
  /* @__PURE__ */ jsx(
@@ -484,8 +591,8 @@ function AppSwitcher({ className, onLockedAppClick }) {
484
591
  value: query,
485
592
  onChange: (e) => setQuery(e.target.value),
486
593
  onKeyDown: onSearchKeyDown,
487
- placeholder: "Filter apps\u2026",
488
- "aria-label": "Filter apps",
594
+ placeholder: t("Filter apps\u2026"),
595
+ "aria-label": t("Filter apps"),
489
596
  autoComplete: "off",
490
597
  spellCheck: false,
491
598
  className: "w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
@@ -493,15 +600,7 @@ function AppSwitcher({ className, onLockedAppClick }) {
493
600
  )
494
601
  ] }) }),
495
602
  /* @__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,
603
+ totalMatches === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-[12px] text-muted-foreground", children: t("No apps match \u201C{query}\u201D", { query }) }) : null,
505
604
  entitled.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1", children: entitled.map((app) => {
506
605
  const navIndex = findNavIndex(navigableHrefs, app);
507
606
  return /* @__PURE__ */ jsx(
@@ -523,7 +622,7 @@ function AppSwitcher({ className, onLockedAppClick }) {
523
622
  }) }) : null,
524
623
  locked.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
525
624
  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" }),
625
+ /* @__PURE__ */ jsx("p", { className: "text-mono px-3 py-1.5 text-[10px] font-semibold tracking-[0.18em] text-muted-foreground/70 uppercase", children: t("Available") }),
527
626
  /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1 pt-0", children: locked.map((app) => {
528
627
  const navIndex = findNavIndex(navigableHrefs, app);
529
628
  return /* @__PURE__ */ jsx(
@@ -561,11 +660,12 @@ function AppRow({
561
660
  onLockedAppClick,
562
661
  onNavigate
563
662
  }) {
663
+ const { t } = useCboxLocalization();
564
664
  const isComingSoon = app.comingSoon === true;
565
665
  const isLocked = app.entitled === false || isComingSoon;
566
666
  const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
567
667
  const accent = app.accentColor;
568
- const tileStyle = accent !== void 0 && app.iconUrl === void 0 ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
668
+ const tileStyle = accent !== void 0 && secureIconUrl(app.iconUrl) === null ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
569
669
  const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl(app.url, orgSlug) : app.url;
570
670
  const tile = /* @__PURE__ */ jsx(
571
671
  "div",
@@ -576,7 +676,7 @@ function AppRow({
576
676
  app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
577
677
  isLocked && "opacity-60 grayscale"
578
678
  ),
579
- children: app.iconUrl ? /* @__PURE__ */ jsx("img", { src: app.iconUrl, alt: "", className: "h-5 w-5", loading: "lazy" }) : /* @__PURE__ */ jsx("span", { children: monogram })
679
+ children: /* @__PURE__ */ jsx(CboxAppIcon, { name: app.name, iconUrl: app.iconUrl, iconDarkUrl: app.iconDarkUrl, className: "h-5 w-5", fallback: /* @__PURE__ */ jsx("span", { children: monogram }) })
580
680
  }
581
681
  );
582
682
  const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
@@ -591,7 +691,7 @@ function AppRow({
591
691
  children: app.name
592
692
  }
593
693
  ),
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
694
+ 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: t("current") }) : null
595
695
  ] }),
596
696
  app.description ? /* @__PURE__ */ jsx(
597
697
  "span",
@@ -604,9 +704,9 @@ function AppRow({
604
704
  }
605
705
  ) : null
606
706
  ] });
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: [
707
+ 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: t("soon") }) : isLocked ? /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1 text-[10px] font-medium text-muted-foreground", children: [
608
708
  /* @__PURE__ */ jsx(Lock, {}),
609
- "Add"
709
+ t("Add")
610
710
  ] }) : null;
611
711
  if (isComingSoon) {
612
712
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
@@ -614,7 +714,7 @@ function AppRow({
614
714
  {
615
715
  "aria-disabled": true,
616
716
  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`,
717
+ title: t("{name} \u2014 coming soon", { name: app.name }),
618
718
  children: [
619
719
  tile,
620
720
  body,
@@ -638,7 +738,7 @@ function AppRow({
638
738
  highlighted && "bg-secondary",
639
739
  onLockedAppClick !== void 0 ? "hover:bg-secondary" : "cursor-default"
640
740
  ),
641
- title: `Add ${app.name} to your plan`,
741
+ title: t("Add {name} to your plan", { name: app.name }),
642
742
  children: [
643
743
  tile,
644
744
  body,
@@ -832,6 +932,7 @@ function SurfaceModal({
832
932
  size = "wide",
833
933
  children
834
934
  }) {
935
+ const { t } = useCboxLocalization();
835
936
  const dialogRef = useRef(null);
836
937
  const previousFocusRef = useRef(null);
837
938
  useEffect(() => {
@@ -928,7 +1029,7 @@ function SurfaceModal({
928
1029
  type: "button",
929
1030
  onClick: onClose,
930
1031
  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",
1032
+ "aria-label": t("Close"),
932
1033
  children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4", strokeWidth: 1.8 })
933
1034
  }
934
1035
  )
@@ -947,6 +1048,7 @@ var TONES = {
947
1048
  critical: "destructive"
948
1049
  };
949
1050
  function InboxSurface({ open, onClose }) {
1051
+ const { t, formatDate } = useCboxLocalization();
950
1052
  const transport = useTransport();
951
1053
  const cboxId = useCboxIdOptional();
952
1054
  const [inbox, setInbox] = useState(null);
@@ -974,7 +1076,7 @@ function InboxSurface({ open, onClose }) {
974
1076
  if (!cancelled) setInbox(body);
975
1077
  } catch (e) {
976
1078
  if (!cancelled) {
977
- setError(e instanceof Error ? e.message : "Load failed");
1079
+ setError(e instanceof Error ? e.message : t("Load failed"));
978
1080
  }
979
1081
  } finally {
980
1082
  if (!cancelled) setLoading(false);
@@ -1025,15 +1127,15 @@ function InboxSurface({ open, onClose }) {
1025
1127
  {
1026
1128
  open,
1027
1129
  onClose,
1028
- eyebrow: "Notifications",
1029
- title: unread > 0 ? `Inbox \xB7 ${unread} unread` : "Inbox \xB7 all caught up",
1130
+ eyebrow: t("Notifications"),
1131
+ title: unread > 0 ? t("Inbox \xB7 {count} unread", { count: unread }) : t("Inbox \xB7 all caught up"),
1030
1132
  headerActions: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1031
1133
  /* @__PURE__ */ jsx(
1032
1134
  FilterTab,
1033
1135
  {
1034
1136
  active: filter === "all",
1035
1137
  onClick: () => setFilter("all"),
1036
- children: "All"
1138
+ children: t("All")
1037
1139
  }
1038
1140
  ),
1039
1141
  /* @__PURE__ */ jsx(
@@ -1041,18 +1143,12 @@ function InboxSurface({ open, onClose }) {
1041
1143
  {
1042
1144
  active: filter === "unread",
1043
1145
  onClick: () => setFilter("unread"),
1044
- children: "Unread"
1146
+ children: t("Unread")
1045
1147
  }
1046
1148
  )
1047
1149
  ] }),
1048
1150
  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
- ] }),
1151
+ /* @__PURE__ */ jsx("span", { children: t("Showing {shown} of {total}.", { shown: filtered.length, total: items.length }) }),
1056
1152
  /* @__PURE__ */ jsx(
1057
1153
  "a",
1058
1154
  {
@@ -1060,18 +1156,18 @@ function InboxSurface({ open, onClose }) {
1060
1156
  target: "_blank",
1061
1157
  rel: "noreferrer",
1062
1158
  className: "font-medium text-foreground hover:underline",
1063
- children: "Open in new tab \u2197"
1159
+ children: t("Open in new tab \u2197")
1064
1160
  }
1065
1161
  )
1066
1162
  ] }) : /* @__PURE__ */ jsx("span", { children: "\u2014" }),
1067
1163
  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" }),
1164
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-destructive", children: t("Couldn't load inbox") }),
1069
1165
  /* @__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(
1166
+ ] }) : loading && inbox === null ? /* @__PURE__ */ jsx("div", { className: "px-5 py-12 text-center text-sm text-muted-foreground", children: t("Loading\u2026") }) : filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-5 py-12", children: /* @__PURE__ */ jsx(
1071
1167
  EmptyState,
1072
1168
  {
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."
1169
+ title: filter === "unread" ? t("No unread notifications") : t("No notifications yet"),
1170
+ description: t("When a Cbox app sends you something \u2014 invitation accepted, billing event, security alert \u2014 it lands here.")
1075
1171
  }
1076
1172
  ) }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/70", children: filtered.map((item) => {
1077
1173
  const data = item.data ?? {};
@@ -1105,9 +1201,7 @@ function InboxSurface({ open, onClose }) {
1105
1201
  /* @__PURE__ */ jsxs("p", { className: "text-mono mt-1 text-[10px] text-muted-foreground", children: [
1106
1202
  item.event_type,
1107
1203
  " \xB7 ",
1108
- new Date(
1109
- item.created_at
1110
- ).toLocaleString()
1204
+ formatDate(item.created_at)
1111
1205
  ] })
1112
1206
  ] }),
1113
1207
  item.read_at === null ? /* @__PURE__ */ jsx(
@@ -1120,8 +1214,8 @@ function InboxSurface({ open, onClose }) {
1120
1214
  void markRead(item.id);
1121
1215
  },
1122
1216
  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"
1217
+ "aria-label": t("Mark as read"),
1218
+ children: t("Mark read")
1125
1219
  }
1126
1220
  ) : null
1127
1221
  ] }) })
@@ -1225,6 +1319,7 @@ function useTransport() {
1225
1319
  return t;
1226
1320
  }
1227
1321
  function NotificationsBell({ className }) {
1322
+ const { t, formatDate } = useCboxLocalization();
1228
1323
  const ctx = useCboxIdOptional();
1229
1324
  const surfaces = useCboxSurfacesOptional();
1230
1325
  const [open, setOpen] = useState(false);
@@ -1256,7 +1351,7 @@ function NotificationsBell({ className }) {
1256
1351
  "button",
1257
1352
  {
1258
1353
  type: "button",
1259
- "aria-label": "Notifications",
1354
+ "aria-label": t("Notifications"),
1260
1355
  disabled: true,
1261
1356
  className: "relative flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground opacity-60 cursor-default",
1262
1357
  children: /* @__PURE__ */ jsx(Bell, {})
@@ -1271,7 +1366,7 @@ function NotificationsBell({ className }) {
1271
1366
  {
1272
1367
  type: "button",
1273
1368
  onClick: () => setOpen((v) => !v),
1274
- "aria-label": unreadCount > 0 ? `${unreadCount} unread notifications` : "Notifications",
1369
+ "aria-label": unreadCount > 0 ? t("{count} unread notifications", { count: unreadCount }) : t("Notifications"),
1275
1370
  "aria-expanded": open,
1276
1371
  "aria-haspopup": "menu",
1277
1372
  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",
@@ -1289,15 +1384,12 @@ function NotificationsBell({ className }) {
1289
1384
  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
1385
  children: [
1291
1386
  /* @__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
1387
+ /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] font-semibold tracking-[0.18em] uppercase text-muted-foreground", children: t("Notifications") }),
1388
+ unreadCount > 0 ? /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] tabular-nums text-muted-foreground", children: t("{count} unread", { count: unreadCount }) }) : null
1297
1389
  ] }),
1298
1390
  /* @__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." })
1391
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: t("All caught up") }),
1392
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("No recent notifications.") })
1301
1393
  ] }) : /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border/70", children: previewItems.map((item) => /* @__PURE__ */ jsx(
1302
1394
  PreviewRow,
1303
1395
  {
@@ -1316,14 +1408,14 @@ function NotificationsBell({ className }) {
1316
1408
  surfaces.open("inbox");
1317
1409
  },
1318
1410
  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"
1411
+ children: t("See all notifications \u2192")
1320
1412
  }
1321
1413
  ) : /* @__PURE__ */ jsx(
1322
1414
  "a",
1323
1415
  {
1324
1416
  href: inboxUrl,
1325
1417
  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"
1418
+ children: t("See all notifications \u2192")
1327
1419
  }
1328
1420
  )
1329
1421
  ]
@@ -1336,10 +1428,11 @@ function PreviewRow({
1336
1428
  markReadUrl,
1337
1429
  onMarkedRead
1338
1430
  }) {
1431
+ const { t, formatDate } = useCboxLocalization();
1339
1432
  const tone = item.severity === "critical" ? "bg-destructive" : item.severity === "warning" ? "bg-warning" : "bg-info";
1340
1433
  const ts = (() => {
1341
1434
  try {
1342
- return new Date(item.createdAt).toLocaleString();
1435
+ return formatDate(item.createdAt);
1343
1436
  } catch {
1344
1437
  return item.createdAt;
1345
1438
  }
@@ -1393,8 +1486,8 @@ function PreviewRow({
1393
1486
  type: "button",
1394
1487
  onClick: markRead,
1395
1488
  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",
1489
+ "aria-label": t("Mark as read"),
1490
+ title: t("Mark as read"),
1398
1491
  children: "\u2713"
1399
1492
  }
1400
1493
  ) : null
@@ -1432,7 +1525,8 @@ function Bell() {
1432
1525
  }
1433
1526
  );
1434
1527
  }
1435
- function SearchTrigger({ onOpen, className, label = "Search" }) {
1528
+ function SearchTrigger({ onOpen, className, label }) {
1529
+ const { t } = useCboxLocalization();
1436
1530
  const [shortcut, setShortcut] = useState("\u2318K");
1437
1531
  useEffect(() => {
1438
1532
  if (typeof navigator === "undefined") return;
@@ -1447,10 +1541,10 @@ function SearchTrigger({ onOpen, className, label = "Search" }) {
1447
1541
  "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
1542
  className
1449
1543
  ),
1450
- "aria-label": "Open command palette",
1544
+ "aria-label": t("Open command palette"),
1451
1545
  children: [
1452
1546
  /* @__PURE__ */ jsx(SearchIcon2, {}),
1453
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: label }),
1547
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: label ?? t("Search") }),
1454
1548
  /* @__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
1549
  ]
1456
1550
  }
@@ -1476,18 +1570,28 @@ function SearchIcon2() {
1476
1570
  }
1477
1571
  );
1478
1572
  }
1479
- function UserMenu({ extras, className }) {
1573
+ var Anchor = ({ href, className, children }) => /* @__PURE__ */ jsx("a", { href, className, children });
1574
+ function UserMenu({ extras, className, placement = "bottom-end", LinkComponent: Link = Anchor }) {
1575
+ const { t } = useCboxLocalization();
1480
1576
  const { user, currentOrganization, urls } = useCboxId();
1481
1577
  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: [
1578
+ const initials2 = user.name.split(/\s+/).filter(Boolean).slice(0, 2).map((n) => n[0]?.toUpperCase() ?? "").join("");
1579
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), onKeyDown: (event) => {
1580
+ if (event.key === "Escape") setOpen(false);
1581
+ }, onBlur: (event) => {
1582
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
1583
+ }, children: [
1484
1584
  /* @__PURE__ */ jsx(
1485
1585
  "button",
1486
1586
  {
1487
1587
  type: "button",
1488
1588
  onClick: () => setOpen((v) => !v),
1489
1589
  className: "flex h-9 items-center gap-2 rounded-md px-1.5 hover:bg-secondary",
1490
- "aria-label": "Open user menu",
1590
+ "aria-label": t("Open user menu"),
1591
+ "aria-expanded": open,
1592
+ onKeyDown: (event) => {
1593
+ if (event.key === "Escape") setOpen(false);
1594
+ },
1491
1595
  children: user.avatar_url ? /* @__PURE__ */ jsx(
1492
1596
  "img",
1493
1597
  {
@@ -1495,10 +1599,10 @@ function UserMenu({ extras, className }) {
1495
1599
  alt: "",
1496
1600
  className: "h-7 w-7 rounded-full object-cover"
1497
1601
  }
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 || "?" })
1602
+ ) : /* @__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: initials2 || "?" })
1499
1603
  }
1500
1604
  ),
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: [
1605
+ open ? /* @__PURE__ */ jsxs("div", { className: cn("absolute z-50 w-64 rounded-lg border border-border bg-popover p-1 shadow-md", placement === "top-start" ? "bottom-full left-0 mb-2" : "right-0 top-full mt-1"), children: [
1502
1606
  /* @__PURE__ */ jsxs("div", { className: "px-2 py-2", children: [
1503
1607
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: user.name }),
1504
1608
  /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: user.email })
@@ -1509,24 +1613,24 @@ function UserMenu({ extras, className }) {
1509
1613
  ] }) : null,
1510
1614
  /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
1511
1615
  /* @__PURE__ */ jsxs(
1512
- "a",
1616
+ Link,
1513
1617
  {
1514
1618
  href: urls.accountUrl,
1515
1619
  className: "flex items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-secondary",
1516
1620
  children: [
1517
- /* @__PURE__ */ jsx("span", { children: "Manage account" }),
1621
+ /* @__PURE__ */ jsx("span", { children: t("Manage account") }),
1518
1622
  /* @__PURE__ */ jsx(CrossAppArrow, {})
1519
1623
  ]
1520
1624
  }
1521
1625
  ),
1522
1626
  urls.orgSettingsUrl && currentOrganization ? /* @__PURE__ */ jsxs(
1523
- "a",
1627
+ Link,
1524
1628
  {
1525
1629
  href: urls.orgSettingsUrl,
1526
1630
  className: "flex items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-secondary",
1527
1631
  children: [
1528
1632
  /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
1529
- "Team & billing",
1633
+ t("Team & billing"),
1530
1634
  /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1531
1635
  " \xB7 ",
1532
1636
  currentOrganization.name
@@ -1544,7 +1648,7 @@ function UserMenu({ extras, className }) {
1544
1648
  {
1545
1649
  type: "submit",
1546
1650
  className: "flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-secondary",
1547
- children: "Sign out"
1651
+ children: t("Sign out")
1548
1652
  }
1549
1653
  )
1550
1654
  ] })
@@ -1594,6 +1698,7 @@ function CboxAppShell({
1594
1698
  topbarRightCluster,
1595
1699
  children
1596
1700
  }) {
1701
+ const { t } = useCboxLocalization();
1597
1702
  const ToggleIcon = sidebarCollapsed ? PanelLeftOpen : PanelLeftClose;
1598
1703
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
1599
1704
  useEffect(() => {
@@ -1636,8 +1741,8 @@ function CboxAppShell({
1636
1741
  type: "button",
1637
1742
  onClick: () => setMobileNavOpen(true),
1638
1743
  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",
1744
+ "aria-label": t("Open navigation"),
1745
+ title: t("Open navigation"),
1641
1746
  children: /* @__PURE__ */ jsx(Menu, { className: "h-4 w-4", strokeWidth: 1.8 })
1642
1747
  }
1643
1748
  ),
@@ -1647,8 +1752,8 @@ function CboxAppShell({
1647
1752
  type: "button",
1648
1753
  onClick: onToggleSidebar,
1649
1754
  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",
1755
+ "aria-label": sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar"),
1756
+ title: sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar"),
1652
1757
  children: /* @__PURE__ */ jsx(
1653
1758
  ToggleIcon,
1654
1759
  {
@@ -1678,6 +1783,7 @@ function ContextBar({
1678
1783
  LinkComponent = DefaultLink,
1679
1784
  className
1680
1785
  }) {
1786
+ const { t } = useCboxLocalization();
1681
1787
  if (crumbs.length === 0) {
1682
1788
  return null;
1683
1789
  }
@@ -1685,7 +1791,7 @@ function ContextBar({
1685
1791
  "nav",
1686
1792
  {
1687
1793
  "data-component": "context-bar",
1688
- "aria-label": "Context",
1794
+ "aria-label": t("Context"),
1689
1795
  className: cn("flex min-w-0 items-center gap-1", className),
1690
1796
  children: crumbs.map((crumb, index) => /* @__PURE__ */ jsxs(
1691
1797
  "div",
@@ -1721,6 +1827,7 @@ function ContextCrumbChip({
1721
1827
  crumb,
1722
1828
  LinkComponent
1723
1829
  }) {
1830
+ const { t } = useCboxLocalization();
1724
1831
  const [open, setOpen] = useState(false);
1725
1832
  const ref = useRef(null);
1726
1833
  const listId = useId();
@@ -1742,7 +1849,7 @@ function ContextCrumbChip({
1742
1849
  };
1743
1850
  }, [open]);
1744
1851
  const hasMenu = crumb.items.length > 0 || crumb.footerLink !== void 0;
1745
- return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
1852
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative min-w-0", children: [
1746
1853
  /* @__PURE__ */ jsxs(
1747
1854
  "button",
1748
1855
  {
@@ -1751,7 +1858,7 @@ function ContextCrumbChip({
1751
1858
  "aria-expanded": hasMenu ? open : void 0,
1752
1859
  "aria-haspopup": hasMenu ? "listbox" : void 0,
1753
1860
  "aria-controls": hasMenu ? listId : void 0,
1754
- "aria-label": crumb.ariaLabel ?? `Switch ${crumb.label}`,
1861
+ "aria-label": crumb.ariaLabel ?? t("Switch {name}", { name: crumb.label }),
1755
1862
  disabled: !hasMenu,
1756
1863
  className: cn(
1757
1864
  "flex h-8 min-w-0 items-center gap-2 rounded-md border border-transparent px-2 text-[13px] text-foreground transition-colors",
@@ -1767,7 +1874,7 @@ function ContextCrumbChip({
1767
1874
  size: "sm"
1768
1875
  }
1769
1876
  ) : null,
1770
- /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: crumb.label }),
1877
+ /* @__PURE__ */ jsx("span", { className: "truncate font-semibold", children: crumb.label }),
1771
1878
  hasMenu ? /* @__PURE__ */ jsx(Chevron2, { open }) : null
1772
1879
  ]
1773
1880
  }
@@ -1822,6 +1929,7 @@ function ContextItemRow({
1822
1929
  LinkComponent,
1823
1930
  onAfterClick
1824
1931
  }) {
1932
+ const { t } = useCboxLocalization();
1825
1933
  const body = /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-2.5 px-2.5 py-2", children: [
1826
1934
  item.avatar !== void 0 ? /* @__PURE__ */ jsx(
1827
1935
  ContextAvatarChip,
@@ -1850,7 +1958,7 @@ function ContextItemRow({
1850
1958
  children: /* @__PURE__ */ jsx(
1851
1959
  "span",
1852
1960
  {
1853
- "aria-label": item.settingsAriaLabel ?? `Settings for ${item.label}`,
1961
+ "aria-label": item.settingsAriaLabel ?? t("Settings for {name}", { name: item.label }),
1854
1962
  onClickCapture: (event) => {
1855
1963
  event.stopPropagation();
1856
1964
  onAfterClick();
@@ -1922,8 +2030,8 @@ function ContextAvatarChip({
1922
2030
  fallbackLabel,
1923
2031
  size = "md"
1924
2032
  }) {
1925
- const initials = avatar.initials ?? deriveInitials(fallbackLabel);
1926
- const sizeClass = size === "sm" ? "h-5 w-5 text-[9px]" : "h-7 w-7 text-[11px]";
2033
+ const initials2 = avatar.initials ?? deriveInitials(fallbackLabel);
2034
+ const sizeClass = size === "sm" ? "h-[26px] w-[26px] text-[10px]" : "h-7 w-7 text-[11px]";
1927
2035
  const style = avatar.src === void 0 && avatar.accent !== void 0 ? {
1928
2036
  backgroundColor: avatar.accent,
1929
2037
  color: "#fff"
@@ -1951,7 +2059,7 @@ function ContextAvatarChip({
1951
2059
  avatar.accent === void 0 && "bg-info-soft text-info",
1952
2060
  sizeClass
1953
2061
  ),
1954
- children: initials
2062
+ children: initials2
1955
2063
  }
1956
2064
  );
1957
2065
  }
@@ -2023,6 +2131,7 @@ function CboxOrgContextBar({
2023
2131
  LinkComponent,
2024
2132
  onNavigate
2025
2133
  }) {
2134
+ const { t } = useCboxLocalization();
2026
2135
  const cboxId = useCboxIdOptional();
2027
2136
  if (cboxId === null) {
2028
2137
  return null;
@@ -2041,7 +2150,7 @@ function CboxOrgContextBar({
2041
2150
  // Cogwheel shortcut → id's org-settings page. Cross-app
2042
2151
  // deep link; id handles the auth and rendering.
2043
2152
  settingsHref: `${idBase}/org/${org.slug}/settings`,
2044
- settingsAriaLabel: `Settings for ${org.name}`
2153
+ settingsAriaLabel: t("Settings for {name}", { name: org.name })
2045
2154
  }));
2046
2155
  const onSelect = (slug) => {
2047
2156
  const org = organizations.find((o) => o.slug === slug);
@@ -2063,10 +2172,10 @@ function CboxOrgContextBar({
2063
2172
  items,
2064
2173
  onSelect,
2065
2174
  footerLink: {
2066
- label: "Manage organizations",
2175
+ label: t("Manage organizations"),
2067
2176
  href: `${idBase}/settings/organizations`
2068
2177
  },
2069
- ariaLabel: "Switch organization"
2178
+ ariaLabel: t("Switch organization")
2070
2179
  }
2071
2180
  ]
2072
2181
  }
@@ -2080,6 +2189,7 @@ function deriveInitials2(label) {
2080
2189
  return (label.slice(0, 2) || "?").toUpperCase();
2081
2190
  }
2082
2191
  function OrgSwitcher({ Trigger = DefaultTrigger, className }) {
2192
+ const { t } = useCboxLocalization();
2083
2193
  const { currentOrganization, organizations, onSwitchOrganization } = useCboxId();
2084
2194
  const [open, setOpen] = useState(false);
2085
2195
  const [query, setQuery] = useState("");
@@ -2179,10 +2289,10 @@ function OrgSwitcher({ Trigger = DefaultTrigger, className }) {
2179
2289
  "div",
2180
2290
  {
2181
2291
  role: "dialog",
2182
- "aria-label": "Switch organization",
2292
+ "aria-label": t("Switch organization"),
2183
2293
  className: "absolute left-0 top-full z-50 mt-1 w-64 overflow-hidden rounded-md border border-border bg-popover shadow-md",
2184
2294
  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" }) }),
2295
+ /* @__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: t("Switch organization") }) }),
2186
2296
  /* @__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
2297
  /* @__PURE__ */ jsx(SearchIcon3, {}),
2188
2298
  /* @__PURE__ */ jsx(
@@ -2193,23 +2303,15 @@ function OrgSwitcher({ Trigger = DefaultTrigger, className }) {
2193
2303
  value: query,
2194
2304
  onChange: (e) => setQuery(e.target.value),
2195
2305
  onKeyDown: onSearchKeyDown,
2196
- placeholder: "Filter\u2026",
2197
- "aria-label": "Filter organizations",
2306
+ placeholder: t("Filter\u2026"),
2307
+ "aria-label": t("Filter organizations"),
2198
2308
  autoComplete: "off",
2199
2309
  spellCheck: false,
2200
2310
  className: "w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
2201
2311
  }
2202
2312
  )
2203
2313
  ] }) }),
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) => {
2314
+ /* @__PURE__ */ jsx("div", { className: "max-h-[50vh] overflow-y-auto p-1", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-[12px] text-muted-foreground", children: t("No organizations match \u201C{query}\u201D", { query }) }) : /* @__PURE__ */ jsx("ul", { className: "space-y-0.5", children: filtered.map((org, index) => {
2213
2315
  const active = currentOrganization?.id === org.id;
2214
2316
  const highlighted = index === activeIndex;
2215
2317
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
@@ -2224,7 +2326,7 @@ function OrgSwitcher({ Trigger = DefaultTrigger, className }) {
2224
2326
  ),
2225
2327
  children: [
2226
2328
  /* @__PURE__ */ jsx("span", { className: "truncate", children: org.name }),
2227
- active ? /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] text-muted-foreground", children: "current" }) : null
2329
+ active ? /* @__PURE__ */ jsx("span", { className: "text-mono text-[10px] text-muted-foreground", children: t("current") }) : null
2228
2330
  ]
2229
2331
  }
2230
2332
  ) }, org.id);
@@ -2320,6 +2422,7 @@ function TopBar({
2320
2422
  );
2321
2423
  }
2322
2424
  function AppLauncher({ className, onLockedAppClick }) {
2425
+ const { t } = useCboxLocalization();
2323
2426
  const cboxId = useCboxIdOptional();
2324
2427
  const [open, setOpen] = useState(false);
2325
2428
  const containerRef = useRef(null);
@@ -2360,7 +2463,7 @@ function AppLauncher({ className, onLockedAppClick }) {
2360
2463
  "button",
2361
2464
  {
2362
2465
  type: "button",
2363
- "aria-label": "Open app launcher",
2466
+ "aria-label": t("Open app launcher"),
2364
2467
  "aria-expanded": open,
2365
2468
  onClick: () => setOpen((v) => !v),
2366
2469
  className: cn(
@@ -2374,10 +2477,10 @@ function AppLauncher({ className, onLockedAppClick }) {
2374
2477
  "div",
2375
2478
  {
2376
2479
  role: "dialog",
2377
- "aria-label": "Cbox app launcher",
2480
+ "aria-label": t("Cbox app launcher"),
2378
2481
  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
2482
  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" }) }),
2483
+ /* @__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: t("Cbox suite") }) }),
2381
2484
  /* @__PURE__ */ jsxs("div", { className: "max-h-[60vh] overflow-y-auto py-1", children: [
2382
2485
  entitled.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1", children: entitled.map((app) => /* @__PURE__ */ jsx(
2383
2486
  AppRow2,
@@ -2390,7 +2493,7 @@ function AppLauncher({ className, onLockedAppClick }) {
2390
2493
  )) }) : null,
2391
2494
  locked.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
2392
2495
  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" }),
2496
+ /* @__PURE__ */ jsx("p", { className: "text-mono px-3 py-1.5 text-[10px] font-semibold tracking-[0.18em] text-muted-foreground/70 uppercase", children: t("Available") }),
2394
2497
  /* @__PURE__ */ jsx("ul", { className: "space-y-0.5 p-1 pt-0", children: locked.map((app) => /* @__PURE__ */ jsx(
2395
2498
  AppRow2,
2396
2499
  {
@@ -2408,11 +2511,12 @@ function AppLauncher({ className, onLockedAppClick }) {
2408
2511
  ] });
2409
2512
  }
2410
2513
  function AppRow2({ app, orgSlug, onLockedAppClick }) {
2514
+ const { t } = useCboxLocalization();
2411
2515
  const isComingSoon = app.comingSoon === true;
2412
2516
  const isLocked = app.entitled === false || isComingSoon;
2413
2517
  const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
2414
2518
  const accent = app.accentColor;
2415
- const tileStyle = accent !== void 0 && app.iconUrl === void 0 ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
2519
+ const tileStyle = accent !== void 0 && secureIconUrl(app.iconUrl) === null ? { backgroundColor: accent, color: "#fff", borderColor: accent } : void 0;
2416
2520
  const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl2(app.url, orgSlug) : app.url;
2417
2521
  const tile = /* @__PURE__ */ jsx(
2418
2522
  "div",
@@ -2423,7 +2527,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2423
2527
  app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
2424
2528
  isLocked && "opacity-60 grayscale"
2425
2529
  ),
2426
- children: app.iconUrl ? /* @__PURE__ */ jsx("img", { src: app.iconUrl, alt: "", className: "h-5 w-5", loading: "lazy" }) : /* @__PURE__ */ jsx("span", { children: monogram })
2530
+ children: /* @__PURE__ */ jsx(CboxAppIcon, { name: app.name, iconUrl: app.iconUrl, iconDarkUrl: app.iconDarkUrl, className: "h-5 w-5", fallback: /* @__PURE__ */ jsx("span", { children: monogram }) })
2427
2531
  }
2428
2532
  );
2429
2533
  const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
@@ -2438,7 +2542,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2438
2542
  children: app.name
2439
2543
  }
2440
2544
  ),
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
2545
+ 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: t("current") }) : null
2442
2546
  ] }),
2443
2547
  app.description ? /* @__PURE__ */ jsx(
2444
2548
  "span",
@@ -2451,9 +2555,9 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2451
2555
  }
2452
2556
  ) : null
2453
2557
  ] });
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: [
2558
+ 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: t("soon") }) : isLocked ? /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1 text-[10px] font-medium text-muted-foreground", children: [
2455
2559
  /* @__PURE__ */ jsx(Lock2, {}),
2456
- "Add"
2560
+ t("Add")
2457
2561
  ] }) : null;
2458
2562
  if (isComingSoon) {
2459
2563
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
@@ -2461,7 +2565,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2461
2565
  {
2462
2566
  "aria-disabled": true,
2463
2567
  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`,
2568
+ title: t("{name} \u2014 coming soon", { name: app.name }),
2465
2569
  children: [
2466
2570
  tile,
2467
2571
  body,
@@ -2480,7 +2584,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2480
2584
  "group flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left transition-colors",
2481
2585
  onLockedAppClick !== void 0 ? "hover:bg-secondary" : "cursor-default"
2482
2586
  ),
2483
- title: `Add ${app.name} to your plan`,
2587
+ title: t("Add {name} to your plan", { name: app.name }),
2484
2588
  children: [
2485
2589
  tile,
2486
2590
  body,
@@ -2553,12 +2657,182 @@ function Lock2() {
2553
2657
  }
2554
2658
  );
2555
2659
  }
2660
+ var DEFAULT_REFRESH_SECONDS = 60;
2661
+ function parseAppCatalog(ok, body) {
2662
+ const data = typeof body === "object" && body !== null ? body : {};
2663
+ if (!ok) {
2664
+ const authorize = typeof data.authorize_url === "string" ? data.authorize_url : null;
2665
+ if (authorize !== null || data.state === "authorization_required") {
2666
+ return { status: "authorization_required", authorizeUrl: authorize };
2667
+ }
2668
+ return { status: "unavailable" };
2669
+ }
2670
+ if (!Array.isArray(data.apps)) {
2671
+ return { status: "unavailable" };
2672
+ }
2673
+ const apps = [];
2674
+ for (const entry of data.apps) {
2675
+ if (typeof entry !== "object" || entry === null) continue;
2676
+ const app = entry;
2677
+ if (typeof app.key !== "string" || typeof app.name !== "string" || typeof app.launch_url !== "string") continue;
2678
+ if (!/^https?:\/\//.test(app.launch_url)) continue;
2679
+ apps.push({
2680
+ key: app.key,
2681
+ name: app.name,
2682
+ launch_url: app.launch_url,
2683
+ icon_url: typeof app.icon_url === "string" ? app.icon_url : null,
2684
+ icon_dark_url: typeof app.icon_dark_url === "string" ? app.icon_dark_url : null,
2685
+ description: typeof app.description === "string" ? app.description : null
2686
+ });
2687
+ }
2688
+ const refresh = typeof data.refresh_after === "number" && Number.isFinite(data.refresh_after) ? data.refresh_after : DEFAULT_REFRESH_SECONDS;
2689
+ return { status: "ready", apps, refreshAfter: Math.max(1, Math.min(DEFAULT_REFRESH_SECONDS, refresh)) };
2690
+ }
2691
+ function authorizeHref(authorizeUrl, location) {
2692
+ if (authorizeUrl === null) return null;
2693
+ try {
2694
+ const url = new URL(authorizeUrl, location.origin);
2695
+ if (url.origin !== location.origin) return null;
2696
+ url.searchParams.set("return_to", location.pathname + location.search);
2697
+ return url.pathname + url.search;
2698
+ } catch {
2699
+ return null;
2700
+ }
2701
+ }
2702
+ function CboxAppCatalog({ endpoint, currentAppKey, className }) {
2703
+ const { t } = useCboxLocalization();
2704
+ const [open, setOpen] = useState(false);
2705
+ const [state, setState] = useState({ status: "idle" });
2706
+ const container = useRef(null);
2707
+ const flight = useRef(null);
2708
+ const expires = useRef(0);
2709
+ const load = useCallback(async () => {
2710
+ if (endpoint === null || flight.current !== null) return;
2711
+ const request = new AbortController();
2712
+ flight.current = request;
2713
+ setState((current) => current.status === "ready" ? current : { status: "loading" });
2714
+ try {
2715
+ const response = await fetch(endpoint, {
2716
+ credentials: "same-origin",
2717
+ cache: "no-store",
2718
+ headers: { Accept: "application/json", "X-Requested-With": "XMLHttpRequest" },
2719
+ signal: request.signal
2720
+ });
2721
+ const body = await response.json().catch(() => null);
2722
+ if (request.signal.aborted) return;
2723
+ const next = parseAppCatalog(response.ok, body);
2724
+ expires.current = next.status === "ready" ? Date.now() + next.refreshAfter * 1e3 : 0;
2725
+ setState(next);
2726
+ if (next.status === "ready") rememberAppCatalog(next.apps);
2727
+ } catch {
2728
+ if (!request.signal.aborted) {
2729
+ expires.current = 0;
2730
+ setState({ status: "unavailable" });
2731
+ }
2732
+ } finally {
2733
+ if (flight.current === request) flight.current = null;
2734
+ }
2735
+ }, [endpoint]);
2736
+ const refresh = useCallback(() => {
2737
+ if (document.visibilityState === "visible" && Date.now() >= expires.current) void load();
2738
+ }, [load]);
2739
+ useEffect(() => {
2740
+ if (!open) return;
2741
+ refresh();
2742
+ const outside = (event) => {
2743
+ if (container.current !== null && !container.current.contains(event.target)) setOpen(false);
2744
+ };
2745
+ const key = (event) => {
2746
+ if (event.key === "Escape") setOpen(false);
2747
+ };
2748
+ window.addEventListener("pointerdown", outside);
2749
+ window.addEventListener("keydown", key);
2750
+ window.addEventListener("focus", refresh);
2751
+ document.addEventListener("visibilitychange", refresh);
2752
+ return () => {
2753
+ window.removeEventListener("pointerdown", outside);
2754
+ window.removeEventListener("keydown", key);
2755
+ window.removeEventListener("focus", refresh);
2756
+ document.removeEventListener("visibilitychange", refresh);
2757
+ };
2758
+ }, [open, refresh]);
2759
+ useEffect(() => {
2760
+ if (!open || state.status !== "ready") return;
2761
+ const timer = window.setTimeout(refresh, Math.max(50, expires.current - Date.now()));
2762
+ return () => window.clearTimeout(timer);
2763
+ }, [open, state, refresh]);
2764
+ useEffect(() => () => flight.current?.abort(), []);
2765
+ useEffect(() => {
2766
+ expires.current = 0;
2767
+ setState({ status: "idle" });
2768
+ }, [endpoint]);
2769
+ const reconnect = state.status === "authorization_required" && typeof window !== "undefined" ? authorizeHref(state.authorizeUrl, window.location) : null;
2770
+ return /* @__PURE__ */ jsxs("div", { ref: container, className: cn("relative", className), children: [
2771
+ /* @__PURE__ */ jsx(
2772
+ "button",
2773
+ {
2774
+ type: "button",
2775
+ "aria-label": t("Switch app"),
2776
+ title: t("Switch app"),
2777
+ "aria-haspopup": "dialog",
2778
+ "aria-expanded": open,
2779
+ disabled: endpoint === null,
2780
+ onClick: () => setOpen((value) => !value),
2781
+ className: cn(
2782
+ "flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent",
2783
+ open && "bg-secondary text-foreground"
2784
+ ),
2785
+ children: /* @__PURE__ */ jsx(Grid2, {})
2786
+ }
2787
+ ),
2788
+ open ? /* @__PURE__ */ jsxs("div", { role: "dialog", "aria-label": t("Switch app"), className: "absolute right-0 top-full z-50 mt-1.5 w-[min(18.25rem,calc(100vw-1rem))] overflow-hidden rounded-lg border border-border bg-popover p-3 shadow-lg", children: [
2789
+ /* @__PURE__ */ jsx("h2", { className: "mb-2 text-sm font-semibold text-foreground", children: t("Switch app") }),
2790
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
2791
+ (state.status === "idle" || state.status === "loading") && /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-sm text-muted-foreground", children: t("Loading apps\u2026") }),
2792
+ state.status === "unavailable" && /* @__PURE__ */ jsxs("div", { className: "space-y-2 px-1 py-2 text-sm", children: [
2793
+ /* @__PURE__ */ jsx("p", { role: "alert", className: "text-muted-foreground", children: t("Apps could not be loaded.") }),
2794
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void load(), className: "font-medium text-foreground underline", children: t("Try again") })
2795
+ ] }),
2796
+ state.status === "authorization_required" && /* @__PURE__ */ jsxs("div", { className: "space-y-2 px-1 py-2 text-sm", children: [
2797
+ /* @__PURE__ */ jsx("p", { role: "alert", className: "text-muted-foreground", children: t("Apps could not be loaded.") }),
2798
+ reconnect !== null ? /* @__PURE__ */ jsx("a", { href: reconnect, className: "font-medium text-foreground underline", children: t("Reconnect to Cbox ID") }) : /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void load(), className: "font-medium text-foreground underline", children: t("Try again") })
2799
+ ] }),
2800
+ state.status === "ready" && (state.apps.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-sm text-muted-foreground", children: t("No apps available") }) : /* @__PURE__ */ jsx("nav", { "aria-label": t("Switch app"), className: "grid grid-cols-3 gap-1", children: state.apps.map((app) => {
2801
+ const current = app.key === currentAppKey;
2802
+ return /* @__PURE__ */ jsxs(
2803
+ "a",
2804
+ {
2805
+ href: app.launch_url,
2806
+ "aria-current": current ? "true" : void 0,
2807
+ title: app.description ?? app.name,
2808
+ onClick: () => setOpen(false),
2809
+ className: cn("relative flex min-h-[60px] min-w-0 flex-col items-center justify-center gap-1.5 rounded-md p-2 text-center text-[13px] font-medium leading-tight text-foreground hover:bg-secondary", current && "bg-accent-soft"),
2810
+ children: [
2811
+ /* @__PURE__ */ jsx(CboxAppIcon, { name: app.name, iconUrl: app.icon_url, iconDarkUrl: app.icon_dark_url }),
2812
+ /* @__PURE__ */ jsx("span", { className: "max-w-full break-words", children: app.name.replace(/^Cbox\s*·\s*/i, "") }),
2813
+ current && /* @__PURE__ */ jsx(Check2, {})
2814
+ ]
2815
+ },
2816
+ app.key
2817
+ );
2818
+ }) }))
2819
+ ] })
2820
+ ] }) : null
2821
+ ] });
2822
+ }
2823
+ function Grid2() {
2824
+ return /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "currentColor", "aria-hidden": true, children: [3, 9, 15].flatMap((y) => [3, 9, 15].map((x) => /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r: "1.65" }, `${x}-${y}`))) });
2825
+ }
2826
+ function Check2() {
2827
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": true, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", className: "absolute right-2 top-1.5 h-3 w-3 text-accent", children: /* @__PURE__ */ jsx("path", { d: "m5 12 4 4L19 6" }) });
2828
+ }
2556
2829
  function Combobox(props) {
2830
+ const { t } = useCboxLocalization();
2557
2831
  const {
2558
2832
  items,
2559
- placeholder = "Select\u2026",
2560
- searchPlaceholder = "Search\u2026",
2561
- emptyText = "Nothing matches.",
2833
+ placeholder = t("Select\u2026"),
2834
+ searchPlaceholder = t("Search\u2026"),
2835
+ emptyText = t("Nothing matches."),
2562
2836
  footerLink,
2563
2837
  disabled = false,
2564
2838
  required = false,
@@ -2855,7 +3129,7 @@ function ComboboxGroupRows({
2855
3129
  /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-foreground", children: opt.label }),
2856
3130
  opt.secondary !== void 0 ? /* @__PURE__ */ jsx("span", { className: "text-mono truncate text-[10px] text-muted-foreground", children: opt.secondary }) : null
2857
3131
  ] }),
2858
- isActive ? /* @__PURE__ */ jsx(Check2, {}) : null
3132
+ isActive ? /* @__PURE__ */ jsx(Check3, {}) : null
2859
3133
  ] });
2860
3134
  const className = cn(
2861
3135
  "group block w-full rounded-sm transition-colors text-left",
@@ -2947,7 +3221,7 @@ function ComboboxAvatarChip({
2947
3221
  avatar,
2948
3222
  fallbackLabel
2949
3223
  }) {
2950
- const initials = avatar.initials ?? deriveInitials3(fallbackLabel);
3224
+ const initials2 = avatar.initials ?? deriveInitials3(fallbackLabel);
2951
3225
  const style = avatar.src === void 0 && avatar.accent !== void 0 ? { backgroundColor: avatar.accent, color: "#fff" } : void 0;
2952
3226
  if (avatar.src !== void 0) {
2953
3227
  return /* @__PURE__ */ jsx(
@@ -2968,7 +3242,7 @@ function ComboboxAvatarChip({
2968
3242
  "flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-[9px] font-semibold tracking-tight",
2969
3243
  avatar.accent === void 0 && "bg-info-soft text-info"
2970
3244
  ),
2971
- children: initials
3245
+ children: initials2
2972
3246
  }
2973
3247
  );
2974
3248
  }
@@ -3037,7 +3311,7 @@ function Chevron4({ open }) {
3037
3311
  }
3038
3312
  );
3039
3313
  }
3040
- function Check2() {
3314
+ function Check3() {
3041
3315
  return /* @__PURE__ */ jsx(
3042
3316
  "svg",
3043
3317
  {
@@ -3103,7 +3377,7 @@ function DataPanel({
3103
3377
  "section",
3104
3378
  {
3105
3379
  className: cn(
3106
- "rounded-lg border border-border bg-card text-card-foreground shadow-[0_1px_2px_rgba(0,0,0,0.02)]",
3380
+ "rounded-lg border border-border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_4px_12px_rgb(0_0_0/0.03)]",
3107
3381
  className
3108
3382
  ),
3109
3383
  ...rest,
@@ -3289,12 +3563,13 @@ function PageTabs({
3289
3563
  );
3290
3564
  }
3291
3565
  function DesktopStrip({ items, activeHref, LinkComponent }) {
3566
+ const { t } = useCboxLocalization();
3292
3567
  return /* @__PURE__ */ jsx(
3293
3568
  "nav",
3294
3569
  {
3295
3570
  className: "flex min-h-12 flex-1 items-center gap-1 overflow-x-auto overflow-y-hidden py-1.5",
3296
3571
  role: "tablist",
3297
- "aria-label": "Section navigation",
3572
+ "aria-label": t("Section navigation"),
3298
3573
  children: items.map((item) => {
3299
3574
  const Icon = item.icon;
3300
3575
  const active = item.href === activeHref;
@@ -3330,6 +3605,7 @@ function DesktopStrip({ items, activeHref, LinkComponent }) {
3330
3605
  );
3331
3606
  }
3332
3607
  function MobileDropdown({ items, activeHref, LinkComponent }) {
3608
+ const { t } = useCboxLocalization();
3333
3609
  const [open, setOpen] = useState(false);
3334
3610
  const ref = useRef(null);
3335
3611
  const listId = useId();
@@ -3389,7 +3665,7 @@ function MobileDropdown({ items, activeHref, LinkComponent }) {
3389
3665
  {
3390
3666
  id: listId,
3391
3667
  role: "listbox",
3392
- "aria-label": "Section navigation",
3668
+ "aria-label": t("Section navigation"),
3393
3669
  className: "absolute left-0 right-0 top-full z-30 mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-lg",
3394
3670
  children: items.map((item) => {
3395
3671
  const Icon = item.icon;
@@ -3590,7 +3866,7 @@ function StatTile({
3590
3866
  function urlString(defaultValue = "") {
3591
3867
  return {
3592
3868
  parse: (raw) => raw ?? defaultValue,
3593
- serialize: (value) => value === "" ? null : value,
3869
+ serialize: (value) => value,
3594
3870
  isDefault: (value) => value === defaultValue
3595
3871
  };
3596
3872
  }
@@ -3665,7 +3941,152 @@ function buildUrlSearch(state, schema) {
3665
3941
  }
3666
3942
  return params.toString();
3667
3943
  }
3944
+ function navMatchLength(path, href) {
3945
+ if (href === "/") {
3946
+ return path === "/" ? 1 : -1;
3947
+ }
3948
+ return path === href || path.startsWith(href + "/") ? href.length : -1;
3949
+ }
3950
+ function activeNavItem(path, items) {
3951
+ let best;
3952
+ let length = -1;
3953
+ for (const item of items) {
3954
+ const match = navMatchLength(path, item.href);
3955
+ if (match > length) {
3956
+ best = item;
3957
+ length = match;
3958
+ }
3959
+ }
3960
+ return best;
3961
+ }
3962
+ function activeNavArea(path, areas) {
3963
+ let best;
3964
+ let length = -1;
3965
+ for (const area of areas) {
3966
+ const candidates = [area.href, ...(area.items ?? []).map((item) => item.href)];
3967
+ const match = Math.max(...candidates.map((href) => navMatchLength(path, href)));
3968
+ if (match > length) {
3969
+ best = area;
3970
+ length = match;
3971
+ }
3972
+ }
3973
+ return best;
3974
+ }
3975
+ function useControllable(value, fallback, onChange) {
3976
+ const [own, setOwn] = useState(fallback);
3977
+ const current = value ?? own;
3978
+ const ref = useRef(current);
3979
+ ref.current = current;
3980
+ const set = useCallback((next) => {
3981
+ const resolved = typeof next === "function" ? next(ref.current) : next;
3982
+ if (value === void 0) setOwn(resolved);
3983
+ onChange?.(resolved);
3984
+ }, [value, onChange]);
3985
+ return [current, set];
3986
+ }
3987
+ function CboxWorkspaceShell({ appKey, brandSubmark, areas, currentPath, LinkComponent: Link, context, tools, account, children, brandHref = "/", mainId = "main-content", pinned: pinnedProp, defaultPinned = false, onPinnedChange, collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange }) {
3988
+ const { t } = useCboxLocalization();
3989
+ const [pinned, setPinned] = useControllable(pinnedProp, defaultPinned, onPinnedChange);
3990
+ const [collapsed, setCollapsed] = useControllable(collapsedProp, defaultCollapsed, onCollapsedChange);
3991
+ const [expanded, setExpanded] = useState(false);
3992
+ const [mobile, setMobile] = useState(false);
3993
+ const dialog = useRef(null);
3994
+ const path = currentPath.split(/[?#]/)[0] ?? "/";
3995
+ const area = activeNavArea(path, areas);
3996
+ const subnav = area?.items && area.items.length > 1 ? area : void 0;
3997
+ useEffect(() => {
3998
+ setMobile(false);
3999
+ }, [currentPath]);
4000
+ useEffect(() => {
4001
+ const el = dialog.current;
4002
+ if (mobile && el && !el.open) el.showModal();
4003
+ if (!mobile && el?.open) el.close();
4004
+ }, [mobile]);
4005
+ useEffect(() => {
4006
+ const key = (event) => {
4007
+ if ((event.metaKey || event.ctrlKey) && event.key === ".") {
4008
+ event.preventDefault();
4009
+ setCollapsed((value) => !value);
4010
+ }
4011
+ };
4012
+ addEventListener("keydown", key);
4013
+ return () => removeEventListener("keydown", key);
4014
+ }, [setCollapsed]);
4015
+ const open = pinned || expanded;
4016
+ const links = (items) => {
4017
+ const active = activeNavItem(path, items);
4018
+ return items.map((item) => /* @__PURE__ */ jsxs(Link, { href: item.href, className: `cbx-workspace-link ${item === active ? "is-active" : ""}`, "aria-label": item.label, "aria-current": item === active ? "page" : void 0, children: [
4019
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0", strokeWidth: 1.7 }),
4020
+ /* @__PURE__ */ jsx("span", { children: item.label })
4021
+ ] }, item.href));
4022
+ };
4023
+ return /* @__PURE__ */ jsxs("div", { className: "cbx-workspace", children: [
4024
+ /* @__PURE__ */ jsx("div", { className: "cbx-rail-slot", "data-pinned": pinned, children: /* @__PURE__ */ jsxs("aside", { className: "cbx-rail", "data-open": open, onMouseEnter: () => setExpanded(true), onMouseLeave: () => setExpanded(false), onFocusCapture: () => setExpanded(true), onBlurCapture: (event) => {
4025
+ if (!event.currentTarget.contains(event.relatedTarget)) setExpanded(false);
4026
+ }, children: [
4027
+ /* @__PURE__ */ jsxs("div", { className: "cbx-rail-brand", children: [
4028
+ /* @__PURE__ */ jsxs(Link, { href: brandHref, "aria-label": t("Home"), children: [
4029
+ /* @__PURE__ */ jsx("span", { className: "dark:hidden", children: /* @__PURE__ */ jsx(CboxBrand, { appKey, submark: brandSubmark, collapsed: !open }) }),
4030
+ /* @__PURE__ */ jsx("span", { className: "hidden dark:block", children: /* @__PURE__ */ jsx(CboxBrand, { appKey, submark: brandSubmark, collapsed: !open, tone: "light" }) })
4031
+ ] }),
4032
+ open && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPinned((value) => !value), "aria-label": t(pinned ? "Unpin navigation" : "Pin navigation"), "aria-pressed": pinned, children: pinned ? /* @__PURE__ */ jsx(PinOff, { size: 16 }) : /* @__PURE__ */ jsx(Pin, { size: 16 }) })
4033
+ ] }),
4034
+ /* @__PURE__ */ jsx("nav", { "aria-label": brandSubmark, children: areas.map((item) => /* @__PURE__ */ jsxs(Link, { href: item.href, "aria-label": item.label, "aria-current": area === item ? "page" : void 0, title: open ? void 0 : item.label, className: `cbx-workspace-link ${area === item ? "is-active" : ""}`, children: [
4035
+ /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0", strokeWidth: 1.7 }),
4036
+ open && /* @__PURE__ */ jsx("span", { children: item.label })
4037
+ ] }, item.href)) }),
4038
+ /* @__PURE__ */ jsx("div", { className: "cbx-rail-account", children: account })
4039
+ ] }) }),
4040
+ /* @__PURE__ */ jsxs("div", { className: "cbx-workspace-column", children: [
4041
+ /* @__PURE__ */ jsxs("header", { className: "cbx-workspace-header", children: [
4042
+ /* @__PURE__ */ jsx("button", { className: "cbx-mobile-trigger", type: "button", onClick: () => setMobile(true), "aria-label": t("Open navigation"), children: /* @__PURE__ */ jsx(Menu, { size: 18 }) }),
4043
+ /* @__PURE__ */ jsx("div", { className: "cbx-workspace-context", children: context }),
4044
+ /* @__PURE__ */ jsx("div", { className: "cbx-workspace-tools", children: tools })
4045
+ ] }),
4046
+ /* @__PURE__ */ jsxs("div", { className: "cbx-workspace-body", children: [
4047
+ subnav && /* @__PURE__ */ jsx("aside", { className: "cbx-subnav", "data-collapsed": collapsed, children: collapsed ? /* @__PURE__ */ jsxs("button", { className: "cbx-subnav-strip", type: "button", onClick: () => setCollapsed(false), "aria-label": t("Expand sidebar"), children: [
4048
+ /* @__PURE__ */ jsx(PanelLeftOpen, { size: 16 }),
4049
+ /* @__PURE__ */ jsx("span", { children: subnav.label })
4050
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4051
+ /* @__PURE__ */ jsxs("div", { className: "cbx-subnav-header", children: [
4052
+ /* @__PURE__ */ jsx("strong", { children: subnav.label }),
4053
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setCollapsed(true), "aria-label": t("Collapse sidebar"), title: "\u2318.", children: /* @__PURE__ */ jsx(PanelLeftClose, { size: 16 }) })
4054
+ ] }),
4055
+ /* @__PURE__ */ jsx("nav", { "aria-label": subnav.label, children: links(subnav.items ?? []) })
4056
+ ] }) }),
4057
+ /* @__PURE__ */ jsx("main", { id: mainId, className: "canvas-gradient cbx-workspace-main", children })
4058
+ ] })
4059
+ ] }),
4060
+ /* @__PURE__ */ jsxs("dialog", { ref: dialog, className: "cbx-workspace-mobile", onClose: () => setMobile(false), onClick: (event) => {
4061
+ if (event.target === event.currentTarget) setMobile(false);
4062
+ }, children: [
4063
+ /* @__PURE__ */ jsxs("div", { className: "cbx-subnav-header", children: [
4064
+ /* @__PURE__ */ jsx("strong", { children: brandSubmark }),
4065
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setMobile(false), "aria-label": t("Close navigation"), children: /* @__PURE__ */ jsx(X, { size: 18 }) })
4066
+ ] }),
4067
+ /* @__PURE__ */ jsx("nav", { "aria-label": t("Navigation"), children: areas.map((item) => /* @__PURE__ */ jsxs("section", { children: [
4068
+ links([item]),
4069
+ item.items && /* @__PURE__ */ jsx("div", { className: "pl-4", children: links(item.items) })
4070
+ ] }, item.href)) }),
4071
+ /* @__PURE__ */ jsx("div", { className: "cbx-rail-account", children: account })
4072
+ ] })
4073
+ ] });
4074
+ }
4075
+ var CHART_PALETTE = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];
4076
+ function Sparkline({ values, color = "var(--chart-1)", width = 90, height = 20 }) {
4077
+ if (values.length === 0) {
4078
+ return null;
4079
+ }
4080
+ const clean = values.map((value) => Number.isFinite(value) ? value : 0);
4081
+ const max = Math.max(1e-9, ...clean);
4082
+ const points = clean.map((value, index) => [index / Math.max(1, clean.length - 1) * (width - 2) + 1, height - 1 - value / max * (height - 3)]);
4083
+ const d = points.map(([px, py], index) => `${index === 0 ? "M" : "L"}${px.toFixed(1)} ${py.toFixed(1)}`).join("");
4084
+ return /* @__PURE__ */ jsxs("svg", { width, height, viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", children: [
4085
+ /* @__PURE__ */ jsx("path", { d: `${d}L${width - 1} ${height}L1 ${height}Z`, fill: color, opacity: 0.14 }),
4086
+ /* @__PURE__ */ jsx("path", { d, fill: "none", stroke: color, strokeWidth: 1.5, strokeLinejoin: "round", strokeLinecap: "round" })
4087
+ ] });
4088
+ }
3668
4089
 
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 };
4090
+ export { AppContent, AppLauncher, AppShell, AppSwitcher, CHART_PALETTE, CboxAppCatalog, CboxAppIcon, CboxAppShell, CboxAppSidebar, CboxBrand, CboxIdProvider, CboxLocalizationProvider, CboxMobileNavDrawer, CboxOrgContextBar, CboxServiceMark, CboxSurfaces, CboxWorkspaceShell, Combobox, ContextAvatarChip, ContextBar, DataPanel, EmptyState, Heading, KeyValue, KeyValueList, NotificationsBell, OrgSwitcher, PageHeader, PageShell, PageTabs, PanelBody, RowLink, SearchTrigger, SectionHeader, Sparkline, StatTile, StatusPill, TopBar, UserMenu, activeNavArea, activeNavItem, authorizeHref, buildUrlSearch, defineUrlSchema, interpolate, navMatchLength, parseAppCatalog, parseUrlSearch, rememberAppCatalog, secureIconUrl, urlBoolean, urlEnum, urlNumber, urlString, urlStringArray, useCboxId, useCboxIdOptional, useCboxLocalization, useCboxSurfaces, useCboxSurfacesOptional, useKnownAppCatalog };
3670
4091
  //# sourceMappingURL=index.js.map
3671
4092
  //# sourceMappingURL=index.js.map