@cboxdk/cbox-ui 0.10.0 → 0.10.2

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
@@ -278,16 +278,39 @@ type Shell = {
278
278
  page?: { title; route? }; // "On this page" and the chat's page intent
279
279
  nav: { areas: NavArea[]; activeArea; activePage? };
280
280
  context: { levels: ContextLevel[]; switchUrl }; // 1–3 levels, organization first
281
- palette: { searchUrl?; gotoUrl?; actions: PaletteAction[] };
281
+ palette: { searchUrl?; gotoUrl?; actions: PaletteAction[]; placeholder? };
282
282
  help: { enabled; topic?; sessionUrl?; scriptUrl?; portalUrl?; updatesUrl?; searchUrl?; articlesUrl? };
283
283
  notifications: { enabled: false } | { enabled: true; unread; url };
284
284
  apps: { catalogUrl; productsUrl? };
285
- account: { profileUrl; switchAccountUrl; signOutUrl; signOutMethod?; languages?; formats?; signedInWith? };
285
+ account?: { profileUrl?; switchAccountUrl?; signOutUrl?; signOutMethod?; languages?; formats?; signedInWith? };
286
286
  banner?: { kind: 'sandbox' | 'impersonation' | 'notice'; text; action? };
287
287
  preferences: { theme; locale; railPinned; helpOpen; subnavCollapsed?; regionalFormat?; timeZone? };
288
288
  };
