@exxatdesignux/ui 0.2.15 → 0.2.17

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 (110) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
  3. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +151 -3
  4. package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
  7. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +17 -1
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
  10. package/package.json +3 -3
  11. package/src/components/ui/banner.tsx +2 -0
  12. package/src/components/ui/chart.tsx +57 -2
  13. package/src/components/ui/sidebar.tsx +1 -0
  14. package/src/globals.css +21 -2
  15. package/src/theme.css +4 -2
  16. package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
  17. package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
  18. package/template/AGENTS.md +23 -18
  19. package/template/app/(app)/data-list/page.tsx +2 -2
  20. package/template/app/(app)/question-bank/layout.tsx +27 -7
  21. package/template/app/(app)/question-bank/new/page.tsx +58 -0
  22. package/template/app/globals.css +136 -2
  23. package/template/app/layout.tsx +41 -5
  24. package/template/components/app-sidebar.tsx +141 -59
  25. package/template/components/ask-leo-sidebar.tsx +1 -4
  26. package/template/components/brand-color-picker.tsx +344 -0
  27. package/template/components/compliance-list-view.tsx +33 -51
  28. package/template/components/compliance-table.tsx +24 -0
  29. package/template/components/data-table/index.tsx +68 -24
  30. package/template/components/data-table/pagination.tsx +0 -1
  31. package/template/components/data-table/types.ts +4 -1
  32. package/template/components/data-table/use-table-state.ts +243 -94
  33. package/template/components/data-views/data-row-list.tsx +183 -0
  34. package/template/components/data-views/finder-panel-view.tsx +2 -2
  35. package/template/components/data-views/index.ts +26 -3
  36. package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
  37. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
  38. package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
  39. package/template/components/data-views/os-folder-glyph.tsx +8 -0
  40. package/template/components/data-views/outline-tree-menu.tsx +157 -0
  41. package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
  42. package/template/components/export-drawer.tsx +1 -1
  43. package/template/components/exxat-product-logo.tsx +173 -379
  44. package/template/components/folder-details-shell.tsx +1 -1
  45. package/template/components/hub-tree-panel-view.tsx +88 -80
  46. package/template/components/invite-collaborators-drawer.tsx +5 -3
  47. package/template/components/key-metrics.tsx +116 -51
  48. package/template/components/new-placement-form.tsx +4 -2
  49. package/template/components/new-question-composer.tsx +2208 -0
  50. package/template/components/page-breadcrumb-trail.tsx +131 -0
  51. package/template/components/page-header.tsx +21 -11
  52. package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
  53. package/template/components/placement-detail.tsx +1 -1
  54. package/template/components/placements-board-view.tsx +1 -1
  55. package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
  56. package/template/components/placements-list-view.tsx +18 -132
  57. package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
  58. package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
  59. package/template/components/placements-table-columns.tsx +2 -2
  60. package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
  61. package/template/components/product-switcher.tsx +26 -11
  62. package/template/components/product-wordmark.tsx +285 -0
  63. package/template/components/question-bank-client.tsx +130 -70
  64. package/template/components/question-bank-hub-client.tsx +108 -115
  65. package/template/components/question-bank-list-view.tsx +30 -54
  66. package/template/components/question-bank-new-folder-sheet.tsx +1 -1
  67. package/template/components/question-bank-page-header.tsx +18 -2
  68. package/template/components/question-bank-secondary-nav.tsx +12 -228
  69. package/template/components/question-bank-table.tsx +30 -5
  70. package/template/components/rotations-empty-state.tsx +3 -0
  71. package/template/components/secondary-panel.tsx +24 -4
  72. package/template/components/settings-appearance-card.tsx +584 -141
  73. package/template/components/site-header.tsx +56 -32
  74. package/template/components/sites-list-view.tsx +31 -36
  75. package/template/components/sites-table.tsx +24 -0
  76. package/template/components/table-properties/drawer.tsx +1 -1
  77. package/template/components/team-client.tsx +1 -1
  78. package/template/components/team-list-view.tsx +34 -50
  79. package/template/components/team-table.tsx +29 -3
  80. package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
  81. package/template/components/templates/list-page.tsx +1 -3
  82. package/template/components/templates/nested-secondary-panel-shell.tsx +11 -6
  83. package/template/components/ui/dot-pattern.tsx +50 -26
  84. package/template/components/ui/leo-icon.tsx +23 -3
  85. package/template/contexts/product-context.tsx +51 -7
  86. package/template/contexts/system-banner-context.tsx +112 -4
  87. package/template/docs/collaboration-access-pattern.md +2 -0
  88. package/template/docs/question-bank-hub-header-pattern.md +25 -0
  89. package/template/eslint.config.mjs +18 -0
  90. package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
  91. package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
  92. package/template/lib/data-list-persistence.ts +57 -257
  93. package/template/lib/dev-log.test.ts +6 -5
  94. package/template/lib/exxat-palette.json +1462 -0
  95. package/template/lib/exxat-palette.ts +136 -0
  96. package/template/lib/list-page-table-properties.ts +1 -1
  97. package/template/lib/list-status-badges.ts +1 -1
  98. package/template/lib/mailto.ts +29 -0
  99. package/template/lib/mock/navigation.tsx +30 -1
  100. package/template/lib/placement-board-card-layout.ts +1 -1
  101. package/template/lib/product-brand.ts +268 -0
  102. package/template/lib/question-bank-authoring.ts +308 -0
  103. package/template/lib/question-bank-nav.ts +70 -0
  104. package/template/lib/raf-throttle.ts +45 -0
  105. package/template/lib/table-state-lifecycle.ts +474 -0
  106. package/template/next.config.mjs +156 -0
  107. package/template/package.json +6 -6
  108. package/template/stores/app-store.ts +46 -1
  109. package/template/components/command-menu-01.tsx +0 -133
  110. package/template/components/command-menu-02.tsx +0 -386
