@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.
- package/CHANGELOG.md +23 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +151 -3
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +17 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +1 -0
- package/src/globals.css +21 -2
- package/src/theme.css +4 -2
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +23 -18
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/question-bank/layout.tsx +27 -7
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/globals.css +136 -2
- package/template/app/layout.tsx +41 -5
- package/template/components/app-sidebar.tsx +141 -59
- package/template/components/ask-leo-sidebar.tsx +1 -4
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +24 -0
- package/template/components/data-table/index.tsx +68 -24
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +243 -94
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +26 -3
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/os-folder-glyph.tsx +8 -0
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +173 -379
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +116 -51
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +21 -11
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +18 -132
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
- package/template/components/product-switcher.tsx +26 -11
- package/template/components/product-wordmark.tsx +285 -0
- package/template/components/question-bank-client.tsx +130 -70
- package/template/components/question-bank-hub-client.tsx +108 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -228
- package/template/components/question-bank-table.tsx +30 -5
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +24 -4
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/site-header.tsx +56 -32
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +24 -0
- package/template/components/table-properties/drawer.tsx +1 -1
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +29 -3
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +11 -6
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +51 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +70 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/table-state-lifecycle.ts +474 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +6 -6
- package/template/stores/app-store.ts +46 -1
- package/template/components/command-menu-01.tsx +0 -133
- 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
|
-
|
|
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
|
-
|
|
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
|
package/template/AGENTS.md
CHANGED
|
@@ -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**
|
|
22
|
-
10. **Before** adding **
|
|
23
|
-
11. **Before** adding **
|
|
24
|
-
12. **Before**
|
|
25
|
-
13. **Before**
|
|
26
|
-
14. **Before**
|
|
27
|
-
15.
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
**
|
|
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 {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
|
11
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/template/app/globals.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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); }
|
package/template/app/layout.tsx
CHANGED
|
@@ -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
|
-
{/*
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
{/*
|
|
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"
|