@cboxdk/cbox-ui 0.10.0 → 0.10.1

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 }) {
@@ -2000,7 +2052,7 @@ function CommandPalette({ onClose }) {
2000
2052
  if (shell.palette.searchUrl !== void 0) {
2001
2053
  run(
2002
2054
  "records",
2003
- async () => rows(await requestJson(withQuery(shell.palette.searchUrl, { q: q2 }), { signal })).flatMap((row) => {
2055
+ async () => rows(await requestJson(withQuery(shell.palette.searchUrl, { q: q2, group: "records" }), { signal }), "records").flatMap((row) => {
2004
2056
  const url = safeUrl(row.url);
2005
2057
  if (typeof row.title !== "string" || url === void 0) return [];
2006
2058
  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 +2062,19 @@ function CommandPalette({ onClose }) {
2010
2062
  const organization = shell.context.levels[0];
2011
2063
  run("goto", async () => {
2012
2064
  const loads = [];
2013
- if (organization !== void 0) {
2065
+ const kitGoto = shell.palette.gotoUrl === void 0 && shell.palette.searchUrl !== void 0;
2066
+ if (kitGoto) {
2067
+ loads.push(
2068
+ requestJson(withQuery(shell.palette.searchUrl, { q: q2, group: "goto" }), { signal }).then(
2069
+ (body) => rows(body, "goto").flatMap((row) => {
2070
+ const url = safeUrl(row.url);
2071
+ if (typeof row.title !== "string" || url === void 0) return [];
2072
+ 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) }];
2073
+ })
2074
+ )
2075
+ );
2076
+ }
2077
+ if (organization !== void 0 && !kitGoto) {
2014
2078
  loads.push(
2015
2079
  requestJson(withQuery(organization.listUrl, { q: q2, limit: "5" }), { signal }).then(
2016
2080
  (body) => rows(body).flatMap((row) => {
@@ -2069,7 +2133,7 @@ function CommandPalette({ onClose }) {
2069
2133
  return () => controller.abort();
2070
2134
  }, [query2, shell]);
2071
2135
  const q = input.trim();
2072
- const pages = useMemo(() => localPages(shell, q, open), [shell, q]);
2136
+ const pages = useMemo(() => localPages(shell, q, open, t("Locked")), [shell, q, t]);
2073
2137
  const actions = useMemo(
2074
2138
  () => shell.palette.actions.filter((action) => matchScore(`${action.label} ${(action.keywords ?? []).join(" ")}`, q) > 0).map((action) => {
2075
2139
  const Icon = action.icon !== void 0 ? resolveIcon(action.icon) : Zap;
@@ -2178,8 +2242,8 @@ function CommandPalette({ onClose }) {
2178
2242
  "aria-controls": `${id}-list`,
2179
2243
  "aria-activedescendant": activeOption !== void 0 ? `${id}-${active}` : void 0,
2180
2244
  "aria-autocomplete": "list",
2181
- "aria-label": t("Search or ask\u2026"),
2182
- placeholder: t("Search or ask\u2026"),
2245
+ "aria-label": shell.palette.placeholder ?? t("Search or ask\u2026"),
2246
+ placeholder: shell.palette.placeholder ?? t("Search or ask\u2026"),
2183
2247
  value: input,
2184
2248
  onChange: (event) => setInput(event.target.value),
2185
2249
  autoComplete: "off",
@@ -2358,16 +2422,31 @@ function CboxShell({ shell, children, navigate = defaultNavigate, LinkComponent
2358
2422
  const helpOpen = useHelpState(store, (state) => helpVisible(state, docked));
2359
2423
  const banner = shell.banner !== void 0 && (shell.banner.kind !== "sandbox" || testData) ? shell.banner : void 0;
2360
2424
  return /* @__PURE__ */ jsx(ShellContext.Provider, { value: api, children: /* @__PURE__ */ jsxs("div", { className: "cbx-shell", "data-help-open": helpOpen, children: [
2425
+ /* @__PURE__ */ jsx(
2426
+ "a",
2427
+ {
2428
+ className: "cbx-skip-link",
2429
+ href: `#${mainId}`,
2430
+ onClick: (event) => {
2431
+ const main = document.getElementById(mainId);
2432
+ if (main === null) return;
2433
+ event.preventDefault();
2434
+ main.focus();
2435
+ main.scrollIntoView?.({ block: "start" });
2436
+ },
2437
+ children: t("Skip to content")
2438
+ }
2439
+ ),
2361
2440
  /* @__PURE__ */ jsx(Rail, { brandAssetBase }),
2362
2441
  /* @__PURE__ */ jsxs("div", { className: "cbx-shell-column", children: [
2363
2442
  /* @__PURE__ */ jsx(Header, { mobile }),
2364
2443
  banner !== void 0 && /* @__PURE__ */ jsxs("div", { className: `cbx-banner cbx-banner-${banner.kind}`, role: "status", children: [
2365
2444
  /* @__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 })
2445
+ banner.action !== void 0 && /* @__PURE__ */ jsx(LinkComponent, { href: navUrl(banner.action), className: "cbx-banner-action", children: banner.action.label })
2367
2446
  ] }),
2368
2447
  /* @__PURE__ */ jsxs("div", { className: "cbx-shell-body", children: [
2369
2448
  /* @__PURE__ */ jsx(Subnav, {}),
2370
- /* @__PURE__ */ jsx("main", { id: mainId, className: "cbx-shell-main canvas-gradient", children })
2449
+ /* @__PURE__ */ jsx("main", { id: mainId, tabIndex: -1, className: "cbx-shell-main canvas-gradient", children })
2371
2450
  ] })
2372
2451
  ] }),
2373
2452
  /* @__PURE__ */ jsx(HelpColumn, { docked }),
@@ -2424,7 +2503,7 @@ function Rail({ brandAssetBase }) {
2424
2503
  /* @__PURE__ */ jsx("nav", { className: "cbx-rail-nav", "aria-label": name, children: shell.nav.areas.map((area) => {
2425
2504
  const Icon = resolveIcon(area.icon, icons);
2426
2505
  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: [
2506
+ 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
2507
  /* @__PURE__ */ jsx(Icon, { size: 18, strokeWidth: 1.7, "aria-hidden": true }),
2429
2508
  /* @__PURE__ */ jsx("span", { className: "cbx-rail-label", children: area.label })
2430
2509
  ] }, area.key);
@@ -2471,10 +2550,19 @@ function Subnav() {
2471
2550
  /* @__PURE__ */ jsx("strong", { className: "cbx-truncate", children: area.label }),
2472
2551
  /* @__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
2552
  ] }),
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
- }) })
2553
+ /* @__PURE__ */ jsx("nav", { "aria-label": area.label, className: "cbx-subnav-list", children: /* @__PURE__ */ jsx(
2554
+ NavPageList,
2555
+ {
2556
+ pages,
2557
+ renderPage: (page) => {
2558
+ const active = page.key === shell.nav.activePage;
2559
+ 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: [
2560
+ /* @__PURE__ */ jsx("span", { className: "cbx-truncate", title: page.label, children: page.label }),
2561
+ /* @__PURE__ */ jsx(NavPageMarks, { page })
2562
+ ] }, page.key);
2563
+ }
2564
+ }
2565
+ ) })
2478
2566
  ] });
2479
2567
  }
2480
2568
  function Header({ mobile }) {