289
289
  ```
290
290
 
291
+ ```ts
292
+ type NavArea = { key; label; url; icon?; pages?: NavPage[] };
293
+ type NavPage = {
294
+ key; label; url; icon?;
295
+ locked?: boolean; // not in the plan: a lock and "(locked)", `url` leads to the upgrade prompt
296
+ count?: number; // a badge (open drafts); hidden at 0
297
+ group?: string; // a heading in the subnav and the phone nav ("Organization", "Project")
298
+ };
299
+ ```
300
+
301
+ Pages of one `group` show together under its heading, in the order each group first appears.
302
+ Locked pages still show in the palette's Pages, marked "Locked". Every nav link (and the
303
+ banner action) also accepts `href` in place of `url`, the name laravel-console-kit 0.2.2 sends.
304
+ `palette.placeholder` replaces "Search or ask…" in the palette's field (the header button keeps
305
+ "Search or ask…").
306
+
307
+ `account` and each of its URLs are optional: the account menu leaves out a row it has no URL
308
+ for, and without `account` it holds only the preferences (laravel-console-kit's
309
+ `->account()` builds it).
310
+
311
+ The shell's first focusable element is a "Skip to content" link, hidden until focused. It
312
+ moves focus to `<main id={mainId}>` (default `main-content`).
313
+
291
314
  `ContextLevel.settingsUrl` is a URL template on the wire: `{id}` is replaced with the row's id
292
315
  (`/orgs/{id}/settings`). Nav and action icons are Lucide names (`"rocket"`, `"server"`); pass
293
316
  `icons` to `CboxShell` for others.
package/dist/index.d.ts CHANGED
@@ -294,22 +294,45 @@ declare const SHELL_ICONS: Readonly<Record<string, ShellIcon>>;
294
294
  */
295
295
  /** The product key (URL standard §1). Unknown keys are allowed so a new app needs no release here. */
296
296
  type AppKey = 'id' | 'billing' | 'telemetry' | 'work' | 'brain' | 'infra' | 'engage' | 'assistant' | 'sites' | 'stache' | 'status' | 'tax' | 'signals' | 'domains' | (string & {});
297
+ /**
298
+ * A labelled link. `href` is the same URL under the name laravel-console-kit 0.2 sends; the
299
+ * shell reads `url` first, then `href`.
300
+ */
297
301
  type Link = {
298
302
  label: string;
299
303
  url: string;
304
+ href?: string;
300
305
  };
301
- /** One page inside an area. `icon` is a Lucide icon name (`"git-branch"`), resolved by the shell. */
306
+ /**
307
+ * One page inside an area. `icon` is a Lucide icon name (`"git-branch"`), resolved by the shell.
308
+ * `href` is the same URL under the name laravel-console-kit 0.2 sends; the shell reads `url`
309
+ * first, then `href`.
310
+ */
302
311
  type NavPage = {
303
312
  key: string;
304
313
  label: string;
305
314
  url: string;
315
+ href?: string;
306
316
  icon?: string;
317
+ /**
318
+ * The page is not part of the organization's plan: it shows with a lock and an accessible
319
+ * "(locked)", and `url` leads to the upgrade prompt.
320
+ */
321
+ locked?: boolean;
322
+ /** A badge, such as open drafts. Not shown when 0 or absent. */
323
+ count?: number;
324
+ /**
325
+ * A heading inside the area's subnav (Settings → "Organization", "Project"). Pages of one
326
+ * group show together under it, in the order each group first appears.
327
+ */
328
+ group?: string;
307
329
  };
308
330
  /** One area in the rail. The subnav shows its pages when it has more than one. */
309
331
  type NavArea = {
310
332
  key: string;
311
333
  label: string;
312
334
  url: string;
335
+ href?: string;
313
336
  icon?: string;
314
337
  pages?: NavPage[];
315
338
  };
@@ -411,12 +434,15 @@ type ShellOption = {
411
434
  code: string;
412
435
  label: string;
413
436
  };
414
- /** What the account menu (§10) links to. */
437
+ /**
438
+ * What the account menu (§10) links to. A row whose URL is absent is not shown (laravel-console-kit
439
+ * leaves out what the app has no route for).
440
+ */
415
441
  type ShellAccount = {
416
442
  /** Profile and security, in Cbox ID. */
417
- profileUrl: string;
418
- switchAccountUrl: string;
419
- signOutUrl: string;
443
+ profileUrl?: string;
444
+ switchAccountUrl?: string;
445
+ signOutUrl?: string;
420
446
  /** Default `post`. */
421
447
  signOutMethod?: 'get' | 'post';
422
448
  /** Languages the app is translated into. */
@@ -461,6 +487,8 @@ type Shell = {
461
487
  /** `GET ?q=` → level 2/3 combinations across organizations (§6.1.4). */
462
488
  gotoUrl?: string;
463
489
  actions: PaletteAction[];
490
+ /** The palette search field's placeholder, e.g. "Search customers, invoices… or pages". Default "Search or ask…". */
491
+ placeholder?: string;
464
492
  };
465
493
  help: ShellHelp;
466
494
  notifications: ShellNotifications;
@@ -470,7 +498,8 @@ type Shell = {
470
498
  /** "All products ↗". Default https://cbox.dk. */
471
499
  productsUrl?: string;
472
500
  };
473
- account: ShellAccount;
501
+ /** The account menu's links. Absent (laravel-console-kit 0.2.2): the menu has only the preferences. */
502
+ account?: ShellAccount;
474
503
  banner?: ShellBanner;
475
504
  preferences: ShellPreferences;
476
505
  };
@@ -534,7 +563,7 @@ type CboxShellProps = {
534
563
  helpStore?: HelpStore;
535
564
  /** Where the Cbox wordmark is served. Default `/brand`. */
536
565
  brandAssetBase?: string;
537
- /** `id` of the `<main>` landmark for a skip link. Default `main-content`. */
566
+ /** `id` of the `<main>` landmark, which the shell's "Skip to content" link targets. Default `main-content`. */
538
567
  mainId?: string;
539
568
  };
540
569
  /**
package/dist/index.js CHANGED
@@ -350,7 +350,8 @@ function Avatar({ size = 30 }) {
350
350
  }
351
351
  function AccountMenu({ inline = false, onDone }) {
352
352
  const { shell, t, preferences, setPreference, navigate } = useShell();
353
- const { account, person } = shell;
353
+ const { person } = shell;
354
+ const account = shell.account ?? {};
354
355
  const [view, setView] = useState("main");
355
356
  const menu = useRef(null);
356
357
  const tab = inline ? 0 : -1;
@@ -396,7 +397,7 @@ function AccountMenu({ inline = false, onDone }) {
396
397
  ] })
397
398
  ] }),
398
399
  /* @__PURE__ */ jsxs("div", { className: "cbx-list", role: "none", children: [
399
- /* @__PURE__ */ jsxs("a", { role: "menuitem", tabIndex: tab, className: "cbx-row cbx-row-action", href: account.profileUrl, onClick: onDone, children: [
400
+ account.profileUrl !== void 0 && /* @__PURE__ */ jsxs("a", { role: "menuitem", tabIndex: tab, className: "cbx-row cbx-row-action", href: account.profileUrl, onClick: onDone, children: [
400
401
  /* @__PURE__ */ jsx(UserRound, { size: 16, strokeWidth: 1.8, "aria-hidden": true }),
401
402
  /* @__PURE__ */ jsx("span", { className: "cbx-row-name", children: t("Profile and security") }),
402
403
  /* @__PURE__ */ jsx(ArrowUpRight, { className: "cbx-row-trail", size: 14, strokeWidth: 1.8, "aria-hidden": true })
@@ -439,12 +440,12 @@ function AccountMenu({ inline = false, onDone }) {
439
440
  )) })
440
441
  ] })
441
442
  ] }),
442
- /* @__PURE__ */ jsxs("div", { className: "cbx-list cbx-list-foot", role: "none", children: [
443
- /* @__PURE__ */ jsxs("a", { role: "menuitem", tabIndex: tab, className: "cbx-row cbx-row-action", href: account.switchAccountUrl, onClick: onDone, children: [
443
+ (account.switchAccountUrl !== void 0 || account.signOutUrl !== void 0) && /* @__PURE__ */ jsxs("div", { className: "cbx-list cbx-list-foot", role: "none", children: [
444
+ account.switchAccountUrl !== void 0 && /* @__PURE__ */ jsxs("a", { role: "menuitem", tabIndex: tab, className: "cbx-row cbx-row-action", href: account.switchAccountUrl, onClick: onDone, children: [
444
445
  /* @__PURE__ */ jsx(Users, { size: 16, strokeWidth: 1.8, "aria-hidden": true }),
445
446
  /* @__PURE__ */ jsx("span", { className: "cbx-row-name", children: t("Switch account") })
446
447
  ] }),
447
- /* @__PURE__ */ jsxs("button", { role: "menuitem", tabIndex: tab, type: "button", className: "cbx-row cbx-row-action", onClick: () => {
448
+ account.signOutUrl !== void 0 && /* @__PURE__ */ jsxs("button", { role: "menuitem", tabIndex: tab, type: "button", className: "cbx-row cbx-row-action", onClick: () => {
448
449
  onDone?.();
449
450
  navigate(account.signOutUrl, { method: account.signOutMethod ?? "post" });
450
451
  }, children: [
@@ -1799,6 +1800,40 @@ function resolveIcon(name, extra = {}) {
1799
1800
  if (name === void 0) return Circle;
1800
1801
  return extra[name] ?? SHELL_ICONS[name] ?? Circle;
1801
1802
  }
1803
+ function navUrl(entry) {
1804
+ return entry.url ?? entry.href ?? "#";
1805
+ }
1806
+ function groupPages(pages) {
1807
+ const runs = /* @__PURE__ */ new Map();
1808
+ for (const page of pages) {
1809
+ const key = page.group === void 0 || page.group === "" ? "" : `g:${page.group}`;
1810
+ const run = runs.get(key) ?? { group: key === "" ? void 0 : page.group, pages: [] };
1811
+ run.pages.push(page);
1812
+ runs.set(key, run);
1813
+ }
1814
+ return Array.from(runs.values());
1815
+ }
1816
+ function NavPageMarks({ page }) {
1817
+ const { t } = useShell();
1818
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1819
+ page.locked === true && /* @__PURE__ */ jsxs("span", { className: "cbx-nav-lock", title: t("Not in your plan"), children: [
1820
+ /* @__PURE__ */ jsx(Lock, { size: 13, strokeWidth: 1.9, "aria-hidden": true }),
1821
+ /* @__PURE__ */ jsx("span", { className: "cbx-sr-only", children: t("(locked)") })
1822
+ ] }),
1823
+ page.locked !== true && page.count !== void 0 && page.count > 0 && /* @__PURE__ */ jsx("span", { className: "cbx-nav-count", children: page.count > 999 ? "999+" : page.count })
1824
+ ] });
1825
+ }
1826
+ function NavPageList({ pages, renderPage }) {
1827
+ const id = useId();
1828
+ const runs = groupPages(pages);
1829
+ if (runs.every((run) => run.group === void 0)) return /* @__PURE__ */ jsx(Fragment, { children: pages.map(renderPage) });
1830
+ return /* @__PURE__ */ jsx(Fragment, { children: runs.map(
1831
+ (run, index) => run.group === void 0 ? /* @__PURE__ */ jsx("div", { className: "cbx-nav-run", children: run.pages.map(renderPage) }, `run-${index}`) : /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": `${id}-${index}`, className: "cbx-nav-run", children: [
1832
+ /* @__PURE__ */ jsx("div", { id: `${id}-${index}`, className: "cbx-nav-group", children: run.group }),
1833
+ run.pages.map(renderPage)
1834
+ ] }, `run-${index}`)
1835
+ ) });
1836
+ }
1802
1837
  function Sheet({ label, side, onClose, children, head }) {
1803
1838
  const { t } = useShell();
1804
1839
  const panel = useRef(null);
@@ -1861,14 +1896,23 @@ function NavSheet({ onClose }) {
1861
1896
  const Icon = resolveIcon(area.icon, icons);
1862
1897
  const activeArea = area.key === shell.nav.activeArea;
1863
1898
  return /* @__PURE__ */ jsxs("div", { className: "cbx-sheet-area", children: [
1864
- /* @__PURE__ */ jsxs(Link, { href: area.url, className: `cbx-row cbx-row-action ${activeArea ? (area.pages ?? []).length > 1 ? "is-open" : "is-current" : ""}`, "aria-current": activeArea && (area.pages ?? []).length <= 1 ? "page" : void 0, onClick: onClose, children: [
1899
+ /* @__PURE__ */ jsxs(Link, { href: navUrl(area), className: `cbx-row cbx-row-action ${activeArea ? (area.pages ?? []).length > 1 ? "is-open" : "is-current" : ""}`, "aria-current": activeArea && (area.pages ?? []).length <= 1 ? "page" : void 0, onClick: onClose, children: [
1865
1900
  /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": true }),
1866
1901
  /* @__PURE__ */ jsx("span", { className: "cbx-row-name", children: area.label })
1867
1902
  ] }),
1868
- (area.pages ?? []).length > 1 && /* @__PURE__ */ jsx("div", { className: "cbx-sheet-pages", children: (area.pages ?? []).map((page) => {
1869
- const on = activeArea && page.key === shell.nav.activePage;
1870
- return /* @__PURE__ */ jsx(Link, { href: page.url, className: `cbx-row cbx-row-action ${on ? "is-current" : ""}`, "aria-current": on ? "page" : void 0, onClick: onClose, children: /* @__PURE__ */ jsx("span", { className: "cbx-row-name", children: page.label }) }, page.key);
1871
- }) })
1903
+ (area.pages ?? []).length > 1 && /* @__PURE__ */ jsx("div", { className: "cbx-sheet-pages", children: /* @__PURE__ */ jsx(
1904
+ NavPageList,
1905
+ {
1906
+ pages: area.pages ?? [],
1907
+ renderPage: (page) => {
1908
+ const on = activeArea && page.key === shell.nav.activePage;
1909
+ return /* @__PURE__ */ jsxs(Link, { href: navUrl(page), className: `cbx-row cbx-row-action ${on ? "is-current" : ""} ${page.locked === true ? "is-locked" : ""}`, "aria-current": on ? "page" : void 0, "data-locked": page.locked === true ? "true" : void 0, onClick: onClose, children: [
1910
+ /* @__PURE__ */ jsx("span", { className: "cbx-row-name", children: page.label }),
1911
+ /* @__PURE__ */ jsx(NavPageMarks, { page })
1912
+ ] }, page.key);
1913
+ }
1914
+ }
1915
+ ) })
1872
1916
  ] }, area.key);
1873
1917
  }) }),
1874
1918
  /* @__PURE__ */ jsxs("div", { className: "cbx-list cbx-list-foot", children: [
@@ -1936,13 +1980,16 @@ function matchScore(text, query2) {
1936
1980
  }
1937
1981
  return score;
1938
1982
  }
1939
- function localPages(shell, query2, open) {
1983
+ function localPages(shell, query2, open, lockedLabel) {
1940
1984
  const entries = [];
1941
1985
  for (const area of shell.nav.areas) {
1942
- entries.push({ id: `area:${area.key}`, title: area.label, url: area.url, icon: area.icon });
1986
+ const areaUrl = navUrl(area);
1987
+ entries.push({ id: `area:${area.key}`, title: area.label, url: areaUrl, icon: area.icon });
1943
1988
  for (const page of area.pages ?? []) {
1944
- if (page.url === area.url) continue;
1945
- entries.push({ id: `page:${area.key}:${page.key}`, title: page.label, subtitle: area.label, url: page.url, icon: page.icon ?? area.icon });
1989
+ const url = navUrl(page);
1990
+ if (url === areaUrl) continue;
1991
+ const subtitle = page.locked === true ? `${area.label} \xB7 ${lockedLabel}` : area.label;
1992
+ entries.push({ id: `page:${area.key}:${page.key}`, title: page.label, subtitle, url, icon: page.locked === true ? "lock" : page.icon ?? area.icon });
1946
1993
  }
1947
1994
  }
1948
1995
  return entries.map((entry) => ({ entry, score: matchScore(`${entry.title} ${entry.subtitle ?? ""}`, query2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).map(({ entry }) => {
@@ -1950,8 +1997,13 @@ function localPages(shell, query2, open) {
1950
1997
  return { id: entry.id, title: entry.title, subtitle: entry.subtitle, href: entry.url, icon: /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 1.8, "aria-hidden": true }), run: (newTab) => open(entry.url, newTab) };
1951
1998
  });
1952
1999
  }
1953
- function rows(body) {
1954
- const data = Array.isArray(body) ? body : typeof body === "object" && body !== null ? body.data : null;
2000
+ function rows(body, group) {
2001
+ let data = Array.isArray(body) ? body : typeof body === "object" && body !== null ? body.data : null;
2002
+ if (data === void 0 && group !== void 0 && typeof body === "object" && body !== null) {
2003
+ const groups = body.groups;
2004
+ const match = Array.isArray(groups) ? groups.find((g) => typeof g === "object" && g !== null && g.key === group) : void 0;
2005
+ data = match?.items;
2006
+ }
1955
2007
  return Array.isArray(data) ? data.filter((row) => typeof row === "object" && row !== null) : [];
1956
2008
  }
1957
2009
  function CommandPalette({ onClose }) {
@@ -1959,6 +2011,7 @@ function CommandPalette({ onClose }) {
1959
2011
  const [input, setInput] = useState("");
1960
2012
  const [query2, setQuery] = useState("");
1961
2013
  const [groups, setGroups] = useState({});
2014
+ const [groupsFor, setGroupsFor] = useState("");
1962
2015
  const [expanded, setExpanded] = useState(null);
1963
2016
  const [activeKey, setActiveKey] = useState(null);
1964
2017
  const field = useRef(null);
@@ -1985,14 +2038,15 @@ function CommandPalette({ onClose }) {
1985
2038
  const q2 = query2.trim();
1986
2039
  const controller = new AbortController();
1987
2040
  const run = (key, load) => {
1988
- setGroups((current) => ({ ...current, [key]: { status: "loading", items: current[key]?.items ?? [] } }));
2041
+ setGroups((current2) => ({ ...current2, [key]: { status: "loading", items: current2[key]?.items ?? [] } }));
1989
2042
  load().then((items) => {
1990
- if (!controller.signal.aborted) setGroups((current) => ({ ...current, [key]: { status: "ready", items } }));
2043
+ if (!controller.signal.aborted) setGroups((current2) => ({ ...current2, [key]: { status: "ready", items } }));
1991
2044
  }).catch(() => {
1992
- if (!controller.signal.aborted) setGroups((current) => ({ ...current, [key]: { status: "error", items: [] } }));
2045
+ if (!controller.signal.aborted) setGroups((current2) => ({ ...current2, [key]: { status: "error", items: [] } }));
1993
2046
  });
1994
2047
  };
1995
2048
  const signal = controller.signal;
2049
+ setGroupsFor(q2);
1996
2050
  if (q2 === "") {
1997
2051
  setGroups({});
1998
2052
  return () => controller.abort();
@@ -2000,7 +2054,7 @@ function CommandPalette({ onClose }) {
2000
2054
  if (shell.palette.searchUrl !== void 0) {
2001
2055
  run(
2002
2056
  "records",
2003
- async () => rows(await requestJson(withQuery(shell.palette.searchUrl, { q: q2 }), { signal })).flatMap((row) => {
2057
+ async () => rows(await requestJson(withQuery(shell.palette.searchUrl, { q: q2, group: "records" }), { signal }), "records").flatMap((row) => {
2004
2058
  const url = safeUrl(row.url);
2005
2059
  if (typeof row.title !== "string" || url === void 0) return [];
2006
2060
  return [{ id: `record:${String(row.id ?? url)}`, title: row.title, subtitle: typeof row.subtitle === "string" ? row.subtitle : void 0, href: url, icon: /* @__PURE__ */ jsx(FileText, { size: 16, strokeWidth: 1.8, "aria-hidden": true }), run: (newTab) => open(url, newTab) }];
@@ -2010,7 +2064,19 @@ function CommandPalette({ onClose }) {
2010
2064
  const organization = shell.context.levels[0];
2011
2065
  run("goto", async () => {
2012
2066
  const loads = [];
2013
- if (organization !== void 0) {
2067
+ const kitGoto = shell.palette.gotoUrl === void 0 && shell.palette.searchUrl !== void 0;
2068
+ if (kitGoto) {
2069
+ loads.push(
2070
+ requestJson(withQuery(shell.palette.searchUrl, { q: q2, group: "goto" }), { signal }).then(
2071
+ (body) => rows(body, "goto").flatMap((row) => {
2072
+ const url = safeUrl(row.url);
2073
+ if (typeof row.title !== "string" || url === void 0) return [];
2074
+ return [{ id: `goto:${String(row.id ?? url)}`, title: row.title, subtitle: typeof row.subtitle === "string" ? row.subtitle : void 0, href: url, icon: /* @__PURE__ */ jsx(Building2, { size: 16, strokeWidth: 1.8, "aria-hidden": true }), run: (newTab) => open(url, newTab) }];
2075
+ })
2076
+ )
2077
+ );
2078
+ }
2079
+ if (organization !== void 0 && !kitGoto) {
2014
2080
  loads.push(
2015
2081
  requestJson(withQuery(organization.listUrl, { q: q2, limit: "5" }), { signal }).then(
2016
2082
  (body) => rows(body).flatMap((row) => {
@@ -2069,7 +2135,7 @@ function CommandPalette({ onClose }) {
2069
2135
  return () => controller.abort();
2070
2136
  }, [query2, shell]);
2071
2137
  const q = input.trim();
2072
- const pages = useMemo(() => localPages(shell, q, open), [shell, q]);
2138
+ const pages = useMemo(() => localPages(shell, q, open, t("Locked")), [shell, q, t]);
2073
2139
  const actions = useMemo(
2074
2140
  () => shell.palette.actions.filter((action) => matchScore(`${action.label} ${(action.keywords ?? []).join(" ")}`, q) > 0).map((action) => {
2075
2141
  const Icon = action.icon !== void 0 ? resolveIcon(action.icon) : Zap;
@@ -2117,8 +2183,14 @@ function CommandPalette({ onClose }) {
2117
2183
  return { ...section, start, count: items.length + (more ? 1 : 0) };
2118
2184
  });
2119
2185
  const keyOf = (option) => option.kind === "item" ? `${option.group}:${option.item.id}` : `all:${option.group}`;
2120
- const found = activeKey === null ? -1 : options.findIndex((option) => keyOf(option) === activeKey);
2121
- const active = found >= 0 ? found : 0;
2186
+ const debounced = q === query2.trim() && groupsFor === q;
2187
+ const current = (group) => group === "pages" || group === "actions" || debounced && sections.find((section) => section.key === group)?.state.status === "ready";
2188
+ const busy = !debounced || sections.some((section) => section.state.status === "loading");
2189
+ const firstResult = options.find((option) => option.group !== "ask" && current(option.group));
2190
+ const autoKey = firstResult !== void 0 ? keyOf(firstResult) : !busy && options[0] !== void 0 ? keyOf(options[0]) : null;
2191
+ const indexOf = (key) => key === null ? -1 : options.findIndex((option) => keyOf(option) === key);
2192
+ const found = indexOf(activeKey);
2193
+ const active = found >= 0 ? found : indexOf(autoKey);
2122
2194
  const setActive = (next) => {
2123
2195
  const index = typeof next === "function" ? next(active) : next;
2124
2196
  const option = options[index];
@@ -2127,10 +2199,9 @@ function CommandPalette({ onClose }) {
2127
2199
  useEffect(() => {
2128
2200
  setActiveKey(null);
2129
2201
  }, [q]);
2130
- const firstKey = options[0] === void 0 ? null : keyOf(options[0]);
2131
2202
  useEffect(() => {
2132
- if (activeKey === null && firstKey !== null) setActiveKey(firstKey);
2133
- }, [activeKey, firstKey]);
2203
+ if (activeKey === null && autoKey !== null) setActiveKey(autoKey);
2204
+ }, [activeKey, autoKey]);
2134
2205
  useEffect(() => {
2135
2206
  revealInList(`${id}-${active}`);
2136
2207
  }, [active, id]);
@@ -2178,8 +2249,8 @@ function CommandPalette({ onClose }) {
2178
2249
  "aria-controls": `${id}-list`,
2179
2250
  "aria-activedescendant": activeOption !== void 0 ? `${id}-${active}` : void 0,
2180
2251
  "aria-autocomplete": "list",
2181
- "aria-label": t("Search or ask\u2026"),
2182
- placeholder: t("Search or ask\u2026"),
2252
+ "aria-label": shell.palette.placeholder ?? t("Search or ask\u2026"),
2253
+ placeholder: shell.palette.placeholder ?? t("Search or ask\u2026"),
2183
2254
  value: input,
2184
2255
  onChange: (event) => setInput(event.target.value),
2185
2256
  autoComplete: "off",
@@ -2358,16 +2429,31 @@ function CboxShell({ shell, children, navigate = defaultNavigate, LinkComponent
2358
2429
  const helpOpen = useHelpState(store, (state) => helpVisible(state, docked));
2359
2430
  const banner = shell.banner !== void 0 && (shell.banner.kind !== "sandbox" || testData) ? shell.banner : void 0;
2360
2431
  return /* @__PURE__ */ jsx(ShellContext.Provider, { value: api, children: /* @__PURE__ */ jsxs("div", { className: "cbx-shell", "data-help-open": helpOpen, children: [
2432
+ /* @__PURE__ */ jsx(
2433
+ "a",
2434
+ {
2435
+ className: "cbx-skip-link",
2436
+ href: `#${mainId}`,
2437
+ onClick: (event) => {
2438
+ const main = document.getElementById(mainId);
2439
+ if (main === null) return;
2440
+ event.preventDefault();
2441
+ main.focus();
2442
+ main.scrollIntoView?.({ block: "start" });
2443
+ },
2444
+ children: t("Skip to content")
2445
+ }
2446
+ ),
2361
2447
  /* @__PURE__ */ jsx(Rail, { brandAssetBase }),
