@marktiderman/genesis-ui 2.10.1 → 2.12.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.
Files changed (32) hide show
  1. package/SKILL.md +59 -1
  2. package/dist/AppShell-DpQdcLE-.d.ts +347 -0
  3. package/dist/{chunk-C7PQ54JI.js → chunk-3TWCZYZY.js} +43 -13
  4. package/dist/chunk-3TWCZYZY.js.map +1 -0
  5. package/dist/{chunk-WGMEVX3Y.js → chunk-KC43CNMA.js} +2 -2
  6. package/dist/{chunk-WGMEVX3Y.js.map → chunk-KC43CNMA.js.map} +1 -1
  7. package/dist/{chunk-UYI3GLFJ.js → chunk-SZMXNC5Q.js} +722 -159
  8. package/dist/chunk-SZMXNC5Q.js.map +1 -0
  9. package/dist/{chunk-OITWCIG2.js → chunk-XGG5RUDE.js} +3 -3
  10. package/dist/{chunk-OITWCIG2.js.map → chunk-XGG5RUDE.js.map} +1 -1
  11. package/dist/components/data/index.d.ts +5 -5
  12. package/dist/components/data/index.js +32 -6
  13. package/dist/components/data/index.js.map +1 -1
  14. package/dist/components/layout/index.d.ts +2 -2
  15. package/dist/components/layout/index.js +3 -2
  16. package/dist/components/rich-text/rich-text-editor.js +1 -1
  17. package/dist/hooks/index.d.ts +2 -2
  18. package/dist/hooks/index.js +3 -3
  19. package/dist/hooks/use-data-filters.d.ts +1 -1
  20. package/dist/hooks/use-data-filters.js +2 -2
  21. package/dist/hooks/use-resource-url-state.d.ts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/{navigation-Bha0R3Fu.d.ts → navigation-By1DrzaM.d.ts} +24 -0
  25. package/dist/router/react-router.d.ts +1 -1
  26. package/dist/router/tanstack-router.d.ts +1 -1
  27. package/dist/router/tanstack-router.js +1 -1
  28. package/dist/{use-resource-table-DCVf_lEG.d.ts → use-resource-table-DrTkD5O1.d.ts} +32 -3
  29. package/package.json +4 -3
  30. package/dist/AppShell-BVr4V0UK.d.ts +0 -174
  31. package/dist/chunk-C7PQ54JI.js.map +0 -1
  32. package/dist/chunk-UYI3GLFJ.js.map +0 -1
@@ -4,14 +4,16 @@ import { Collapsible, CollapsibleContent } from './chunk-B2GIBUQD.js';
4
4
  import { Badge } from './chunk-NPNADZHM.js';
5
5
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './chunk-QJ6P2DW6.js';
6
6
  import { Switch } from './chunk-27ELYTGE.js';
7
+ import { Tooltip } from './chunk-G5YKBPBF.js';
7
8
  import { Popover, PopoverTrigger, PopoverContent } from './chunk-22XB3Q6H.js';
8
9
  import { Button } from './chunk-NWSCYLPT.js';
9
10
  import { Label } from './chunk-F47U5KNW.js';
10
11
  import { cn } from './chunk-NZ5WBA4R.js';
11
- import { resolveNavigation, DefaultLink, isPathActive } from './chunk-WGMEVX3Y.js';
12
- import { useId, useState, useEffect, useMemo, useCallback } from 'react';
13
- import { ListFilter, MoreHorizontal, SlidersHorizontal, Menu, ArrowLeft, X, PanelLeft, PanelLeftClose, Sun, Moon, ChevronDown } from 'lucide-react';
12
+ import { isPathActive, resolveNavigation, DefaultLink } from './chunk-KC43CNMA.js';
13
+ import { createContext, useId, useState, useEffect, useMemo, useCallback, useContext, useRef, useLayoutEffect, useSyncExternalStore } from 'react';
14
+ import { ListFilter, MoreHorizontal, SlidersHorizontal, Menu, ArrowLeft, X, Sun, Moon, PanelLeft, PanelLeftClose, ChevronRight, ChevronDown } from 'lucide-react';
14
15
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
+ import { createPortal } from 'react-dom';
15
17
 
16
18
  // src/option-storage.ts
17
19
  function readStoredOptions(key) {
@@ -391,6 +393,206 @@ function PageHeader({
391
393
  ] }) })
392
394
  ] });
393
395
  }
