@marktiderman/genesis-ui 2.2.1 → 2.3.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/EXTENDING.md ADDED
@@ -0,0 +1,104 @@
1
+ # Extending genesis
2
+
3
+ Genesis is a starting point. If it ever becomes a ceiling, that is a bug in
4
+ genesis, not a reason to fork it.
5
+
6
+ **REUSE > EXTEND > INVENT** only works if extending is actually available. When
7
+ the only two options are "take the pattern exactly as shipped" or "rebuild it
8
+ yourself", every surface that needs one small difference takes the second, and
9
+ the design system stops being a system.
10
+
11
+ So every pattern component offers a **ladder**. You step down one rung — not
12
+ off — when a rung stops fitting.
13
+
14
+ ## The four rungs
15
+
16
+ ### 1 · The pattern
17
+
18
+ ```tsx
19
+ <ResourcePage title="Questions" resource="questions" columns={["title"]} />
20
+ ```
21
+
22
+ The whole page, wired. Take this when it fits.
23
+
24
+ ### 2 · The pattern, with one part swapped — `slots` / `slotProps`
25
+
26
+ The rung that keeps you on the ladder. Keep every piece of wiring that was
27
+ already right, change the one part that was not.
28
+
29
+ ```tsx
30
+ <ResourcePage
31
+ title="Questions"
32
+ // Replace a part. Yours receives exactly the props genesis would have
33
+ // passed the default — so wrap the default, don't reimplement it.
34
+ slots={{ filters: MyFilters }}
35
+ // Or keep the part and pass it more props. Wins over ours.
36
+ slotProps={{ table: { className: "text-xs" } }}
37
+ />
38
+ ```
39
+
40
+ **The eject button** is `null`:
41
+
42
+ ```tsx
43
+ // Shell, filters and grid — no bulk bar. Selection still works; it just
44
+ // isn't shown. A pattern is a default, not a mandate.
45
+ <ResourcePage slots={{ bulkBar: null }} />
46
+ ```
47
+
48
+ Every interior part a pattern composes is named in its slot map
49
+ (`ResourcePageSlotMap`). That map is the contract: **a part added to the
50
+ render tree gets added to the map in the same change**, otherwise the next
51
+ surface that needs it forks the page instead.
52
+
53
+ ### 3 · The primitives
54
+
55
+ ```tsx
56
+ <DataPageShell title="Questions" filters={<DataFilters … />}>
57
+ <DataTable … />
58
+ </DataPageShell>
59
+ ```
60
+
61
+ 75 subpath exports (`@marktiderman/genesis-ui/data`, `/button`, `/table`, …).
62
+ Compose the page yourself, with genesis's parts.
63
+
64
+ ### 4 · The hook
65
+
66
+ ```tsx
67
+ const page = useResourcePage({ … }); // state, paging, selection, sorting
68
+ return <WhateverYouWant {...page} />;
69
+ ```
70
+
71
+ Nine hooks under `@marktiderman/genesis-ui/hooks`. All of the logic, none of
72
+ the markup.
73
+
74
+ ## Which rung?
75
+
76
+ | You need to… | Rung |
77
+ | --- | --- |
78
+ | Ship a standard list page | 1 |
79
+ | Change one part, keep the rest | **2 — `slots`** |
80
+ | Drop a part you don't want | **2 — `slots={{ x: null }}`** |
81
+ | Add a class or a `data-testid` | **2 — `slotProps`, or `className`** |
82
+ | Lay the page out differently | 3 |
83
+ | Keep the behaviour, none of the look | 4 |
84
+
85
+ Dropping to rung 3 because one part was wrong means re-deriving all the
86
+ wiring that was already correct — and that re-derivation is where forks come
87
+ from. Reach for rung 2 first.
88
+
89
+ ## The rules that keep this true
90
+
91
+ 1. **Every pattern root takes `className`,** merged with `cn()`, never
92
+ replacing its own layout.
93
+ 2. **Every interior part is named in the slot map.** Unreachable parts are how
94
+ a consumer ends up forking.
95
+ 3. **`slotProps` wins over genesis's own value.** A hatch that silently loses
96
+ to the default is not a hatch — the consumer sets the prop, sees nothing
97
+ change, and goes back to forking.
98
+ 4. **Prefer a data prop to an opaque `ReactNode` slot** for the ordinary case.
99
+ `secondaryActions` exists because `actions: ReactNode` let every surface
100
+ spend the header's action budget differently, and one of them blew it.
101
+ Keep the `ReactNode` slot for genuinely arbitrary chrome.
102
+ 5. **When a downstream app hand-builds something genesis nearly had, that is
103
+ the bug report.** `ListPage`, a hand-rolled bulk bar and a forked header
104
+ were each a missing prop away from not existing.
@@ -32,6 +32,17 @@ interface PageHeaderOptions {
32
32
  */
33
33
  configurable?: boolean;
34
34
  }
