@apliteni/apliteni-ui 0.61.2 → 0.62.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
@@ -220,6 +220,12 @@ react/ # React components — private workspace, built to reac
220
220
  dist/ # tsup output; shipped as …/react and …/react/css
221
221
  ```
222
222
 
223
+ `SidebarNav` renders flat items or captioned sections with one level of nested
224
+ disclosure, counts, disabled and danger rows, an artwork slot, and a footer slot. Its
225
+ `renderLink(item, linkProps)` hook supports router links; spread the supplied props
226
+ to retain names, children and current-page semantics. `collapsed` changes only
227
+ presentation, keeping nested links reachable and named.
228
+
223
229
  ## Contribute
224
230
 
225
231
  Use Node 20 or newer. Install [jq](https://jqlang.github.io/jq/), then run `npm ci` at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.61.2",
3
+ "version": "0.62.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -659,7 +659,57 @@ type TooltipProps = {
659
659
  };
660
660
  declare function Tooltip({ text, children }: TooltipProps): react.JSX.Element;
661
661
 
662
+ type SidebarNavBadge = string | number | {
663
+ text: string | number;
664
+ tone?: 'neutral' | 'accent' | 'live' | 'danger';
665
+ };
666
+ /** A row that goes somewhere. A group takes these as its children. */
667
+ type SidebarNavLeaf = {
668
+ id: string;
669
+ label: string;
670
+ href?: string;
671
+ target?: string;
672
+ icon?: string;
673
+ leading?: ReactNode;
674
+ badge?: SidebarNavBadge;
675
+ disabled?: boolean;
676
+ danger?: boolean;
677
+ };
678
+ /**
679
+ * One level of nesting, the depth `sidebarNav()` renders: its `sideGroup()` maps
680
+ * children through `sideLeaf()`, which has no group branch. A child's own `items`
681
+ * are dropped here too, so the two halves render the same tree.
682
+ */
683
+ type SidebarNavItem = SidebarNavLeaf & {
684
+ items?: SidebarNavLeaf[];
685
+ defaultOpen?: boolean;
686
+ };
687
+ type SidebarNavSection = {
688
+ label?: string;
689
+ items: SidebarNavItem[];
690
+ };
691
+ type SidebarNavProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
692
+ items?: SidebarNavItem[];
693
+ sections?: SidebarNavSection[];
694
+ active?: string;
695
+ activeIs?: 'page' | 'section';
696
+ collapsed?: boolean;
697
+ footer?: ReactNode;
698
+ /** Spread all link props onto the router link to retain its name and current state. */
699
+ renderLink?: (item: SidebarNavItem, props: ComponentPropsWithoutRef<'a'>) => ReactNode;
700
+ };
701
+ declare const SidebarNav: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "children"> & {
702
+ items?: SidebarNavItem[];
703
+ sections?: SidebarNavSection[];
704
+ active?: string;
705
+ activeIs?: "page" | "section";
706
+ collapsed?: boolean;
707
+ footer?: ReactNode;
708
+ /** Spread all link props onto the router link to retain its name and current state. */
709
+ renderLink?: (item: SidebarNavItem, props: ComponentPropsWithoutRef<"a">) => ReactNode;
710
+ } & react.RefAttributes<HTMLElement>>;
711
+
662
712
  declare const PAGE_SIZES: readonly number[];
663
713
  declare const DEFAULT_PAGE_SIZE: number;
664
714
 
665
- export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
715
+ export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
@@ -2452,9 +2452,108 @@ function Toast({ children }) {
2452
2452
  }
2453
2453
 
2454
2454
  // src/AppShell.tsx
2455
- import { useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
2455
+ import { useEffect as useEffect13, useId as useId12, useState as useState12 } from "react";
2456
2456
  import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
2457
+
2458
+ // src/SidebarNav.tsx
2459
+ import { forwardRef as forwardRef5, useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
2460
+ import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
2457
2461
  import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
2462
+ var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
2463
+ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
2464
+ const uid = useId11();
2465
+ const group = !sub && !!item.items?.length;
2466
+ const childActive = group && item.items.some((child) => child.id === active);
2467
+ const [expanded, setExpanded] = useState11();
2468
+ const open = expanded ?? item.defaultOpen ?? childActive;
2469
+ const [entering, setEntering] = useState11(false);
2470
+ useEffect12(() => {
2471
+ if (!entering) return;
2472
+ const timer = setTimeout(() => setEntering(false), ENTRANCE_FALLBACK_MS);
2473
+ return () => clearTimeout(timer);
2474
+ }, [entering]);
2475
+ const count = badgeText(item.badge);
2476
+ const name = `${item.label}${count == null || count === "" ? "" : ` ${count}`}`;
2477
+ const isActive = !group && item.id === active;
2478
+ const className = [
2479
+ "ui-nav__item",
2480
+ sub && "ui-nav__item--sub",
2481
+ group && "ui-nav__toggle",
2482
+ isActive && "is-active",
2483
+ childActive && "is-current",
2484
+ item.disabled && "is-disabled",
2485
+ item.danger && "is-danger"
2486
+ ].filter(Boolean).join(" ");
2487
+ const children = /* @__PURE__ */ jsxs23(Fragment7, { children: [
2488
+ (item.leading ?? item.icon) && /* @__PURE__ */ jsx28("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx28(Icon, { name: item.icon }) }),
2489
+ /* @__PURE__ */ jsx28("span", { className: "ui-nav__label", children: item.label }),
2490
+ count != null && count !== "" && /* @__PURE__ */ jsx28("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
2491
+ group && /* @__PURE__ */ jsx28("span", { className: "ui-nav__caret", "aria-hidden": "true" })
2492
+ ] });
2493
+ const common = { className, "aria-label": name, title: collapsed ? name : void 0, children };
2494
+ if (group) return /* @__PURE__ */ jsxs23("li", { className: `ui-nav__group${open ? " is-open" : ""}`, children: [
2495
+ /* @__PURE__ */ jsx28(
2496
+ "button",
2497
+ {
2498
+ ...common,
2499
+ type: "button",
2500
+ disabled: item.disabled,
2501
+ "aria-expanded": open,
2502
+ "aria-controls": uid,
2503
+ onClick: () => {
2504
+ setExpanded(!open);
2505
+ setEntering(!open);
2506
+ }
2507
+ }
2508
+ ),
2509
+ /* @__PURE__ */ jsx28("ul", { id: uid, className: `ui-nav__sub${entering ? " is-entering" : ""}`, hidden: !open, onAnimationEnd: () => setEntering(false), children: item.items.map((child) => /* @__PURE__ */ jsx28(Row, { item: child, active, activeIs, collapsed, renderLink, sub: true }, child.id)) })
2510
+ ] });
2511
+ if (item.disabled) return /* @__PURE__ */ jsx28("li", { children: /* @__PURE__ */ jsx28("span", { ...common, role: "link", "aria-disabled": "true" }) });
2512
+ const props = {
2513
+ ...common,
2514
+ href: item.href ?? `#${item.id}`,
2515
+ target: item.target,
2516
+ "aria-current": isActive ? activeIs === "section" ? "true" : "page" : void 0
2517
+ };
2518
+ return /* @__PURE__ */ jsx28("li", { children: renderLink ? renderLink(item, props) : /* @__PURE__ */ jsx28("a", { ...props }) });
2519
+ }
2520
+ function Section({ section, ...props }) {
2521
+ const uid = useId11();
2522
+ return /* @__PURE__ */ jsxs23("div", { className: "ui-nav__section", children: [
2523
+ section.label && /* @__PURE__ */ jsx28("div", { className: "ui-nav__cap", id: uid, "aria-hidden": props.collapsed || void 0, children: section.label }),
2524
+ /* @__PURE__ */ jsx28("ul", { className: "ui-nav__list", "aria-labelledby": section.label ? uid : void 0, children: section.items.map((item) => /* @__PURE__ */ jsx28(Row, { item, ...props }, item.id)) })
2525
+ ] });
2526
+ }
2527
+ var SidebarNav = forwardRef5(function SidebarNav2({
2528
+ items = [],
2529
+ sections,
2530
+ active,
2531
+ activeIs = "page",
2532
+ collapsed = false,
2533
+ footer,
2534
+ renderLink,
2535
+ className,
2536
+ "aria-label": label = "Sidebar",
2537
+ ...rest
2538
+ }, ref) {
2539
+ return /* @__PURE__ */ jsxs23("nav", { ...rest, ref, "aria-label": label, className: ["ui-nav", "ui-nav--side", collapsed && "is-collapsed", className].filter(Boolean).join(" "), children: [
2540
+ (sections?.length ? sections : [{ items }]).map((section, index) => /* @__PURE__ */ jsx28(
2541
+ Section,
2542
+ {
2543
+ section,
2544
+ active,
2545
+ activeIs,
2546
+ collapsed,
2547
+ renderLink
2548
+ },
2549
+ index
2550
+ )),
2551
+ footer && /* @__PURE__ */ jsx28("div", { className: "ui-nav__foot", children: footer })
2552
+ ] });
2553
+ });
2554
+
2555
+ // src/AppShell.tsx
2556
+ import { Fragment as Fragment8, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2458
2557
  var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
2459
2558
  function AppShell({
2460
2559
  sections,
@@ -2475,21 +2574,21 @@ function AppShell({
2475
2574
  palette,
2476
2575
  renderLink
2477
2576
  }) {
2478
- const uid = useId11();
2479
- const [collapsed, setCollapsed] = useState11(defaultCollapsed);
2480
- const [search, setSearch] = useState11(false);
2481
- const [more, setMore] = useState11(false);
2482
- const [theme, setTheme] = useState11("dark");
2577
+ const uid = useId12();
2578
+ const [collapsed, setCollapsed] = useState12(defaultCollapsed);
2579
+ const [search, setSearch] = useState12(false);
2580
+ const [more, setMore] = useState12(false);
2581
+ const [theme, setTheme] = useState12("dark");
2483
2582
  const current = path(pathname);
2484
2583
  const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
2485
- useEffect12(() => {
2584
+ useEffect13(() => {
2486
2585
  setCollapsed(railCollapsed() ?? defaultCollapsed);
2487
2586
  }, [defaultCollapsed]);
2488
- useEffect12(() => {
2587
+ useEffect13(() => {
2489
2588
  setMore(false);
2490
2589
  setSearch(false);
2491
2590
  }, [pathname]);
2492
- useEffect12(() => {
2591
+ useEffect13(() => {
2493
2592
  const root = document.documentElement;
2494
2593
  const sync2 = () => setTheme(root.getAttribute("data-theme") === "light" ? "light" : "dark");
2495
2594
  sync2();
@@ -2497,7 +2596,7 @@ function AppShell({
2497
2596
  observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
2498
2597
  return () => observer.disconnect();
2499
2598
  }, []);
2500
- useEffect12(() => {
2599
+ useEffect13(() => {
2501
2600
  const hotkey = (event) => {
2502
2601
  if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
2503
2602
  const target = event.target;
@@ -2520,49 +2619,68 @@ function AppShell({
2520
2619
  const linkProps = {
2521
2620
  href: section.href,
2522
2621
  className: `ui-nav__item${section === active ? " is-active" : ""}`,
2523
- "aria-current": section === active ? "page" : void 0,
2622
+ // guidelines/going-back.md, section-lit: the same row, so the same reading as
2623
+ // the rail's. "page" here would announce the list as the page the reader is on.
2624
+ "aria-current": section === active ? back ? "true" : "page" : void 0,
2524
2625
  "aria-label": `${section.label}${section.count == null ? "" : ` ${section.count}`}`,
2525
2626
  onClick: (event) => {
2526
2627
  if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2527
2628
  },
2528
- children: /* @__PURE__ */ jsxs23(Fragment7, { children: [
2529
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx28(Icon, { name: section.icon }) }),
2530
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__label", children: section.label }),
2531
- section.count != null && /* @__PURE__ */ jsx28("span", { className: "ui-nav__badge", children: section.count })
2629
+ children: /* @__PURE__ */ jsxs24(Fragment8, { children: [
2630
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx29(Icon, { name: section.icon }) }),
2631
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: section.label }),
2632
+ section.count != null && /* @__PURE__ */ jsx29("span", { className: "ui-nav__badge is-neutral", children: section.count })
2532
2633
  ] })
2533
2634
  };
2534
- return /* @__PURE__ */ jsx28("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx28("a", { ...linkProps }) }, section.href);
2635
+ return /* @__PURE__ */ jsx29("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx29("a", { ...linkProps }) }, section.href);
2535
2636
  });
2637
+ const sidebar = (items, label) => /* @__PURE__ */ jsx29(
2638
+ SidebarNav,
2639
+ {
2640
+ "aria-label": label,
2641
+ items: items.map((section) => ({ ...section, id: section.href, badge: section.count })),
2642
+ active: active?.href,
2643
+ activeIs: back ? "section" : "page",
2644
+ renderLink: (item, props) => {
2645
+ const section = items.find((section2) => section2.href === item.id);
2646
+ const linkProps = { ...props, onClick: (event) => {
2647
+ if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2648
+ } };
2649
+ return renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx29("a", { ...linkProps });
2650
+ }
2651
+ }
2652
+ );
2536
2653
  const foldLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
2537
2654
  const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
2538
2655
  const remaining = sections.slice(visible.length);
2539
2656
  const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
2540
- return /* @__PURE__ */ jsxs23("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2541
- /* @__PURE__ */ jsxs23("div", { className: "ui-app__rail", children: [
2542
- /* @__PURE__ */ jsx28("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs23("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2543
- brand ?? /* @__PURE__ */ jsx28("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2544
- /* @__PURE__ */ jsx28("span", { children: word })
2657
+ return /* @__PURE__ */ jsxs24("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2658
+ /* @__PURE__ */ jsxs24("div", { className: "ui-app__rail", children: [
2659
+ /* @__PURE__ */ jsx29("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs24("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2660
+ brand ?? /* @__PURE__ */ jsx29("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2661
+ /* @__PURE__ */ jsx29("span", { children: word })
2545
2662
  ] }) }),
2546
- /* @__PURE__ */ jsx28("nav", { className: "ui-nav ui-nav--side", "aria-label": "Sections", children: /* @__PURE__ */ jsx28("ul", { className: "ui-nav__list", children: links(sections) }) }),
2547
- /* @__PURE__ */ jsx28("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx28("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs23("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2548
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs23("svg", { className: "ui-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2549
- /* @__PURE__ */ jsx28("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2550
- /* @__PURE__ */ jsx28("path", { className: "ui-app__fold-seam", d: "M9 3v18" })
2663
+ sidebar(sections, "Sections"),
2664
+ /* @__PURE__ */ jsx29("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx29("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs24("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2665
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs24("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2666
+ /* @__PURE__ */ jsx29("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2667
+ /* @__PURE__ */ jsx29("path", { className: "ui-app__fold-seam", d: "M9 3v18" }),
2668
+ /* @__PURE__ */ jsx29("path", { className: "ui-app__fold-arrow", d: "m16 15-3-3 3-3" })
2551
2669
  ] }) }),
2552
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__label", children: foldLabel })
2670
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: foldLabel })
2553
2671
  ] }) }) })
2554
2672
  ] }),
2555
- /* @__PURE__ */ jsxs23("div", { className: "ui-app__well", children: [
2556
- /* @__PURE__ */ jsxs23("header", { className: "ui-app__bar", children: [
2557
- /* @__PURE__ */ jsxs23("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2558
- /* @__PURE__ */ jsx28("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx28(Icon, { name: "search" }) }),
2559
- /* @__PURE__ */ jsx28("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2560
- /* @__PURE__ */ jsx28("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
2673
+ /* @__PURE__ */ jsxs24("div", { className: "ui-app__well", children: [
2674
+ /* @__PURE__ */ jsxs24("header", { className: "ui-app__bar", children: [
2675
+ /* @__PURE__ */ jsxs24("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2676
+ /* @__PURE__ */ jsx29("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx29(Icon, { name: "search" }) }),
2677
+ /* @__PURE__ */ jsx29("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2678
+ /* @__PURE__ */ jsx29("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
2561
2679
  ] }),
2562
- /* @__PURE__ */ jsx28("span", { className: "ui-app__bar-gap" }),
2680
+ /* @__PURE__ */ jsx29("span", { className: "ui-app__bar-gap" }),
2563
2681
  bandControl,
2564
- /* @__PURE__ */ jsx28("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx28("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
2565
- /* @__PURE__ */ jsx28("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx28(
2682
+ /* @__PURE__ */ jsx29("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx29("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
2683
+ /* @__PURE__ */ jsx29("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx29(
2566
2684
  Dropdown,
2567
2685
  {
2568
2686
  variant: "menu",
@@ -2571,27 +2689,27 @@ function AppShell({
2571
2689
  ariaLabel: `Signed in as ${account.name}, ${account.email}`,
2572
2690
  triggerClass: "ui-app__user-trigger",
2573
2691
  panelClass: "ui-app__user-panel",
2574
- triggerContent: /* @__PURE__ */ jsx28("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2575
- header: /* @__PURE__ */ jsxs23("div", { className: "ui-dropdown__head", children: [
2576
- /* @__PURE__ */ jsx28("b", { children: account.name }),
2577
- /* @__PURE__ */ jsx28("span", { children: account.email })
2692
+ triggerContent: /* @__PURE__ */ jsx29("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2693
+ header: /* @__PURE__ */ jsxs24("div", { className: "ui-dropdown__head", children: [
2694
+ /* @__PURE__ */ jsx29("b", { children: account.name }),
2695
+ /* @__PURE__ */ jsx29("span", { children: account.email })
2578
2696
  ] }),
2579
2697
  items: [{ label: "Sign out", icon: "logout", danger: true }],
2580
2698
  onSelect: onSignOut
2581
2699
  }
2582
2700
  ) })
2583
2701
  ] }),
2584
- /* @__PURE__ */ jsxs23("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2585
- back && /* @__PURE__ */ jsx28(BackLink, { ...back }),
2586
- /* @__PURE__ */ jsx28("h1", { children: title }),
2587
- lede && /* @__PURE__ */ jsx28("p", { className: "ui-app__sub", children: lede }),
2588
- actions && /* @__PURE__ */ jsx28("div", { className: "ui-react-app__actions", children: actions }),
2589
- /* @__PURE__ */ jsx28("div", { className: "ui-app__body", children })
2702
+ /* @__PURE__ */ jsxs24("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2703
+ back && /* @__PURE__ */ jsx29(BackLink, { ...back }),
2704
+ /* @__PURE__ */ jsx29("h1", { children: title }),
2705
+ lede && /* @__PURE__ */ jsx29("p", { className: "ui-app__sub", children: lede }),
2706
+ actions && /* @__PURE__ */ jsx29("div", { className: "ui-react-app__actions", children: actions }),
2707
+ /* @__PURE__ */ jsx29("div", { className: "ui-app__body", children })
2590
2708
  ] })
2591
2709
  ] }),
2592
- /* @__PURE__ */ jsx28("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs23("ul", { className: "ui-nav__list", children: [
2710
+ /* @__PURE__ */ jsx29("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs24("ul", { className: "ui-nav__list", children: [
2593
2711
  links(visible),
2594
- remaining.length > 0 && /* @__PURE__ */ jsx28("li", { children: /* @__PURE__ */ jsxs23(
2712
+ remaining.length > 0 && /* @__PURE__ */ jsx29("li", { children: /* @__PURE__ */ jsxs24(
2595
2713
  "button",
2596
2714
  {
2597
2715
  type: "button",
@@ -2600,20 +2718,20 @@ function AppShell({
2600
2718
  "aria-expanded": more,
2601
2719
  onClick: () => setMore(true),
2602
2720
  children: [
2603
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx28(Icon, { name: "moreHorizontal" }) }),
2604
- /* @__PURE__ */ jsx28("span", { className: "ui-nav__label", children: "More" })
2721
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx29(Icon, { name: "moreHorizontal" }) }),
2722
+ /* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: "More" })
2605
2723
  ]
2606
2724
  }
2607
2725
  ) })
2608
2726
  ] }) }),
2609
- /* @__PURE__ */ jsx28(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: /* @__PURE__ */ jsx28("nav", { className: "ui-nav ui-nav--side", "aria-label": "More sections", children: /* @__PURE__ */ jsx28("ul", { className: "ui-nav__list", children: links(remaining) }) }) }),
2610
- /* @__PURE__ */ jsx28(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
2727
+ /* @__PURE__ */ jsx29(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: sidebar(remaining, "More sections") }),
2728
+ /* @__PURE__ */ jsx29(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
2611
2729
  ] });
2612
2730
  }
2613
2731
 
2614
2732
  // src/EmptyState.tsx
2615
2733
  import { icon as kitIcon2, illo } from "@apliteni/apliteni-ui";
2616
- import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2734
+ import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2617
2735
  var cases = {
2618
2736
  "first-run": { title: "Nothing needs you yet", sub: "New items will appear here when they need your attention.", icon: "check" },
2619
2737
  "no-matches": { title: "No results in this view", sub: "Try another search or clear your filters.", icon: "search" },
@@ -2625,18 +2743,18 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon6, art, actio
2625
2743
  const heading = title ?? preset.title;
2626
2744
  const guidance = sub ?? preset.sub;
2627
2745
  const glyph = icon6 ?? preset.icon;
2628
- return /* @__PURE__ */ jsxs24("div", { className: "ui-empty", children: [
2629
- art ? typeof art === "string" ? /* @__PURE__ */ jsx29("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx29("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx29("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
2630
- heading && /* @__PURE__ */ jsx29("div", { className: "ui-empty__title", children: heading }),
2631
- guidance && /* @__PURE__ */ jsx29("div", { className: "ui-empty__sub", children: guidance }),
2632
- actions && /* @__PURE__ */ jsx29("div", { className: "ui-empty__actions", children: actions })
2746
+ return /* @__PURE__ */ jsxs25("div", { className: "ui-empty", children: [
2747
+ art ? typeof art === "string" ? /* @__PURE__ */ jsx30("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx30("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx30("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
2748
+ heading && /* @__PURE__ */ jsx30("div", { className: "ui-empty__title", children: heading }),
2749
+ guidance && /* @__PURE__ */ jsx30("div", { className: "ui-empty__sub", children: guidance }),
2750
+ actions && /* @__PURE__ */ jsx30("div", { className: "ui-empty__actions", children: actions })
2633
2751
  ] });
2634
2752
  }
2635
2753
 
2636
2754
  // src/ThemeToggle.tsx
2637
2755
  import { useSyncExternalStore } from "react";
2638
2756
  import { icon as icon5 } from "@apliteni/apliteni-ui";
2639
- import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2757
+ import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2640
2758
  var STORAGE_KEY = "apliteni-strategy-theme";
2641
2759
  var CHOICE_EVENT = "apliteni-theme-choice";
2642
2760
  var THEME_INIT_SCRIPT = `(()=>{let t;try{t=localStorage.getItem('apliteni-strategy-theme')}catch{}document.documentElement.setAttribute('data-theme',t==='dark'||t==='light'?t:window.matchMedia?.('(prefers-color-scheme: light)').matches?'light':'dark')})()`;
@@ -2694,10 +2812,10 @@ function ThemeToggle({ labelled = false }) {
2694
2812
  }
2695
2813
  window.dispatchEvent(new Event(CHOICE_EVENT));
2696
2814
  };
2697
- const button = /* @__PURE__ */ jsx30("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx30("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon5(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
2698
- return labelled ? /* @__PURE__ */ jsxs25("span", { className: "ui-theme-toggle", children: [
2815
+ const button = /* @__PURE__ */ jsx31("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx31("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon5(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
2816
+ return labelled ? /* @__PURE__ */ jsxs26("span", { className: "ui-theme-toggle", children: [
2699
2817
  button,
2700
- /* @__PURE__ */ jsxs25("span", { children: [
2818
+ /* @__PURE__ */ jsxs26("span", { children: [
2701
2819
  label,
2702
2820
  " theme"
2703
2821
  ] })
@@ -2738,6 +2856,7 @@ export {
2738
2856
  RowIdentity,
2739
2857
  Segmented,
2740
2858
  SelectField,
2859
+ SidebarNav,
2741
2860
  Skeleton,
2742
2861
  SkeletonTable,
2743
2862
  Snippet,
@@ -135,12 +135,17 @@ export function railCollapsed(cookies) {
135
135
  // The name says what the press will do, and aria-expanded says what the rail is.
136
136
  const railName = (collapsed) => (collapsed ? 'Expand sidebar' : 'Collapse sidebar');
137
137
 
138
- // A frame that holds still and a seam that crosses it. Only the two nodes are
139
- // written here — a seam that travels has to be a child a stylesheet can reach —
140
- // and they are spliced into icon()'s own wrapper rather than a copy of it.
141
- // why: docs/specification.md#the-page-shell
138
+ // Lucide `panel-left-close`, unmodified: a frame, the seam at 9, and a chevron
139
+ // pointing the way the press moves the rail's edge. The folded rail mirrors the
140
+ // chevron about 15 in layout.css, which lands it exactly on Lucide
141
+ // `panel-left-open` — so both states are a shipped glyph and neither is drawn by
142
+ // hand. The seam does not move: it IS the rail, and the compartment it cuts off
143
+ // stays on the side the rail is on. Only the nodes are written here — a child a
144
+ // stylesheet can reach — and they are spliced into icon()'s own wrapper rather
145
+ // than a copy of it. why: docs/specification.md#the-page-shell
142
146
  const MARK = '<rect x="3" y="3" width="18" height="18" rx="2"/>'
143
- + '<path class="ui-app__fold-seam" d="M9 3v18"/>';
147
+ + '<path class="ui-app__fold-seam" d="M9 3v18"/>'
148
+ + '<path class="ui-app__fold-arrow" d="m16 15-3-3 3-3"/>';
144
149
  const railMark = () => icon('').replace('></svg>', `>${MARK}</svg>`);
145
150
 
146
151
  // The rail's own skin, outside the <nav>: folding the rail is not a place to go.
@@ -106,13 +106,14 @@
106
106
  /* Which row is current is structural here, not chromatic: the resting glyphs
107
107
  step down so the current one is the brightest whatever --accent is doing, and
108
108
  the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
109
- smallest of three weak signals doing the most work, so it grows with the row. */
109
+ smallest of three weak signals doing the most work, so it grows with the row.
110
+ The current glyph's own ink is nav.css's rule now, not a copy here: since #429
111
+ EVERY rail reads it that way, so the rail's copy was the same sentence in a
112
+ second file. What is left here is the rail's own resting step. */
110
113
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
111
114
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
112
- stories/apps/shell-states.test.js:1456 `a resting glyph is dimmer but still legible`. */
115
+ stories/apps/shell-states.test.js:1491 `a resting glyph is dimmer but still legible`. */
113
116
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
114
- .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
115
- .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
116
117
  .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
117
118
 
118
119
  /* A destructive row of the rail is a navigation row and rests like one; the danger
@@ -224,12 +225,27 @@
224
225
  .ui-app .ui-app__fold { color: var(--text); }
225
226
  .ui-app .ui-app__fold:hover,
226
227
  .ui-app .ui-app__fold:focus-visible { color: var(--strong); }
227
- /* The width's own clock, not the words', so the mark and the closing edge arrive
228
- together; no `!important`, so the reduced-motion net still takes it. 6 is not a
229
- number: shell.js draws the frame 3..21 and the seam at 9, and 6 mirrors it about
230
- the centre. why: docs/specification.md#the-page-shell */
231
- .ui-app__fold .ui-app__fold-seam { transition: transform var(--dur-med) var(--ease); }
232
- .ui-app.is-collapsed .ui-app__fold .ui-app__fold-seam { transform: translateX(6px); }
228
+ /* The seam holds at 9 at both widths, because it is the rail: the compartment it
229
+ cuts off is the narrow one, on the side the rail is on. What travels is the
230
+ chevron, and it says what the press will do rather than repeating the state —
231
+ the same thing the control's own accessible name says. shell.js draws it at
232
+ 13..16, Lucide `panel-left-close`; mirrored about 15, the centre of the
233
+ compartment it stands in, it lands at 14..17, which is Lucide
234
+ `panel-left-open`. So the two states are two Lucide glyphs and the CSS between
235
+ them is one reflection, not a hand-drawn position. `transform-origin` is
236
+ written out because a scale, unlike the translate this replaces, is read from
237
+ wherever the origin is — left to the initial 0 0 it would fold the mark
238
+ through the viewBox's left edge.
239
+
240
+ The width's own clock, not the words', so the mark and the closing edge arrive
241
+ together; no `!important`, so the reduced-motion net still takes it.
242
+ why: docs/specification.md#the-page-shell */
243
+ .ui-app__fold .ui-app__fold-arrow {
244
+ transform-box: view-box;
245
+ transform-origin: 15px 12px;
246
+ transition: transform var(--dur-med) var(--ease);
247
+ }
248
+ .ui-app.is-collapsed .ui-app__fold .ui-app__fold-arrow { transform: scaleX(-1); }
233
249
 
234
250
  /* -- The collapsed rail --------------------------------------------------- */
235
251
  /* Each rule here has its twin in the 720px block below; nothing lays a row out
@@ -257,6 +273,16 @@
257
273
  :where(.ui-app:not(.ui-app--topbar).is-collapsed) .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
258
274
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
259
275
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
276
+ /* The current row is ONE plate on the glyph column. Same rules as nav.css writes
277
+ for a standalone folded rail, and for the same reasons: the bar stands in the
278
+ row's own left padding, which on the strip is the rail's edge; the hairline is
279
+ drawn on a row that keeps the open column, so its right edge is off the strip;
280
+ and --ui-nav-strip is twice the glyph's centre, so a box that wide at the row's
281
+ edge is centred on the glyph. why: docs/specification.md#the-page-shell */
282
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
283
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
284
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
285
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
260
286
  /* The label IS the chip. `fixed` is the fallback placement, out of the rail's own
261
287
  clip; the @supports branch below pins it to the row and is the only one that
262
288
  survives a scroll. The toggle's selector is unscoped, because it is wordless at
@@ -452,6 +478,10 @@
452
478
  .ui-app__brand span { opacity: 0; pointer-events: none; }
453
479
  .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
454
480
  .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
481
+ .ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
482
+ .ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
483
+ .ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
484
+ .ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
455
485
  /* The one line of this block that is deliberately NOT the reader's fold: a finger
456
486
  is the only pointer this width has, so a row and the account block are held to
457
487
  44px. Neither floor can spread to the press, where growing the box is the one
@@ -93,8 +93,12 @@
93
93
 
94
94
  .ui-nav__ic { display: inline-flex; flex: none; }
95
95
  .ui-nav__ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; opacity: 0.72; }
96
+ /* The glyph brightens with its row and takes the row's ink, not the accent. One
97
+ accent signal per row: that is the marker above, which is the only thing on a
98
+ rail that means "here". A glyph in --accent beside it is the same sentence
99
+ said twice, and beside an accent counter it was said three times. See #429. */
96
100
  .ui-nav__item.is-active .ui-nav__ic svg,
97
- .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
101
+ .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
98
102
  .ui-nav__label {
99
103
  flex: 1 1 auto; min-width: 0;
100
104
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -146,16 +150,14 @@
146
150
  .ui-nav__badge.is-live { background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--chip-success-ink); }
147
151
  .ui-nav__badge.is-danger { background: var(--glow-pink); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ color: var(--pink); }
148
152
  .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
149
- /* Same shape, one row up: on an active row the accent counter takes the row's
150
- OWN surface instead of laying its wash over it. An active row is already
151
- painted --surface, so an opaque counter avoids stacking tinted fills. Every
152
- figure here is measured against the tokens this kit now ships. The accent on
153
- that pair falls under WCAG AA in four of the eight theme x accent cells —
154
- 4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
155
- it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
156
- Emerald. So the accent wash is painted on a base surface, never a raised one.
157
- See #157 and docs/specification.md#colour-and-contrast. */
158
- .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--accent); }
153
+ /* Same shape, one row up: on the current row the accent counter drops both the
154
+ wash and the accent and reads as the neutral one beside it. The row already
155
+ carries the one accent signal it is allowed — the marker — and the count is a
156
+ number, not a second answer to "where am I". It also retires the pair #157
157
+ recorded: accent ink on --surface fell under WCAG AA in four of the eight
158
+ theme x accent cells (4.08 in dark Nebula), and no alpha cleared it. Nothing
159
+ now paints that pair. See #429 and docs/specification.md#colour-and-contrast. */
160
+ .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
159
161
 
160
162
  /* Collapsible group */
161
163
  .ui-nav__group { display: flex; flex-direction: column; }
@@ -248,6 +250,43 @@
248
250
  }
249
251
  .ui-nav--side.is-collapsed .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
250
252
 
253
+ /* The current row on a folded rail: ONE plate on the glyph column, and nothing
254
+ else. The open rail's marker is a 3px bar standing in the row's own left
255
+ padding; on the strip that padding is the rail's edge, so the bar reads as a
256
+ tick on the rail rather than a mark on a row — and a nested row puts it
257
+ --space-3 further out again, outside the rail altogether. The hairline cannot
258
+ close either: a row keeps the open column and the rail clips it, so its right
259
+ edge is drawn off the strip.
260
+
261
+ The plate is the strip. --ui-nav-strip is twice a row's own glyph centre, so a
262
+ box that wide standing at the row's edge is centred on the glyph, and the mark
263
+ lands where the reader is already looking. --surface-3 is the kit's quiet
264
+ non-text fill — a step off the rail in both themes, carrying a glyph and never
265
+ a word. The references mark current the same way and with no hue at all:
266
+ Cloudflare's docs rail paints a flat plate, and lessly-ui's rail is "a plate and
267
+ the weight step, no hue". Here the glyph's own step from 0.66 to full ink is the
268
+ second half of that pair. See #429.
269
+
270
+ `isolation` is what puts the plate behind the glyph: a positioned ::before paints
271
+ over its row's content, and a negative z-index with no stacking context of its
272
+ own would drop it behind the rail instead. */
273
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active { isolation: isolate; background: none; }
274
+ /* At rest only — box-shadow carries the hairline AND the focus ring (WCAG 2.4.7). */
275
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
276
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active::before {
277
+ left: 0;
278
+ width: var(--ui-nav-strip);
279
+ height: 100%;
280
+ border-radius: 10px;
281
+ background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
282
+ z-index: -1;
283
+ }
284
+ /* Hover's hairline is an outline on a row that keeps the open column, so on the
285
+ strip it too is drawn with no right edge. A folded rail answers a pointer with
286
+ the row's own ink and the name beside it, which is the whole readout at this
287
+ width. At rest only, so the focus ring is left standing. */
288
+ .ui-nav--side.is-collapsed .ui-nav__item:hover:not(:focus-visible) { outline: none; }
289
+
251
290
  /* -- Horizontal tabs ----------------------------------------------------- */
252
291
  .ui-nav--tabs {
253
292
  display: flex;