396
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
397
+ function itemIsActive(item, activePath, isActive) {
398
+ if (item.end) {
399
+ return isPathActive(item.to, activePath, { end: true });
400
+ }
401
+ if (isActive(item.to, activePath)) return true;
402
+ return Boolean(
403
+ item.children?.some((child) => itemIsActive(child, activePath, isActive))
404
+ );
405
+ }
406
+ function mostSpecificActive(items, activePath, isActive) {
407
+ let best;
408
+ for (const item of items) {
409
+ if (!itemIsActive(item, activePath, isActive)) continue;
410
+ if (!best || item.to.length > best.to.length) best = item;
411
+ }
412
+ return best;
413
+ }
414
+ function BadgeChip({ badge, active }) {
415
+ return /* @__PURE__ */ jsx(
416
+ "span",
417
+ {
418
+ className: cn(
419
+ "inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium tabular-nums",
420
+ active ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
421
+ ),
422
+ children: badge
423
+ }
424
+ );
425
+ }
426
+ function SubNavTabs({
427
+ items,
428
+ activePath,
429
+ linkComponent: LinkComp,
430
+ isActive = isPathActive,
431
+ end,
432
+ label = "Page sections",
433
+ className
434
+ }) {
435
+ const current = mostSpecificActive(items, activePath, isActive);
436
+ return /* @__PURE__ */ jsxs(
437
+ "div",
438
+ {
439
+ className: cn("flex min-w-0 items-end gap-4 border-b", className),
440
+ "data-testid": "app-shell-sub-nav",
441
+ children: [
442
+ /* @__PURE__ */ jsx("nav", { "aria-label": label, className: "-mb-px min-w-0 flex-1 overflow-x-auto", children: /* @__PURE__ */ jsx("ul", { className: "flex items-end gap-6", children: items.map((item) => {
443
+ const active = item === current;
444
+ const Icon = item.icon;
445
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
446
+ Icon ? /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0", "aria-hidden": true }) : null,
447
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: item.label }),
448
+ item.badge != null ? /* @__PURE__ */ jsx(BadgeChip, { badge: item.badge, active }) : null
449
+ ] });
450
+ const cls = cn(
451
+ "flex items-center gap-1.5 border-b-2 pb-2.5 pt-2 text-sm transition-colors motion-reduce:transition-none",
452
+ active ? "border-primary font-semibold text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"
453
+ );
454
+ return /* @__PURE__ */ jsx("li", { className: "shrink-0", children: item.disabled ? /* @__PURE__ */ jsx("span", { className: cn(cls, "opacity-40"), role: "link", "aria-disabled": "true", children: body }) : /* @__PURE__ */ jsx(
455
+ LinkComp,
456
+ {
457
+ href: item.to,
458
+ "aria-current": active ? "page" : void 0,
459
+ className: cn(
460
+ cls,
461
+ "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
462
+ ),
463
+ testID: `app-shell-sub-nav-${item.label}`,
464
+ children: body
465
+ }
466
+ ) }, item.to + item.label);
467
+ }) }) }),
468
+ end != null ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center pb-2", children: end }) : null
469
+ ]
470
+ }
471
+ );
472
+ }
473
+ var SubNavContext = createContext(null);
474
+ function innermost(claims) {
475
+ let best;
476
+ for (const c of claims) if (!best || c.order > best.order) best = c;
477
+ return best?.id;
478
+ }
479
+ function useSubNavChannel() {
480
+ const [entries, setEntries] = useState([]);
481
+ const [endSlot, setEndSlot] = useState(null);
482
+ const [headerSlot, setHeaderSlot] = useState(null);
483
+ const [claims, setClaims] = useState({ header: [], end: [] });
484
+ const register = useCallback((id, order, value) => {
485
+ setEntries(
486
+ (prev) => prev.some((e) => e.id === id) ? prev.map((e) => e.id === id ? { ...value, id, order } : e) : [...prev, { ...value, id, order }]
487
+ );
488
+ }, []);
489
+ const unregister = useCallback((id) => {
490
+ setEntries((prev) => prev.filter((e) => e.id !== id));
491
+ }, []);
492
+ const claimSlot = useCallback((slot, id, order) => {
493
+ setClaims(
494
+ (prev) => prev[slot].some((c) => c.id === id) ? prev : { ...prev, [slot]: [...prev[slot], { id, order }] }
495
+ );
496
+ }, []);
497
+ const releaseSlot = useCallback((slot, id) => {
498
+ setClaims(
499
+ (prev) => prev[slot].some((c) => c.id === id) ? { ...prev, [slot]: prev[slot].filter((c) => c.id !== id) } : prev
500
+ );
501
+ }, []);
502
+ const headerOwner = innermost(claims.header);
503
+ const endOwner = innermost(claims.end);
504
+ const channel = useMemo(
505
+ () => ({
506
+ register,
507
+ unregister,
508
+ claimSlot,
509
+ releaseSlot,
510
+ slots: {
511
+ header: { el: headerSlot, owner: headerOwner },
512
+ end: { el: endSlot, owner: endOwner }
513
+ }
514
+ }),
515
+ [register, unregister, claimSlot, releaseSlot, headerSlot, headerOwner, endSlot, endOwner]
516
+ );
517
+ const byDepth = [...entries].sort((a, b) => b.order - a.order);
518
+ const items = byDepth.find((e) => e.items)?.items;
519
+ const mode = byDepth.find((e) => e.mode)?.mode;
520
+ return {
521
+ page: items || mode ? { items, mode } : void 0,
522
+ channel,
523
+ setEndSlot,
524
+ setHeaderSlot,
525
+ hasHeader: headerOwner !== void 0,
526
+ hasEnd: endOwner !== void 0
527
+ };
528
+ }
529
+ var nextOrder = 0;
530
+ var iconIds = /* @__PURE__ */ new WeakMap();
531
+ var nextIconId = 1;
532
+ function iconKey(icon) {
533
+ if (!icon || typeof icon !== "object" && typeof icon !== "function") return 0;
534
+ let id = iconIds.get(icon);
535
+ if (id == null) {
536
+ id = nextIconId++;
537
+ iconIds.set(icon, id);
538
+ }
539
+ return id;
540
+ }
541
+ function itemsKey(items) {
542
+ return JSON.stringify(
543
+ items.map((i) => [
544
+ i.to,
545
+ i.label,
546
+ i.badge ?? null,
547
+ Boolean(i.disabled),
548
+ Boolean(i.end),
549
+ iconKey(i.icon),
550
+ i.children ? itemsKey(i.children) : null
551
+ ])
552
+ );
553
+ }
554
+ function useAppShellSubNav(items, options) {
555
+ const channel = useContext(SubNavContext);
556
+ const id = useId();
557
+ const [order] = useState(() => ++nextOrder);
558
+ const latest = useRef(items);
559
+ latest.current = items;
560
+ const key = items ? itemsKey(items) : "";
561
+ const mode = options?.mode;
562
+ const register = channel?.register;
563
+ const unregister = channel?.unregister;
564
+ useIsoLayoutEffect(() => {
565
+ if (!register || !unregister) return;
566
+ const value = latest.current ?? void 0;
567
+ if (value || mode) register(id, order, { items: value, mode });
568
+ else unregister(id);
569
+ }, [register, unregister, id, order, key, mode]);
570
+ useIsoLayoutEffect(() => {
571
+ if (!unregister) return;
572
+ return () => unregister(id);
573
+ }, [unregister, id]);
574
+ }
575
+ function useSlotPortal(slot, children) {
576
+ const channel = useContext(SubNavContext);
577
+ const id = useId();
578
+ const [order] = useState(() => ++nextOrder);
579
+ const claimSlot = channel?.claimSlot;
580
+ const releaseSlot = channel?.releaseSlot;
581
+ useIsoLayoutEffect(() => {
582
+ if (!claimSlot || !releaseSlot) return;
583
+ claimSlot(slot, id, order);
584
+ return () => releaseSlot(slot, id);
585
+ }, [claimSlot, releaseSlot, slot, id, order]);
586
+ const target = channel?.slots[slot];
587
+ if (!target?.el || target.owner !== id) return null;
588
+ return createPortal(children, target.el);
589
+ }
590
+ function AppShellSubNavEnd({ children }) {
591
+ return useSlotPortal("end", children);
592
+ }
593
+ function AppShellPageHeader({ children }) {
594
+ return useSlotPortal("header", children);
595
+ }
394
596
  var OPTION_DEFAULTS2 = {
395
597
  showLogo: true,
396
598
  showAppName: true,
@@ -401,11 +603,27 @@ var OPTION_DEFAULTS2 = {
401
603
  showSecondaryNav: true,
402
604
  showMobileBottomNav: true,
403
605
  defaultCollapsed: false,
606
+ collapseShortcut: false,
607
+ collapsedRailWidth: 56,
608
+ subNav: "section",
404
609
  configurable: void 0,
405
610
  density: "comfortable",
406
611
  railWidth: "narrow",
407
612
  scrollRoot: "container"
408
613
  };
614
+ var collapsedListeners = /* @__PURE__ */ new Set();
615
+ function subscribeCollapsed(listener) {
616
+ collapsedListeners.add(listener);
617
+ if (typeof window !== "undefined") {
618
+ window.addEventListener("storage", listener);
619
+ }
620
+ return () => {
621
+ collapsedListeners.delete(listener);
622
+ if (typeof window !== "undefined") {
623
+ window.removeEventListener("storage", listener);
624
+ }
625
+ };
626
+ }
409
627
  function readStoredCollapsed(key) {
410
628
  if (typeof window === "undefined") return void 0;
411
629
  try {
@@ -420,10 +638,39 @@ function readStoredCollapsed(key) {
420
638
  function writeStoredCollapsed(key, collapsed) {
421
639
  if (typeof window === "undefined") return;
422
640
  try {
423
- localStorage.setItem(`${key}:collapsed`, collapsed ? "1" : "0");
641
+ if (collapsed === void 0) localStorage.removeItem(`${key}:collapsed`);
642
+ else localStorage.setItem(`${key}:collapsed`, collapsed ? "1" : "0");
424
643
  } catch {
425
644
  }
645
+ collapsedListeners.forEach((listener) => listener());
426
646
  }
647
+ function useStoredCollapsed(key) {
648
+ return useSyncExternalStore(
649
+ subscribeCollapsed,
650
+ () => key ? readStoredCollapsed(key) : void 0,
651
+ () => void 0
652
+ );
653
+ }
654
+ function isTypingTarget(el) {
655
+ if (!el || typeof el.tagName !== "string") return false;
656
+ const node = el;
657
+ const tag = node.tagName;
658
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || node.isContentEditable === true || node.closest?.('[contenteditable=""], [contenteditable="true"]') != null;
659
+ }
660
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect : useLayoutEffect;
661
+ function ownsKeys(el) {
662
+ const node = el;
663
+ return Boolean(
664
+ node?.closest?.(
665
+ '[role="listbox"], [role="combobox"], [role="menu"], [role="menubar"], [role="menuitem"], [role="option"], [role="tree"], [role="grid"]'
666
+ )
667
+ );
668
+ }
669
+ function dialogOpen() {
670
+ if (typeof document === "undefined") return false;
671
+ return document.querySelector('[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"], [aria-modal="true"]') != null;
672
+ }
673
+ var DESKTOP_QUERY = "(min-width: 768px)";
427
674
  var PARAM = /^\$(.+)$/;
428
675
  function fillParams(to, params) {
429
676
  const cut = to.search(/[?#]/);
@@ -484,15 +731,26 @@ function ThemeToggle({ testId }) {
484
731
  }
485
732
  );
486
733
  }
487
- function itemIsActive(item, activePath, isActive) {
488
- if (item.end) {
489
- return isPathActive(item.to, activePath, { end: true });
734
+ function NavIcon({ item, className }) {
735
+ if (item.icon) {
736
+ const Icon = item.icon;
737
+ return /* @__PURE__ */ jsx(Icon, { className });
490
738
  }
491
- if (isActive(item.to, activePath)) return true;
492
- return Boolean(
493
- item.children?.some((child) => itemIsActive(child, activePath, isActive))
739
+ return /* @__PURE__ */ jsx(
740
+ "span",
741
+ {
742
+ className: cn(
743
+ className,
744
+ "inline-flex items-center justify-center rounded text-[11px] font-semibold uppercase leading-none"
745
+ ),
746
+ "aria-hidden": true,
747
+ children: item.label.trim().charAt(0)
748
+ }
494
749
  );
495
750
  }
751
+ function RailTooltip({ label, children }) {
752
+ return /* @__PURE__ */ jsx(Tooltip, { content: label, side: "right", className: "flex w-full", children: /* @__PURE__ */ jsx("span", { className: "flex w-full", children }) });
753
+ }
496
754
  function NavLinkRow({
497
755
  item,
498
756
  collapsed,
@@ -501,51 +759,145 @@ function NavLinkRow({
501
759
  linkComponent: LinkComp,
502
760
  activePath,
503
761
  isActive,
504
- notCurrent = false
762
+ notCurrent = false,
763
+ active: activeOverride
505
764
  }) {
506
- const active = itemIsActive(item, activePath, isActive);
765
+ const active = activeOverride ?? itemIsActive(item, activePath, isActive);
507
766
  const pad = density === "compact" ? "px-2.5 py-1.5" : "px-3 py-2";
508
767
  if (item.disabled) {
509
- return /* @__PURE__ */ jsxs(
768
+ if (!collapsed) {
769
+ return /* @__PURE__ */ jsxs(
770
+ "span",
771
+ {
772
+ title: item.label,
773
+ className: cn(
774
+ "flex items-center gap-3 rounded-lg text-sm font-medium opacity-40",
775
+ pad
776
+ ),
777
+ "aria-disabled": true,
778
+ children: [
779
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" }),
780
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label })
781
+ ]
782
+ }
783
+ );
784
+ }
785
+ return /* @__PURE__ */ jsx(RailTooltip, { label: item.label, children: /* @__PURE__ */ jsx(
510
786
  "span",
511
787
  {
512
- title: item.label,
513
- className: cn(
514
- "flex items-center gap-3 rounded-lg text-sm font-medium opacity-40",
515
- pad,
516
- collapsed && "justify-center px-2"
517
- ),
518
- "aria-disabled": true,
519
- children: [
520
- /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
521
- !collapsed ? /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label }) : null
522
- ]
788
+ role: "link",
789
+ "aria-disabled": "true",
790
+ "aria-label": item.label,
791
+ className: "flex h-10 w-full items-center justify-center rounded-lg text-sm font-medium opacity-40",
792
+ children: /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" })
523
793
  }
524
- );
794
+ ) });
525
795
  }
526
- return /* @__PURE__ */ jsxs(
796
+ const link = /* @__PURE__ */ jsxs(
527
797
  LinkComp,
528
798
  {
529
799
  href: item.to,
530
800
  onClick: onNavClick,
531
- title: collapsed ? item.label : void 0,
532
- "aria-label": collapsed ? item.label : void 0,
801
+ "aria-label": collapsed ? item.badge != null ? `${item.label} (${item.badge})` : item.label : void 0,
533
802
  "aria-current": active && !notCurrent ? "page" : void 0,
534
803
  className: cn(
535
804
  "flex items-center gap-3 rounded-lg text-sm font-medium transition-colors",
536
805
  pad,
537
- collapsed && "justify-center px-2",
806
+ collapsed && "relative h-10 w-full justify-center px-0 py-0 motion-reduce:transition-none",
538
807
  active ? "bg-sidebar-accent text-sidebar-primary" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
539
808
  ),
540
809
  children: [
541
- /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
810
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" }),
542
811
  !collapsed ? /* @__PURE__ */ jsxs(Fragment, { children: [
543
812
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
544
813
  item.badge != null ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "h-5 shrink-0 px-1.5 text-[10px]", children: item.badge }) : null
545
- ] }) : null
814
+ ] }) : item.badge != null ? (
815
+ // The rail has no room for the count: a dot shows there is one, and
816
+ // the accessible name carries the number.
817
+ /* @__PURE__ */ jsx(
818
+ "span",
819
+ {
820
+ className: "absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-sidebar-primary",
821
+ "aria-hidden": true
822
+ }
823
+ )
824
+ ) : null
546
825
  ]
547
826
  }
548
827
  );
828
+ return collapsed ? /* @__PURE__ */ jsx(RailTooltip, { label: item.label, children: link }) : link;
829
+ }
830
+ function RailGroup({
831
+ item,
832
+ density,
833
+ linkComponent,
834
+ activePath,
835
+ isActive,
836
+ notCurrent
837
+ }) {
838
+ const [open, setOpen] = useState(false);
839
+ const children = item.children ?? [];
840
+ const active = itemIsActive(item, activePath, isActive);
841
+ const entries = item.to ? [{ ...item, children: void 0, end: true }, ...children] : children;
842
+ const current = mostSpecificActive(entries, activePath, isActive);
843
+ useEffect(() => {
844
+ setOpen(false);
845
+ }, [activePath]);
846
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
847
+ /* @__PURE__ */ jsx(RailTooltip, { label: item.label, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
848
+ "button",
849
+ {
850
+ type: "button",
851
+ "aria-label": item.label,
852
+ disabled: item.disabled,
853
+ "data-testid": `nav-rail-group-${item.label}`,
854
+ className: cn(
855
+ "relative flex h-10 w-full items-center justify-center rounded-lg transition-colors motion-reduce:transition-none",
856
+ active ? "bg-sidebar-accent text-sidebar-primary" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
857
+ item.disabled && "pointer-events-none opacity-40"
858
+ ),
859
+ children: [
860
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" }),
861
+ /* @__PURE__ */ jsx(
862
+ ChevronRight,
863
+ {
864
+ className: "absolute right-0.5 h-3 w-3 opacity-50",
865
+ "aria-hidden": true
866
+ }
867
+ )
868
+ ]
869
+ }
870
+ ) }) }),
871
+ /* @__PURE__ */ jsxs(
872
+ PopoverContent,
873
+ {
874
+ side: "right",
875
+ align: "start",
876
+ sideOffset: 8,
877
+ className: "w-56 p-1.5 motion-reduce:animate-none",
878
+ "aria-label": item.label,
879
+ "data-testid": `nav-rail-popover-${item.label}`,
880
+ children: [
881
+ /* @__PURE__ */ jsx("p", { className: "truncate px-2.5 pb-1 pt-1.5 text-xs font-medium uppercase tracking-wider text-muted-foreground", children: item.label }),
882
+ /* @__PURE__ */ jsx("nav", { "aria-label": item.label, className: "space-y-0.5", children: entries.map((entry) => /* @__PURE__ */ jsx(
883
+ NavLinkRow,
884
+ {
885
+ item: entry,
886
+ collapsed: false,
887
+ density,
888
+ onNavClick: () => setOpen(false),
889
+ linkComponent,
890
+ activePath,
891
+ isActive,
892
+ notCurrent,
893
+ active: entry === current
894
+ },
895
+ entry.to + entry.label
896
+ )) })
897
+ ]
898
+ }
899
+ )
900
+ ] });
549
901
  }