package/src/theme.css CHANGED
@@ -219,7 +219,8 @@
219
219
  --sidebar-ring: oklch(0.25 0 0);
220
220
  /* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
221
221
  --sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
222
- --secondary-panel-bg: oklch(0.99 0.008 286.1);
222
+ /* Nested secondary rail — soft brand wash on canvas (not pure `--background`, not full `--sidebar`). */
223
+ --secondary-panel-bg: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
223
224
 
224
225
  /* Browser UI (meta theme-color) — aligned with --brand-tint */
225
226
  --theme-color-chrome: #f3f2f8;
@@ -345,7 +346,8 @@
345
346
  --sidebar-border: oklch(0.38 0.010 270);
346
347
  --sidebar-ring: oklch(0.85 0 0);
347
348
  --sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
348
- --secondary-panel-bg: oklch(0.23 0.02 270);
349
+ /* Nested secondary rail — dark: subtle brand lift on canvas (not flat `--background` only). */
350
+ --secondary-panel-bg: color-mix(in oklch, var(--background) 82%, var(--brand-color) 18%);
349
351
  --theme-color-chrome: #2f2d36;
350
352
 
351
353
  /* Lifted scrim on dark — white-tinted veil, not heavy black */
@@ -21,7 +21,7 @@ description: >
21
21
  - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
22
  - **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
23
23
  - **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
24
- - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
24
+ - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-mono-ids`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
25
25
 
26
26
  ---
27
27
 
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Exxat DS — monospace typography for record IDs, question IDs, and other system identifiers
3
+ globs: apps/web/**/*.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Exxat DS — monospace IDs
8
+
9
+ Use this when rendering **system identifiers** — values a user copies, searches, or matches in APIs and tables (not human-readable names or prose).
10
+
11
+ ## MUST
12
+
13
+ 1. **Class** — Wrap identifier text in **`font-mono tabular-nums`**. Add size/color from context: typically **`text-xs text-muted-foreground`** (secondary line, table meta) or **`text-sm`** when the ID is the primary label in a narrow cell.
14
+ 2. **What counts as an ID** — Question IDs (`questionId`, `Q-YYMM-XXXX`), record/entity keys shown in UI, folder/surface technical keys when displayed as identifiers, hex tokens in pickers, audit/log principals, site/row **`id`** columns meant for lookup.
15
+ 3. **Mixed lines** — When an ID sits beside prose (e.g. page subtitle), only the ID segment is mono; keep separators and labels in the default sans stack.
16
+
17
+ ## SHOULD
18
+
19
+ - Match existing hubs: **`question-bank-table.tsx`**, **`question-bank-list-view.tsx`**, **`new-question-composer.tsx`** (header subtitle), **`sites-table.tsx`** (`row.id`).
20
+ - Prefer **`truncate`** / **`min-w-0`** on mono IDs in tight layouts so long tokens do not blow out columns.
21
+
22
+ ## MUST NOT
23
+
24
+ - Apply **`font-mono`** to **person names**, **folder display names**, **status labels**, **dates**, **counts**, **currency**, or **body copy** — only the identifier token.
25
+ - Use mono for **option letters** (A/B/C) or **step numbers** unless they are literal system IDs.
26
+
27
+ ## See also
28
+
29
+ - **`.cursor/skills/exxat-mono-ids/SKILL.md`**
30
+ - **`apps/web/AGENTS.md`** — §1 item on IDs, §13 checklist
@@ -18,21 +18,22 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
18
18
  6. **Before** adding or changing **board (kanban) cards** on list hubs, read **§4.4** and the **`exxat-board-cards`** skill (**`.cursor/skills/`** or **`.claude/skills/`** at repo root — same content).
19
19
  7. **Before** adding **folder, panel, or other non-table view bodies** (centered grids, reusable shells), read **§4.5** and **`.cursor/rules/exxat-list-page-view-shells.mdc`** / **`.cursor/skills/exxat-list-page-view-shells/SKILL.md`**.
20
20
  8. **Before** adding or changing **Font Awesome** icons in app UI, read **`.cursor/rules/exxat-fontawesome-icons.mdc`** (Kit subsetting, weights, **`aria-hidden`** on **`<i>`**).
21
- 9. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Question bank style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
22
- 10. **Before** adding **shared access / invite collaborators** on a hub (face stack + invite sheet), read **§4.7** and **`.cursor/rules/exxat-collaboration-access.mdc`** / **`.cursor/skills/exxat-collaboration-access/SKILL.md`**.
23
- 11. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
24
- 12. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
25
- 13. **Before** choosing **drawer vs dialog vs new page** for a task flow, read **§6.4**, **`docs/data-views-pattern.md`** (Page vs drawer), and **`docs/drawer-vs-dialog-pattern.md`** (modal vs side panel on the same route).
26
- 14. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
27
- 15. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
21
+ 9. **Before** rendering **record IDs, question IDs, or other system identifiers**, read **`.cursor/rules/exxat-mono-ids.mdc`** and **`.cursor/skills/exxat-mono-ids/SKILL.md`** (**`font-mono tabular-nums`**).
22
+ 10. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Question bank style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
23
+ 11. **Before** adding **shared access / invite collaborators** on a hub (face stack + invite sheet), read **§4.7** and **`.cursor/rules/exxat-collaboration-access.mdc`** / **`.cursor/skills/exxat-collaboration-access/SKILL.md`**.
24
+ 12. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
25
+ 13. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
26
+ 14. **Before** choosing **drawer vs dialog vs new page** for a task flow, read **§6.4**, **`docs/data-views-pattern.md`** (Page vs drawer), and **`docs/drawer-vs-dialog-pattern.md`** (modal vs side panel on the same route).
27
+ 15. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
28
+ 16. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
28
29
  - **MUST** scan `components/` (especially `components/ui/`, `components/data-views/`, `components/templates/`, `components/key-metrics.tsx`, `components/page-header.tsx`, and the charts/banner/dot-pattern surfaces) **before** writing any new UI. If a primitive or composition already exists, **use it** — don't build a parallel one.
29
30
  - **Examples of existing surfaces to reuse:** card grid → `ListPageBoardCard` + `BoardCardIconRow` / `BoardCardTwoLineBlock`; AI / dot animation → `AiThinkingOverlay` + `DotPattern`; search input → `InputGroup` + `InputGroupAddon` + `InputGroupInput`; page title → `PageHeader` (serif via `font-heading`); list hub shell → `ListPageTemplate` (`metrics`, `defaultTabs`, `renderContent`); metrics strip → `KeyMetrics`; **view body gutter + centered max-width** → **`ListPageViewFrame`** (**§4.5**); **shared access / invite** → **`PageHeader` `variant="collaboration"`** + **`InviteCollaboratorsDrawer`** (**§4.7**).
30
31
  - **If** nothing fits and you would add a **new shared primitive or large bespoke widget**: **ask the user** for direction first — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already explicitly approved a greenfield build).
31
- 16. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
32
- 17. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
33
- 18. **Before** choosing **cards vs table rows vs simple lists** for a hub, read **`docs/card-vs-rows-pattern.md`** and **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
34
- 19. **Before** adding **`KeyMetrics`** strips on list hubs or dashboard key-metrics cards, read **`docs/kpi-strip-max-four-pattern.md`** and **`.cursor/rules/exxat-kpi-max-four.mdc`** (at most **four** tiles).
35
- 20. **Before** adding **new shared UI primitives** or bespoke widgets when nothing in **`components/`** fits after scanning, follow **`.cursor/rules/exxat-reuse-before-custom.mdc`** — **ask the user** for direction unless the task already approved a greenfield build.
32
+ 17. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
33
+ 18. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
34
+ 19. **Before** choosing **cards vs table rows vs simple lists** for a hub, read **`docs/card-vs-rows-pattern.md`** and **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
35
+ 20. **Before** adding **`KeyMetrics`** strips on list hubs or dashboard key-metrics cards, read **`docs/kpi-strip-max-four-pattern.md`** and **`.cursor/rules/exxat-kpi-max-four.mdc`** (at most **four** tiles).
36
+ 21. **Before** adding **new shared UI primitives** or bespoke widgets when nothing in **`components/`** fits after scanning, follow **`.cursor/rules/exxat-reuse-before-custom.mdc`** — **ask the user** for direction unless the task already approved a greenfield build.
36
37
 
37
38
  **Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/drawer-vs-dialog-pattern.md`, `docs/card-vs-rows-pattern.md`, `docs/kpi-strip-max-four-pattern.md`, `docs/command-menu-pattern.md`, `docs/collaboration-access-pattern.md` (keep in sync with this handbook for big refactors).