2362
2448
  /* @__PURE__ */ jsxs("div", { className: "cbx-shell-column", children: [
2363
2449
  /* @__PURE__ */ jsx(Header, { mobile }),
2364
2450
  banner !== void 0 && /* @__PURE__ */ jsxs("div", { className: `cbx-banner cbx-banner-${banner.kind}`, role: "status", children: [
2365
2451
  /* @__PURE__ */ jsx("span", { className: "cbx-banner-text", children: banner.text }),
2366
- banner.action !== void 0 && /* @__PURE__ */ jsx(LinkComponent, { href: banner.action.url, className: "cbx-banner-action", children: banner.action.label })
2452
+ banner.action !== void 0 && /* @__PURE__ */ jsx(LinkComponent, { href: navUrl(banner.action), className: "cbx-banner-action", children: banner.action.label })
2367
2453
  ] }),
2368
2454
  /* @__PURE__ */ jsxs("div", { className: "cbx-shell-body", children: [
2369
2455
  /* @__PURE__ */ jsx(Subnav, {}),
2370
- /* @__PURE__ */ jsx("main", { id: mainId, className: "cbx-shell-main canvas-gradient", children })
2456
+ /* @__PURE__ */ jsx("main", { id: mainId, tabIndex: -1, className: "cbx-shell-main canvas-gradient", children })
2371
2457
  ] })
2372
2458
  ] }),