550
902
  function NavGroup({
551
903
  item,
@@ -581,14 +933,11 @@ function NavGroup({
581
933
  );
582
934
  }
583
935
  if (collapsed) {
584
- const target = item.to ? { ...item, children: void 0 } : { ...children[0], children: void 0 };
585
936
  return /* @__PURE__ */ jsx(
586
- NavLinkRow,
937
+ RailGroup,
587
938
  {
588
- item: target,
589
- collapsed: true,
939
+ item,
590
940
  density,
591
- onNavClick,
592
941
  linkComponent,
593
942
  activePath,
594
943
  isActive,
@@ -618,7 +967,7 @@ function NavGroup({
618
967
  "aria-current": selfExact && !notCurrent ? "page" : void 0,
619
968
  className: parentClass,
620
969
  children: [
621
- /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
970
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" }),
622
971
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: item.label })
623
972
  ]
624
973
  }
@@ -632,7 +981,7 @@ function NavGroup({
632
981
  "aria-disabled": item.disabled || void 0,
633
982
  disabled: item.disabled,
634
983
  children: [
635
- /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 shrink-0" }),
984
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-4 w-4 shrink-0" }),
636
985
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: item.label })
637
986
  ]
638
987
  }
@@ -692,12 +1041,68 @@ function SidebarNav({
692
1041
  onNavClick,
693
1042
  linkComponent,
694
1043
  activePath,
695
- isActive
1044
+ isActive,
1045
+ subActive = false,
1046
+ nest = false
696
1047
  }) {
697
1048
  const contextualLabelId = useId();
698
- const contextualActive = Boolean(
1049
+ const contextualActive = subActive || Boolean(
699
1050
  contextualNavItems?.some((item) => itemIsActive(item, activePath, isActive))
700
1051
  );
1052
+ const contextual = contextualNavItems ?? [];
1053
+ const host = nest && contextual.length > 0 ? mostSpecificActive(navItems, activePath, isActive) : void 0;
1054
+ const contextualCurrent = mostSpecificActive(contextual, activePath, isActive);
1055
+ const contextualGroup = (nested) => /* @__PURE__ */ jsxs(
1056
+ "div",
1057
+ {
1058
+ className: cn(
1059
+ nested ? "ml-3 space-y-0.5 border-l border-sidebar-border pl-2" : "mt-4 space-y-1 border-t border-sidebar-border pt-4",
1060
+ !nested && collapsed && "mt-2 pt-2"
1061
+ ),
1062
+ "data-testid": "app-shell-contextual-nav",
1063
+ role: "group",
1064
+ "aria-labelledby": !collapsed && !nested && contextualNavLabel ? contextualLabelId : void 0,
1065
+ "aria-label": collapsed || nested ? contextualNavLabel : void 0,
1066
+ children: [
1067
+ !collapsed && !nested && contextualNavLabel ? /* @__PURE__ */ jsx(
1068
+ "p",
1069
+ {
1070
+ id: contextualLabelId,
1071
+ className: "mb-2 truncate px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/40",
1072
+ children: contextualNavLabel
1073
+ }
1074
+ ) : null,
1075
+ contextual.map(
1076
+ (item) => item.children?.length ? /* @__PURE__ */ jsx(
1077
+ NavGroup,
1078
+ {
1079
+ item,
1080
+ collapsed,
1081
+ density,
1082
+ onNavClick,
1083
+ linkComponent,
1084
+ activePath,
1085
+ isActive
1086
+ },
1087
+ item.to + item.label
1088
+ ) : /* @__PURE__ */ jsx(
1089
+ NavLinkRow,
1090
+ {
1091
+ item,
1092
+ collapsed,
1093
+ density,
1094
+ onNavClick,
1095
+ linkComponent,
1096
+ activePath,
1097
+ isActive,
1098
+ active: nest ? item === contextualCurrent : void 0
1099
+ },
1100
+ item.to + item.label
1101
+ )
1102
+ )
1103
+ ]
1104
+ }
1105
+ );
701
1106
  return /* @__PURE__ */ jsxs(
702
1107
  "nav",
703
1108
  {
@@ -707,56 +1112,42 @@ function SidebarNav({
707
1112
  ),
708
1113
  "aria-label": "Main",
709
1114
  children: [
710
- navItems.map((item) => /* @__PURE__ */ jsx(
711
- NavGroup,
712
- {
713
- item,
714
- collapsed,
715
- density,
716
- onNavClick,
717
- linkComponent,
718
- activePath,
719
- isActive,
720
- notCurrent: contextualActive
721
- },
722
- item.to + item.label
723
- )),
724
- contextualNavItems && contextualNavItems.length > 0 ? /* @__PURE__ */ jsxs(
725
- "div",
726
- {
727
- className: cn(
728
- "mt-4 space-y-1 border-t border-sidebar-border pt-4",
729
- collapsed && "mt-2 pt-2"
730
- ),
731
- "data-testid": "app-shell-contextual-nav",
732
- role: "group",
733
- "aria-labelledby": !collapsed && contextualNavLabel ? contextualLabelId : void 0,
734
- "aria-label": collapsed ? contextualNavLabel : void 0,
735
- children: [
736
- !collapsed && contextualNavLabel ? /* @__PURE__ */ jsx(
737
- "p",
738
- {
739
- id: contextualLabelId,
740
- className: "mb-2 truncate px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/40",
741
- children: contextualNavLabel
742
- }
743
- ) : null,
744
- contextualNavItems.map((item) => /* @__PURE__ */ jsx(
745
- NavGroup,
746
- {
747
- item,
748
- collapsed,
749
- density,
750
- onNavClick,
751
- linkComponent,
752
- activePath,
753
- isActive
754
- },
755
- item.to + item.label
756
- ))
757
- ]
1115
+ navItems.map((item) => {
1116
+ if (item === host && collapsed) {
1117
+ return /* @__PURE__ */ jsx(
1118
+ RailGroup,
1119
+ {
1120
+ item: { ...item, children: [...item.children ?? [], ...contextual] },
1121
+ density,
1122
+ linkComponent,
1123
+ activePath,
1124
+ isActive,
1125
+ notCurrent: false
1126
+ },
1127
+ item.to + item.label
1128
+ );
758
1129
  }
759
- ) : null,
1130
+ const row = /* @__PURE__ */ jsx(
1131
+ NavGroup,
1132
+ {
1133
+ item,
1134
+ collapsed,
1135
+ density,
1136
+ onNavClick,
1137
+ linkComponent,
1138
+ activePath,
1139
+ isActive,
1140
+ notCurrent: contextualActive
1141
+ },
1142
+ item.to + item.label
1143
+ );
1144
+ if (item !== host) return row;
1145
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
1146
+ row,
1147
+ contextualGroup(true)
1148
+ ] }, item.to + item.label);
1149
+ }),
1150
+ contextual.length > 0 && !host ? contextualGroup(false) : null,
760
1151
  showSecondary && secondaryNavItems && secondaryNavItems.length > 0 ? /* @__PURE__ */ jsxs(
761
1152
  "div",
762
1153
  {
@@ -803,7 +1194,18 @@ function SidebarBrand({
803
1194
  collapsed && "justify-center"
804
1195
  ),
805
1196
  children: [
806
- showLogo ? logo ? /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden", children: logo }) : appLogo ? /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-primary-foreground", children: appLogo }) }) : null : null,
1197
+ showLogo ? logo ? /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden", children: logo }) : appLogo || collapsed ? (
1198
+ // Collapsed with no logo at all: the app name's first letter keeps
1199
+ // a mark at the top of the rail.
1200
+ /* @__PURE__ */ jsx(
1201
+ "div",
1202
+ {
1203
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary",
1204
+ "aria-hidden": !appLogo || void 0,
1205
+ children: /* @__PURE__ */ jsx("span", { className: "text-sm font-bold text-primary-foreground", children: appLogo || appName.trim().charAt(0).toUpperCase() })
1206
+ }
1207
+ )
1208
+ ) : null : null,
807
1209
  showAppName && !collapsed ? /* @__PURE__ */ jsx("span", { className: "truncate font-semibold text-sidebar-foreground", children: appName }) : null
808
1210
  ]
809
1211
  }
@@ -829,6 +1231,9 @@ function AppShell({
829
1231
  optionsStorageKey,
830
1232
  collapsed: collapsedProp,
831
1233
  onCollapsedChange,
1234
+ defaultCollapsed: defaultCollapsedProp,
1235
+ subNav: subNavProp,
1236
+ subNavLabel,
832
1237
  linkComponent = DefaultLink,
833
1238
  activePath,
834
1239
  isActive = isPathActive
@@ -841,10 +1246,25 @@ function AppShell({
841
1246
  const globalItems = fillItems(navigation.items, nav.params);
842
1247
  const contextualItems = atRoot ? [] : fillItems(nav.items, nav.params);
843
1248
  const parent = nav.parent && nav.trail.length > 2 && !nav.parent.path.split("/").some((p) => PARAM.test(p)) ? { href: nav.parent.path, label: nav.parent.label } : void 0;
844
- return { current: nav.current, globalItems, contextualItems, parent };
1249
+ const navDefaultCollapsed = [...nav.trail].reverse().find((node) => node.defaultCollapsed !== void 0)?.defaultCollapsed;
1250
+ const navSubNav = [...nav.trail].reverse().find((node) => node.subNav !== void 0)?.subNav;
1251
+ const navParentPath = nav.parent ? fillParams(nav.parent.path, nav.params) : void 0;
1252
+ const backItem = nav.parent && navParentPath ? { href: navParentPath, label: nav.parent.label } : void 0;
1253
+ return {
1254
+ current: nav.current,
1255
+ globalItems,
1256
+ contextualItems,
1257
+ parent,
1258
+ navDefaultCollapsed,
1259
+ navSubNav,
1260
+ backItem
1261
+ };
845
1262
  }, [navigation, activePath]);
846
1263
  const navItems = navItemsProp ?? derived?.globalItems ?? [];
847
- const contextualNavItems = derived?.contextualItems ?? [];
1264
+ const subNavChannel = useSubNavChannel();
1265
+ const pageSubNav = subNavChannel.page;
1266
+ const hasPageHeader = subNavChannel.hasHeader;
1267
+ const contextualNavItems = pageSubNav?.items ?? derived?.contextualItems ?? [];
848
1268
  const parentNavigation = parentNavigationProp === void 0 ? derived?.parent : parentNavigationProp ?? void 0;
849
1269
  const [userOverrides, setUserOverrides] = useState(
850
1270
  {}
@@ -908,32 +1328,104 @@ function AppShell({
908
1328
  },
909
1329
  [lockedOptions]
910
1330
  );
911
- const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(
912
- () => optionsProp?.defaultCollapsed ?? OPTION_DEFAULTS2.defaultCollapsed
1331
+ const storedCollapsed = useStoredCollapsed(optionsStorageKey);
1332
+ const [sessionCollapsed, setSessionCollapsed] = useState(
1333
+ void 0
913
1334
  );
914
- useEffect(() => {
915
- if (!optionsStorageKey || collapsedProp !== void 0) return;
916
- const stored = readStoredCollapsed(optionsStorageKey);
917
- if (stored !== void 0) setUncontrolledCollapsed(stored);
918
- }, [optionsStorageKey, collapsedProp]);
1335
+ const userCollapsed = optionsStorageKey ? storedCollapsed : sessionCollapsed;
1336
+ const pageDefaultCollapsed = defaultCollapsedProp ?? derived?.navDefaultCollapsed ?? optionsProp?.defaultCollapsed ?? OPTION_DEFAULTS2.defaultCollapsed;
919
1337
  const isCollapseControlled = collapsedProp !== void 0;
920
- const collapsed = isCollapseControlled ? collapsedProp : uncontrolledCollapsed;
921
- const setCollapsed = (next) => {
922
- if (!isCollapseControlled) {
923
- setUncontrolledCollapsed(next);
924
- if (optionsStorageKey) writeStoredCollapsed(optionsStorageKey, next);
1338
+ const collapsed = isCollapseControlled ? collapsedProp : userCollapsed ?? pageDefaultCollapsed;
1339
+ const setCollapsed = useCallback(
1340
+ (next) => {
1341
+ if (!isCollapseControlled) {
1342
+ if (optionsStorageKey) writeStoredCollapsed(optionsStorageKey, next);
1343
+ else setSessionCollapsed(next);
1344
+ }
1345
+ onCollapsedChange?.(next);
1346
+ },
1347
+ [isCollapseControlled, optionsStorageKey, onCollapsedChange]
1348
+ );
1349
+ const sidebarRef = useRef(null);
1350
+ const pendingFocus = useRef(null);
1351
+ const toggleCollapsed = useCallback(
1352
+ (fromToggle) => {
1353
+ const aside = sidebarRef.current;
1354
+ const focused = typeof document === "undefined" ? null : document.activeElement;
1355
+ if (fromToggle) {
1356
+ pendingFocus.current = { toggle: true };
1357
+ } else if (aside && focused instanceof HTMLElement && aside.contains(focused)) {
1358
+ const href = focused.getAttribute("href");
1359
+ pendingFocus.current = href ? { href } : { toggle: true };
1360
+ }
1361
+ setCollapsed(!collapsed);
1362
+ },
1363
+ [collapsed, setCollapsed]
1364
+ );
1365
+ const renderedCollapsed = useRef(collapsed);
1366
+ if (renderedCollapsed.current !== collapsed && !pendingFocus.current) {
1367
+ const aside = sidebarRef.current;
1368
+ const focused = typeof document === "undefined" ? null : document.activeElement;
1369
+ if (aside && focused instanceof HTMLElement && aside.contains(focused)) {
1370
+ const href = focused.getAttribute("href");
1371
+ pendingFocus.current = href ? { href } : { toggle: true };
925
1372
  }
926
- onCollapsedChange?.(next);
927
- };
1373
+ }
1374
+ useIsoLayoutEffect2(() => {
1375
+ const changed = renderedCollapsed.current !== collapsed;
1376
+ renderedCollapsed.current = collapsed;
1377
+ const want = pendingFocus.current;
1378
+ pendingFocus.current = null;
1379
+ const aside = sidebarRef.current;
1380
+ if (!changed || !want || !aside) return;
1381
+ const toggleEl = aside.querySelector('[data-testid="app-shell-collapse"]');
1382
+ let target = null;
1383
+ if ("href" in want) {
1384
+ target = Array.from(aside.querySelectorAll("a[href]")).find(
1385
+ (a) => a.getAttribute("href") === want.href
1386
+ ) ?? null;
1387
+ }
1388
+ (target ?? toggleEl)?.focus();
1389
+ });
1390
+ const showCollapseCtrl = resolved.showCollapse !== false;
1391
+ const collapseShortcut = optionsProp?.collapseShortcut ?? OPTION_DEFAULTS2.collapseShortcut;
1392
+ useEffect(() => {
1393
+ if (!collapseShortcut || !showCollapseCtrl) return;
1394
+ if (typeof window === "undefined") return;
1395
+ const onKeyDown = (e) => {
1396
+ if (e.key !== collapseShortcut || e.repeat || e.defaultPrevented) return;
1397
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
1398
+ if (isTypingTarget(e.target)) return;
1399
+ if (ownsKeys(e.target)) return;
1400
+ if (dialogOpen()) return;
1401
+ if (window.matchMedia && !window.matchMedia(DESKTOP_QUERY).matches) return;
1402
+ e.preventDefault();
1403
+ toggleCollapsed(false);
1404
+ };
1405
+ window.addEventListener("keydown", onKeyDown);
1406
+ return () => window.removeEventListener("keydown", onKeyDown);
1407
+ }, [collapseShortcut, showCollapseCtrl, toggleCollapsed]);
928
1408
  const ParentLink = linkComponent;
1409
+ const LinkComp = linkComponent;
929
1410
  const density = resolved.density ?? "comfortable";
930
1411
  const scrollWindow = resolved.scrollRoot === "window";
931
1412
  const railExpanded = resolved.railWidth === "wide" ? "md:w-64" : "md:w-52";
932
- const railClass = collapsed ? "md:w-14" : railExpanded;
1413
+ const railCollapsed = (optionsProp?.collapsedRailWidth ?? OPTION_DEFAULTS2.collapsedRailWidth) === 64 ? "md:w-16" : "md:w-14";
1414
+ const railClass = collapsed ? railCollapsed : railExpanded;
1415
+ const subNavMode = pageSubNav?.mode ?? subNavProp ?? derived?.navSubNav ?? resolved.subNav ?? OPTION_DEFAULTS2.subNav;
1416
+ const subActive = contextualNavItems.some((i) => itemIsActive(i, activePath, isActive));
1417
+ const subNavOn = subNavMode === "tabs" || subNavMode === "sidebar";
933
1418
  const leaves = (items) => items.filter((i) => !i.children || i.children.length === 0);
934
- const contextualLeaves = leaves(contextualNavItems);
935
- const bottomItems = (mobileNavItems ?? (contextualLeaves.length > 0 ? contextualLeaves : leaves(navItems))).filter((i) => !i.disabled);
936
- const bottomNotCurrent = !mobileNavItems && contextualLeaves.length === 0 && contextualNavItems.some((i) => itemIsActive(i, activePath, isActive));
1419
+ const contextualLeaves = subNavMode === "off" ? [] : leaves(contextualNavItems);
1420
+ const subBottom = subNavOn && contextualLeaves.length > 0;
1421
+ const legacyBottom = !subNavOn && !mobileNavItems && contextualLeaves.length > 0;
1422
+ const backTarget = subBottom ? parentNavigation ?? derived?.backItem ?? (() => {
1423
+ const hostItem = mostSpecificActive(navItems, activePath, isActive);
1424
+ return hostItem ? { href: hostItem.to, label: hostItem.label } : void 0;
1425
+ })() : void 0;
1426
+ const bottomItems = (subBottom ? contextualLeaves : mobileNavItems ?? (legacyBottom ? contextualLeaves : leaves(navItems))).filter((i) => !i.disabled);
1427
+ const bottomNotCurrent = !mobileNavItems && !subBottom && !legacyBottom && subActive;
1428
+ const bottomCurrent = subBottom ? mostSpecificActive(bottomItems, activePath, isActive) : void 0;
937
1429
  const toggles = [
938
1430
  { key: "showLogo", label: "Logo", available: hasLogo },
939
1431
  { key: "showAppName", label: "App name", available: true },
@@ -957,11 +1449,11 @@ function AppShell({
957
1449
  }
958
1450
  ].filter((t) => t.available && !lockedOptions?.includes(t.key));
959
1451
  const showTheme = resolved.showThemeToggle !== false;
960
- const showCollapseCtrl = resolved.showCollapse !== false;
961
1452
  const showSecondary = hasSecondary && resolved.showSecondaryNav !== false;
962
1453
  const showFooterSlot = hasFooter && resolved.showFooter !== false;
963
1454
  const showUser = hasUser && resolved.showUserSlot !== false;
964
1455
  const showMobileBottom = bottomItems.length > 0 && resolved.showMobileBottomNav !== false;
1456
+ const showTabs = subNavMode === "tabs" && contextualNavItems.length > 0;
965
1457
  const optionsControl = resolved.configurable && toggles.length > 0 ? /* @__PURE__ */ jsxs(Popover, { children: [
966
1458
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
967
1459
  Button,
@@ -1010,14 +1502,17 @@ function AppShell({
1010
1502
  t.key
1011
1503
  );
1012
1504
  }) }),
1013
- optionsStorageKey && Object.keys(userOverrides).length > 0 ? /* @__PURE__ */ jsx(
1505
+ optionsStorageKey && (Object.keys(userOverrides).length > 0 || storedCollapsed !== void 0) ? /* @__PURE__ */ jsx(
1014
1506
  Button,
1015
1507
  {
1016
1508
  type: "button",
1017
1509
  variant: "ghost",
1018
1510
  size: "sm",
1019
1511
  className: "h-8 w-full text-xs",
1020
- onClick: () => setUserOverrides({}),
1512
+ onClick: () => {
1513
+ setUserOverrides({});
1514
+ writeStoredCollapsed(optionsStorageKey, void 0);
1515
+ },
1021
1516
  "data-testid": "app-shell-options-reset",
1022
1517
  "aria-label": "Reset sidebar options",
1023
1518
  testID: "app-shell-options-reset",
@@ -1026,6 +1521,23 @@ function AppShell({
1026
1521
  ) : null
1027
1522
  ] })
1028
1523
  ] }) : null;
1524
+ const collapseLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
1525
+ const collapseToggle = showCollapseCtrl ? /* @__PURE__ */ jsx(
1526
+ Button,
1527
+ {
1528
+ variant: "ghost",
1529
+ size: "icon",
1530
+ className: "h-8 w-8",
1531
+ onClick: () => toggleCollapsed(true),
1532
+ title: collapseShortcut ? `${collapseLabel} (${collapseShortcut})` : collapseLabel,
1533
+ "aria-label": collapseLabel,
1534
+ "aria-pressed": collapsed,
1535
+ "aria-keyshortcuts": collapseShortcut || void 0,
1536
+ "data-testid": "app-shell-collapse",
1537
+ testID: "app-shell-collapse",
1538
+ children: collapsed ? /* @__PURE__ */ jsx(PanelLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(PanelLeftClose, { className: "h-4 w-4" })
1539
+ }
1540
+ ) : null;
1029
1541
  const sidebarChrome = (opts) => /* @__PURE__ */ jsxs(Fragment, { children: [
1030
1542
  /* @__PURE__ */ jsxs(
1031
1543
  "div",
@@ -1057,24 +1569,14 @@ function AppShell({
1057
1569
  testID: "app-shell-mobile-close",
1058
1570
  children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
1059
1571
  }
1060
- ) : /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
1061
- showCollapseCtrl ? /* @__PURE__ */ jsx(
1062
- Button,
1063
- {
1064
- variant: "ghost",
1065
- size: "icon",
1066
- className: "h-8 w-8",
1067
- onClick: () => setCollapsed(!collapsed),
1068
- title: collapsed ? "Expand sidebar" : "Collapse sidebar",
1069
- "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1070
- "aria-pressed": collapsed,
1071
- "data-testid": "app-shell-collapse",
1072
- testID: "app-shell-collapse",
1073
- children: collapsed ? /* @__PURE__ */ jsx(PanelLeft, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(PanelLeftClose, { className: "h-4 w-4" })
1074
- }
1075
- ) : null,
1076
- !collapsed ? optionsControl : null
1077
- ] })
1572
+ ) : (
1573
+ // Expanded: collapse sits beside the logo. Collapsed: the rail's
1574
+ // header is the logo mark alone; expand moves to the rail's foot.
1575
+ !collapsed ? /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
1576
+ collapseToggle,
1577
+ optionsControl
1578
+ ] }) : null
1579
+ )
1078
1580
  ]
1079
1581
  }
1080
1582
  ),
@@ -1083,8 +1585,10 @@ function AppShell({
1083
1585
  SidebarNav,
1084
1586
  {
1085
1587
  navItems,
1086
- contextualNavItems,
1588
+ contextualNavItems: (opts.mobile ? subNavMode !== "off" : subNavMode === "sidebar" || subNavMode === "section") ? contextualNavItems : void 0,
1589
+ nest: subNavMode === "sidebar" || opts.mobile && subNavOn,
1087
1590
  contextualNavLabel: derived?.current.label,
1591
+ subActive,
1088
1592
  secondaryNavItems,
1089
1593
  secondaryNavLabel,
1090
1594
  showSecondary,
@@ -1096,7 +1600,7 @@ function AppShell({
1096
1600
  isActive
1097
1601
  }
1098
1602
  ),
1099
- (showFooterSlot || showUser || showTheme || collapsed && optionsControl) && !opts.mobile ? /* @__PURE__ */ jsxs("div", { className: "mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-2", children: [
1603
+ (showFooterSlot || showUser || showTheme || collapsed && (optionsControl || collapseToggle)) && !opts.mobile ? /* @__PURE__ */ jsxs("div", { className: "mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-2", children: [
1100
1604
  showFooterSlot && !collapsed ? footer : null,
1101
1605
  showUser ? /* @__PURE__ */ jsx(
1102
1606
  "div",
@@ -1116,7 +1620,8 @@ function AppShell({
1116
1620
  ),
1117
1621
  children: [
1118
1622
  collapsed ? optionsControl : null,
1119
- showTheme ? /* @__PURE__ */ jsx(ThemeToggle, { testId: "app-shell-theme-toggle-desktop" }) : null
1623
+ showTheme ? /* @__PURE__ */ jsx(ThemeToggle, { testId: "app-shell-theme-toggle-desktop" }) : null,
1624
+ collapsed ? collapseToggle : null
1120
1625
  ]
1121
1626
  }
1122
1627
  )
@@ -1139,10 +1644,11 @@ function AppShell({
1139
1644
  "aside",
1140
1645
  {
1141
1646
  className: cn(
1142
- "hidden flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 md:flex",
1647
+ "hidden flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 motion-reduce:transition-none md:flex",
1143
1648
  scrollWindow && "sticky top-0 h-screen",
1144
1649
  railClass
1145
1650
  ),
1651
+ ref: sidebarRef,
1146
1652
  "data-collapsed": collapsed || void 0,
1147
1653
  "data-testid": "app-shell-sidebar",
1148
1654
  children: sidebarChrome({})
@@ -1241,46 +1747,103 @@ function AppShell({
1241
1747
  )
1242
1748
  }
1243
1749
  ) : null,
1750
+ hasPageHeader ? /* @__PURE__ */ jsx(
1751
+ "div",
1752
+ {
1753
+ ref: subNavChannel.setHeaderSlot,
1754
+ className: cn(
1755
+ "min-w-0 px-4 sm:px-6 lg:px-8",
1756
+ parentNavigation ? "pt-2 sm:pt-3 lg:pt-4" : "pt-4 sm:pt-6 lg:pt-8"
1757
+ ),
1758
+ "data-testid": "app-shell-page-header"
1759
+ }
1760
+ ) : null,
1761
+ showTabs ? /* @__PURE__ */ jsx(
1762
+ "div",
1763
+ {
1764
+ className: cn(
1765
+ "px-4 sm:px-6 lg:px-8",
1766
+ // Phones use the bottom bar, unless the app turned it off.
1767
+ showMobileBottom && "hidden md:block",
1768
+ (!parentNavigation || hasPageHeader) && "pt-2 sm:pt-3 lg:pt-4"
1769
+ ),
1770
+ children: /* @__PURE__ */ jsx(
1771
+ SubNavTabs,
1772
+ {
1773
+ items: contextualNavItems,
1774
+ activePath,
1775
+ isActive,
1776
+ linkComponent,
1777
+ label: subNavLabel ?? derived?.current.label ?? "Page sections",
1778
+ end: subNavChannel.hasEnd ? /* @__PURE__ */ jsx("div", { ref: subNavChannel.setEndSlot, className: "flex items-center" }) : void 0
1779
+ }
1780
+ )
1781
+ }
1782
+ ) : null,
1244
1783
  /* @__PURE__ */ jsx(
1245
1784
  "div",
1246
1785
  {
1247
1786
  className: cn(
1248
1787
  "min-w-0 p-4 sm:p-6 lg:p-8",
1249
- parentNavigation && "pt-2 sm:pt-3 lg:pt-4"
1788
+ (parentNavigation || hasPageHeader) && !showTabs && "pt-2 sm:pt-3 lg:pt-4"
1250
1789
  ),
1251
- children
1790
+ children: /* @__PURE__ */ jsx(SubNavContext.Provider, { value: subNavChannel.channel, children })
1252
1791
  }
1253
1792
  )
1254
1793
  ]
1255
1794
  }
1256
1795
  ),
1257
- showMobileBottom ? /* @__PURE__ */ jsx(
1796
+ showMobileBottom ? /* @__PURE__ */ jsxs(
1258
1797
  "nav",
1259
1798
  {
1260
1799
  className: "fixed bottom-0 left-0 right-0 z-30 flex border-t bg-background md:hidden",
1261
1800
  "aria-label": "Primary",
1262
- children: bottomItems.map((item) => {
1263
- const LinkComp = linkComponent;
1264
- const active = itemIsActive(item, activePath, isActive);
1265
- return /* @__PURE__ */ jsxs(
1801
+ children: [
1802
+ backTarget ? /* @__PURE__ */ jsxs(
1266
1803
  LinkComp,
1267
1804
  {
1268
- href: item.to,
1269
- title: item.label,
1270
- "aria-label": item.label,
1271
- "aria-current": active && !bottomNotCurrent ? "page" : void 0,
1272
- className: cn(
1273
- "flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors",
1274
- active ? "text-primary" : "text-muted-foreground"
1275
- ),
1805
+ href: backTarget.href,
1806
+ "aria-label": `Back to ${backTarget.label}`,
1807
+ className: "flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium text-muted-foreground transition-colors",
1808
+ testID: "app-shell-mobile-back",
1276
1809
  children: [
1277
- /* @__PURE__ */ jsx(item.icon, { className: "h-5 w-5" }),
1278
- /* @__PURE__ */ jsx("span", { className: "max-w-full truncate px-0.5", children: item.label })
1810
+ /* @__PURE__ */ jsx(ArrowLeft, { className: "h-5 w-5", "aria-hidden": true }),
1811
+ /* @__PURE__ */ jsx("span", { className: "max-w-full truncate px-0.5", children: backTarget.label })
1279
1812
  ]
1280
- },
1281
- item.to + item.label
1282
- );
1283
- })
1813
+ }
1814
+ ) : null,
1815
+ bottomItems.map((item) => {
1816
+ const active = bottomCurrent ? item === bottomCurrent : itemIsActive(item, activePath, isActive);
1817
+ return /* @__PURE__ */ jsxs(
1818
+ LinkComp,
1819
+ {
1820
+ href: item.to,
1821
+ title: item.label,
1822
+ "aria-label": subNavOn && item.badge != null ? `${item.label} (${item.badge})` : item.label,
1823
+ "aria-current": active && !bottomNotCurrent ? "page" : void 0,
1824
+ className: cn(
1825
+ "flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors",
1826
+ active ? "text-primary" : "text-muted-foreground"
1827
+ ),
1828
+ children: [
1829
+ subNavOn && item.badge != null ? /* @__PURE__ */ jsxs("span", { className: "relative", children: [
1830
+ /* @__PURE__ */ jsx(NavIcon, { item, className: "h-5 w-5" }),
1831
+ /* @__PURE__ */ jsx(
1832
+ "span",
1833
+ {
1834
+ className: "absolute -top-1.5 left-full -ml-1 inline-flex h-4 min-w-4 items-center justify-center whitespace-nowrap rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-primary-foreground ring-2 ring-background",
1835
+ "aria-hidden": true,
1836
+ children: item.badge
1837
+ }
1838
+ )
1839
+ ] }) : /* @__PURE__ */ jsx(NavIcon, { item, className: "h-5 w-5" }),
1840
+ /* @__PURE__ */ jsx("span", { className: "max-w-full truncate px-0.5", children: item.label })
1841
+ ]
1842
+ },
1843
+ item.to + item.label
1844
+ );
1845
+ })
1846
+ ]
1284
1847
  }
1285
1848
  ) : null
1286
1849
  ]
@@ -1291,6 +1854,6 @@ function AppShell({
1291
1854
  );
1292
1855
  }
1293
1856
 
1294
- export { AppShell, PageHeader };
1295
- //# sourceMappingURL=chunk-UYI3GLFJ.js.map
1296
- //# sourceMappingURL=chunk-UYI3GLFJ.js.map
1857
+ export { AppShell, AppShellPageHeader, AppShellSubNavEnd, PageHeader, SubNavTabs, useAppShellSubNav };
1858
+ //# sourceMappingURL=chunk-SZMXNC5Q.js.map
1859
+ //# sourceMappingURL=chunk-SZMXNC5Q.js.map