@exxatdesignux/ui 0.3.0 → 0.4.1
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 +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
package/template/AGENTS.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
**Scope:** The Next.js app in this directory. **Path:** If your workspace root is only this folder, use **`./AGENTS.md`**. If the workspace is the parent monorepo, use **`apps/web/AGENTS.md`** (this file).
|
|
6
6
|
|
|
7
|
+
> **First-time reader?** Open **`docs/HANDBOOK.md`** first — it's a 10-minute orientation that links into the right section here. This file is the **authoritative §-numbered manual**; the handbook is the map. Other quick entry points: **`docs/glossary.md`** (shared vocabulary), **`docs/voice-and-tone.md`** (copy rules), **`docs/reference-implementations.md`** (canonical reference pages).
|
|
8
|
+
|
|
7
9
|
Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tables, keyboard hints, accessibility) when the parent repo is open.
|
|
8
10
|
|
|
9
11
|
---
|
|
@@ -19,7 +21,7 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
|
|
|
19
21
|
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
22
|
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
23
|
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** (
|
|
24
|
+
10. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Library style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
|
|
23
25
|
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
26
|
12. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
|
|
25
27
|
13. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
|
|
@@ -56,7 +58,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
56
58
|
|
|
57
59
|
## 3. Data tables (product lists)
|
|
58
60
|
|
|
59
|
-
**MUST** for any screen that is a **browsable, filterable grid of records** (
|
|
61
|
+
**MUST** for any screen that is a **browsable, filterable grid of records** (directories, tokens, library items, columns showcase, etc.):
|
|
60
62
|
|
|
61
63
|
| Requirement | Action |
|
|
62
64
|
|-------------|--------|
|
|
@@ -65,7 +67,9 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
65
67
|
| Filters | Use the **shared filter model** (`FilterFieldDef`, operators, chips) consistent with existing list pages. |
|
|
66
68
|
| Table properties | Expose **Table properties** via **`TablePropertiesDrawer`** (`@/components/table-properties`) — columns, density, related options — same pattern as Placements / data list. |
|
|
67
69
|
|
|
68
|
-
**Reference:** `components/
|
|
70
|
+
**Reference:** `components/library-table.tsx`, `components/columns-showcase.tsx`, `components/data-table/`.
|
|
71
|
+
|
|
72
|
+
**Scaling past ~2K rows:** Turn on **`pagination={true}`** on **`HubTable`** and pick a page size; lists + boards already auto-virtualize at 100 rows. Beyond ~50K, lift filter / sort / page state via **`paginationOverride`** and fetch one page at a time — full upgrade path in **`docs/large-dataset-strategy.md`**.
|
|
69
73
|
|
|
70
74
|
**MUST NOT:** Build product list pages with only `@/components/ui/table`, raw `<table>`, or a third-party grid that bypasses this stack.
|
|
71
75
|
|
|
@@ -77,7 +81,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
77
81
|
|
|
78
82
|
**MUST:** If the main surface is a **`DataTable`** (or equivalent data grid), wrap it in **`ListPageTemplate`** so the **views toolbar** exists (tabs, add view, per-tab settings). Do **not** place `DataTable` only under `PageHeader` without the tab shell.
|
|
79
83
|
|
|
80
|
-
**Reference implementations:** `components/
|
|
84
|
+
**Reference implementations:** `components/library-hub-client.tsx` (full hub), `components/columns-showcase.tsx` (single-view catalog), `components/tokens-themes-client.tsx` (hub + secondary panel).
|
|
81
85
|
|
|
82
86
|
**Rationale:** Consistent navigation, saved views, per-tab view type (table / list / board / dashboard), export at template level.
|
|
83
87
|
|
|
@@ -87,7 +91,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
87
91
|
|
|
88
92
|
**MUST NOT** ship a **new primary nav hub** as an **empty or placeholder-only page** (e.g. a paragraph saying “replace this later” with no **`DataTable`**, mock data, or connected views). When a route is linked from **`lib/mock/navigation.tsx`**, land users on the same **hub stack** as Team / Placements: **`ListPageTemplate`** + typed mock rows (typically **≥ ~12**), search, filters, **`TablePropertiesDrawer`**, and all view tabs the template supports (**§4.1**), unless the product explicitly scopes a route as a non-data shell (rare).
|
|
89
93
|
|
|
90
|
-
**Mock data:** Put typed row arrays in **`lib/mock/<entity>.ts`**. Add **`lib/mock/<entity>-kpi.ts`** (or colocated helpers) with pure functions **`entityKpiMetrics(rows)`** / **`entityKpiInsight(rows)`** returning **`MetricItem[]`** / **`MetricInsight`** for **`KeyMetrics`**. Each **`MetricItem`** must set **`trend`** to match the signed change; use **`trendPolarity`** when an increase is **not** favorable (defects, review flags, overdue — see **`docs/kpi-trend-pattern.md`** and **`.cursor/rules/exxat-kpi-trends.mdc`**). **`entityKpiMetrics`** for **`ListPageTemplate`** metrics and Data-tab key-metrics cards: return **at most four** **`MetricItem`** — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`** (`KEY_METRICS_KPI_COUNT_MAX`), **`.cursor/rules/exxat-kpi-max-four.mdc`**. The page client passes full mock rows into one table component; KPI helpers receive **`tableState.rows`** inside that component so search/filters apply to list, board, dashboard, and table together.
|
|
94
|
+
**Mock data:** Put typed row arrays in **`lib/mock/<entity>.ts`**. Add **`lib/mock/<entity>-kpi.ts`** (or colocated helpers) with pure functions **`entityKpiMetrics(rows)`** / **`entityKpiInsight(rows)`** returning **`MetricItem[]`** / **`MetricInsight`** for **`KeyMetrics`**. Each **`MetricItem`** must set **`trend`** to match the signed change; use **`trendPolarity`** when an increase is **not** favorable (defects, review flags, overdue — see **`docs/kpi-trend-pattern.md`** and **`.cursor/rules/exxat-kpi-trends.mdc`**). **`delta`** is the **count change** that renders next to the arrow (e.g. `"+5"`, `"-3"`); contextual prose like `"left + right"` / `"vs last week"` goes in **`MetricItem.description`** (caption below the value). When there is no direction *and* no count, leave **`delta: ""`** + **`trend: "neutral"`** — **`KeyMetrics`** hides the chip; **MUST NOT** hand-roll a `—` placeholder. **`entityKpiMetrics`** for **`ListPageTemplate`** metrics and Data-tab key-metrics cards: return **at most four** **`MetricItem`** — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`** (`KEY_METRICS_KPI_COUNT_MAX`), **`.cursor/rules/exxat-kpi-max-four.mdc`**. The page client passes full mock rows into one table component; KPI helpers receive **`tableState.rows`** inside that component so search/filters apply to list, board, dashboard, and table together.
|
|
91
95
|
|
|
92
96
|
**Centralized dataset (rows + table properties + alternate views):** **MUST** use one **`useTableState`** row bag for the **`DataTable`**, **`TablePropertiesDrawer`** (columns/density on **that** table), and **every** record-bearing **`DataListViewType`** — **folder**, **panel**, **tree**, etc. — via **`tableState.rows`**. **MUST NOT** import a second **`lib/mock/<entity>`** array into a view-only module while the grid filters state; **MUST NOT** fork a duplicate row type for inspectors. Shared **properties**: tab labels **`DATA_LIST_VIEW_TILES`** (`lib/data-list-view.ts`), status **`lib/list-status-badges.ts`**, KPI helpers from **`tableState.rows`**. **Presentation:** non-table bodies use **`ListPageViewFrame`** and **`components/data-views/`** primitives fed by the **same** **`tableState.rows`** (**§4.5**). **Rule + skill:** **`.cursor/rules/exxat-centralized-list-dataset.mdc`**, **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`**.
|
|
93
97
|
|
|
@@ -106,7 +110,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
106
110
|
|
|
107
111
|
Thread **`view`** and **`onViewChange`** from the **client** → **table / toolbar wrapper** → **`TablePropertiesDrawer`**. If **`currentView`** is omitted, the drawer defaults to **table** labels and controls even on **Board**, which is incorrect.
|
|
108
112
|
|
|
109
|
-
**Reference:** `components/
|
|
113
|
+
**Reference:** `components/library-table.tsx`, `components/columns-showcase.tsx`, `components/tokens-themes-client.tsx`. Root **`.cursor/rules/exxat-table-properties-drawer.mdc`**.
|
|
110
114
|
|
|
111
115
|
#### Deep-linking the drawer to a specific panel
|
|
112
116
|
|
|
@@ -146,15 +150,15 @@ The drawer's "View type" tile grid (and the Export drawer's "File format" grid)
|
|
|
146
150
|
| Topic | Rule |
|
|
147
151
|
|-------|------|
|
|
148
152
|
| **Accessibility** | Each chart uses **`ChartFigure`** (keyboard + live region) and **`ChartDataTable`** (`sr-only` table fallback), inside **`ChartCard`** — same stack as **`charts-overview.tsx`**. **MUST NOT** ship bare Recharts-only charts on these surfaces. |
|
|
149
|
-
| **Two “dashboard” surfaces** | The **`/dashboard`** route uses **`DashboardTabs`** + **`ChartsOverview`** (gallery / demos). The **Data** tab on
|
|
153
|
+
| **Two “dashboard” surfaces** | The **`/dashboard`** route uses **`DashboardTabs`** + **`ChartsOverview`** (gallery / demos). The **Data** tab on a hub uses a hub-specific `*DashboardChartsSection` (reference: **`LibraryDashboardChartsSection`** in **`components/library-dashboard-charts.tsx`**, wired from **`library-table.tsx`**). Both share **`ChartFigure`**, **`ChartCard`**, and **`useChartVariant()`**; they are **not** duplicate chart engines — product charts belong in the shared components above. |
|
|
150
154
|
| **Keyboard selection (bars & pies)** | Match the **`/dashboard` gallery**: use **`CHART_KBD_ACTIVE_BAR`** and **`CHART_KBD_ACTIVE_PIE_SHAPE`** from **`@/lib/chart-keyboard-selection`** with Recharts **`activeBar` + `activeIndex`** on **`Bar`** and **`activeShape` + `activeIndex`** on **`Pie`**. **MUST NOT** rely on **`fillOpacity` dimming alone** on **`Cell`** as the only keyboard-selected state — it diverges from the gallery and from WCAG-aligned focus feedback. |
|
|
151
155
|
| **Customise UI** | Toggle **Edit layout** on the hub dashboard toolbar inside **`PlacementsTable`** / **`TeamTable`** / **`ComplianceTable`** (the dashboard tab body renders `*DashboardChartsSection` directly — there is no shared `DashboardShell` wrapper). **`layoutEditMode`** shows on-canvas drag reorder, remove, width (half / full width), chart type, add chart, reset — **no** separate Sheet for layout. Target for coach marks: **`[aria-label='Edit dashboard layout']`**. |
|
|
152
156
|
| **Toolbar in edit mode** | Do **not** render **`DataTableToolbar`** while **`layoutEditMode`** — hides search, filters, **Properties**, and the edit affordance in one row. Canvas **Done** / **Cancel** / **Reset** stay on the charts section. |
|
|
153
157
|
| **Key metrics card** | Dashboard **`key-metrics`** uses **`KeyMetrics`** **`variant="card"`** (not **`flat`**). Users choose how many KPIs to show (**1–4**) via the canvas control in edit mode; persist **`keyMetricsKpiCount`** in the same layout object. Half-width (**span 1**) sets **`metricsHalfWidthLayout`**. |
|
|
154
158
|
| **Data wiring** | **`PlacementsDashboardChartsSection`** (and Team / Compliance equivalents) **MUST** receive **`cardSpans`** and **`cardChartTypes`** (or rely on defaults **inside** the component). **MUST NOT** omit them without defaults — runtime crash (`undefined[id]`). |
|
|
155
|
-
| **Persistence (centralized)** |
|
|
159
|
+
| **Persistence (centralized)** | Hub dashboard layouts share one bundle: **`lib/data-view-dashboard-storage.ts`** (key **`exxat-ds:data-view-dashboards:v1`**). Each hub registers under a string scope (e.g. **`library`**); the generic API is **`loadDataViewLayout`** / **`saveDataViewLayout`** + **`mergeDashboardLayoutGeneric`** (`lib/dashboard-layout-merge.ts`) for default-layout safety. **MUST NOT** add a sibling `localStorage` key for the same layout shape without extending this module. |
|
|
156
160
|
|
|
157
|
-
**Reference:** `components/
|
|
161
|
+
**Reference:** `components/library-dashboard-charts.tsx` (`LibraryDashboardChartsSection`), `components/library-table.tsx` (dashboard tab body wires the section + edit toolbar inline), `components/charts-overview.tsx` (full-page gallery), `lib/chart-keyboard-selection.ts`, `lib/data-view-dashboard-storage.ts`, **`apps/web/.cursor/rules/exxat-dashboard-view-charts.mdc`**.
|
|
158
162
|
|
|
159
163
|
### 4.4 Board cards (kanban)
|
|
160
164
|
|
|
@@ -166,10 +170,9 @@ The drawer's "View type" tile grid (and the Export drawer's "File format" grid)
|
|
|
166
170
|
| **Information hierarchy** | **(1)** **`ListPageBoardCardTitleRow`** — title + optional **`ListPageBoardCardAvatar`** (`trailing`). **(2)** **`ListPageBoardCardBadgeRow`** — status / tags as **`Badge`** chips when the entity has a status (not raw body text for status). **(3)** **`ListPageBoardCardBody`** — facts via **`BoardCardTwoLineBlock`** and/or **`BoardCardIconRow`** from **`board-card-primitives.tsx`**. **(4)** Optional **`ListPageBoardCardSecondary`** for empty-state hints. |
|
|
167
171
|
| **Facts rows** | Prefer **`BoardCardTwoLineBlock`** (icon + primary line + optional secondary line) so rows match Placements. **`line2`** may be omitted for a single-line fact. Use **`BoardCardIconRow`** when the cell mirrors **`ColumnDef` cell renderers** (e.g. Placements). |
|
|
168
172
|
| **Avatar** | Use **`ListPageBoardCardAvatar`** with entity **`initials`** when present; otherwise derive with **`initialsFromDisplayName`** from **`lib/initials-from-name.ts`** (e.g. compliance owner name). |
|
|
169
|
-
| **Status labels + colors** | **All list hubs**
|
|
170
|
-
| **Placements-specific** | **`BoardPlacementCard`** may keep domain logic (lifecycle tabs, conditional row background, **`TablePropertiesDrawer`** column wiring); it still composes **`ListPageBoardCard`** parts and primitives. **Placements** status uses **`StatusBadge`** in **`components/placements-table-cells.tsx`**, which wraps **`ListHubStatusBadge`** with **`PLACEMENT_STATUS_*`** maps in **`lib/list-status-badges.ts`** (same system as other hubs). |
|
|
173
|
+
| **Status labels + colors** | **All list hubs** **MUST** render status chips with **`ListHubStatusBadge`** and compose per-domain label / tint / icon maps next to the entity's mock data (reference: `lib/mock/library.ts` + `components/library-board-view.tsx`). Generic semantic tints live in **`lib/list-status-badges.ts`** (**`LIST_HUB_STATUS_TINT_SUCCESS / WARNING / INFO / NEUTRAL / DANGER`**). **`surface="table"`** for **`DataTable`** / **list** rows; **`surface="board"`** in **`ListPageBoardCardBadgeRow`**. **SHOULD** map domain statuses onto **`LIST_HUB_STATUS_TINT_*`** before inventing new palettes. **MUST NOT** duplicate the same tint table across feature files or add **`uppercase`** / **`tracking-wide`**. |
|
|
171
174
|
|
|
172
|
-
**Reference:** **`components/
|
|
175
|
+
**Reference:** **`components/library-board-view.tsx`**, **`components/list-hub-status-badge.tsx`**, **`lib/list-status-badges.ts`** (generic tints), **`components/data-views/list-page-board-template.tsx`**, **`@exxatdesignux/ui/components/list-page-board-card`** (primitive). **Skill (Cursor + Claude):** **`.cursor/skills/exxat-board-cards/SKILL.md`** and **`.claude/skills/exxat-board-cards/SKILL.md`**.
|
|
173
176
|
|
|
174
177
|
### 4.5 View layout shells (centered, reusable, not page-tied)
|
|
175
178
|
|
|
@@ -178,7 +181,7 @@ The drawer's "View type" tile grid (and the Export drawer's "File format" grid)
|
|
|
178
181
|
| Topic | Rule |
|
|
179
182
|
|-------|------|
|
|
180
183
|
| **Folder / icon / panel-style views** | Compose **`FolderGridView`**, **`FinderPanelView`**, or another **`data-views/`** primitive; those shells **should** use **`ListPageViewFrame`** internally or as a direct wrapper so every hub gets the same rhythm. |
|
|
181
|
-
| **New view types** | Implement the **generic** grid/shell under **`components/data-views/`** with render props; **`TeamTable`** / **`
|
|
184
|
+
| **New view types** | Implement the **generic** grid/shell under **`components/data-views/`** with render props; **`TeamTable`** / **`LibraryTable`** **MUST** only wire data + callbacks — **MUST NOT** own one-off full-page grid markup that another entity would duplicate. |
|
|
182
185
|
| **`DataTable` branch** | **MUST NOT** wrap **`DataTable`** in **`ListPageViewFrame`** when it would **stack** horizontal inset with the table toolbar (**§5**). |
|
|
183
186
|
|
|
184
187
|
**Cursor rule:** **`.cursor/rules/exxat-list-page-view-shells.mdc`**. **Skill:** **`.cursor/skills/exxat-list-page-view-shells/SKILL.md`**.
|
|
@@ -191,14 +194,14 @@ The drawer's "View type" tile grid (and the Export drawer's "File format" grid)
|
|
|
191
194
|
|
|
192
195
|
| Step | Action |
|
|
193
196
|
|------|--------|
|
|
194
|
-
| **Nav** | Set **`secondaryPanel`** on the **`NavLinkItem`** in **`lib/mock/navigation.tsx`** to a **stable id** (e.g. **`"
|
|
197
|
+
| **Nav** | Set **`secondaryPanel`** on the **`NavLinkItem`** in **`lib/mock/navigation.tsx`** to a **stable id** (e.g. **`"library"`**). **`url`** stays the **hub route**. |
|
|
195
198
|
| **Registry** | Map that id in **`PANELS`** inside **`components/secondary-panel.tsx`** to a component that renders **panel header** + **secondary nav** UI. |
|
|
196
|
-
| **Route** | Mount **`*PanelActivator`** on the hub that calls **`useAutoPanel(id)`** with the **same id** so the panel opens while the route is mounted (see **`
|
|
197
|
-
| **Data** | Keep using **one** **`useTableState`** row bag; **scope** filters via **URL** + shared helpers (**`lib/
|
|
199
|
+
| **Route** | Mount **`*PanelActivator`** on the hub that calls **`useAutoPanel(id)`** with the **same id** so the panel opens while the route is mounted (see **`LibraryPanelActivator`** on **`LibraryClient`**). |
|
|
200
|
+
| **Data** | Keep using **one** **`useTableState`** row bag; **scope** filters via **URL** + shared helpers (**`lib/library-nav.ts`**) so **refresh** and **links** match the panel — **`.cursor/rules/exxat-centralized-list-dataset.mdc`**. |
|
|
198
201
|
|
|
199
202
|
**MUST NOT:** Set **`secondaryPanel`** without **`PANELS[id]`** and **`useAutoPanel`** — users see **collapsed** main nav with **no** panel body.
|
|
200
203
|
|
|
201
|
-
**Folder-scoped library (
|
|
204
|
+
**Folder-scoped library (Library):** When the URL is scoped to a folder (**`scope === "folder"`** + **`folderId`** via **`lib/library-nav.ts`**), the hub **`LibraryPageHeader`** **⋯ More** menu **MUST** include **Customize folder** and open **`LibraryNewFolderSheet`** from the **hub client** so the action works on **every** **`ListPageTemplate`** view tab — not only inside **`LibraryTable`** branches that mount their own sheet. **Pattern:** **`docs/library-hub-header-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-library-hub-header.mdc`**.
|
|
202
205
|
|
|
203
206
|
**Surface elevation:** Secondary panel = **level 1** between primary sidebar (**`--sidebar`**, level 0) and page canvas (**`--background`**, level 2). **`NestedSecondaryPanelShell`** uses **`bg-[var(--secondary-panel-bg)]`** — OKLCH mix from **`--brand-tint*`** per active product (**One** indigo, **Prism** rose, **`theme-custom`** when accent differs from default). **MUST NOT** set panel to **`bg-sidebar`** or a fixed rose fill for all products. **`docs/shell-surface-elevation-pattern.md`**.
|
|
204
207
|
|
|
@@ -221,7 +224,7 @@ The drawer's "View type" tile grid (and the Export drawer's "File format" grid)
|
|
|
221
224
|
|
|
222
225
|
**MUST NOT:** **`Select`** in **`InputGroupAddon`** without **`InputGroupInput`** / **`SelectGroup`**; per-person cards in the roster; a second invite control **beside** an existing face rail.
|
|
223
226
|
|
|
224
|
-
**Narrative:** **`docs/collaboration-access-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-collaboration-access.mdc`**. **Skill:** **`.cursor/skills/exxat-collaboration-access/SKILL.md`**. **Reference:**
|
|
227
|
+
**Narrative:** **`docs/collaboration-access-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-collaboration-access.mdc`**. **Skill:** **`.cursor/skills/exxat-collaboration-access/SKILL.md`**. **Reference:** Library header + client + **`InviteCollaboratorsDrawer`**.
|
|
225
228
|
|
|
226
229
|
### 4.8 Dedicated search (landing vs results)
|
|
227
230
|
|
|
@@ -260,7 +263,7 @@ Below the threshold, these MAY be omitted unless the page is a **primary hub** (
|
|
|
260
263
|
|
|
261
264
|
Match **Placements**:
|
|
262
265
|
|
|
263
|
-
- **Primary CTA:** one **default (filled)** `Button`, often `size="lg"` — e.g. New
|
|
266
|
+
- **Primary CTA:** one **default (filled)** `Button`, often `size="lg"` — e.g. "New question", "Invite collaborator", "Add token". **MUST NOT** use `variant="outline"` for that primary action.
|
|
264
267
|
- **More (⋯):** outline **icon** button → menu including **Export** → **`ExportDrawer`** (or same pattern).
|
|
265
268
|
|
|
266
269
|
**Subtitle:** Short line with **count + freshness** (e.g. `24 records · Last updated now`) when useful — see `PlacementsPageHeader` / `TeamPageHeader`.
|
|
@@ -307,7 +310,7 @@ Follow root **`.cursor/rules/exxat-kbd-shortcuts.mdc`**. Summary:
|
|
|
307
310
|
|
|
308
311
|
### 7.1 Global command palette (⌘K)
|
|
309
312
|
|
|
310
|
-
**Product intent:** **`CommandMenu`** is **global search** and the primary **AI entry**—not a second nav tree. Config: **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**, provider in **`app/(app)/layout.tsx`**. Optional searchable rows (e.g.
|
|
313
|
+
**Product intent:** **`CommandMenu`** is **global search** and the primary **AI entry**—not a second nav tree. Config: **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**, provider in **`app/(app)/layout.tsx`**. Optional searchable rows (e.g. student names, question stems, record IDs) come from **`dataGroups`**, typically via **`getCommandMenuSearchDataGroups()`** in **`lib/command-menu-search-data.ts`**.
|
|
311
314
|
|
|
312
315
|
| SHOULD | Rationale |
|
|
313
316
|
|--------|-----------|
|
|
@@ -326,7 +329,7 @@ Follow root **`.cursor/rules/exxat-kbd-shortcuts.mdc`**. Summary:
|
|
|
326
329
|
|
|
327
330
|
**Standard:** **WCAG 2.1 Level AA** (and **2.2** where noted, e.g. target size).
|
|
328
331
|
|
|
329
|
-
**Authoritative detail (badges,
|
|
332
|
+
**Authoritative detail (badges, sidebar count colors, audit table):** **`.cursor/skills/exxat-accessibility/SKILL.md`** at the monorepo root (when the parent repo is open). If the skill path differs in your checkout, search for **`exxat-accessibility`**.
|
|
330
333
|
|
|
331
334
|
### 8.1 ARIA roles & structure (SC 1.3.1)
|
|
332
335
|
|
|
@@ -450,7 +453,7 @@ Keyboard shortcut hints rendered **inline inside a `Button`** (primary, secondar
|
|
|
450
453
|
<TooltipContent><span>Close</span><Kbd>Esc</Kbd></TooltipContent>
|
|
451
454
|
```
|
|
452
455
|
|
|
453
|
-
Reference: `components/new-
|
|
456
|
+
Reference: `components/new-library-item-form.tsx` (Next/Back buttons); full shortcut table in **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
|
|
454
457
|
|
|
455
458
|
---
|
|
456
459
|
|
|
@@ -461,16 +464,16 @@ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcu
|
|
|
461
464
|
| View tabs + shell | `ListPageTemplate` | `components/templates/list-page.tsx` |
|
|
462
465
|
| Table + toolbar | `DataTable`, `DataTableToolbar`, `useTableState` | `components/data-table/` |
|
|
463
466
|
| Properties | `TablePropertiesDrawer` (+ **`currentView`** / **`onViewChange`** when using view tabs — §4.2) | `@/components/table-properties` |
|
|
464
|
-
|
|
|
465
|
-
|
|
|
466
|
-
| Dashboard view tab (KPIs + charts) | **`DashboardReportCharts`**; default **`ChartsOverview`** (
|
|
467
|
+
| Full hub (table / board / dashboard) | `LibraryHubClient`, `LibraryTable` | `components/library-hub-client.tsx`, `components/library-table.tsx` |
|
|
468
|
+
| Single-view catalog hub | `ColumnsClient`, `ColumnsShowcase` | `components/columns-client.tsx`, `components/columns-showcase.tsx` |
|
|
469
|
+
| Dashboard view tab (KPIs + charts) | **`DashboardReportCharts`**; default **`ChartsOverview`** (full-page gallery). Hubs pass **`chartsSection`** with their own `*DashboardChartsSection` so graphs match the row set. KPIs from **`tableState.rows`**. | `components/dashboard-report-charts.tsx`, `components/library-dashboard-charts.tsx`, `components/library-table.tsx` |
|
|
467
470
|
| Data view layout + graph keyboard tokens | **`loadDataViewLayout` / `saveDataViewLayout`**, **`CHART_KBD_ACTIVE_BAR`**, **`CHART_KBD_ACTIVE_PIE_SHAPE`** | `lib/data-view-dashboard-storage.ts`, `lib/chart-keyboard-selection.ts` |
|
|
468
|
-
| Customize dashboard coach marks | Shared steps in **`lib/dashboard-customize-coach-mark.ts`**;
|
|
469
|
-
| Board columns (simple hubs) | **`ListPageBoardTemplate`** + **`ListPageBoardCard`** + primitives + **`lib/list-status-badges`** + **`ListHubStatusBadge`** (when applicable) | `components/data-views/list-page-board-template.tsx`, `list-hub-status-badge.tsx`, `
|
|
471
|
+
| Customize dashboard coach marks | Shared steps in **`lib/dashboard-customize-coach-mark.ts`**; one flow per hub scope (e.g. **`library-dashboard-customize`**) | `hooks/use-coach-mark.ts` (`enabled`, `dependsOnDismissedFlowId`), `library-table.tsx` |
|
|
472
|
+
| Board columns (simple hubs) | **`ListPageBoardTemplate`** + **`ListPageBoardCard`** + primitives + **`lib/list-status-badges`** + **`ListHubStatusBadge`** (when applicable) | `components/data-views/list-page-board-template.tsx`, `list-hub-status-badge.tsx`, `library-board-view.tsx`, **`§4.4`** |
|
|
470
473
|
| Full dashboard route | `DashboardTabs`, `KeyMetrics`, `ChartsOverview` | `app/(app)/dashboard/page.tsx`, `components/dashboard-tabs.tsx` |
|
|
471
|
-
| Board cards | **`ListPageBoardCard`** + primitives + entity card (**§4.4**) | `components/data-views/list-page-board-card.tsx`, `board-card-primitives.tsx`, `
|
|
474
|
+
| Board cards | **`ListPageBoardCard`** + primitives + entity card (**§4.4**) | `components/data-views/list-page-board-card.tsx`, `board-card-primitives.tsx`, `library-board-view.tsx` |
|
|
472
475
|
| **Application sidebar** (school/program, product, profile, child nav) | **`AppSidebar`**, **`TeamSwitcher`**, **`NavUser`**, collapsible + **popover** (icon rail) | `components/app-sidebar.tsx`, `nav-user.tsx`, `product-switcher.tsx`, `lib/mock/navigation.tsx`, `lib/logo-dev.ts`, `lib/stock-portrait.ts` — patterns in **exxat-ds-skill §3.1** |
|
|
473
|
-
| **Collaboration & access** (face rail + invite sheet) | **`PageHeader` `variant="collaboration"`**, **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`** | `components/page-header.tsx`, `components/invite-collaborators-drawer.tsx`, `components/
|
|
476
|
+
| **Collaboration & access** (face rail + invite sheet) | **`PageHeader` `variant="collaboration"`**, **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`** | `components/page-header.tsx`, `components/invite-collaborators-drawer.tsx`, `components/library-page-header.tsx`, `components/library-client.tsx`, **`§4.7`**, **`docs/collaboration-access-pattern.md`** |
|
|
474
477
|
| **Dedicated search** (empty `?q=` landing vs results) | **`DedicatedSearchLandingTemplate`**, **`DedicatedSearchUrlComposer`**, **`DedicatedSearchRecents`**, **`DedicatedSearchResultsHeaderChrome`**, **`lib/dedicated-search-recents.ts`** | **`§4.8`**, **`components/templates/dedicated-search-*`**, **`components/dedicated-search-*.tsx`** |
|
|
475
478
|
| Persistence (example) | Page + lifecycle keys | `lib/data-list-persistence.ts`, `PlacementsClient` / `PlacementsTable` |
|
|
476
479
|
| Coach marks / tours | `CoachMark`, `useCoachMark`, coach mark registry | `components/ui/coach-mark.tsx`, `hooks/use-coach-mark.ts`, `lib/coach-mark-registry.ts` |
|
|
@@ -527,7 +530,7 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
527
530
|
- **Brand-colored:** popover background is `bg-brand-deep text-white` — not `bg-popover`.
|
|
528
531
|
- **Persistent:** once completed/skipped, the flow is dismissed via `localStorage` and won't reshow until reset from Settings.
|
|
529
532
|
- **Settings page:** `/settings` lists all registered flows with reset/preview controls.
|
|
530
|
-
- **Sequencing / gating:** `useCoachMark` supports **`enabled`** (e.g. only when **`view === "dashboard"`**) and **`dependsOnDismissedFlowId`** (e.g. customize-dashboard after
|
|
533
|
+
- **Sequencing / gating:** `useCoachMark` supports **`enabled`** (e.g. only when **`view === "dashboard"`**) and **`dependsOnDismissedFlowId`** (e.g. customize-dashboard after a "views" tour completes). Completed flows dispatch **`COACH_MARK_FLOW_COMPLETED_EVENT`** on `window` so follow-up tours can open in the same tab.
|
|
531
534
|
- **Customize Data dashboard:** registered flows target **`[aria-label='Edit dashboard layout']`**; shared step copy lives in **`lib/dashboard-customize-coach-mark.ts`**.
|
|
532
535
|
|
|
533
536
|
### Variants
|
|
@@ -537,13 +540,14 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
537
540
|
- **With image** — set `image` + `imageAlt` on the step
|
|
538
541
|
- **Without image** — text-only
|
|
539
542
|
|
|
540
|
-
**Reference:** `references/coach-marks.md` in the skill, `components/dashboard-tabs.tsx` (dashboard tour), `components/
|
|
543
|
+
**Reference:** `references/coach-marks.md` in the skill, `components/dashboard-tabs.tsx` (dashboard tour), `components/library-hub-client.tsx` (views tour).
|
|
541
544
|
|
|
542
545
|
---
|
|
543
546
|
|
|
544
547
|
## 12. Documentation
|
|
545
548
|
|
|
546
549
|
- **Deep dive:** `docs/data-views-pattern.md` (includes **Page vs drawer** with **§6.4**)
|
|
550
|
+
- **Scaling to 5K+ rows:** `docs/large-dataset-strategy.md` (client mode, pagination, server mode, virtualization follow-up)
|
|
547
551
|
- **Drawer vs dialog (same route):** `docs/drawer-vs-dialog-pattern.md` — **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**
|
|
548
552
|
- **Cards vs table rows:** `docs/card-vs-rows-pattern.md` — **`.cursor/rules/exxat-card-vs-list-rows.mdc`**
|
|
549
553
|
- **KPI strip (max four tiles):** `docs/kpi-strip-max-four-pattern.md` — **`.cursor/rules/exxat-kpi-max-four.mdc`**
|
|
@@ -591,7 +595,7 @@ When you **introduce** a new token in `packages/ui/src/globals.css` (the canonic
|
|
|
591
595
|
|
|
592
596
|
| MUST | MUST NOT |
|
|
593
597
|
|------|----------|
|
|
594
|
-
| Use `
|
|
598
|
+
| Use **`HubTable`** (from **`@/components/data-views`**) inside **`ListPageTemplate.renderContent`** — it wires `useTableState`, the **toolbar** (search + filter chips + filter dropdown + sort), and **`TablePropertiesDrawerButton`** in one place. Pass **`view`** / **`onViewChange`** through (§4.2); raw **`DataTable`** is fine **only** outside hubs (drawer/dialog mini-grids) | Mount raw **`<DataTable>`** in a hub or showcase — users lose filter chips and Properties; introduce a second table stack for the same surfaces; omit **`currentView`** on multi-view pages |
|
|
595
599
|
| Wrap main `DataTable` in `ListPageTemplate` | `DataTable` only under `PageHeader` without view tabs |
|
|
596
600
|
| Use primary template (`ListPageTemplate` + metrics + export pattern) for primary hubs with large data | Hub pages that look like “nested cards” with staggered margins |
|
|
597
601
|
| Match Placements for export + primary CTA + More menu | Outline button as the single primary CTA on exportable pages |
|
|
@@ -607,9 +611,9 @@ When you **introduce** a new token in `packages/ui/src/globals.css` (the canonic
|
|
|
607
611
|
| 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` |
|
|
608
612
|
| 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) |
|
|
609
613
|
| **§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**) |
|
|
610
|
-
| **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav; **folder URL scope** → header **⋯** **Customize folder** + client-mounted **`
|
|
614
|
+
| **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav; **folder URL scope** → header **⋯** **Customize folder** + client-mounted **`LibraryNewFolderSheet`** (**`exxat-library-hub-header.mdc`**) | **`secondaryPanel`** id with no panel component or activator; folder scope with customize **only** inside a single view tab’s subtree |
|
|
611
615
|
| **§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 |
|
|
612
|
-
| **§4.8** — **`DedicatedSearch*`** templates + composer + recents; **no** `localStorage` in **`useState`** initial paint; hub-specific **`patchSearchParams`** only | Forked `*
|
|
616
|
+
| **§4.8** — **`DedicatedSearch*`** templates + composer + recents; **no** `localStorage` in **`useState`** initial paint; hub-specific **`patchSearchParams`** only | Forked `*Library*SearchLanding*` shells for another entity; hydration mismatch on recents |
|
|
613
617
|
| **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 |
|
|
614
618
|
| **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 |
|
|
615
619
|
|
|
@@ -620,6 +624,7 @@ When you **introduce** a new token in `packages/ui/src/globals.css` (the canonic
|
|
|
620
624
|
Copy and complete when implementing or reviewing:
|
|
621
625
|
|
|
622
626
|
- [ ] **Centralized dataset:** One **`useTableState`** / **`tableState.rows`** for **all** view tabs and inspectors; **TablePropertiesDrawer** on the **same** `DataTable`; **no** parallel mock arrays per view — **`.cursor/rules/exxat-centralized-list-dataset.mdc`**.
|
|
627
|
+
- [ ] **Hub primitive (`HubTable`):** Every page that mounts a hub grid inside **`ListPageTemplate`** uses **`HubTable`** (from **`@/components/data-views`**), not raw **`<DataTable>`**. **`HubTable`** wires `useTableState`, the toolbar (search + filter chips + filter dropdown + sort), and **`TablePropertiesDrawerButton`** in one place; raw **`<DataTable>`** is reserved for tiny embedded grids (drawer/dialog body). When a column ships **`filter:`**, the chips appear automatically — **MUST NOT** add a parallel search/filter UI above the table — **`.cursor/rules/exxat-data-tables.mdc`**.
|
|
623
628
|
- [ ] **Reuse:** `ListPageTemplate`, `DataTable` / `useTableState`, `TablePropertiesDrawer` — no parallel bespoke tabs/filters. **New shared primitives:** **ask the user** after scanning **`components/`** + **§9** — **`.cursor/rules/exxat-reuse-before-custom.mdc`**.
|
|
624
629
|
- [ ] **Tabs:** Any main `DataTable` sits under `ListPageTemplate` with appropriate view tabs.
|
|
625
630
|
- [ ] **Inset:** No double horizontal padding around `DataTable`.
|
|
@@ -644,14 +649,15 @@ Copy and complete when implementing or reviewing:
|
|
|
644
649
|
- [ ] **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.
|
|
645
650
|
- [ ] **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`**.
|
|
646
651
|
- [ ] **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`**.
|
|
647
|
-
- [ ] **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`**. Panel shell uses **`--secondary-panel-bg`** (brand OKLCH, not **`bg-sidebar`**) — **`docs/shell-surface-elevation-pattern.md`**. **
|
|
652
|
+
- [ ] **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`**. Panel shell uses **`--secondary-panel-bg`** (brand OKLCH, not **`bg-sidebar`**) — **`docs/shell-surface-elevation-pattern.md`**. **Library folder scope:** header **⋯** → **Customize folder** + **`LibraryNewFolderSheet`** on **`LibraryClient`** — **`docs/library-hub-header-pattern.md`**, **`.cursor/rules/exxat-library-hub-header.mdc`**.
|
|
648
653
|
- [ ] **Flat KPI strip:** **`KeyMetrics variant="flat"`** — transparent cells, radial glow only, **`flatMetricsHairlineClass`** borders — **`docs/kpi-flat-band-pattern.md`**, **`.cursor/rules/exxat-kpi-flat-band.mdc`**.
|
|
649
654
|
- [ ] **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`**.
|
|
650
655
|
- [ ] **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`**.
|
|
651
|
-
- [ ] **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`**.
|
|
656
|
+
- [ ] **KPI trends:** **`MetricItem.trend`** matches the delta direction; **`trendPolarity`** set for “more is worse” metrics (flags, defects, overdue); **`delta`** is a count (not prose) and is **left empty** when there is no comparison (chip is suppressed — no `—` placeholder); supporting prose lives in **`MetricItem.description`** (renders below the value) — **`docs/kpi-trend-pattern.md`**, **`.cursor/rules/exxat-kpi-trends.mdc`**.
|
|
657
|
+
- [ ] **Person rails — no overlap:** Face rails / reviewer piles / collaborator stacks render avatars **side-by-side** with **`gap-1`** / **`gap-1.5`** (use **`AvatarGroup`** + **`AvatarGroupCount`** for **`+N`**); **MUST NOT** restore negative-margin overlap (`-space-x-*` + `*:ring-*`) — **`.cursor/rules/exxat-person-identity-display.mdc`**.
|
|
652
658
|
- [ ] **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`**.
|
|
653
659
|
- [ ] **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`**.
|
|
654
660
|
|
|
655
661
|
---
|
|
656
662
|
|
|
657
|
-
*Last updated: KPI flat band + shell surface elevation pattern docs/rules/skills; §4.6 secondary panel OKLCH; monospace system IDs;
|
|
663
|
+
*Last updated: HubTable is the canonical hub primitive (raw DataTable only outside hubs); filter button tooltip nesting fix; KPI delta vs description (chip suppressed when empty) + no overlapping avatars (`AvatarGroup` is gapped, not negative-margin); KPI flat band + shell surface elevation pattern docs/rules/skills; §4.6 secondary panel OKLCH; monospace system IDs; library folder header; drawer vs dialog / card vs rows / KPI max-four; §4.8 dedicated search; §4.7 collaboration; §4.1 centralized dataset; §4.5 view shells; Font Awesome; §9.1 sidebar; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Suspense } from "react"
|
|
2
|
+
|
|
3
|
+
import { LibraryClient } from "@/components/library-client"
|
|
4
|
+
|
|
5
|
+
/** Discovery hub composer results — same hub chrome as the library, distinct from `/library/list`. */
|
|
6
|
+
export default function LibraryHubFindPage() {
|
|
7
|
+
return (
|
|
8
|
+
<Suspense fallback={null}>
|
|
9
|
+
<LibraryClient />
|
|
10
|
+
</Suspense>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -5,24 +5,24 @@ import { usePathname } from "next/navigation"
|
|
|
5
5
|
|
|
6
6
|
import { useSecondaryPanel } from "@/components/sidebar"
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@/lib/
|
|
8
|
+
LIBRARY_ENTRY_PATH,
|
|
9
|
+
LIBRARY_HUB_FIND_PATH,
|
|
10
|
+
LIBRARY_LIST_PATH,
|
|
11
|
+
} from "@/lib/library-nav"
|
|
12
12
|
|
|
13
|
-
/** Full-page focused flows under `/
|
|
14
|
-
const
|
|
13
|
+
/** Full-page focused flows under `/library/*` that suppress the secondary panel. */
|
|
14
|
+
const LIBRARY_FOCUSED_FLOW_PATHS: readonly string[] = ["/library/new"]
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
16
|
+
function isLibraryFocusedFlow(pathname: string): boolean {
|
|
17
|
+
return LIBRARY_FOCUSED_FLOW_PATHS.some(p => pathname === p || pathname.startsWith(`${p}/`))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Keeps the nested secondary panel open across library navigations.
|
|
22
|
-
* **Question hub** (`/
|
|
22
|
+
* **Question hub** (`/library`), **Search** (`/find`, `/list`), and
|
|
23
23
|
* focused authoring routes (`/new`) stay full-width — no secondary rail.
|
|
24
24
|
*/
|
|
25
|
-
export default function
|
|
25
|
+
export default function LibraryLayout({ children }: { children: React.ReactNode }) {
|
|
26
26
|
const pathname = usePathname()
|
|
27
27
|
const { openPanel, closePanel, activePanel } = useSecondaryPanel()
|
|
28
28
|
const closePanelRef = React.useRef(closePanel)
|
|
@@ -36,7 +36,7 @@ export default function QuestionBankLayout({ children }: { children: React.React
|
|
|
36
36
|
openPanelRef.current = openPanel
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
/** Leaving `/
|
|
39
|
+
/** Leaving `/library/*` entirely — close nested panel without forcing primary sidebar open (⌘B / cookie). */
|
|
40
40
|
React.useEffect(() => {
|
|
41
41
|
return () => {
|
|
42
42
|
closePanelRef.current({ mainSidebar: "leave" })
|
|
@@ -46,18 +46,18 @@ export default function QuestionBankLayout({ children }: { children: React.React
|
|
|
46
46
|
/** Only react to route changes — refs carry latest open/close. */
|
|
47
47
|
React.useEffect(() => {
|
|
48
48
|
const isDiscoveryHubRoot =
|
|
49
|
-
pathname ===
|
|
49
|
+
pathname === LIBRARY_ENTRY_PATH || pathname === `${LIBRARY_ENTRY_PATH}/`
|
|
50
50
|
|
|
51
51
|
const isDedicatedSearchSurface =
|
|
52
|
-
pathname ===
|
|
52
|
+
pathname === LIBRARY_HUB_FIND_PATH || pathname === LIBRARY_LIST_PATH
|
|
53
53
|
|
|
54
|
-
if (isDiscoveryHubRoot || isDedicatedSearchSurface ||
|
|
54
|
+
if (isDiscoveryHubRoot || isDedicatedSearchSurface || isLibraryFocusedFlow(pathname)) {
|
|
55
55
|
closePanelRef.current({ mainSidebar: "leave" })
|
|
56
56
|
return undefined
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (activePanel !== "
|
|
60
|
-
openPanelRef.current("
|
|
59
|
+
if (activePanel !== "library") {
|
|
60
|
+
openPanelRef.current("library")
|
|
61
61
|
}
|
|
62
62
|
return undefined
|
|
63
63
|
}, [pathname, activePanel])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Suspense } from "react"
|
|
2
|
+
|
|
3
|
+
import { LibraryClient } from "@/components/library-client"
|
|
4
|
+
|
|
5
|
+
/** Library list surface — same hub as `/library/all`, optimized for `?q=` search landings. */
|
|
6
|
+
export default function LibraryListPage() {
|
|
7
|
+
return (
|
|
8
|
+
<Suspense fallback={null}>
|
|
9
|
+
<LibraryClient />
|
|
10
|
+
</Suspense>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/
|
|
2
|
+
* `/library/new` — focused authoring page for a new question.
|
|
3
3
|
*
|
|
4
|
-
* The composer (`
|
|
4
|
+
* The composer (`NewLibraryItemForm`) owns the `NewFocusTemplate` (form-inspector
|
|
5
5
|
* variant) underneath. This route file is intentionally thin so the same composer
|
|
6
6
|
* can be mounted from other entry points (deep-link, command palette).
|
|
7
7
|
*
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* in the metadata rail.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { NewLibraryItemForm } from "@/components/new-library-item-form"
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
} from "@/lib/mock/
|
|
18
|
-
import { generateDraftQuestionId } from "@/lib/
|
|
19
|
-
import { newQuestionBackNav } from "@/lib/
|
|
15
|
+
DEFAULT_LIBRARY_FOLDERS,
|
|
16
|
+
type LibraryFolder,
|
|
17
|
+
} from "@/lib/mock/library-folders"
|
|
18
|
+
import { generateDraftQuestionId } from "@/lib/library-authoring"
|
|
19
|
+
import { newQuestionBackNav } from "@/lib/library-nav"
|
|
20
20
|
|
|
21
21
|
interface NewQuestionPageProps {
|
|
22
22
|
searchParams: Promise<{ folderId?: string }>
|
|
@@ -24,7 +24,7 @@ interface NewQuestionPageProps {
|
|
|
24
24
|
|
|
25
25
|
export default async function NewQuestionPage({ searchParams }: NewQuestionPageProps) {
|
|
26
26
|
const params = await searchParams
|
|
27
|
-
const folders:
|
|
27
|
+
const folders: LibraryFolder[] = DEFAULT_LIBRARY_FOLDERS
|
|
28
28
|
|
|
29
29
|
const requested = typeof params.folderId === "string" ? params.folderId : undefined
|
|
30
30
|
const matched = requested ? folders.find(f => f.id === requested) : undefined
|
|
@@ -34,7 +34,7 @@ export default async function NewQuestionPage({ searchParams }: NewQuestionPageP
|
|
|
34
34
|
const draftQuestionId = generateDraftQuestionId()
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<
|
|
37
|
+
<NewLibraryItemForm
|
|
38
38
|
draftQuestionId={draftQuestionId}
|
|
39
39
|
defaultFolderId={defaultFolderId}
|
|
40
40
|
backHref={back.href}
|
|
@@ -43,7 +43,7 @@ export interface AskLeoComposerProps {
|
|
|
43
43
|
*/
|
|
44
44
|
animatedPlaceholderMaxLines?: 1 | 2
|
|
45
45
|
/**
|
|
46
|
-
* `attachments` — plus menu + file picker (default). `ai-mark` — Leo-style icon only (e.g.
|
|
46
|
+
* `attachments` — plus menu + file picker (default). `ai-mark` — Leo-style icon only (e.g. library hub).
|
|
47
47
|
*/
|
|
48
48
|
leadingSlot?: "attachments" | "ai-mark"
|
|
49
49
|
/** Accessible name for the textarea (paired with `htmlFor`). */
|
|
@@ -51,7 +51,7 @@ export interface AskLeoComposerProps {
|
|
|
51
51
|
/** `aria-label` on the submit control when the field has text. */
|
|
52
52
|
submitButtonAriaLabel?: string
|
|
53
53
|
/**
|
|
54
|
-
* `send` — paper plane (chat / Ask Leo). `search` — magnifying glass (
|
|
54
|
+
* `send` — paper plane (chat / Ask Leo). `search` — magnifying glass (library hub + dedicated search).
|
|
55
55
|
*/
|
|
56
56
|
submitAppearance?: "send" | "search"
|
|
57
57
|
/** Lets the parent swap pill vs card chrome when the field grows (multiline / long text). */
|