38
39
 
@@ -42,8 +43,8 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
42
43
 
43
44
  1. **User / task instructions** in the current session (highest).
44
45
  2. This **`AGENTS.md`** for Exxat DS product patterns.
45
- 3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-drawer-vs-dialog`, `exxat-card-vs-list-rows`, `exxat-kpi-max-four`, `exxat-reuse-before-custom`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-collaboration-access`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
46
- 4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**), **exxat-collaboration-access** (face rail + invite sheet + library access), **exxat-dedicated-search-surfaces** (landing vs results split, **`DedicatedSearch*`** templates + recents without hydration drift), **exxat-drawer-vs-dialog**, **exxat-card-vs-list-rows**, **exxat-kpi-max-four**.
46
+ 3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-drawer-vs-dialog`, `exxat-card-vs-list-rows`, `exxat-kpi-max-four`, `exxat-reuse-before-custom`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, **`exxat-mono-ids`**, `exxat-primary-nav-secondary-panel`, `exxat-collaboration-access`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
47
+ 4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**), **exxat-collaboration-access** (face rail + invite sheet + library access), **exxat-dedicated-search-surfaces** (landing vs results split, **`DedicatedSearch*`** templates + recents without hydration drift), **exxat-drawer-vs-dialog**, **exxat-card-vs-list-rows**, **exxat-kpi-max-four**, **`exxat-mono-ids`** (monospace system identifiers).
47
48
 
48
49
  If two documents conflict, prefer the **more specific** rule for the file type, then **newer** team decisions captured in `AGENTS.md`.
49
50
 
@@ -162,7 +163,9 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
162
163
 
163
164
  **MUST NOT:** Set **`secondaryPanel`** without **`PANELS[id]`** and **`useAutoPanel`** — users see **collapsed** main nav with **no** panel body.
164
165
 
165
- **Cursor rule:** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
166
+ **Folder-scoped library (Question bank):** When the URL is scoped to a folder (**`scope === "folder"`** + **`folderId`** via **`lib/question-bank-nav.ts`**), the hub **`QuestionBankPageHeader`** **⋯ More** menu **MUST** include **Customize folder** and open **`QuestionBankNewFolderSheet`** from the **hub client** so the action works on **every** **`ListPageTemplate`** view tab — not only inside **`QuestionBankTable`** branches that mount their own sheet. **Pattern:** **`docs/question-bank-hub-header-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
167
+
168
+ **Cursor rule (panel wiring):** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
166
169
 
167
170
  ### 4.7 Collaboration & access (shared hubs)
168
171
 
@@ -534,10 +537,11 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
534
537
  | Data view charts: **`ChartFigure`** + **`ChartDataTable`**; keyboard highlight via **`chart-keyboard-selection`** (§4.3); layout via **`data-view-dashboard-storage`** | Ad-hoc `localStorage` keys for dashboard layout; opacity-only “selection” without `activeBar`/`activeShape` |