35
+ /** One entry in {@link PageHeaderProps.secondaryActions}. */
36
+ interface PageHeaderAction {
37
+ label: string;
38
+ onClick: () => void;
39
+ icon?: LucideIcon;
40
+ /** `data-testid` on the menu item, so e2e can drive it. */
41
+ testId?: string;
42
+ disabled?: boolean;
43
+ /** Omits the action entirely — for one gated on data. */
44
+ hidden?: boolean;
45
+ }
35
46
  interface PageHeaderProps {
36
47
  title: string;
37
48
  subtitle?: string;
@@ -64,8 +75,22 @@ interface PageHeaderProps {
64
75
  density?: Density;
65
76
  onDensityChange?: (density: Density) => void;
66
77
  createAction?: ReactNode;
78
+ /**
79
+ * Additional actions, rendered in ONE overflow menu — never as more
80
+ * side-by-side buttons.
81
+ *
82
+ * The budget is the point. A header with three top-level buttons squeezes
83
+ * its own title to an ellipsis at real viewport widths, so a surface that
84
+ * genuinely has four actions needs somewhere to put the other three. Before
85
+ * this existed the only place was a fork of the header, which is exactly
86
+ * what one consumer did.
87
+ *
88
+ * Pass `ReactNode` via `actions` instead when you need arbitrary chrome;
89
+ * this prop is for the ordinary case, and it keeps the budget for you.
90
+ */
91
+ secondaryActions?: PageHeaderAction[];
67
92
  }
68
- declare function PageHeader({ title, subtitle, icon: Icon, options: optionsProp, optionsStorageKey, actions, details, detailsOpen, onDetailsOpenChange, detailsActive, detailsHint, count, totalCount, entityName, countActive, viewMode, onViewModeChange, pageSize, onPageSizeChange, density, onDensityChange, createAction, }: PageHeaderProps): React.JSX.Element;
93
+ declare function PageHeader({ title, subtitle, icon: Icon, options: optionsProp, optionsStorageKey, actions, details, detailsOpen, onDetailsOpenChange, detailsActive, detailsHint, count, totalCount, entityName, countActive, viewMode, onViewModeChange, pageSize, onPageSizeChange, density, onDensityChange, createAction, secondaryActions, }: PageHeaderProps): React.JSX.Element;
69
94
 
70
95
  interface NavItem {
71
96
  to: string;
@@ -195,4 +220,4 @@ interface AppShellProps {
195
220
  }
196
221
  declare function AppShell({ appName, appLogo, logo, navItems, secondaryNavItems, secondaryNavLabel, mobileNavItems, headerSlot, collapsedHeaderSlot, footer, userSlot, children, showThemeToggle: showThemeToggleProp, options: optionsProp, optionsStorageKey, collapsed: collapsedProp, onCollapsedChange, linkComponent, activePath, isActive, }: AppShellProps): React.JSX.Element;
197
222
 
198
- export { AppShell as A, type NavItem as N, PageHeader as P, type AppShellOptions as a, type AppShellProps as b, type PageHeaderOptions as c, type PageHeaderProps as d };
223
+ export { AppShell as A, type NavItem as N, PageHeader as P, type AppShellOptions as a, type AppShellProps as b, type PageHeaderOptions as c, type PageHeaderProps as d, type PageHeaderAction as e };
@@ -0,0 +1,12 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+
3
+ // src/slots.tsx
4
+ function renderSlot(Default, override, props, extra) {
5
+ if (override === null) return null;
6
+ const Component = override ?? Default;
7
+ return /* @__PURE__ */ jsx(Component, { ...props, ...extra });
8
+ }
9
+
10
+ export { renderSlot };
11
+ //# sourceMappingURL=chunk-7S7LKQBD.js.map
12
+ //# sourceMappingURL=chunk-7S7LKQBD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/slots.tsx"],"names":[],"mappings":";;;AA6DO,SAAS,UAAA,CACd,OAAA,EACA,QAAA,EACA,KAAA,EACA,KAAA,EACW;AACX,EAAA,IAAI,QAAA,KAAa,MAAM,OAAO,IAAA;AAC9B,EAAA,MAAM,YAAY,QAAA,IAAY,OAAA;AAC9B,EAAA,uBAAO,GAAA,CAAC,SAAA,EAAA,EAAW,GAAG,KAAA,EAAQ,GAAI,KAAA,EAAa,CAAA;AACjD","file":"chunk-7S7LKQBD.js","sourcesContent":["import type { ComponentType, ReactNode } from \"react\";\n\n/**\n * The slot contract — genesis's escape hatch from an all-or-nothing pattern.\n *\n * A pattern component (`ResourcePage`, `DataPageShell`, …) is a composition of\n * interior parts. Without a way to reach those parts, a consumer who needs ONE\n * of them to differ has exactly two options: accept the whole thing as shipped,\n * or fork it. Both are bad, and the second is what actually happens — which is\n * how a downstream app ends up maintaining its own copy of a header it only\n * wanted to change one button in.\n *\n * Genesis offers a customisation ladder, and this is its missing rung:\n *\n * 1. the pattern `<ResourcePage title=\"Questions\" />`\n * 2. the pattern + slots ← here: keep the wiring, swap or drop one part\n * 3. the primitives `<DataPageShell><DataTable/></DataPageShell>`\n * 4. the hook `useResourcePage()`, render it all yourself\n *\n * Rungs 1, 3 and 4 already existed (75 subpath exports, 9 hooks). Rung 2 is\n * the one that keeps a consumer ON the ladder: dropping to rung 3 because a\n * single part was wrong means re-deriving all the wiring that was already\n * right, and that re-derivation is where the forks come from.\n *\n * Two knobs, following the `slots`/`slotProps` convention Material UI settled\n * on (mui/material-ui#33416) so the shape is one a React developer already\n * knows rather than one genesis invented:\n *\n * `slots` — REPLACE a part. Your component receives exactly the props\n * genesis would have passed the default, so it can wrap the\n * default and change one thing rather than reimplement it.\n * `slotProps` — KEEP the part, pass it more props. The common case: a\n * `className`, a `data-testid`, an `aria-label`.\n *\n * And the eject button: `slots={{ bulkBar: null }}` drops that part entirely.\n * A pattern is a default, not a mandate — a consumer who wants the shell and\n * the grid but none of the bulk bar should not have to leave to get that.\n */\n\n/** Replace a part (`ComponentType`), or eject it (`null`). */\nexport type SlotOverride<P> = ComponentType<P> | null;\n\n/** `{ [part]: Component | null }` — see {@link renderSlot}. */\nexport type Slots<M extends Record<string, object>> = {\n [K in keyof M]?: SlotOverride<M[K]>;\n};\n\n/** `{ [part]: extraProps }` — merged over what genesis passes. */\nexport type SlotPropsFor<M extends Record<string, object>> = {\n [K in keyof M]?: Partial<M[K]>;\n};\n\n/**\n * Renders one slot: the override if given, the default otherwise, nothing if\n * ejected with `null`.\n *\n * `extra` merges LAST, so `slotProps` wins over genesis's own value. That is\n * deliberate: a consumer reaching for `slotProps` is overriding on purpose,\n * and a hatch that silently loses to the default is not a hatch. `className`\n * is the one prop a caller should merge with `cn()` instead of replacing.\n */\nexport function renderSlot<P extends object>(\n Default: ComponentType<P>,\n override: SlotOverride<P> | undefined,\n props: P,\n extra?: Partial<P>,\n): ReactNode {\n if (override === null) return null;\n const Component = override ?? Default;\n return <Component {...props} {...(extra as P)} />;\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { ViewToggle, ViewSettings } from './chunk-46TZGMFW.js';
3
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './chunk-QJ6P2DW6.js';
3
4
  import { Collapsible, CollapsibleContent } from './chunk-B2GIBUQD.js';
4
5
  import { Badge } from './chunk-NPNADZHM.js';
5
6
  import { Popover, PopoverTrigger, PopoverContent } from './chunk-22XB3Q6H.js';
@@ -9,7 +10,7 @@ import { Label } from './chunk-F47U5KNW.js';
9
10
  import { cn } from './chunk-NZ5WBA4R.js';
10
11
  import { DefaultLink, isPathActive } from './chunk-6NPEMLBA.js';
11
12
  import { useId, useState, useEffect, useMemo, useCallback } from 'react';
12
- import { ListFilter, SlidersHorizontal, Menu, X, PanelLeft, PanelLeftClose, Sun, Moon, ChevronDown } from 'lucide-react';
13
+ import { ListFilter, MoreHorizontal, SlidersHorizontal, Menu, X, PanelLeft, PanelLeftClose, Sun, Moon, ChevronDown } from 'lucide-react';
13
14
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
14
15
 
15
16
  // src/option-storage.ts
@@ -103,7 +104,8 @@ function PageHeader({
103
104
  onPageSizeChange,
104
105
  density,
105
106
  onDensityChange,
106
- createAction
107
+ createAction,
108
+ secondaryActions
107
109
  }) {
108
110
  const detailsRegionId = useId();
109
111
  const hasDetails = details != null;
@@ -111,6 +113,7 @@ function PageHeader({
111
113
  const hasViewToggle = Boolean(viewMode && onViewModeChange);
112
114
  const hasViewSettings = Boolean(onPageSizeChange && onDensityChange);
113
115
  const hasCreate = createAction != null;
116
+ const visibleSecondary = (secondaryActions ?? []).filter((a) => !a.hidden);
114
117
  const [userOverrides, setUserOverrides] = useState(
115
118
  {}
116
119
  );
@@ -238,6 +241,35 @@ function PageHeader({
238
241
  }
239
242
  ) : null,
240
243
  hasCreate && resolved.showCreate !== false ? createAction : null,
244
+ visibleSecondary.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
245
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
246
+ Button,
247
+ {
248
+ type: "button",
249
+ variant: "ghost",
250
+ size: "icon",
251
+ "aria-label": "More actions",
252
+ "data-testid": "page-header-more-actions",
253
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" })
254
+ }
255
+ ) }),
256
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", children: visibleSecondary.map((action) => {
257
+ const ActionIcon = action.icon;
258
+ return /* @__PURE__ */ jsxs(
259
+ DropdownMenuItem,
260
+ {
261
+ onSelect: action.onClick,
262
+ disabled: action.disabled,
263
+ "data-testid": action.testId,
264
+ children: [
265
+ ActionIcon ? /* @__PURE__ */ jsx(ActionIcon, { className: "mr-2 h-4 w-4", "aria-hidden": "true" }) : null,
266
+ action.label
267
+ ]
268
+ },
269
+ action.label
270
+ );
271
+ }) })
272
+ ] }) : null,
241
273
  actions,
