@cboxdk/cbox-ui 0.7.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/README.md CHANGED
@@ -200,12 +200,12 @@ grant) and the component renders its answer, refreshes it when `refresh_after` e
200
200
  while the panel is open, and offers the way back to Cbox ID when the grant is missing.
201
201
 
202
202
  ```tsx
203
- <CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" icons={serviceMarks} />
203
+ <CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" />
204
204
  ```
205
205
 
206
206
  The endpoint contract (`parseAppCatalog` is exported, so a host can test its own answer):
207
207
 
208
- - `200 { apps: [{ key, name, launch_url, icon_url?, description? }], refresh_after?: seconds }`
208
+ - `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
209
209
  — `refresh_after` is clamped to 1–60 s; apps without an `http(s)` launch URL are dropped.
210
210
  - `401`/`403` with `{ authorize_url }` or `{ state: 'authorization_required' }` — the panel
211
211
  says the apps could not be loaded and links "Reconnect to Cbox ID" to `authorize_url` with
@@ -213,8 +213,15 @@ The endpoint contract (`parseAppCatalog` is exported, so a host can test its own
213
213
  - Anything else — "Apps could not be loaded." with a retry.
214
214
 
215
215
  `endpoint={null}` renders the control disabled, so the chrome keeps its shape on pages
216
- without an organization. `icons` maps an app key to a mark the host bundles (the family's
217
- service marks); without one the catalog's `icon_url` is shown, then the name's initials.
216
+ without an organization.
217
+
218
+ App icons come from Cbox ID's catalog: `icon_url` for light surfaces and `icon_dark_url` for
219
+ dark ones. The tiles render both and the token stylesheet shows the one for the current theme
220
+ (`.dark` or `[data-theme="dark"]` on the root); an app without an icon shows its initials. Apps
221
+ keep no local copy of the marks. Each answer is kept in this browser (`cbox-app-catalog`), so
222
+ `<CboxServiceMark appKey="infra" />` and the collapsed `CboxBrand` in the rail paint the app's
223
+ own icon at once on the next page. A host that loads the catalog itself calls
224
+ `rememberAppCatalog(apps)`; `CboxAppIcon` renders any single app's icon.
218
225
 
219
226
  ### Workspace navigation (current design contract)
220
227
 
package/dist/index.d.ts CHANGED
@@ -62,8 +62,10 @@ type CboxAppLink = {
62
62
  * Defaults to the first letter of `name`.
63
63
  */
64
64
  monogram?: string;
65
- /** Optional inline SVG path or URL for the launcher tile icon. */
66
- iconUrl?: string;
65
+ /** Cbox ID's catalog `icon_url`: the app's icon for light surfaces (`https:` only). */
66
+ iconUrl?: string | null;
67
+ /** Cbox ID's catalog `icon_dark_url`: the app's icon for dark surfaces. */
68
+ iconDarkUrl?: string | null;
67
69
  /**
68
70
  * Accent colour for the monogram tile (and current-app highlight).
69
71
  * Hex string or any CSS colour the browser accepts. Falls back to
@@ -394,7 +396,9 @@ type CboxBrandProps = {
394
396
  * `light` on dark / coloured surfaces. */
395
397
  tone?: 'dark' | 'light';
396
398
  /** Collapsed mode renders only the square icon — used in the
397
- * collapsed-sidebar state. */
399
+ * collapsed-sidebar state. The icon is the app's own icon from
400
+ * Cbox ID's catalog (light and dark) once this browser has seen
401
+ * the catalog, and the Cbox icon until then. */
398
402
  collapsed?: boolean;
399
403
  className?: string;
400
404
  };
@@ -504,7 +508,10 @@ type CboxCatalogApp = {
504
508
  key: string;
505
509
  name: string;
506
510
  launch_url: string;
511
+ /** Cbox ID's uploaded icon for light surfaces. */
507
512
  icon_url?: string | null;
513
+ /** Cbox ID's uploaded icon for dark surfaces. */
514
+ icon_dark_url?: string | null;
508
515
  description?: string | null;
509
516
  };
510
517
  type CboxAppCatalogState = {
@@ -528,17 +535,12 @@ type CboxAppCatalogProps = {
528
535
  endpoint: string | null;
529
536
  /** The app key of this app; its tile is marked current and does not navigate. */
530
537
  currentAppKey: string;
531
- /**
532
- * Marks per app key, rendered instead of the catalog's `icon_url` (a host that bundles the
533
- * family's service marks). An app without one shows its `icon_url`, then its initials.
534
- */
535
- icons?: Record<string, ReactNode>;
536
538
  className?: string;
537
539
  };
538
540
  /**
539
541
  * Interpret the catalog endpoint's answer. The contract every Cbox app serves:
540
542
  *
541
- * - `200 { apps: [{ key, name, launch_url, icon_url?, description? }], refresh_after?: seconds }`
543
+ * - `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
542
544
  * - `401`/`403 { authorize_url?: string }` or `{ state: 'authorization_required' }`: the