535
538
  | Board cards: **`ListPageBoardCard`** shell; status via **`ListHubStatusBadge`** + **`lib/list-status-badges`**; no **`uppercase`** on status chips (§4.4) | One-off board card markup; status as plain body text; duplicated status maps outside **`list-status-badges`**; **empty placeholder** primary hubs (§4.1) |
536
539
  | **§4.5** — Non-table view bodies use **`ListPageViewFrame`** (+ **`data-views/`** shells); new grids are generic components, not route-only markup | Duplicated `mx-4` / `max-w-*` per hub; wrapping **`DataTable`** so inset **doubles** (**§5**) |
537
- | **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav | **`secondaryPanel`** id with no panel component or activator |
540
+ | **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav; **folder URL scope** → header **⋯** **Customize folder** + client-mounted **`QuestionBankNewFolderSheet`** (**`exxat-question-bank-hub-header.mdc`**) | **`secondaryPanel`** id with no panel component or activator; folder scope with customize **only** inside a single view tab’s subtree |
538
541
  | **§4.7** — **`PageHeader` `variant="collaboration"`** + **`CollaborationAccessFlow`** / **`InviteCollaboratorsDrawer`**; empty **Add collaborator** + non-empty face rail; roster + invite from **`collaborator-access.ts`** | Extra invite beside a populated face rail; per-person roster cards; forked access enums; toast on invite |
539
542
  | **§4.8** — **`DedicatedSearch*`** templates + composer + recents; **no** `localStorage` in **`useState`** initial paint; hub-specific **`patchSearchParams`** only | Forked `*QuestionBank*SearchLanding*` shells for another entity; hydration mismatch on recents |
540
543
  | **Font Awesome** — Kit in **`app/layout.tsx`**; **`fa-light` / `fa-solid`** conventions; **`aria-hidden`** on decorative **`<i>`**; run **`fa:subset-audit`** when adding glyphs (**`exxat-fontawesome-icons.mdc`**) | Parallel icon libraries for the same product chrome |
544
+ | **System IDs** — **`font-mono tabular-nums`** on question/record keys; mono **only** the ID token in mixed subtitles (**`exxat-mono-ids.mdc`**) | Mono on names, statuses, dates, or whole subtitle lines |
541
545
 
542
546
  ---
543
547
 
@@ -570,12 +574,13 @@ Copy and complete when implementing or reviewing:
570
574
  - [ ] **Accessibility:** §8 — tablist/toolbar patterns, **≥24px** targets for icon-only controls, contrast on tinted surfaces, dialog/sheet/drawer **titles**; **every icon that communicates info has a text alternative** — adjacent label (preferred) OR `aria-label` + `Tooltip` (§8.6 Case A/B/C, covers informational icons like calendar-for-date, status dots, AND icon-only buttons); **kbd inside a button uses `<Kbd variant="bare">`** (§8.7); re-run **axe** on Placements when changing views toolbar.
571
575
  - [ ] **Coach marks (§11):** `CoachMark` + `useCoachMark`; register in **`coach-mark-registry`**; use **`enabled`** / **`dependsOnDismissedFlowId`** when a tour must wait for another flow or a specific view (e.g. **dashboard**); customize-dashboard flows use **`lib/dashboard-customize-coach-mark.ts`**.
572
576
  - [ ] **Application sidebar (§9.1):** **`ExxatProductLogo`** for product; **`logoDevUrl`** for schools; team switcher **`DropdownMenuContent`** keeps the explicit wide school/program surface (**`!w-max`** + min/max width); expanded switcher **`h-auto min-h-12`**; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child links **popover** on icon rail; profile **`stockPortraitUrl`** + **`referrerPolicy="no-referrer"`** on **`AvatarImage`**.
573
- - [ ] **Secondary panel (§4.6):** If **`NavLinkItem.secondaryPanel`** is set — **`PANELS[id]`** in **`secondary-panel.tsx`**, hub mounts **`useAutoPanel(id)`**, scope syncs to URL + **`tableState.rows`** — **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
577
+ - [ ] **Secondary panel (§4.6):** If **`NavLinkItem.secondaryPanel`** is set — **`PANELS[id]`** in **`secondary-panel.tsx`**, hub mounts **`useAutoPanel(id)`**, scope syncs to URL + **`tableState.rows`** — **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Question bank folder scope:** header **⋯** → **Customize folder** + **`QuestionBankNewFolderSheet`** on **`QuestionBankClient`** — **`docs/question-bank-hub-header-pattern.md`**, **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
574
578
  - [ ] **Collaboration & access (§4.7):** Shared hubs use **`variant="collaboration"`**, empty **Add collaborator** / non-empty face rail, **⋯ → Invite people**, **`CollaborationAccessFlow`** or **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`**, roster **name → email → role tags** — **`.cursor/rules/exxat-collaboration-access.mdc`**.
575
579
  - [ ] **Dedicated search (§4.8):** Landing uses **`DedicatedSearchLandingTemplate`**; results use **`DedicatedSearchResultsHeaderChrome`** + outer **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`**; **`DedicatedSearchUrlComposer`** + **`DedicatedSearchRecents`** with **`createDedicatedSearchRecentsController`** — **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**.
576
580
  - [ ] **KPI trends:** **`MetricItem.trend`** matches the delta direction; **`trendPolarity`** set for “more is worse” metrics (flags, defects, overdue) — **`docs/kpi-trend-pattern.md`**, **`.cursor/rules/exxat-kpi-trends.mdc`**.
577
581
  - [ ] **Font Awesome:** New glyphs covered by **`fa:subset-audit`** / Kit subset; decorative **`<i>`** has **`aria-hidden`**; icon-only controls follow **§8.6** — **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