242
274
  resolved.configurable && toggles.length > 0 ? /* @__PURE__ */ jsxs(Popover, { children: [
243
275
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -1096,5 +1128,5 @@ function AppShell({
1096
1128
  }
1097
1129
 
1098
1130
  export { AppShell, PageHeader };
1099
- //# sourceMappingURL=chunk-FEJAZFP3.js.map
1100
- //# sourceMappingURL=chunk-FEJAZFP3.js.map
1131
+ //# sourceMappingURL=chunk-UBCYXOXT.js.map
1132
+ //# sourceMappingURL=chunk-UBCYXOXT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/option-storage.ts","../src/components/layout/PageHeader.tsx","../src/components/layout/AppShell.tsx"],"names":["readStoredOptions","OPTION_DEFAULTS","useState","useEffect","jsx","jsxs","useMemo","useCallback","SlidersHorizontal"],"mappings":"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","file":"chunk-UBCYXOXT.js","sourcesContent":["/**\n * Generic localStorage-backed persistence for \"customize chrome\" option\n * objects (AppShell, PageHeader, …). SSR-safe (no-ops without `window`) and\n * swallows quota / private-mode errors so a storage failure never breaks\n * rendering.\n *\n * Callers own their own shape validation — this module only handles the\n * JSON <-> localStorage plumbing shared across chrome components.\n */\nexport function readStoredOptions<T extends object>(key: string): Partial<T> {\n if (typeof window === \"undefined\") return {};\n try {\n const raw = localStorage.getItem(key);\n if (!raw) return {};\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== \"object\") return {};\n return parsed as Partial<T>;\n } catch {\n return {};\n }\n}\n\nexport function writeStoredOptions<T extends object>(\n key: string,\n value: Partial<T>,\n): void {\n if (typeof window === \"undefined\") return;\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {\n // quota / private mode\n }\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useId, useMemo, useState, type ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { ListFilter, MoreHorizontal, SlidersHorizontal } from \"lucide-react\";\nimport { Badge } from \"../ui/badge\";\nimport { Button } from \"../ui/button\";\nimport { Collapsible, CollapsibleContent } from \"../ui/collapsible\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Label } from \"../ui/label\";\nimport { ViewToggle, type ViewMode } from \"../patterns/view-toggle\";\nimport { ViewSettings } from \"../patterns/view-settings\";\nimport type { Density } from \"../../hooks/use-view-settings\";\nimport { cn } from \"../../utils\";\nimport {\n readStoredOptions as readStoredOptionsRaw,\n writeStoredOptions,\n} from \"../../option-storage\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\n\n/**\n * Declarative chrome for PageHeader — developer defaults + optional\n * in-header user toggles (Notion/Linear “customize view” pattern).\n *\n * @stability Beta\n */\nexport interface PageHeaderOptions {\n showIcon?: boolean;\n showSubtitle?: boolean;\n showCount?: boolean;\n showViewToggle?: boolean;\n showViewSettings?: boolean;\n showCreate?: boolean;\n /** Ghost ListFilter control in the actions cluster */\n showDetailsToggle?: boolean;\n /** Quiet hint beside actions while details are collapsed */\n showDetailsHint?: boolean;\n /** Move subtitle into the details panel (default true when details exist) */\n subtitleInDetails?: boolean;\n defaultDetailsOpen?: boolean;\n detailsLabel?: string;\n /**\n * Show the in-header Header options popover so users can customize chrome.\n * Default true when the header has more than one chrome affordance.\n */\n configurable?: boolean;\n}\n\n/** @stability Beta */\nexport type PageHeaderOptionKey = keyof PageHeaderOptions;\n\n/** Naive English pluralization for the count badge — singular at exactly 1. */\nfunction pluralizeEntity(entity: string, count: number): string {\n if (count === 1) return entity;\n return entity.endsWith(\"s\") ? entity : `${entity}s`;\n}\n\nconst OPTION_DEFAULTS: Required<\n Omit<PageHeaderOptions, \"detailsLabel\" | \"defaultDetailsOpen\" | \"subtitleInDetails\" | \"configurable\">\n> &\n Pick<PageHeaderOptions, \"detailsLabel\" | \"defaultDetailsOpen\" | \"subtitleInDetails\" | \"configurable\"> = {\n showIcon: true,\n showSubtitle: true,\n showCount: true,\n showViewToggle: true,\n showViewSettings: true,\n showCreate: true,\n showDetailsToggle: true,\n showDetailsHint: true,\n subtitleInDetails: undefined,\n defaultDetailsOpen: false,\n detailsLabel: \"Details\",\n configurable: undefined,\n};\n\nconst BOOLEAN_OPTION_KEYS: PageHeaderOptionKey[] = [\n \"showIcon\",\n \"showSubtitle\",\n \"showCount\",\n \"showViewToggle\",\n \"showViewSettings\",\n \"showCreate\",\n \"showDetailsToggle\",\n \"showDetailsHint\",\n \"subtitleInDetails\",\n \"defaultDetailsOpen\",\n \"configurable\",\n];\nconst STRING_OPTION_KEYS: PageHeaderOptionKey[] = [\"detailsLabel\"];\n\n/**\n * Reads persisted options and keeps only known keys whose stored value has\n * the expected primitive type — a malformed or hand-edited localStorage\n * value can't smuggle in an unexpected key or wrong-typed override.\n */\nfunction readStoredOptions(key: string): Partial<PageHeaderOptions> {\n const raw = readStoredOptionsRaw<PageHeaderOptions>(key);\n const sanitized: Partial<PageHeaderOptions> = {};\n for (const optKey of BOOLEAN_OPTION_KEYS) {\n const value = raw[optKey];\n if (typeof value === \"boolean\") {\n (sanitized as Record<string, boolean>)[optKey] = value;\n }\n }\n for (const optKey of STRING_OPTION_KEYS) {\n const value = raw[optKey];\n if (typeof value === \"string\") {\n (sanitized as Record<string, string>)[optKey] = value;\n }\n }\n return sanitized;\n}\n\n/** One entry in {@link PageHeaderProps.secondaryActions}. */\nexport interface PageHeaderAction {\n label: string;\n onClick: () => void;\n icon?: LucideIcon;\n /** `data-testid` on the menu item, so e2e can drive it. */\n testId?: string;\n disabled?: boolean;\n /** Omits the action entirely — for one gated on data. */\n hidden?: boolean;\n}\n\nexport interface PageHeaderProps {\n title: string;\n subtitle?: string;\n icon?: LucideIcon;\n /**\n * Header chrome config (developer defaults). Merged under user picks from\n * the in-header options menu when `options.configurable` / storage is on.\n */\n options?: PageHeaderOptions;\n /** Persist user header-option overrides (localStorage) */\n optionsStorageKey?: string;\n /** Extra trailing actions (after create); prefer first-class slots below */\n actions?: ReactNode;\n /** Collapsible details region below the title row */\n details?: ReactNode;\n detailsOpen?: boolean;\n onDetailsOpenChange?: (open: boolean) => void;\n detailsActive?: boolean;\n detailsHint?: ReactNode;\n /** Count badge */\n count?: number;\n totalCount?: number;\n /** Singular entity for “N rows” copy when unfiltered */\n entityName?: string;\n countActive?: boolean;\n viewMode?: ViewMode;\n onViewModeChange?: (mode: ViewMode) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n density?: Density;\n onDensityChange?: (density: Density) => void;\n createAction?: ReactNode;\n /**\n * Additional actions, rendered in ONE overflow menu — never as more\n * side-by-side buttons.\n *\n * The budget is the point. A header with three top-level buttons squeezes\n * its own title to an ellipsis at real viewport widths, so a surface that\n * genuinely has four actions needs somewhere to put the other three. Before\n * this existed the only place was a fork of the header, which is exactly\n * what one consumer did.\n *\n * Pass `ReactNode` via `actions` instead when you need arbitrary chrome;\n * this prop is for the ordinary case, and it keeps the budget for you.\n */\n secondaryActions?: PageHeaderAction[];\n}\n\ntype ToggleDef = {\n key: keyof PageHeaderOptions;\n label: string;\n available: boolean;\n};\n\nexport function PageHeader({\n title,\n subtitle,\n icon: Icon,\n options: optionsProp,\n optionsStorageKey,\n actions,\n details,\n detailsOpen,\n onDetailsOpenChange,\n detailsActive = false,\n detailsHint,\n count,\n totalCount,\n entityName,\n countActive = false,\n viewMode,\n onViewModeChange,\n pageSize,\n onPageSizeChange,\n density,\n onDensityChange,\n createAction,\n secondaryActions,\n}: PageHeaderProps) {\n const detailsRegionId = useId();\n const hasDetails = details != null;\n const hasCount = count !== undefined && totalCount !== undefined;\n const hasViewToggle = Boolean(viewMode && onViewModeChange);\n const hasViewSettings = Boolean(onPageSizeChange && onDensityChange);\n const hasCreate = createAction != null;\n // `hidden` is filtered here rather than at the call site: an action gated on\n // data (\"Review drafts\", only when drafts exist) is the common case, and a\n // conditional array at every call site is what makes those call sites drift.\n const visibleSecondary = (secondaryActions ?? []).filter((a) => !a.hidden);\n\n const [userOverrides, setUserOverrides] = useState<Partial<PageHeaderOptions>>(\n {},\n );\n // Tracks which storage key the current overrides were hydrated from, so a\n // key change (routing between pages with distinct keys while this header\n // stays mounted) can't flush the previous key's overrides into the new key\n // before the read effect re-hydrates.\n const [hydratedKey, setHydratedKey] = useState<string | undefined>(\n optionsStorageKey ? undefined : \"\",\n );\n\n useEffect(() => {\n if (!optionsStorageKey) {\n setUserOverrides({});\n setHydratedKey(\"\");\n return;\n }\n setUserOverrides(readStoredOptions(optionsStorageKey));\n setHydratedKey(optionsStorageKey);\n }, [optionsStorageKey]);\n\n useEffect(() => {\n // Persist only once overrides belong to the current key (see hydratedKey).\n if (!optionsStorageKey || hydratedKey !== optionsStorageKey) return;\n writeStoredOptions(optionsStorageKey, userOverrides);\n }, [optionsStorageKey, userOverrides, hydratedKey]);\n\n const resolved = useMemo(() => {\n const merged: PageHeaderOptions = {\n ...OPTION_DEFAULTS,\n ...optionsProp,\n ...userOverrides,\n };\n if (merged.subtitleInDetails === undefined) {\n merged.subtitleInDetails = hasDetails;\n }\n if (merged.configurable === undefined) {\n merged.configurable =\n hasDetails || hasCount || hasViewSettings || hasViewToggle;\n }\n return merged;\n }, [\n optionsProp,\n userOverrides,\n hasDetails,\n hasCount,\n hasViewSettings,\n hasViewToggle,\n ]);\n\n const patchOption = useCallback((key: keyof PageHeaderOptions, value: boolean) => {\n setUserOverrides((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const moveSubtitle = Boolean(resolved.subtitleInDetails) && hasDetails;\n const showTitleSubtitle =\n Boolean(subtitle) &&\n Boolean(resolved.showSubtitle) &&\n !moveSubtitle;\n\n const [uncontrolledOpen, setUncontrolledOpen] = useState(\n () => optionsProp?.defaultDetailsOpen ?? OPTION_DEFAULTS.defaultDetailsOpen!,\n );\n const isControlled = detailsOpen !== undefined;\n const open = isControlled ? detailsOpen! : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n onDetailsOpenChange?.(next);\n };\n\n const detailsLabel = resolved.detailsLabel ?? \"Details\";\n const entity = entityName || \"item\";\n\n const toggles: ToggleDef[] = (\n [\n { key: \"showIcon\", label: \"Icon\", available: Boolean(Icon) },\n {\n key: \"showSubtitle\",\n label: \"Subtitle\",\n available: Boolean(subtitle) && !moveSubtitle,\n },\n { key: \"showCount\", label: \"Count\", available: hasCount },\n { key: \"showViewToggle\", label: \"View toggle\", available: hasViewToggle },\n {\n key: \"showViewSettings\",\n label: \"Display settings\",\n available: hasViewSettings,\n },\n { key: \"showCreate\", label: \"Create action\", available: hasCreate },\n {\n key: \"showDetailsToggle\",\n label: \"Filters / details\",\n available: hasDetails,\n },\n {\n key: \"showDetailsHint\",\n label: \"Collapsed hint\",\n available: hasDetails && detailsHint != null,\n },\n ] as const satisfies readonly ToggleDef[]\n ).filter((t) => t.available);\n\n const showDetailsToggle =\n hasDetails && Boolean(resolved.showDetailsToggle);\n const showHint =\n hasDetails &&\n !open &&\n Boolean(resolved.showDetailsHint) &&\n detailsHint != null;\n\n const actionsCluster = (\n <div className=\"flex items-center gap-1.5 shrink-0\">\n {showHint ? (\n <span className=\"hidden max-w-[14rem] truncate text-xs text-muted-foreground sm:inline\">\n {detailsHint}\n </span>\n ) : null}\n\n {hasCount && resolved.showCount !== false ? (\n <Badge variant=\"secondary\" className=\"text-xs\">\n {countActive\n ? `${count} of ${totalCount}`\n : `${totalCount} ${pluralizeEntity(entity, totalCount!)}`}\n </Badge>\n ) : null}\n\n {hasViewToggle && resolved.showViewToggle !== false ? (\n <ViewToggle viewMode={viewMode!} onViewModeChange={onViewModeChange!} />\n ) : null}\n\n {showDetailsToggle ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className={cn(\n \"relative h-8 w-8\",\n (open || detailsActive) && \"bg-accent text-accent-foreground\",\n )}\n onClick={() => setOpen(!open)}\n data-testid=\"page-header-details-toggle\"\n title={open ? `Hide ${detailsLabel}` : `Show ${detailsLabel}`}\n aria-label={open ? `Hide ${detailsLabel}` : `Show ${detailsLabel}`}\n aria-expanded={open}\n aria-controls={detailsRegionId}\n testID=\"page-header-details-toggle\"\n >\n <ListFilter className=\"h-4 w-4\" />\n {detailsActive ? (\n <span\n className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-primary\"\n aria-hidden\n />\n ) : null}\n </Button>\n ) : null}\n\n {hasViewSettings && resolved.showViewSettings !== false ? (\n <ViewSettings\n pageSize={pageSize ?? 25}\n onPageSizeChange={onPageSizeChange!}\n density={density ?? \"comfortable\"}\n onDensityChange={onDensityChange!}\n />\n ) : null}\n\n {hasCreate && resolved.showCreate !== false ? createAction : null}\n\n {visibleSecondary.length > 0 ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"More actions\"\n data-testid=\"page-header-more-actions\"\n >\n <MoreHorizontal className=\"h-4 w-4\" aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {visibleSecondary.map((action) => {\n const ActionIcon = action.icon;\n return (\n <DropdownMenuItem\n key={action.label}\n onSelect={action.onClick}\n disabled={action.disabled}\n data-testid={action.testId}\n >\n {ActionIcon ? (\n <ActionIcon className=\"mr-2 h-4 w-4\" aria-hidden=\"true\" />\n ) : null}\n {action.label}\n </DropdownMenuItem>\n );\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n ) : null}\n\n {actions}\n\n {resolved.configurable && toggles.length > 0 ? (\n <Popover>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n title=\"Header options\"\n aria-label=\"Header options\"\n data-testid=\"page-header-options\"\n testID=\"page-header-options\"\n >\n <SlidersHorizontal className=\"h-4 w-4\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-64 space-y-3\">\n <div>\n <h4 className=\"text-sm font-semibold\">Header options</h4>\n <p className=\"text-xs text-muted-foreground\">\n Customize this page header. Saved on this device when a storage\n key is set.\n </p>\n </div>\n <ul className=\"space-y-3\">\n {toggles.map((t) => {\n const checked = Boolean(resolved[t.key] ?? true);\n return (\n <li\n key={t.key}\n className=\"flex items-center justify-between gap-3\"\n >\n <Label\n htmlFor={`page-header-opt-${t.key}`}\n className=\"text-sm font-normal\"\n >\n {t.label}\n </Label>\n <Switch\n id={`page-header-opt-${t.key}`}\n checked={checked}\n onCheckedChange={(v) => patchOption(t.key, v)}\n />\n </li>\n );\n })}\n </ul>\n {optionsStorageKey && Object.keys(userOverrides).length > 0 ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-full text-xs\"\n onClick={() => setUserOverrides({})}\n data-testid=\"page-header-options-reset\"\n aria-label=\"Reset header options\"\n testID=\"page-header-options-reset\"\n >\n Reset to defaults\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n ) : null}\n </div>\n );\n\n const titleRow = (\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3 min-w-0\">\n {Icon && resolved.showIcon !== false ? (\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-primary/10\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n ) : null}\n <div className=\"min-w-0\">\n <h1\n data-testid=\"page-title\"\n className=\"truncate font-display text-xl font-bold tracking-tight sm:text-2xl\"\n >\n {title}\n </h1>\n {showTitleSubtitle ? (\n <p className=\"truncate text-sm text-muted-foreground\">{subtitle}</p>\n ) : null}\n </div>\n </div>\n {actionsCluster}\n </div>\n );\n\n if (!hasDetails) {\n return titleRow;\n }\n\n return (\n <Collapsible open={open} onOpenChange={setOpen}>\n {titleRow}\n <CollapsibleContent id={detailsRegionId} className=\"mt-3\">\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card/40 p-3 sm:p-4\">\n {moveSubtitle && subtitle && resolved.showSubtitle !== false ? (\n <p className=\"text-sm text-muted-foreground\">{subtitle}</p>\n ) : null}\n {details}\n </div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n","\"use client\";\n\nimport {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport {\n Menu,\n X,\n Sun,\n Moon,\n SlidersHorizontal,\n PanelLeftClose,\n PanelLeft,\n ChevronDown,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport { Badge } from \"../ui/badge\";\nimport { Label } from \"../ui/label\";\nimport { Switch } from \"../ui/switch\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { Collapsible, CollapsibleContent } from \"../ui/collapsible\";\nimport { cn } from \"../../utils\";\nimport { readStoredOptions, writeStoredOptions } from \"../../option-storage\";\nimport {\n DefaultLink,\n isPathActive,\n type LinkComponent,\n} from \"../../navigation\";\n\nexport interface NavItem {\n to: string;\n icon: LucideIcon;\n label: string;\n /** Nested items — rendered as a collapsible group when the rail is expanded */\n children?: NavItem[];\n badge?: string | number;\n disabled?: boolean;\n /** Exact-match active state (e.g. overview `/`) */\n end?: boolean;\n}\n\n/**\n * Declarative chrome for AppShell — developer defaults + optional\n * in-sidebar user toggles (Notion/Linear “customize chrome” pattern).\n *\n * @stability Beta\n */\nexport interface AppShellOptions {\n showLogo?: boolean;\n showAppName?: boolean;\n showThemeToggle?: boolean;\n /** Desktop rail collapse control */\n showCollapse?: boolean;\n showUserSlot?: boolean;\n showFooter?: boolean;\n showSecondaryNav?: boolean;\n /** Mobile fixed bottom tab bar */\n showMobileBottomNav?: boolean;\n defaultCollapsed?: boolean;\n /**\n * Show the in-sidebar Sidebar options popover so users can customize chrome.\n * Default true when collapse / secondary / theme / slots are available.\n */\n configurable?: boolean;\n /**\n * Toggle keys the developer has decided on and users may not override —\n * e.g. hiding `showThemeToggle` for a brand that manages theme elsewhere.\n * Locked keys are dropped from the options popover and any stored user\n * override for them is ignored on resolve.\n */\n lockedOptions?: (keyof AppShellOptions)[];\n /** Comfortable (default) vs compact row padding */\n density?: \"comfortable\" | \"compact\";\n /** Expanded rail width in rem-ish tokens: narrow ≈ 13rem, wide ≈ 16rem */\n railWidth?: \"narrow\" | \"wide\";\n /**\n * Where page content scrolls.\n * - `container` (default): main panel scrolls — classic app-shell chrome.\n * - `window`: document/window scrolls with a sticky rail so React Router\n * `ScrollRestoration` keeps working (dashboard dogfood).\n */\n scrollRoot?: \"container\" | \"window\";\n}\n\nconst OPTION_DEFAULTS: Required<\n Omit<AppShellOptions, \"configurable\" | \"defaultCollapsed\" | \"lockedOptions\">\n> &\n Pick<AppShellOptions, \"configurable\" | \"defaultCollapsed\" | \"lockedOptions\"> = {\n showLogo: true,\n showAppName: true,\n showThemeToggle: true,\n showCollapse: true,\n showUserSlot: true,\n showFooter: true,\n showSecondaryNav: true,\n showMobileBottomNav: true,\n defaultCollapsed: false,\n configurable: undefined,\n density: \"comfortable\",\n railWidth: \"narrow\",\n scrollRoot: \"container\",\n};\n\nfunction readStoredCollapsed(key: string): boolean | undefined {\n if (typeof window === \"undefined\") return undefined;\n try {\n const raw = localStorage.getItem(`${key}:collapsed`);\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredCollapsed(key: string, collapsed: boolean) {\n if (typeof window === \"undefined\") return;\n try {\n localStorage.setItem(`${key}:collapsed`, collapsed ? \"1\" : \"0\");\n } catch {\n // quota / private mode\n }\n}\n\n/**\n * @stability Beta\n */\nexport interface AppShellProps {\n /** App name shown in sidebar header */\n appName: string;\n /** App logo letter or short text (ignored when `logo` is set) */\n appLogo?: string;\n /** Custom logo node — preferred over `appLogo` letter tile */\n logo?: ReactNode;\n /** Primary navigation items */\n navItems: NavItem[];\n /** Secondary nav (e.g. “More”) — rendered below a divider when shown */\n secondaryNavItems?: NavItem[];\n /** Label above secondary nav (default “More”) */\n secondaryNavLabel?: string;\n /**\n * Items for the mobile bottom tab bar. Defaults to top-level `navItems`\n * (no children). Pass a short list for apps with large sidebars; pass `[]`\n * and set `options.showMobileBottomNav: false` to hide.\n */\n mobileNavItems?: NavItem[];\n /** Slot below the logo row (product switcher, workspace picker, …) */\n headerSlot?: ReactNode;\n /**\n * Compact slot shown in the **collapsed** desktop rail, where `headerSlot`\n * is hidden for space. Use it to keep a core affordance reachable without\n * expanding — e.g. an icon-only product switcher. Omit to hide the header\n * region entirely while collapsed.\n */\n collapsedHeaderSlot?: ReactNode;\n /** Footer region above the user slot (version, links) */\n footer?: ReactNode;\n /**\n * User / account block at the bottom of the sidebar. When the rail is\n * collapsed, AppShell hides any descendant carrying the\n * `data-shell-user-detail` attribute (e.g. name/email text) so only a\n * compact element (avatar, initial) remains — tag the detail wrapper you\n * want hidden in the collapsed rail with that attribute.\n */\n userSlot?: ReactNode;\n /** Page content */\n children: ReactNode;\n /**\n * @deprecated Prefer `options.showThemeToggle`. Kept for sample-app\n * back-compat; merges into resolved options when `options` omits it.\n */\n showThemeToggle?: boolean;\n /** Chrome config (developer defaults + optional in-sidebar menu) */\n options?: AppShellOptions;\n /** Persist user sidebar-option overrides + collapse state (localStorage) */\n optionsStorageKey?: string;\n /** Controlled collapse (desktop rail) */\n collapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n /**\n * Injectable link component. Defaults to a plain `<a>` (no router). Pass a\n * router adapter — e.g. React Router `({ href, ...p }) => <Link to={href} {...p} />`\n * or Next.js `next/link` — to get client-side navigation.\n */\n linkComponent?: LinkComponent;\n /**\n * Current path used to highlight the active nav item. Router consumers pass\n * `useLocation().pathname` (RR) or `usePathname()` (Next). Omit to disable\n * active highlighting.\n */\n activePath?: string;\n /**\n * Optional custom active-state matcher. Defaults to a prefix match (with `/`\n * treated as exact), mirroring React Router `NavLink`.\n */\n isActive?: (to: string, activePath?: string) => boolean;\n}\n\nfunction ThemeToggle({ testId }: { testId: string }) {\n const [dark, setDark] = useState(false);\n\n useEffect(() => {\n const stored = localStorage.getItem(\"theme\");\n const prefersDark = window.matchMedia(\n \"(prefers-color-scheme: dark)\",\n ).matches;\n const isDark = stored === \"dark\" || (!stored && prefersDark);\n setDark(isDark);\n document.documentElement.classList.toggle(\"dark\", isDark);\n }, []);\n\n function toggle() {\n const next = !dark;\n setDark(next);\n document.documentElement.classList.toggle(\"dark\", next);\n localStorage.setItem(\"theme\", next ? \"dark\" : \"light\");\n }\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={toggle}\n className=\"h-8 w-8\"\n title=\"Toggle theme\"\n aria-label=\"Toggle theme\"\n data-testid={testId}\n testID={testId}\n >\n {dark ? <Sun className=\"h-4 w-4\" /> : <Moon className=\"h-4 w-4\" />}\n </Button>\n );\n}\n\nfunction itemIsActive(\n item: NavItem,\n activePath: string | undefined,\n isActive: (to: string, activePath?: string) => boolean,\n): boolean {\n if (item.end) {\n return Boolean(activePath && activePath === item.to);\n }\n if (isActive(item.to, activePath)) return true;\n return Boolean(\n item.children?.some((child) => itemIsActive(child, activePath, isActive)),\n );\n}\n\nfunction NavLinkRow({\n item,\n collapsed,\n density,\n onNavClick,\n linkComponent: LinkComp,\n activePath,\n isActive,\n}: {\n item: NavItem;\n collapsed: boolean;\n density: \"comfortable\" | \"compact\";\n onNavClick?: () => void;\n linkComponent: LinkComponent;\n activePath?: string;\n isActive: (to: string, activePath?: string) => boolean;\n}) {\n const active = itemIsActive(item, activePath, isActive);\n const pad = density === \"compact\" ? \"px-2.5 py-1.5\" : \"px-3 py-2\";\n\n if (item.disabled) {\n return (\n <span\n title={item.label}\n className={cn(\n \"flex items-center gap-3 rounded-lg text-sm font-medium opacity-40\",\n pad,\n collapsed && \"justify-center px-2\",\n )}\n aria-disabled\n >\n <item.icon className=\"h-4 w-4 shrink-0\" />\n {!collapsed ? <span className=\"truncate\">{item.label}</span> : null}\n </span>\n );\n }\n\n return (\n <LinkComp\n href={item.to}\n onClick={onNavClick}\n title={collapsed ? item.label : undefined}\n aria-label={collapsed ? item.label : undefined}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"flex items-center gap-3 rounded-lg text-sm font-medium transition-colors\",\n pad,\n collapsed && \"justify-center px-2\",\n active\n ? \"bg-sidebar-accent text-sidebar-primary\"\n : \"text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n )}\n >\n <item.icon className=\"h-4 w-4 shrink-0\" />\n {!collapsed ? (\n <>\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.badge != null ? (\n <Badge variant=\"secondary\" className=\"h-5 shrink-0 px-1.5 text-[10px]\">\n {item.badge}\n </Badge>\n ) : null}\n </>\n ) : null}\n </LinkComp>\n );\n}\n\nfunction NavGroup({\n item,\n collapsed,\n density,\n onNavClick,\n linkComponent,\n activePath,\n isActive,\n}: {\n item: NavItem;\n collapsed: boolean;\n density: \"comfortable\" | \"compact\";\n onNavClick?: () => void;\n linkComponent: LinkComponent;\n activePath?: string;\n isActive: (to: string, activePath?: string) => boolean;\n}) {\n const children = item.children ?? [];\n const childActive = children.some((c) =>\n itemIsActive(c, activePath, isActive),\n );\n const [open, setOpen] = useState(childActive);\n\n useEffect(() => {\n if (childActive) setOpen(true);\n }, [childActive]);\n\n if (children.length === 0) {\n return (\n <NavLinkRow\n item={item}\n collapsed={collapsed}\n density={density}\n onNavClick={onNavClick}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n );\n }\n\n // Collapsed rail: only the parent link (or first child if parent is a group stub)\n if (collapsed) {\n const target: NavItem = item.to\n ? { ...item, children: undefined }\n : { ...children[0]!, children: undefined };\n return (\n <NavLinkRow\n item={target}\n collapsed\n density={density}\n onNavClick={onNavClick}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n );\n }\n\n // Parent may be its own route (`item.to`) — keep a real link and a separate\n // expand control so groups under an existing page stay navigable + highlightable.\n // Visual highlight may use prefix match; aria-current is exact-only so a child\n // route does not leave two \"page\" currents in the tree (CMT-322-008).\n const selfExact = Boolean(item.to && activePath && activePath === item.to);\n const selfActive = item.to\n ? item.end\n ? selfExact\n : isActive(item.to, activePath)\n : false;\n const groupHighlight = selfActive || childActive;\n const pad = density === \"compact\" ? \"px-2.5 py-1.5\" : \"px-3 py-2\";\n const LinkComp = linkComponent;\n const parentClass = cn(\n \"flex min-w-0 flex-1 items-center gap-3 rounded-lg text-sm font-medium transition-colors\",\n pad,\n groupHighlight\n ? \"bg-sidebar-accent/60 text-sidebar-primary\"\n : \"text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n );\n\n return (\n <Collapsible open={open} onOpenChange={setOpen}>\n <div className=\"space-y-0.5\">\n <div className=\"flex items-center gap-0.5\" data-testid={`nav-group-${item.label}`}>\n {item.to && !item.disabled ? (\n <LinkComp\n href={item.to}\n onClick={onNavClick}\n aria-current={selfExact ? \"page\" : undefined}\n className={parentClass}\n >\n <item.icon className=\"h-4 w-4 shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">{item.label}</span>\n </LinkComp>\n ) : (\n <button\n type=\"button\"\n className={parentClass}\n onClick={() => setOpen((o) => !o)}\n aria-expanded={open}\n aria-disabled={item.disabled || undefined}\n disabled={item.disabled}\n >\n <item.icon className=\"h-4 w-4 shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">{item.label}</span>\n </button>\n )}\n <button\n type=\"button\"\n className={cn(\n \"shrink-0 rounded-lg p-1.5 text-sidebar-foreground transition-colors hover:bg-sidebar-accent\",\n groupHighlight && \"text-sidebar-primary\",\n item.disabled && \"pointer-events-none opacity-40\",\n )}\n onClick={() => setOpen((o) => !o)}\n aria-expanded={open}\n aria-label={open ? `Collapse ${item.label}` : `Expand ${item.label}`}\n aria-disabled={item.disabled || undefined}\n disabled={item.disabled}\n data-testid={`nav-group-toggle-${item.label}`}\n >\n <ChevronDown\n className={cn(\n \"h-3.5 w-3.5 opacity-60 transition-transform\",\n open && \"rotate-180\",\n )}\n />\n </button>\n </div>\n <CollapsibleContent>\n <div className=\"ml-3 space-y-0.5 border-l border-sidebar-border pl-2\">\n {children.map((child) => (\n <NavLinkRow\n key={child.to + child.label}\n item={child}\n collapsed={false}\n density={density}\n onNavClick={onNavClick}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n ))}\n </div>\n </CollapsibleContent>\n </div>\n </Collapsible>\n );\n}\n\nfunction SidebarNav({\n navItems,\n secondaryNavItems,\n secondaryNavLabel,\n showSecondary,\n collapsed,\n density,\n onNavClick,\n linkComponent,\n activePath,\n isActive,\n}: {\n navItems: NavItem[];\n secondaryNavItems?: NavItem[];\n secondaryNavLabel: string;\n showSecondary: boolean;\n collapsed: boolean;\n density: \"comfortable\" | \"compact\";\n onNavClick?: () => void;\n linkComponent: LinkComponent;\n activePath?: string;\n isActive: (to: string, activePath?: string) => boolean;\n}) {\n return (\n <nav\n className={cn(\n \"flex-1 space-y-1 overflow-y-auto\",\n collapsed ? \"p-2\" : \"p-3\",\n )}\n aria-label=\"Main\"\n >\n {navItems.map((item) => (\n <NavGroup\n key={item.to + item.label}\n item={item}\n collapsed={collapsed}\n density={density}\n onNavClick={onNavClick}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n ))}\n\n {showSecondary && secondaryNavItems && secondaryNavItems.length > 0 ? (\n <div\n className={cn(\n \"mt-4 space-y-1 border-t border-sidebar-border pt-4\",\n collapsed && \"mt-2 pt-2\",\n )}\n >\n {!collapsed ? (\n <p className=\"mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/40\">\n {secondaryNavLabel}\n </p>\n ) : null}\n {secondaryNavItems.map((item) => (\n <NavGroup\n key={item.to + item.label}\n item={item}\n collapsed={collapsed}\n density={density}\n onNavClick={onNavClick}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n ))}\n </div>\n ) : null}\n </nav>\n );\n}\n\nfunction SidebarBrand({\n appName,\n appLogo,\n logo,\n showLogo,\n showAppName,\n collapsed,\n}: {\n appName: string;\n appLogo?: string;\n logo?: ReactNode;\n showLogo: boolean;\n showAppName: boolean;\n collapsed: boolean;\n}) {\n if (!showLogo && !showAppName) return null;\n\n return (\n <div\n className={cn(\n \"flex min-w-0 items-center gap-2\",\n collapsed && \"justify-center\",\n )}\n >\n {showLogo ? (\n logo ? (\n <div className=\"flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden\">\n {logo}\n </div>\n ) : appLogo ? (\n <div className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-primary\">\n <span className=\"text-sm font-bold text-primary-foreground\">\n {appLogo}\n </span>\n </div>\n ) : null\n ) : null}\n {showAppName && !collapsed ? (\n <span className=\"truncate font-semibold text-sidebar-foreground\">\n {appName}\n </span>\n ) : null}\n </div>\n );\n}\n\ntype ToggleDef = {\n key: keyof AppShellOptions;\n label: string;\n available: boolean;\n};\n\nexport function AppShell({\n appName,\n appLogo,\n logo,\n navItems,\n secondaryNavItems,\n secondaryNavLabel = \"More\",\n mobileNavItems,\n headerSlot,\n collapsedHeaderSlot,\n footer,\n userSlot,\n children,\n showThemeToggle: showThemeToggleProp,\n options: optionsProp,\n optionsStorageKey,\n collapsed: collapsedProp,\n onCollapsedChange,\n linkComponent = DefaultLink,\n activePath,\n isActive = isPathActive,\n}: AppShellProps) {\n const [mobileOpen, setMobileOpen] = useState(false);\n const [userOverrides, setUserOverrides] = useState<Partial<AppShellOptions>>(\n {},\n );\n // Tracks which storage key the current overrides were hydrated from, so a\n // key change can't flush the previous key's overrides into the new key\n // before the read effect re-hydrates.\n const [hydratedKey, setHydratedKey] = useState<string | undefined>(\n optionsStorageKey ? undefined : \"\",\n );\n\n // Close the drawer on any path change (product switcher, deep links, …).\n useEffect(() => {\n setMobileOpen(false);\n }, [activePath]);\n\n const hasSecondary = Boolean(secondaryNavItems && secondaryNavItems.length);\n const hasUser = userSlot != null;\n const hasFooter = footer != null;\n const hasLogo = Boolean(logo || appLogo);\n\n useEffect(() => {\n if (!optionsStorageKey) {\n setUserOverrides({});\n setHydratedKey(\"\");\n return;\n }\n setUserOverrides(readStoredOptions<AppShellOptions>(optionsStorageKey));\n setHydratedKey(optionsStorageKey);\n }, [optionsStorageKey]);\n\n useEffect(() => {\n // Persist only once overrides belong to the current key (see hydratedKey).\n if (!optionsStorageKey || hydratedKey !== optionsStorageKey) return;\n writeStoredOptions<AppShellOptions>(optionsStorageKey, userOverrides);\n }, [optionsStorageKey, userOverrides, hydratedKey]);\n\n const lockedOptions = optionsProp?.lockedOptions;\n\n const resolved = useMemo(() => {\n const merged: AppShellOptions = {\n ...OPTION_DEFAULTS,\n ...(showThemeToggleProp !== undefined\n ? { showThemeToggle: showThemeToggleProp }\n : {}),\n ...optionsProp,\n ...userOverrides,\n };\n if (merged.configurable === undefined) {\n merged.configurable =\n hasSecondary ||\n hasUser ||\n hasFooter ||\n Boolean(merged.showCollapse) ||\n Boolean(merged.showThemeToggle);\n }\n // Locked keys always resolve to the developer default, never a stored\n // or in-session user override.\n if (lockedOptions?.length) {\n const defaults = OPTION_DEFAULTS as Record<string, unknown>;\n const devOptions = optionsProp as Record<string, unknown> | undefined;\n const target = merged as Record<string, unknown>;\n for (const key of lockedOptions) {\n target[key] = devOptions?.[key] ?? defaults[key];\n }\n }\n return merged;\n }, [\n optionsProp,\n userOverrides,\n showThemeToggleProp,\n hasSecondary,\n hasUser,\n hasFooter,\n lockedOptions,\n ]);\n\n const patchOption = useCallback(\n (key: keyof AppShellOptions, value: boolean) => {\n if (lockedOptions?.includes(key)) return;\n setUserOverrides((prev) => ({ ...prev, [key]: value }));\n },\n [lockedOptions],\n );\n\n const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(\n () =>\n optionsProp?.defaultCollapsed ?? OPTION_DEFAULTS.defaultCollapsed!,\n );\n\n useEffect(() => {\n if (!optionsStorageKey || collapsedProp !== undefined) return;\n const stored = readStoredCollapsed(optionsStorageKey);\n if (stored !== undefined) setUncontrolledCollapsed(stored);\n }, [optionsStorageKey, collapsedProp]);\n\n const isCollapseControlled = collapsedProp !== undefined;\n const collapsed = isCollapseControlled\n ? collapsedProp!\n : uncontrolledCollapsed;\n\n const setCollapsed = (next: boolean) => {\n if (!isCollapseControlled) {\n setUncontrolledCollapsed(next);\n if (optionsStorageKey) writeStoredCollapsed(optionsStorageKey, next);\n }\n onCollapsedChange?.(next);\n };\n\n const density = resolved.density ?? \"comfortable\";\n const scrollWindow = resolved.scrollRoot === \"window\";\n const railExpanded =\n resolved.railWidth === \"wide\" ? \"md:w-64\" : \"md:w-52\";\n const railClass = collapsed ? \"md:w-14\" : railExpanded;\n\n const bottomItems = (\n mobileNavItems ??\n navItems.filter((i) => !i.children || i.children.length === 0)\n ).filter((i) => !i.disabled);\n\n const toggles: ToggleDef[] = (\n [\n { key: \"showLogo\", label: \"Logo\", available: hasLogo },\n { key: \"showAppName\", label: \"App name\", available: true },\n {\n key: \"showThemeToggle\",\n label: \"Theme toggle\",\n available: true,\n },\n { key: \"showCollapse\", label: \"Collapse control\", available: true },\n {\n key: \"showSecondaryNav\",\n label: \"Secondary nav\",\n available: hasSecondary,\n },\n { key: \"showFooter\", label: \"Footer\", available: hasFooter },\n { key: \"showUserSlot\", label: \"User block\", available: hasUser },\n {\n key: \"showMobileBottomNav\",\n label: \"Mobile bottom nav\",\n available: bottomItems.length > 0,\n },\n ] as const satisfies readonly ToggleDef[]\n ).filter((t) => t.available && !lockedOptions?.includes(t.key));\n\n const showTheme = resolved.showThemeToggle !== false;\n const showCollapseCtrl = resolved.showCollapse !== false;\n const showSecondary = hasSecondary && resolved.showSecondaryNav !== false;\n const showFooterSlot = hasFooter && resolved.showFooter !== false;\n const showUser = hasUser && resolved.showUserSlot !== false;\n const showMobileBottom =\n bottomItems.length > 0 && resolved.showMobileBottomNav !== false;\n\n const optionsControl =\n resolved.configurable && toggles.length > 0 ? (\n <Popover>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n title=\"Sidebar options\"\n aria-label=\"Sidebar options\"\n data-testid=\"app-shell-options\"\n testID=\"app-shell-options\"\n >\n <SlidersHorizontal className=\"h-4 w-4\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" side=\"top\" className=\"w-64 space-y-3\">\n <div>\n <h4 className=\"text-sm font-semibold\">Sidebar options</h4>\n <p className=\"text-xs text-muted-foreground\">\n Customize this sidebar. Saved on this device when a storage key is\n set.\n </p>\n </div>\n <ul className=\"space-y-3\">\n {toggles.map((t) => {\n const checked = Boolean(resolved[t.key] ?? true);\n return (\n <li\n key={t.key}\n className=\"flex items-center justify-between gap-3\"\n >\n <Label\n htmlFor={`app-shell-opt-${t.key}`}\n className=\"text-sm font-normal\"\n >\n {t.label}\n </Label>\n <Switch\n id={`app-shell-opt-${t.key}`}\n checked={checked}\n onCheckedChange={(v) => patchOption(t.key, v)}\n />\n </li>\n );\n })}\n </ul>\n {optionsStorageKey && Object.keys(userOverrides).length > 0 ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-full text-xs\"\n onClick={() => setUserOverrides({})}\n data-testid=\"app-shell-options-reset\"\n aria-label=\"Reset sidebar options\"\n testID=\"app-shell-options-reset\"\n >\n Reset to defaults\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n ) : null;\n\n const sidebarChrome = (opts: { mobile?: boolean }) => (\n <>\n <div\n className={cn(\n \"flex h-14 shrink-0 items-center gap-2 border-b border-sidebar-border\",\n collapsed && !opts.mobile ? \"justify-center px-2\" : \"px-3\",\n )}\n >\n <div className=\"min-w-0 flex-1\">\n <SidebarBrand\n appName={appName}\n appLogo={appLogo}\n logo={logo}\n showLogo={resolved.showLogo !== false}\n showAppName={resolved.showAppName !== false}\n collapsed={collapsed && !opts.mobile}\n />\n </div>\n {opts.mobile ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setMobileOpen(false)}\n aria-label=\"Close navigation\"\n data-testid=\"app-shell-mobile-close\"\n testID=\"app-shell-mobile-close\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n ) : (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n {showCollapseCtrl ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => setCollapsed(!collapsed)}\n title={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n aria-label={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n aria-pressed={collapsed}\n data-testid=\"app-shell-collapse\"\n testID=\"app-shell-collapse\"\n >\n {collapsed ? (\n <PanelLeft className=\"h-4 w-4\" />\n ) : (\n <PanelLeftClose className=\"h-4 w-4\" />\n )}\n </Button>\n ) : null}\n {!collapsed ? optionsControl : null}\n </div>\n )}\n </div>\n\n {collapsed && !opts.mobile ? (\n collapsedHeaderSlot ? (\n <div className=\"shrink-0 border-b border-sidebar-border p-2\">\n {collapsedHeaderSlot}\n </div>\n ) : null\n ) : headerSlot ? (\n <div className=\"shrink-0 border-b border-sidebar-border\">\n {headerSlot}\n </div>\n ) : null}\n\n <SidebarNav\n navItems={navItems}\n secondaryNavItems={secondaryNavItems}\n secondaryNavLabel={secondaryNavLabel}\n showSecondary={showSecondary}\n collapsed={collapsed && !opts.mobile}\n density={density}\n onNavClick={opts.mobile ? () => setMobileOpen(false) : undefined}\n linkComponent={linkComponent}\n activePath={activePath}\n isActive={isActive}\n />\n\n {(showFooterSlot || showUser || showTheme || (collapsed && optionsControl)) &&\n !opts.mobile ? (\n <div className=\"mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-2\">\n {showFooterSlot && !collapsed ? footer : null}\n {showUser ? (\n <div\n className={cn(\n collapsed &&\n \"flex justify-center [&_[data-shell-user-detail]]:hidden\",\n )}\n >\n {userSlot}\n </div>\n ) : null}\n <div\n className={cn(\n \"flex items-center gap-1\",\n collapsed ? \"flex-col\" : \"justify-end\",\n )}\n >\n {collapsed ? optionsControl : null}\n {showTheme ? (\n <ThemeToggle testId=\"app-shell-theme-toggle-desktop\" />\n ) : null}\n </div>\n </div>\n ) : null}\n\n {opts.mobile && (showFooterSlot || showUser || showTheme) ? (\n <div className=\"mt-auto shrink-0 space-y-2 border-t border-sidebar-border p-3\">\n {showFooterSlot ? footer : null}\n {showUser ? userSlot : null}\n {showTheme ? (\n <div className=\"flex justify-end\">\n <ThemeToggle testId=\"app-shell-theme-toggle-mobile-drawer\" />\n </div>\n ) : null}\n </div>\n ) : null}\n </>\n );\n\n return (\n <div\n className={cn(\n \"flex\",\n scrollWindow ? \"min-h-screen\" : \"h-screen overflow-hidden\",\n )}\n >\n {/* Desktop sidebar */}\n <aside\n className={cn(\n \"hidden flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 md:flex\",\n scrollWindow && \"sticky top-0 h-screen\",\n railClass,\n )}\n data-collapsed={collapsed || undefined}\n data-testid=\"app-shell-sidebar\"\n >\n {sidebarChrome({})}\n </aside>\n\n {/* Mobile overlay */}\n {mobileOpen ? (\n <div\n className=\"fixed inset-0 z-40 bg-black/50 md:hidden\"\n onClick={() => setMobileOpen(false)}\n aria-hidden\n />\n ) : null}\n\n {/* Mobile sidebar — inert when closed so off-screen links leave tab order */}\n <aside\n className={cn(\n \"fixed inset-y-0 left-0 z-50 flex w-60 flex-col border-r border-sidebar-border bg-sidebar transition-transform md:hidden\",\n mobileOpen ? \"translate-x-0\" : \"-translate-x-full pointer-events-none\",\n )}\n id=\"app-shell-mobile-nav\"\n aria-hidden={!mobileOpen}\n inert={!mobileOpen || undefined}\n >\n {sidebarChrome({ mobile: true })}\n </aside>\n\n {/* Main content */}\n <div\n className={cn(\n \"flex min-w-0 flex-1 flex-col\",\n !scrollWindow && \"overflow-hidden\",\n )}\n >\n <header\n className={cn(\n \"flex h-14 items-center gap-3 border-b px-4 md:hidden\",\n scrollWindow && \"sticky top-0 z-20 bg-background\",\n )}\n >\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setMobileOpen(true)}\n aria-label=\"Open navigation\"\n aria-expanded={mobileOpen}\n aria-controls=\"app-shell-mobile-nav\"\n data-testid=\"app-shell-mobile-open\"\n testID=\"app-shell-mobile-open\"\n >\n <Menu className=\"h-5 w-5\" />\n </Button>\n <span className=\"flex-1 truncate font-semibold\">{appName}</span>\n {showTheme ? (\n <ThemeToggle testId=\"app-shell-theme-toggle-mobile-header\" />\n ) : null}\n </header>\n\n <main\n className={cn(\n \"flex-1\",\n !scrollWindow && \"overflow-y-auto\",\n showMobileBottom ? \"pb-16 md:pb-0\" : \"pb-0\",\n )}\n >\n <div className=\"min-w-0 p-4 sm:p-6 lg:p-8\">{children}</div>\n </main>\n\n {showMobileBottom ? (\n <nav\n className=\"fixed bottom-0 left-0 right-0 z-30 flex border-t bg-background md:hidden\"\n aria-label=\"Primary\"\n >\n {bottomItems.map((item) => {\n const LinkComp = linkComponent;\n const active = itemIsActive(item, activePath, isActive);\n return (\n <LinkComp\n key={item.to + item.label}\n href={item.to}\n title={item.label}\n aria-label={item.label}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"flex flex-1 flex-col items-center gap-1 py-2 text-xs font-medium transition-colors\",\n active ? \"text-primary\" : \"text-muted-foreground\",\n )}\n >\n <item.icon className=\"h-5 w-5\" />\n <span className=\"max-w-full truncate px-0.5\">\n {item.label}\n </span>\n </LinkComp>\n );\n })}\n </nav>\n ) : null}\n </div>\n </div>\n );\n}\n"]}