543
545
  * person has to (re)connect their catalog grant; `authorize_url` is where.
544
546
  * - anything else: unavailable right now.
@@ -556,9 +558,63 @@ declare function authorizeHref(authorizeUrl: string | null, location: {
556
558
  * The Switch-app control: a grid of the Cbox apps this person can open, read live from the
557
559
  * host's catalog endpoint. The host owns the endpoint and the grant; this renders the answer,
558
560
  * refreshes it when it expires while the panel is open, and offers the way back to Cbox ID
559
- * when the grant is missing.
561
+ * when the grant is missing. Each tile shows the app's icon from the catalog (light and dark),
562
+ * and the answer is kept so `CboxServiceMark` paints the app's own mark from it.
563
+ */
564
+ declare function CboxAppCatalog({ endpoint, currentAppKey, className }: CboxAppCatalogProps): react_jsx_runtime.JSX.Element;
565
+
566
+ /**
567
+ * Every Cbox app's icon comes from Cbox ID's catalog: one light and one dark upload per app
568
+ * (`icon_url`, `icon_dark_url`). There is no local copy of the marks in this package or in an
569
+ * app. The last catalog the browser saw is kept here so an app's own mark and the switcher
570
+ * paint at once on the next page; the live answer replaces it when it arrives.
560
571
  */
561
- declare function CboxAppCatalog({ endpoint, currentAppKey, icons, className }: CboxAppCatalogProps): react_jsx_runtime.JSX.Element;
572
+ type CboxKnownApp = {
573
+ key: string;
574
+ name: string;
575
+ icon_url: string | null;
576
+ icon_dark_url: string | null;
577
+ };
578
+ /** Keep the catalog the person just loaded, so every mark on this origin repaints from it. */
579
+ declare function rememberAppCatalog(apps: ReadonlyArray<{
580
+ key: string;
581
+ name: string;
582
+ icon_url?: string | null;
583
+ icon_dark_url?: string | null;
584
+ }>): void;
585
+ /** The last catalog this browser kept; empty on the server and before the first answer. */
586
+ declare function useKnownAppCatalog(): CboxKnownApp[];
587
+ /** Only `https:` icon URLs are rendered: the icons are hosted by Cbox ID. */
588
+ declare function secureIconUrl(url: unknown): string | null;
589
+ type CboxAppIconProps = {
590
+ /** The app's name; its first two letters stand in when it has no icon. */
591
+ name: string;
592
+ /** Catalog `icon_url`: the icon for light surfaces (or the only one). */
593
+ iconUrl?: string | null;
594
+ /** Catalog `icon_dark_url`: the icon for dark surfaces. */
595
+ iconDarkUrl?: string | null;
596
+ /** Size and shape of the icon box, e.g. `h-6 w-6`. */
597
+ className?: string;
598
+ /** Rendered instead of initials when the app has no usable icon. */
599
+ fallback?: ReactNode;
600
+ };
601
+ /**
602
+ * One app's icon from the catalog. With both uploads it renders two images and the token
603
+ * stylesheet shows the one for the current theme (`.dark` or `[data-theme="dark"]` on the
604
+ * root, the same switch the tokens use). Without an icon, or when it fails to load, it shows
605
+ * the app's initials.
606
+ */
607
+ declare function CboxAppIcon({ name, iconUrl, iconDarkUrl, className, fallback }: CboxAppIconProps): react_jsx_runtime.JSX.Element;
608
+ type CboxServiceMarkProps = {
609
+ /** The app key in Cbox ID's catalog (`signals`, `infra`, …). */
610
+ appKey: string;
611
+ /** Size of the mark. Default `h-6 w-6`. */
612
+ className?: string;
613
+ /** Shown while the catalog has not told this browser the app's icon yet. Default nothing. */
614
+ fallback?: ReactNode;
615
+ };
616
+ /** An app's own mark (its rail or brand icon), read from the catalog this browser last kept. */
617
+ declare function CboxServiceMark({ appKey, className, fallback }: CboxServiceMarkProps): react_jsx_runtime.JSX.Element;
562
618
 
563
619
  type AppSwitcherProps = {
564
620
  className?: string;
@@ -1524,4 +1580,4 @@ declare function Sparkline({ values, color, width, height }: {
1524
1580
  height?: number;
1525
1581
  }): react_jsx_runtime.JSX.Element | null;
1526
1582
 
1527
- export { AppContent, AppLauncher, AppShell, AppSwitcher, CHART_PALETTE, CboxAppCatalog, type CboxAppCatalogProps, type CboxAppCatalogState, type CboxAppKey, type CboxAppLink, CboxAppShell, type CboxAppShellProps, CboxAppSidebar, type CboxAppSidebarProps, CboxBrand, type CboxBrandProps, type CboxCatalogApp, type CboxIdContextValue, CboxIdProvider, CboxLocalizationProvider, CboxMobileNavDrawer, type CboxMobileNavDrawerProps, type CboxNavArea, type CboxNavItem, type CboxNavSection, type CboxNotificationPreviewItem, type CboxNotificationsState, CboxOrgContextBar, type CboxOrgContextBarProps, type CboxOrganization, CboxSurfaces, type CboxSurfacesProps, type CboxTranslationValues, type CboxTranslator, type CboxUser, CboxWorkspaceShell, type CboxWorkspaceShellProps, Combobox, type ComboboxAvatar, type ComboboxGroup, type ComboboxOption, type ComboboxProps, type ContextAvatar, ContextAvatarChip, ContextBar, type ContextCrumb, type ContextItem, DataPanel, EmptyState, Heading, type InboxState, type InferUrlState, KeyValue, KeyValueList, type LinkComponent, NotificationsBell, OrgSwitcher, PageHeader, PageShell, type PageTabSpec, PageTabs, type PageTabsProps, type PageWidth, PanelBody, type RealtimeState, type RealtimeStatus, RowLink, type RowLinkProps, SearchTrigger, SectionHeader, Sparkline, StatTile, StatusPill, type SurfaceArgs, type SurfaceConfig, type SurfaceController, type SurfaceId, TopBar, type Transport, type UrlField, type UrlSchema, UserMenu, activeNavArea, activeNavItem, authorizeHref, buildUrlSearch, cn, defineUrlSchema, interpolate, navMatchLength, parseAppCatalog, parseUrlSearch, urlBoolean, urlEnum, urlNumber, urlString, urlStringArray, useCboxId, useCboxIdOptional, useCboxLocalization, useCboxSurfaces, useCboxSurfacesOptional };
1583
+ export { AppContent, AppLauncher, AppShell, AppSwitcher, CHART_PALETTE, CboxAppCatalog, type CboxAppCatalogProps, type CboxAppCatalogState, CboxAppIcon, type CboxAppIconProps, type CboxAppKey, type CboxAppLink, CboxAppShell, type CboxAppShellProps, CboxAppSidebar, type CboxAppSidebarProps, CboxBrand, type CboxBrandProps, type CboxCatalogApp, type CboxIdContextValue, CboxIdProvider, type CboxKnownApp, CboxLocalizationProvider, CboxMobileNavDrawer, type CboxMobileNavDrawerProps, type CboxNavArea, type CboxNavItem, type CboxNavSection, type CboxNotificationPreviewItem, type CboxNotificationsState, CboxOrgContextBar, type CboxOrgContextBarProps, type CboxOrganization, CboxServiceMark, type CboxServiceMarkProps, CboxSurfaces, type CboxSurfacesProps, type CboxTranslationValues, type CboxTranslator, type CboxUser, CboxWorkspaceShell, type CboxWorkspaceShellProps, Combobox, type ComboboxAvatar, type ComboboxGroup, type ComboboxOption, type ComboboxProps, type ContextAvatar, ContextAvatarChip, ContextBar, type ContextCrumb, type ContextItem, DataPanel, EmptyState, Heading, type InboxState, type InferUrlState, KeyValue, KeyValueList, type LinkComponent, NotificationsBell, OrgSwitcher, PageHeader, PageShell, type PageTabSpec, PageTabs, type PageTabsProps, type PageWidth, PanelBody, type RealtimeState, type RealtimeStatus, RowLink, type RowLinkProps, SearchTrigger, SectionHeader, Sparkline, StatTile, StatusPill, type SurfaceArgs, type SurfaceConfig, type SurfaceController, type SurfaceId, TopBar, type Transport, type UrlField, type UrlSchema, UserMenu, activeNavArea, activeNavItem, authorizeHref, buildUrlSearch, cn, defineUrlSchema, interpolate, navMatchLength, parseAppCatalog, parseUrlSearch, rememberAppCatalog, secureIconUrl, urlBoolean, urlEnum, urlNumber, urlString, urlStringArray, useCboxId, useCboxIdOptional, useCboxLocalization, useCboxSurfaces, useCboxSurfacesOptional, useKnownAppCatalog };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { cn } from './chunk-OFTNLF7H.js';
2
2
  export { cn, niceTicks, timeTicks } from './chunk-OFTNLF7H.js';
3
- import { createContext, useContext, useMemo, useEffect, useState, useRef, useCallback, useId } from 'react';
4
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { createContext, useContext, useMemo, useSyncExternalStore, useState, useEffect, useRef, useCallback, useId } from 'react';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
  import { X, Menu, PanelLeftOpen, PanelLeftClose, PinOff, Pin } from 'lucide-react';
6
6
 
7
7
  var CboxIdContext = createContext(null);
@@ -60,6 +60,97 @@ function useCboxLocalization() {
60
60
  function defaultDate(value) {
61
61
  return new Date(value).toLocaleString();
62
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
+ }
63
154
  function CboxBrand({
64
155
  appKey,
65
156
  submark,
@@ -69,7 +160,7 @@ function CboxBrand({
69
160
  className
70
161
  }) {
71
162
  if (collapsed) {
72
- return /* @__PURE__ */ jsx(
163
+ const cboxIcon = /* @__PURE__ */ jsx(
73
164
  "img",
74
165
  {
75
166
  src: `${assetBase}/cbox-icon.png`,
@@ -80,6 +171,7 @@ function CboxBrand({
80
171
  className: cn("h-9 w-9 rounded-md", className)
81
172
  }
82
173
  );
174
+ return /* @__PURE__ */ jsx(CboxServiceMark, { appKey, className: cn("h-9 w-9", className), fallback: cboxIcon });
83
175
  }
84
176
  const wordmark = tone === "light" ? `${assetBase}/cbox-logo-white.png` : `${assetBase}/cbox-logo.png`;
85
177
  const wordmarkRetina = tone === "light" ? `${assetBase}/cbox-logo-white@2x.png` : `${assetBase}/cbox-logo@2x.png`;
@@ -467,21 +559,13 @@ function AppSwitcher({ className, onLockedAppClick }) {
467
559
  /* @__PURE__ */ jsx(
468
560
  "span",
469
561
  {
470
- style: triggerAccent !== void 0 ? {
562
+ style: triggerAccent !== void 0 && secureIconUrl(currentApp?.iconUrl) === null ? {
471
563
  backgroundColor: triggerAccent,
472
564
  color: "#fff",
473
565
  borderColor: triggerAccent
474
566
  } : void 0,
475
567
  className: "flex h-5 w-5 shrink-0 items-center justify-center rounded-sm border border-border bg-secondary text-[10px] font-bold",
476
- children: currentApp?.iconUrl ? /* @__PURE__ */ jsx(
477
- "img",
478
- {
479
- src: currentApp.iconUrl,
480
- alt: "",
481
- className: "h-3.5 w-3.5",
482
- loading: "eager"
483
- }
484
- ) : 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
485
569
  }
486
570
  ),
487
571
  /* @__PURE__ */ jsx("span", { className: "max-w-[140px] truncate", children: triggerName }),
@@ -581,7 +665,7 @@ function AppRow({
581
665
  const isLocked = app.entitled === false || isComingSoon;
582
666
  const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
583
667
  const accent = app.accentColor;
584
- 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;
585
669
  const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl(app.url, orgSlug) : app.url;
586
670
  const tile = /* @__PURE__ */ jsx(
587
671
  "div",
@@ -592,7 +676,7 @@ function AppRow({
592
676
  app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
593
677
  isLocked && "opacity-60 grayscale"
594
678
  ),
595
- 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 }) })
596
680
  }
597
681
  );
598
682
  const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
@@ -1491,7 +1575,7 @@ function UserMenu({ extras, className, placement = "bottom-end", LinkComponent:
1491
1575
  const { t } = useCboxLocalization();
1492
1576
  const { user, currentOrganization, urls } = useCboxId();
1493
1577
  const [open, setOpen] = useState(false);
1494
- const initials = user.name.split(/\s+/).filter(Boolean).slice(0, 2).map((n) => n[0]?.toUpperCase() ?? "").join("");
1578
+ const initials2 = user.name.split(/\s+/).filter(Boolean).slice(0, 2).map((n) => n[0]?.toUpperCase() ?? "").join("");
1495
1579
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), onKeyDown: (event) => {
1496
1580
  if (event.key === "Escape") setOpen(false);
1497
1581
  }, onBlur: (event) => {
@@ -1515,7 +1599,7 @@ function UserMenu({ extras, className, placement = "bottom-end", LinkComponent:
1515
1599
  alt: "",
1516
1600
  className: "h-7 w-7 rounded-full object-cover"
1517
1601
  }
1518
- ) : /* @__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 || "?" })
1519
1603
  }
1520
1604
  ),
1521
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: [
@@ -1946,7 +2030,7 @@ function ContextAvatarChip({
1946
2030
  fallbackLabel,
1947
2031
  size = "md"
1948
2032
  }) {
1949
- const initials = avatar.initials ?? deriveInitials(fallbackLabel);
2033
+ const initials2 = avatar.initials ?? deriveInitials(fallbackLabel);
1950
2034
  const sizeClass = size === "sm" ? "h-[26px] w-[26px] text-[10px]" : "h-7 w-7 text-[11px]";
1951
2035
  const style = avatar.src === void 0 && avatar.accent !== void 0 ? {
1952
2036
  backgroundColor: avatar.accent,
@@ -1975,7 +2059,7 @@ function ContextAvatarChip({
1975
2059
  avatar.accent === void 0 && "bg-info-soft text-info",
1976
2060
  sizeClass
1977
2061
  ),
1978
- children: initials
2062
+ children: initials2
1979
2063
  }
1980
2064
  );
1981
2065
  }
@@ -2432,7 +2516,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2432
2516
  const isLocked = app.entitled === false || isComingSoon;
2433
2517
  const monogram = app.monogram ?? app.name.slice(0, 1).toUpperCase();
2434
2518
  const accent = app.accentColor;
2435
- 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;
2436
2520
  const href = orgSlug !== void 0 && app.current !== true ? buildOrgUrl2(app.url, orgSlug) : app.url;
2437
2521
  const tile = /* @__PURE__ */ jsx(
2438
2522
  "div",
@@ -2443,7 +2527,7 @@ function AppRow2({ app, orgSlug, onLockedAppClick }) {
2443
2527
  app.current && "ring-2 ring-accent-edge ring-offset-1 ring-offset-popover",
2444
2528
  isLocked && "opacity-60 grayscale"
2445
2529
  ),
2446
- 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 }) })
2447
2531
  }
2448
2532
  );
2449
2533
  const body = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [
@@ -2597,6 +2681,7 @@ function parseAppCatalog(ok, body) {
2597
2681
  name: app.name,
2598
2682
  launch_url: app.launch_url,
2599
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,
2600
2685
  description: typeof app.description === "string" ? app.description : null
2601
2686
  });
2602
2687
  }
@@ -2614,7 +2699,7 @@ function authorizeHref(authorizeUrl, location) {
2614
2699
  return null;
2615
2700
  }
2616
2701
  }
2617
- function CboxAppCatalog({ endpoint, currentAppKey, icons, className }) {
2702
+ function CboxAppCatalog({ endpoint, currentAppKey, className }) {
2618
2703
  const { t } = useCboxLocalization();
2619
2704
  const [open, setOpen] = useState(false);
2620
2705
  const [state, setState] = useState({ status: "idle" });
@@ -2638,6 +2723,7 @@ function CboxAppCatalog({ endpoint, currentAppKey, icons, className }) {
2638
2723
  const next = parseAppCatalog(response.ok, body);
2639
2724
  expires.current = next.status === "ready" ? Date.now() + next.refreshAfter * 1e3 : 0;
2640
2725
  setState(next);
2726
+ if (next.status === "ready") rememberAppCatalog(next.apps);
2641
2727
  } catch {
2642
2728
  if (!request.signal.aborted) {
2643
2729
  expires.current = 0;
@@ -2722,7 +2808,7 @@ function CboxAppCatalog({ endpoint, currentAppKey, icons, className }) {
2722
2808
  onClick: () => setOpen(false),
2723
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"),
2724
2810
  children: [
2725
- /* @__PURE__ */ jsx(AppMark, { app, icon: icons?.[app.key] }),
2811
+ /* @__PURE__ */ jsx(CboxAppIcon, { name: app.name, iconUrl: app.icon_url, iconDarkUrl: app.icon_dark_url }),
2726
2812
  /* @__PURE__ */ jsx("span", { className: "max-w-full break-words", children: app.name.replace(/^Cbox\s*·\s*/i, "") }),
2727
2813
  current && /* @__PURE__ */ jsx(Check2, {})
2728
2814
  ]
@@ -2734,16 +2820,6 @@ function CboxAppCatalog({ endpoint, currentAppKey, icons, className }) {
2734
2820
  ] }) : null
2735
2821
  ] });
2736
2822
  }
2737
- function AppMark({ app, icon }) {
2738
- const [failed, setFailed] = useState(false);
2739
- if (icon !== void 0) {
2740
- return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-6 w-6 items-center justify-center", children: icon });
2741
- }
2742
- if (typeof app.icon_url === "string" && app.icon_url !== "" && !failed) {
2743
- return /* @__PURE__ */ jsx("img", { src: app.icon_url, alt: "", className: "h-6 w-6 rounded object-contain", loading: "lazy", onError: () => setFailed(true) });
2744
- }
2745
- return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "grid h-6 w-6 place-items-center rounded bg-secondary text-[11px] font-semibold text-foreground", children: app.name.slice(0, 2) });
2746
- }
2747
2823
  function Grid2() {
2748
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}`))) });
2749
2825
  }
@@ -3145,7 +3221,7 @@ function ComboboxAvatarChip({
3145
3221
  avatar,
3146
3222
  fallbackLabel
3147
3223
  }) {
3148
- const initials = avatar.initials ?? deriveInitials3(fallbackLabel);
3224
+ const initials2 = avatar.initials ?? deriveInitials3(fallbackLabel);
3149
3225
  const style = avatar.src === void 0 && avatar.accent !== void 0 ? { backgroundColor: avatar.accent, color: "#fff" } : void 0;
3150
3226
  if (avatar.src !== void 0) {
3151
3227
  return /* @__PURE__ */ jsx(
@@ -3166,7 +3242,7 @@ function ComboboxAvatarChip({
3166
3242
  "flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-[9px] font-semibold tracking-tight",
3167
3243
  avatar.accent === void 0 && "bg-info-soft text-info"
3168
3244
  ),
3169
- children: initials
3245
+ children: initials2
3170
3246
  }
3171
3247
  );
3172
3248
  }
@@ -4011,6 +4087,6 @@ function Sparkline({ values, color = "var(--chart-1)", width = 90, height = 20 }
4011
4087
  ] });
4012
4088
  }
4013
4089
 
4014
- export { AppContent, AppLauncher, AppShell, AppSwitcher, CHART_PALETTE, CboxAppCatalog, CboxAppShell, CboxAppSidebar, CboxBrand, CboxIdProvider, CboxLocalizationProvider, CboxMobileNavDrawer, CboxOrgContextBar, 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, urlBoolean, urlEnum, urlNumber, urlString, urlStringArray, useCboxId, useCboxIdOptional, useCboxLocalization, 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 };
4015
4091
  //# sourceMappingURL=index.js.map
4016
4092
  //# sourceMappingURL=index.js.map