582
+ - [ ] **System IDs:** Visible **`questionId`**, record keys, and copy-pasteable identifiers use **`font-mono tabular-nums`**; mixed lines mono-wrap **only** the ID — **`.cursor/rules/exxat-mono-ids.mdc`**, **`.cursor/skills/exxat-mono-ids/SKILL.md`**.
578
583
 
579
584
  ---
580
585
 
581
- *Last updated: drawer vs dialog / card vs rows / KPI max-four pattern docs + rules + skills; §6.4 table; §4.8 dedicated search templates; §4.7 collaboration & access; §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
586
+ *Last updated: monospace system IDs rule/skill; question bank folder-scoped header Customize + rule/skill; drawer vs dialog / card vs rows / KPI max-four pattern docs + rules + skills; §6.4 table; §4.8 dedicated search templates; §4.7 collaboration & access; §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
@@ -1,10 +1,10 @@
1
- import { DataListClient } from "@/components/data-list-client"
1
+ import { PlacementsClient } from "@/components/placements-client"
2
2
  import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
3
3
 
4
4
  export default function DataListPage() {
5
5
  return (
6
6
  <PrimaryPageTemplate siteHeader={{ title: "List hub" }}>
7
- <DataListClient />
7
+ <PlacementsClient />
8
8
  </PrimaryPageTemplate>
9
9
  )
10
10
  }
@@ -4,19 +4,37 @@ import * as React from "react"
4
4
  import { usePathname } from "next/navigation"
5
5
 
6
6
  import { useSecondaryPanel } from "@/components/secondary-panel"
7
- import { QUESTION_BANK_ENTRY_PATH } from "@/lib/question-bank-nav"
7
+ import {
8
+ QUESTION_BANK_ENTRY_PATH,
9
+ QUESTION_BANK_HUB_FIND_PATH,
10
+ QUESTION_BANK_LIST_PATH,
11
+ } from "@/lib/question-bank-nav"
12
+
13
+ /** Full-page focused flows under `/question-bank/*` that suppress the secondary panel. */
14
+ const QUESTION_BANK_FOCUSED_FLOW_PATHS: readonly string[] = ["/question-bank/new"]
15
+
16
+ function isQuestionBankFocusedFlow(pathname: string): boolean {
17
+ return QUESTION_BANK_FOCUSED_FLOW_PATHS.some(p => pathname === p || pathname.startsWith(`${p}/`))
18
+ }
8
19
 
9
20
  /**
10
- * Keeps the nested secondary panel open across library / list / find navigations.
11
- * The discovery hub (`/question-bank`) is full-width no secondary bar there.
21
+ * Keeps the nested secondary panel open across library navigations.
22
+ * **Question hub** (`/question-bank`), **Search** (`/find`, `/list`), and
23
+ * focused authoring routes (`/new`) stay full-width — no secondary rail.
12
24
  */