2373
2459
  /* @__PURE__ */ jsx(HelpColumn, { docked }),
@@ -2424,7 +2510,7 @@ function Rail({ brandAssetBase }) {
2424
2510
  /* @__PURE__ */ jsx("nav", { className: "cbx-rail-nav", "aria-label": name, children: shell.nav.areas.map((area) => {
2425
2511
  const Icon = resolveIcon(area.icon, icons);
2426
2512
  const active = area.key === shell.nav.activeArea;
2427
- return /* @__PURE__ */ jsxs(Link, { href: area.url, className: `cbx-rail-link ${active ? "is-active" : ""}`, "aria-current": active ? "page" : void 0, title: open ? void 0 : area.label, "aria-label": area.label, children: [
2513
+ return /* @__PURE__ */ jsxs(Link, { href: navUrl(area), className: `cbx-rail-link ${active ? "is-active" : ""}`, "aria-current": active ? "page" : void 0, title: open ? void 0 : area.label, "aria-label": area.label, children: [
2428
2514
  /* @__PURE__ */ jsx(Icon, { size: 18, strokeWidth: 1.7, "aria-hidden": true }),
2429
2515
  /* @__PURE__ */ jsx("span", { className: "cbx-rail-label", children: area.label })
2430
2516
  ] }, area.key);
@@ -2471,10 +2557,19 @@ function Subnav() {
2471
2557
  /* @__PURE__ */ jsx("strong", { className: "cbx-truncate", children: area.label }),
2472
2558
  /* @__PURE__ */ jsx("button", { type: "button", className: "cbx-icon-button", onClick: () => setPreference("subnavCollapsed", true), "aria-label": t("Collapse {area}", { area: area.label }), title: "\u2318.", children: /* @__PURE__ */ jsx(PanelLeftClose, { size: 16, strokeWidth: 1.8, "aria-hidden": true }) })
2473
2559
  ] }),
2474
- /* @__PURE__ */ jsx("nav", { "aria-label": area.label, className: "cbx-subnav-list", children: pages.map((page) => {
2475
- const active = page.key === shell.nav.activePage;
2476
- return /* @__PURE__ */ jsx(Link, { href: page.url, className: `cbx-subnav-link ${active ? "is-active" : ""}`, "aria-current": active ? "page" : void 0, children: /* @__PURE__ */ jsx("span", { className: "cbx-truncate", title: page.label, children: page.label }) }, page.key);
2477
- }) })
2560
+ /* @__PURE__ */ jsx("nav", { "aria-label": area.label, className: "cbx-subnav-list", children: /* @__PURE__ */ jsx(
2561
+ NavPageList,
2562
+ {
2563
+ pages,
2564
+ renderPage: (page) => {
2565
+ const active = page.key === shell.nav.activePage;
2566
+ return /* @__PURE__ */ jsxs(Link, { href: navUrl(page), className: `cbx-subnav-link ${active ? "is-active" : ""} ${page.locked === true ? "is-locked" : ""}`, "aria-current": active ? "page" : void 0, "data-locked": page.locked === true ? "true" : void 0, children: [
2567
+ /* @__PURE__ */ jsx("span", { className: "cbx-truncate", title: page.label, children: page.label }),
2568
+ /* @__PURE__ */ jsx(NavPageMarks, { page })
2569
+ ] }, page.key);
2570
+ }
2571
+ }
2572
+ ) })
2478
2573
  ] });
2479
2574
  }
2480
2575
  function Header({ mobile }) {