13
25
  export default function QuestionBankLayout({ children }: { children: React.ReactNode }) {
14
26
  const pathname = usePathname()
15
27
  const { openPanel, closePanel, activePanel } = useSecondaryPanel()
16
28
  const closePanelRef = React.useRef(closePanel)
17
29
  const openPanelRef = React.useRef(openPanel)
18
- closePanelRef.current = closePanel
19
- openPanelRef.current = openPanel
30
+ // "Latest ref" pattern — keep callbacks current without re-running the
31
+ // route effect below on every render. `useEffect` (no deps) updates the
32
+ // refs after each render; React's `refs` rule disallows direct ref writes
33
+ // during render so the assignment lives here instead.
34
+ React.useEffect(() => {
35
+ closePanelRef.current = closePanel
36
+ openPanelRef.current = openPanel
37
+ })
20
38
 
21
39
  /** Leaving `/question-bank/*` entirely — close nested panel without forcing primary sidebar open (⌘B / cookie). */
22
40
  React.useEffect(() => {
@@ -30,7 +48,10 @@ export default function QuestionBankLayout({ children }: { children: React.React
30
48
  const isDiscoveryHubRoot =
31
49
  pathname === QUESTION_BANK_ENTRY_PATH || pathname === `${QUESTION_BANK_ENTRY_PATH}/`
32
50
 
33
- if (isDiscoveryHubRoot) {
51
+ const isDedicatedSearchSurface =
52
+ pathname === QUESTION_BANK_HUB_FIND_PATH || pathname === QUESTION_BANK_LIST_PATH
53
+
54
+ if (isDiscoveryHubRoot || isDedicatedSearchSurface || isQuestionBankFocusedFlow(pathname)) {
34
55
  closePanelRef.current({ mainSidebar: "leave" })
35
56
  return undefined
36
57
  }
@@ -39,7 +60,6 @@ export default function QuestionBankLayout({ children }: { children: React.React
39
60
  openPanelRef.current("question-bank")
40
61
  }
41
62
  return undefined
42
- // eslint-disable-next-line react-hooks/exhaustive-deps -- pathname + activePanel drive when to ensure panel id
43
63
  }, [pathname, activePanel])
44
64
 
45
65
  return children
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `/question-bank/new` — full-page authoring composer.
3
+ *
4
+ * Mirrors focused flows: `SiteHeader` back nav (← Question hub) + `PageHeader` title
5
+ * • `SidebarAutoCollapse` mounted in `beforeSiteHeader` so the main sidebar
6
+ * collapses on enter and restores to its prior state on leave
7
+ * • A wider max-width than the placement form (the composer carries a
8
+ * two-column document + metadata-rail layout)
9
+ *
10
+ * The body is `NewQuestionComposer` — see `components/new-question-composer.tsx`.
11
+ *
12
+ * Folder pre-selection: callers may pass `?folderId=<id>` so users dropped into
13
+ * the composer from a folder-scoped library land with that folder pre-selected
14
+ * in the metadata rail.
15
+ */
16
+
17
+ import { NewQuestionComposer } from "@/components/new-question-composer"
18
+ import { SidebarAutoCollapse } from "@/components/sidebar-auto-collapse"
19
+ import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
20
+ import {
21
+ DEFAULT_QUESTION_BANK_FOLDERS,
22
+ type QuestionBankFolder,
23
+ } from "@/lib/mock/question-bank-folders"
24
+ import { generateDraftQuestionId } from "@/lib/question-bank-authoring"
25
+ import { newQuestionBackNav } from "@/lib/question-bank-nav"
26
+
27
+ interface NewQuestionPageProps {
28
+ searchParams: Promise<{ folderId?: string }>
29
+ }
30
+
31
+ export default async function NewQuestionPage({ searchParams }: NewQuestionPageProps) {
32
+ const params = await searchParams
33
+ const folders: QuestionBankFolder[] = DEFAULT_QUESTION_BANK_FOLDERS
34
+
35
+ const requested = typeof params.folderId === "string" ? params.folderId : undefined
36
+ const matched = requested ? folders.find(f => f.id === requested) : undefined
37
+ const defaultFolderId = matched?.id
38
+
39
+ const back = newQuestionBackNav(folders, defaultFolderId)
40
+ const draftQuestionId = generateDraftQuestionId()
41
+
42
+ return (
43
+ <PrimaryPageTemplate
44
+ beforeSiteHeader={<SidebarAutoCollapse />}
45
+ siteHeader={{ back }}
46
+ bodyClassName="min-h-0 flex-1 overflow-y-auto overscroll-y-contain"
47
+ maxWidthClassName="mx-auto w-full max-w-[1100px]"
48
+ contentClassName="px-8 py-4 pb-16"
49
+ >
50
+ <NewQuestionComposer
51
+ draftQuestionId={draftQuestionId}
52
+ defaultFolderId={defaultFolderId}
53
+ backHref={back.href}
54
+ folders={folders}
55
+ />
56
+ </PrimaryPageTemplate>
57
+ )
58
+ }
@@ -151,6 +151,14 @@ html[data-text-size="large"] {
151
151
  via a scoped class, not globally on :root. */
152
152
  /* Minimum readable UI copy: 11px (`text-xs` / --text-xs). Do not use smaller arbitrary sizes. */
153
153
 
154
+ /* ── Layout ─────────────────────────────────────────────────── */
155
+ /* SiteHeader / breadcrumb height. Mirrored on the SidebarProvider for
156
+ descendants; declared here too so JS that reads from documentElement
157
+ (e.g. DataTable sticky-head offset) can resolve it. Plain px so
158
+ `parseFloat` can read it as a number. Keep in sync with `headerHeight`
159
+ in components/sidebar-shell.tsx (currently `calc(var(--spacing) * 12)` = 48px). */
160
+ --header-height: 48px;
161
+
154
162
  /* ── Typography ─────────────────────────────────────────────── */
155
163
  /* Ivy Presto loaded via Adobe Fonts Kit wuk5wqn (use.typekit.net) */
156
164
  --font-heading: "ivypresto-text";
@@ -171,6 +179,19 @@ html[data-text-size="large"] {
171
179
  --brand-preview-one: var(--brand-tint);
172
180
  --brand-preview-prism: oklch(0.57 0.24 342);
173
181
 
182
+ /**
183
+ * Ask Leo panel tints (same mixes as the vertical wash). Use for blobs, cards, etc.
184
+ * `--leo-surface-gradient` is the full linear wash on `AskLeoSidebar`.
185
+ */
186
+ --leo-surface-tint-a: color-mix(in oklch, var(--brand-color) 4%, var(--background));
187
+ --leo-surface-tint-b: color-mix(in oklch, var(--brand-color) 8%, var(--background));
188
+ --leo-surface-gradient: linear-gradient(180deg, var(--leo-surface-tint-a) 0%, var(--leo-surface-tint-b) 100%);
189
+
190
+ /* KeyMetrics `variant="flat"` — soft KPI band (lavender wash → canvas) */
191
+ --key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-tint-light) 90%, var(--background));
192
+ --key-metrics-flat-grad-mid: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
193
+ --key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 80%, var(--brand-tint-light));
194
+
174
195
  /* ── Surfaces ────────────────────────────────────────────────── */
175
196
  --background: oklch(1 0 0);
176
197
  --foreground: oklch(0.145 0 0); /* ≈ #1A1A1A — 17:1 on white ✓ */
@@ -249,7 +270,8 @@ html[data-text-size="large"] {
249
270
  --sidebar-ring: oklch(0.25 0 0);
250
271
  /* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
251
272
  --sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
252
- --secondary-panel-bg: oklch(0.99 0.008 286.1);
273
+ /* Nested secondary rail — soft brand wash on canvas (not pure `--background`, not full `--sidebar`). */
274
+ --secondary-panel-bg: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
253
275
 
254
276
  /* Browser UI (meta theme-color) — aligned with --brand-tint */
255
277
  --theme-color-chrome: #f3f2f8;
@@ -357,6 +379,10 @@ html[data-text-size="large"] {
357
379
  --destructive: oklch(0.65 0.20 25); /* brighter for dark bg */
358
380
  --destructive-foreground: oklch(0.10 0 0);
359
381
 
382
+ --key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-color) 14%, var(--background));
383
+ --key-metrics-flat-grad-mid: color-mix(in oklch, var(--muted) 42%, var(--background));
384
+ --key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 88%, var(--brand-color));
385
+
360
386
  /* Borders — visible but not washed out on dark surfaces */
361
387
  --border: oklch(0.38 0.008 270);
362
388
  --border-control: oklch(0.72 0.012 270);
@@ -405,7 +431,8 @@ html[data-text-size="large"] {
405
431
  --sidebar-border: oklch(0.38 0.010 270);
406
432
  --sidebar-ring: oklch(0.85 0 0);
407
433
  --sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
408
- --secondary-panel-bg: oklch(0.23 0.02 270);
434
+ /* Nested secondary rail — dark: subtle brand lift on canvas (not flat `--background` only). */
435
+ --secondary-panel-bg: color-mix(in oklch, var(--background) 82%, var(--brand-color) 18%);
409
436
  --theme-color-chrome: #2f2d36;
410
437
 
411
438
  /* Lifted scrim on dark — white-tinted veil, not heavy black */
@@ -506,6 +533,99 @@ html[data-text-size="large"] {
506
533
  --theme-color-chrome: #2a2428;
507
534
  }
508
535
 
536
+ /* ==========================================================================
537
+ Theme: Exxat Assessment · Green · hue 159.88
538
+ Usage: <html class="theme-assessment">
539
+ ========================================================================== */
540
+ .theme-assessment {
541
+ --brand-tint: oklch(0.965 0.018 159.88);
542
+ --brand-tint-light: oklch(0.992 0.008 159.88);
543
+ --brand-tint-subtle: oklch(0.93 0.028 159.88);
544
+ --brand-color: oklch(0.7 0.0913 159.88);
545
+ --brand-color-light: oklch(0.82 0.072 159.88);
546
+ --brand-color-dark: oklch(0.48 0.0913 159.88);
547
+ --brand-color-deep: oklch(0.34 0.075 159.88);
548
+ --ring: var(--brand-color-dark);
549
+ }
550
+
551
+ .theme-assessment:not(.dark) {
552
+ --sidebar: var(--brand-tint);
553
+ --sidebar-accent: oklch(0.945 0.026 159.88);
554
+ --sidebar-border: oklch(0.90 0.026 159.88);
555
+ --secondary: oklch(0.95 0.012 159.88);
556
+ --accent: oklch(0.925 0.016 159.88);
557
+ --muted: oklch(0.945 0.008 159.88);
558
+ --theme-color-chrome: #f1faf5;
559
+ }
560
+
561
+ .theme-assessment.dark,
562
+ .dark.theme-assessment {
563
+ --ring: var(--brand-color);
564
+ --background: oklch(0.20 0.008 159.88);
565
+ --sidebar: oklch(0.245 0.015 159.88);
566
+ --sidebar-accent: oklch(0.30 0.012 159.88);
567
+ --sidebar-border: oklch(0.38 0.010 159.88);
568
+ --secondary: oklch(0.31 0.04 159.88);
569
+ --muted: oklch(0.31 0.04 159.88);
570
+ --accent: oklch(0.33 0.06 159.88);
571
+ --theme-color-chrome: #242a27;
572
+ }
573
+
574
+ /* ==========================================================================
575
+ Theme: Custom product · user-selected hue
576
+ Usage: <html class="theme-custom" style="--custom-product-brand-color: …">
577
+ ========================================================================== */
578
+ /*
579
+ * Tint chroma is **derived from the source's chroma** so neighbouring hues
580
+ * (e.g. Blue h=252 vs Indigo h=280 vs Purple h=286) read as distinct pale
581
+ * tints instead of collapsing to the same near-white. The earlier formula
582
+ * pinned chroma to a fixed `0.018`, which is below the hue-discrimination
583
+ * threshold at L≈0.96 for closely-spaced hues — that's why Blue / Indigo /
584
+ * Purple looked identical in the sidebar.
585
+ *
586
+ * `max(<floor>, calc(c * <fraction>))` rules:
587
+ * - vibrant brand colours (c ≈ 0.10–0.23) get a meaningfully tinted
588
+ * sidebar / accent / muted scale → product chrome visibly changes per
589
+ * pick
590
+ * - low-chroma custom-hex picks (greys) fall back to the floor so they
591
+ * don't render as pure white
592
+ * - lightness + hue stay pinned to the original mock-up values, so the
593
+ * overall "very pale background" feel is unchanged
594
+ */
595
+ .theme-custom {
596
+ --brand-tint: oklch(from var(--custom-product-brand-color) 0.965 max(0.018, calc(c * 0.20)) h);
597
+ --brand-tint-light: oklch(from var(--custom-product-brand-color) 0.992 max(0.008, calc(c * 0.08)) h);
598
+ --brand-tint-subtle: oklch(from var(--custom-product-brand-color) 0.93 max(0.028, calc(c * 0.30)) h);
599
+ --brand-color: var(--custom-product-brand-color);
600
+ --brand-color-light: oklch(from var(--custom-product-brand-color) 0.82 c h);
601
+ --brand-color-dark: oklch(from var(--custom-product-brand-color) 0.48 c h);
602
+ --brand-color-deep: oklch(from var(--custom-product-brand-color) 0.34 c h);
603
+ --ring: var(--brand-color-dark);
604
+ }
605
+
606
+ .theme-custom:not(.dark) {
607
+ --sidebar: var(--brand-tint);
608
+ --sidebar-accent: oklch(from var(--custom-product-brand-color) 0.945 max(0.026, calc(c * 0.28)) h);
609
+ --sidebar-border: oklch(from var(--custom-product-brand-color) 0.90 max(0.026, calc(c * 0.28)) h);
610
+ --secondary: oklch(from var(--custom-product-brand-color) 0.95 max(0.012, calc(c * 0.14)) h);
611
+ --accent: oklch(from var(--custom-product-brand-color) 0.925 max(0.016, calc(c * 0.18)) h);
612
+ --muted: oklch(from var(--custom-product-brand-color) 0.945 max(0.008, calc(c * 0.10)) h);
613
+ --theme-color-chrome: color-mix(in oklch, var(--custom-product-brand-color) 10%, white);
614
+ }
615
+
616
+ .theme-custom.dark,
617
+ .dark.theme-custom {
618
+ --ring: var(--brand-color);
619
+ --background: oklch(from var(--custom-product-brand-color) 0.20 max(0.008, calc(c * 0.10)) h);
620
+ --sidebar: oklch(from var(--custom-product-brand-color) 0.245 max(0.015, calc(c * 0.18)) h);
621
+ --sidebar-accent: oklch(from var(--custom-product-brand-color) 0.30 max(0.012, calc(c * 0.14)) h);
622
+ --sidebar-border: oklch(from var(--custom-product-brand-color) 0.38 max(0.010, calc(c * 0.12)) h);
623
+ --secondary: oklch(from var(--custom-product-brand-color) 0.31 max(0.04, calc(c * 0.40)) h);
624
+ --muted: oklch(from var(--custom-product-brand-color) 0.31 max(0.04, calc(c * 0.40)) h);
625
+ --accent: oklch(from var(--custom-product-brand-color) 0.33 max(0.06, calc(c * 0.50)) h);
626
+ --theme-color-chrome: color-mix(in oklch, var(--custom-product-brand-color) 12%, black);
627
+ }
628
+
509
629
  /* ==========================================================================
510
630
  HIGH CONTRAST MODE
511
631
  ──────────────────────────────────────────────────────────────────────────
@@ -1777,6 +1897,20 @@ html:is([data-contrast="high"], [data-contrast="windows"]) [data-slot="coach-mar
1777
1897
  100% { opacity: 1; transform: scale(1) rotate(0); }
1778
1898
  }
1779
1899
 
1900
+ /* Radix Collapsible / Accordion — slide the children open/closed using the
1901
+ `--radix-collapsible-content-height` CSS var that Radix sets on the
1902
+ content element. Mirrors the shadcn accordion pattern (Tailwind v3
1903
+ "accordion-down" / "accordion-up"); we keep our own name so it can be
1904
+ reused by both Collapsible and Accordion primitives. */
1905
+ @keyframes collapsible-down {
1906
+ from { height: 0; }
1907
+ to { height: var(--radix-collapsible-content-height); }
1908
+ }
1909
+ @keyframes collapsible-up {
1910
+ from { height: var(--radix-collapsible-content-height); }
1911
+ to { height: 0; }
1912
+ }
1913
+
1780
1914
  /* Ask Leo suggestion chips — staggered fade-in + lift on first render. */
1781
1915
  @keyframes leo-chip-in {
1782
1916
  0% { opacity: 0; transform: translateY(8px) scale(0.96); }
@@ -59,14 +59,50 @@ export default function RootLayout({
59
59
  <head>
60
60
  {/* Default until ThemeColorSync hydrates (brand + mode override client-side) */}
61
61
  <meta name="theme-color" content="#f6f3ff" />
62
- {/* Adobe Fonts — preconnect + preload Ivy Presto · Kit ID: wuk5wqn */}
63
- <link rel="preconnect" href="https://use.typekit.net" />
62
+ {/*
63
+ * Adobe Fonts — preconnect + preload Ivy Presto · Kit ID: wuk5wqn.
64
+ *
65
+ * Trust model & Subresource Integrity (SRI):
66
+ * - Adobe Typekit serves *dynamically subsetted* CSS that hashes
67
+ * differently per response, so SRI cannot be applied — the kit URL
68
+ * is locked to a single Adobe-owned origin instead.
69
+ * - `crossOrigin=""` (anonymous CORS) is set on both the preconnect
70
+ * and the stylesheet so the browser uses one CORS-correct
71
+ * connection for preconnect + preload + fetch, and so styles
72
+ * cannot read first-party cookies or credentials.
73
+ * - The same origins are pinned in `style-src` / `font-src` /
74
+ * `connect-src` of the Content-Security-Policy declared in
75
+ * `next.config.mjs`, which is what actually prevents arbitrary
76
+ * third-party CSS/fonts from loading.
77
+ */}
78
+ <link rel="preconnect" href="https://use.typekit.net" crossOrigin="" />
64
79
  <link rel="preconnect" href="https://p.typekit.net" crossOrigin="" />
65
- <link rel="preload" href="https://use.typekit.net/wuk5wqn.css" as="style" />
66
- <link rel="stylesheet" href="https://use.typekit.net/wuk5wqn.css" />
80
+ <link
81
+ rel="preload"
82
+ href="https://use.typekit.net/wuk5wqn.css"
83
+ as="style"
84
+ crossOrigin=""
85
+ />
86
+ <link
87
+ rel="stylesheet"
88
+ href="https://use.typekit.net/wuk5wqn.css"
89
+ crossOrigin=""
90
+ />
67
91
  </head>
68
92
  <body className="bg-sidebar text-foreground font-sans">
69
- {/* Font Awesome Pro Kit — subset via fontawesome-subset.manifest.json + fontawesome.com/kits (Icon Selection). */}
93
+ {/*
94
+ * Font Awesome Pro Kit — subset via fontawesome-subset.manifest.json +
95
+ * fontawesome.com/kits (Icon Selection).
96
+ *
97
+ * Trust model & SRI: the kit loader URL is content-versioned by
98
+ * fontawesome.com and rotates whenever the subset changes, so SRI
99
+ * cannot be pinned. We instead:
100
+ * - Restrict the script to kit.fontawesome.com (and font/data fetch
101
+ * to ka-f.fontawesome.com) via CSP `script-src` / `font-src` /
102
+ * `connect-src` in `next.config.mjs`.
103
+ * - Load with `crossOrigin="anonymous"` so the script runs with
104
+ * CORS semantics and no credentials.
105
+ */}
70
106
  <Script
71
107
  src="https://kit.fontawesome.com/d9bd5774e0.js"
72
108
  crossOrigin="anonymous"