@exxatdesignux/ui 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +17 -4
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/AGENTS.md +82 -27
  28. package/template/app/(app)/examples/page.tsx +2 -1
  29. package/template/app/(app)/help/page.tsx +6 -0
  30. package/template/app/(app)/layout.tsx +7 -4
  31. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  32. package/template/app/(app)/question-bank/layout.tsx +46 -0
  33. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  34. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  35. package/template/app/(app)/question-bank/page.tsx +4 -3
  36. package/template/app/globals.css +1 -2
  37. package/template/components/app-sidebar.tsx +51 -13
  38. package/template/components/ask-leo-composer.tsx +173 -45
  39. package/template/components/ask-leo-sidebar.tsx +9 -1
  40. package/template/components/chart-area-interactive.tsx +3 -13
  41. package/template/components/charts-overview.tsx +33 -6
  42. package/template/components/collaboration-access-flow.tsx +144 -0
  43. package/template/components/compliance-page-header.tsx +1 -1
  44. package/template/components/compliance-table.tsx +2 -2
  45. package/template/components/dashboard-tabs.tsx +4 -3
  46. package/template/components/data-list-table-cells.tsx +1 -1
  47. package/template/components/data-list-table.tsx +1 -1
  48. package/template/components/data-table/index.tsx +5 -5
  49. package/template/components/data-table/use-table-state.ts +18 -2
  50. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  51. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts.tsx +62 -227
  53. package/template/components/dedicated-search-recents.tsx +96 -0
  54. package/template/components/dedicated-search-url-composer.tsx +112 -0
  55. package/template/components/getting-started.tsx +1 -1
  56. package/template/components/hub-tree-panel-view.tsx +10 -26
  57. package/template/components/invite-collaborators-drawer.tsx +453 -0
  58. package/template/components/key-metrics.tsx +54 -8
  59. package/template/components/nav-documents.tsx +1 -1
  60. package/template/components/new-placement-form.tsx +3 -3
  61. package/template/components/page-header.tsx +76 -59
  62. package/template/components/placements-board-view.tsx +3 -3
  63. package/template/components/placements-page-header.tsx +1 -1
  64. package/template/components/placements-table-columns.tsx +3 -2
  65. package/template/components/product-switcher.tsx +0 -1
  66. package/template/components/question-bank-board-view.tsx +35 -47
  67. package/template/components/question-bank-client.tsx +293 -81
  68. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  69. package/template/components/question-bank-favorite-button.tsx +46 -0
  70. package/template/components/question-bank-hub-client.tsx +436 -0
  71. package/template/components/question-bank-list-view.tsx +26 -19
  72. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  73. package/template/components/question-bank-os-folder-view.tsx +3 -14
  74. package/template/components/question-bank-page-header.tsx +85 -53
  75. package/template/components/question-bank-panel-activator.tsx +3 -4
  76. package/template/components/question-bank-secondary-nav.tsx +523 -65
  77. package/template/components/question-bank-table.tsx +125 -343
  78. package/template/components/secondary-panel.tsx +130 -63
  79. package/template/components/settings-client.tsx +3 -1
  80. package/template/components/sidebar-shell.tsx +2 -0
  81. package/template/components/sites-all-client.tsx +1 -1
  82. package/template/components/sites-table.tsx +1 -1
  83. package/template/components/system-banner-slot.tsx +2 -1
  84. package/template/components/table-properties/drawer.tsx +3 -3
  85. package/template/components/table-properties/sort-card.tsx +1 -1
  86. package/template/components/team-page-header.tsx +1 -1
  87. package/template/components/team-table.tsx +8 -4
  88. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  89. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  90. package/template/components/templates/discovery-hub-template.tsx +273 -0
  91. package/template/components/templates/list-page.tsx +11 -4
  92. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  93. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  94. package/template/docs/card-vs-rows-pattern.md +36 -0
  95. package/template/docs/collaboration-access-pattern.md +114 -0
  96. package/template/docs/data-views-pattern.md +12 -4
  97. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  98. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  99. package/template/docs/kpi-trend-pattern.md +43 -0
  100. package/template/fontawesome-subset.manifest.json +2 -2
  101. package/template/hooks/use-location-hash.ts +14 -8
  102. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  103. package/template/lib/ask-leo-route-context.ts +24 -0
  104. package/template/lib/collaborator-access.ts +92 -0
  105. package/template/lib/command-menu-config.ts +8 -1
  106. package/template/lib/command-menu-search-data.ts +11 -8
  107. package/template/lib/data-list-display-options.ts +1 -1
  108. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  109. package/template/lib/date-filter.ts +1 -0
  110. package/template/lib/dedicated-search-recents.ts +76 -0
  111. package/template/lib/dedicated-search-url.ts +23 -0
  112. package/template/lib/discovery-hub.ts +15 -0
  113. package/template/lib/list-status-badges.ts +1 -21
  114. package/template/lib/mock/navigation.tsx +4 -2
  115. package/template/lib/mock/placements.ts +9 -9
  116. package/template/lib/mock/question-bank-folders.ts +7 -0
  117. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  118. package/template/lib/mock/question-bank-inspector.ts +1 -2
  119. package/template/lib/mock/question-bank-kpi.ts +38 -26
  120. package/template/lib/mock/question-bank.ts +43 -16
  121. package/template/lib/question-bank-dedicated-search.ts +19 -0
  122. package/template/lib/question-bank-hub-search.ts +90 -0
  123. package/template/lib/question-bank-nav.ts +322 -6
  124. package/template/lib/question-bank-recent-searches.ts +22 -0
  125. package/template/package.json +0 -1
@@ -10,7 +10,7 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
10
10
 
11
11
  ## 1. How to use this file (for AI agents)
12
12
 
13
- 1. **Before** adding or changing a **list, table, board, or data-heavy page**, read **§3–§6** (including **§6.4** page vs drawer when scoping flows) and run the **§13 checklist**.
13
+ 1. **Before** adding or changing a **list, table, board, or data-heavy page**, read **§3–§6** (including **§6.4** drawer vs dialog vs route when scoping overlays and flows) and run the **§13 checklist**.
14
14
  2. **Before** changing **keyboard hints or shortcuts**, read **§7** and root `.cursor/rules/exxat-kbd-shortcuts.mdc`.
15
15
  3. **Before** changing **table behavior**, read **§3** and root `.cursor/rules/exxat-data-tables.mdc`. **Before** wiring **`TablePropertiesDrawer`** on **`ListPageTemplate`** (view tabs), read **§4.2** and **`.cursor/rules/exxat-table-properties-drawer.mdc`**.
16
16
  4. **Before** building or changing **tabs, nav, dialogs, icon-only controls, or color/contrast**, read **§8** and **`.cursor/skills/exxat-accessibility/SKILL.md`** (from monorepo root when the parent repo is open).
@@ -19,17 +19,22 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
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
21
  9. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Question bank style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
22
- 10. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
23
- 11. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
24
- 12. **Before** choosing **drawer vs new page** for a task flow, read **§6.4** and **`docs/data-views-pattern.md`** (Page vs drawer).
25
- 13. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
26
- 14. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
22
+ 10. **Before** adding **shared access / invite collaborators** on a hub (face stack + invite sheet), read **§4.7** and **`.cursor/rules/exxat-collaboration-access.mdc`** / **`.cursor/skills/exxat-collaboration-access/SKILL.md`**.
23
+ 11. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
24
+ 12. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
25
+ 13. **Before** choosing **drawer vs dialog vs new page** for a task flow, read **§6.4**, **`docs/data-views-pattern.md`** (Page vs drawer), and **`docs/drawer-vs-dialog-pattern.md`** (modal vs side panel on the same route).
26
+ 14. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
27
+ 15. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
27
28
  - **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.
28
- - **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**).
29
- 15. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
30
- 16. **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).
29
+ - **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
+ - **If** nothing fits and you would add a **new shared primitive or large bespoke widget**: **ask the user** for direction first — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already explicitly approved a greenfield build).
31
+ 16. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
32
+ 17. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
33
+ 18. **Before** choosing **cards vs table rows vs simple lists** for a hub, read **`docs/card-vs-rows-pattern.md`** and **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
34
+ 19. **Before** adding **`KeyMetrics`** strips on list hubs or dashboard key-metrics cards, read **`docs/kpi-strip-max-four-pattern.md`** and **`.cursor/rules/exxat-kpi-max-four.mdc`** (at most **four** tiles).
35
+ 20. **Before** adding **new shared UI primitives** or bespoke widgets when nothing in **`components/`** fits after scanning, follow **`.cursor/rules/exxat-reuse-before-custom.mdc`** — **ask the user** for direction unless the task already approved a greenfield build.
31
36
 
32
- **Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/command-menu-pattern.md` (keep in sync with this handbook for big refactors).
37
+ **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).
33
38
 
34
39
  ---
35
40
 
@@ -37,8 +42,8 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
37
42
 
38
43
  1. **User / task instructions** in the current session (highest).
39
44
  2. This **`AGENTS.md`** for Exxat DS product patterns.
40
- 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-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
41
- 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`**).
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**.
42
47
 
43
48
  If two documents conflict, prefer the **more specific** rule for the file type, then **newer** team decisions captured in `AGENTS.md`.
44
49
 
@@ -77,7 +82,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
77
82
 
78
83
  **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).
79
84
 
80
- **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`**. 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.
85
+ **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.
81
86
 
82
87
  **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`**.
83
88
 
@@ -159,6 +164,40 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
159
164
 
160
165
  **Cursor rule:** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
161
166
 
167
+ ### 4.7 Collaboration & access (shared hubs)
168
+
169
+ **Use when** a hub is **shared** and users need a **who has access** roster plus **invite by email** with **library access** (Owner / Editor / Commenter / Viewer). **Directory role tags** (Faculty, Program coordinator, Director) are **separate** from library access.
170
+
171
+ **MUST:**
172
+
173
+ | Step | Action |
174
+ |------|--------|
175
+ | **Header** | **`PageHeader`** **`variant="collaboration"`** with **`collaborators`**; **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). |
176
+ | **Invite entry** | **⋯ More** → **Invite people**; header empty CTA / face rail → **`InviteCollaboratorsDrawer`** (floating **`Sheet`**, same family as **`ExportDrawer`**). |
177
+ | **Hub client** | Prefer **`CollaborationAccessFlow`** (or own **`collaborators`** + **`inviteOpen`**); successful invite updates **`collaborators`** for header + sheet. |
178
+ | **Types** | **`PageHeaderCollaborator`** + **`lib/collaborator-access.ts`** — **one** access map per product; customize invite copy per hub, not enum forks. |
179
+ | **Roster** | Single bordered list, row dividers; **name → email → role tags**; trailing **access** badge. |
180
+ | **Invite field** | **`FieldGroup`** + **`Field`**; email + access in **`InputGroup`** (**`InputGroupInput`** + **`InputGroupAddon`** **`Select`** with **`SelectGroup`**); **`FieldDescription`** for email format; **no** toast (**§6.5**). |
181
+
182
+ **MUST NOT:** **`Select`** in **`InputGroupAddon`** without **`InputGroupInput`** / **`SelectGroup`**; per-person cards in the roster; a second invite control **beside** an existing face rail.
183
+
184
+ **Narrative:** **`docs/collaboration-access-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-collaboration-access.mdc`**. **Skill:** **`.cursor/skills/exxat-collaboration-access/SKILL.md`**. **Reference:** Question bank header + client + **`InviteCollaboratorsDrawer`**.
185
+
186
+ ### 4.8 Dedicated search (landing vs results)
187
+
188
+ **Use when** a hub uses **one primary query param** (typically **`?q=`**) with two product states: **empty** → centered **landing** (composer ± recents) vs **non-empty** → **`ListPageTemplate`** / **`DataTable`** results on the same hub stack.
189
+
190
+ **MUST:**
191
+
192
+ | Step | Action |
193
+ |------|--------|
194
+ | **Templates** | **`DedicatedSearchLandingTemplate`** for the empty-query shell; **`DedicatedSearchResultsHeaderChrome`** + **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`** for the results branch chrome. |
195
+ | **Composer** | **`DedicatedSearchUrlComposer`** — hub passes **`patchSearchParams`** (preserve scope / feature flags while merging text) and optional **`onRecordSubmission`**. |
196
+ | **Recents** | **`DedicatedSearchRecents`** + **`createDedicatedSearchRecentsController`** — **MUST NOT** read **`localStorage`** in **`useState`** initializers (**hydration**). |
197
+ | **Naming** | Keep **`DedicatedSearch*`** / **`dedicated-search-*`** generic; domain copy + patchers live next to the hub (**`lib/<entity>-dedicated-search.ts`**) or inline in the hub client. |
198
+
199
+ **Cursor rule:** **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**. **Skill:** **`.cursor/skills/exxat-dedicated-search-surfaces/SKILL.md`**.
200
+
162
201
  ---
163
202
 
164
203
  ## 5. Layout alignment (avoid double inset)
@@ -194,18 +233,18 @@ Match **Placements**:
194
233
 
195
234
  **MUST NOT** treat a main hub table page as a “light” sub-section: use the same shell as Placements (tabs, optional metrics strip, template-level export).
196
235
 
197
- ### 6.4 Page vs drawer (actions and auxiliary views)
236
+ ### 6.4 Page vs drawer vs dialog (actions and auxiliary views)
198
237
 
199
- **SHOULD** choose the surface by whether the user must keep **page context** while acting:
238
+ **SHOULD** choose the surface by whether the user must keep **page context** while acting, and whether the hub may stay **interactable**:
200
239
 
201
- | Use a **drawer / sheet** (side panel) | Use a **new page** (dedicated route) |
202
- |--------------------------------------|----------------------------------------|
203
- | The user needs **the current page behind them** (list, hub, or parent task) **and** a **quick view**, **quick actions**, or a **short auxiliary step** | The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL**, bookmark, or history entry **without** the parent page visible |
204
- | Examples: table/column properties, export, glance at row metadata, lightweight “do one thing and return” | Examples: full create/edit forms, wizards, deep detail that is the main task |
240
+ | Use a **drawer / sheet** (side panel) | Use a **dialog** (modal) | Use a **new page** (dedicated route) |
241
+ |--------------------------------------|---------------------------|----------------------------------------|
242
+ | The user needs **the current page behind them** (list, hub, or parent task) **and** a **quick view**, **quick actions**, or a **short auxiliary step** — e.g. properties, export, invite | A **blocking** short choice — confirm/alert/destructive ack — where the page **must not** stay interactable until answered | The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL**, bookmark, or history entry **without** the parent page visible |
243
+ | Examples: table/column properties, export, glance at row metadata | Examples: `AlertDialog`, delete confirm, compact “save changes?” | Examples: full create/edit forms, wizards, deep detail that is the main task |
205
244
 
206
- **Rationale:** Drawers preserve **spatial context** and reduce navigation churn; full pages avoid cramming complex work into a narrow overlay.
245
+ **Rationale:** Drawers preserve **spatial context**; dialogs enforce **focus**; full pages avoid cramming complex work into overlays.
207
246
 
208
- **Details:** `docs/data-views-pattern.md` (Page vs drawer). Root **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
247
+ **Details:** `docs/data-views-pattern.md` (Page vs drawer), **`docs/drawer-vs-dialog-pattern.md`** (drawer vs modal on the same route). Root **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
209
248
 
210
249
  ### 6.5 Messaging — no toast
211
250
 
@@ -391,6 +430,8 @@ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcu
391
430
  | Full dashboard route | `DashboardTabs`, `KeyMetrics`, `ChartsOverview` | `app/(app)/dashboard/page.tsx`, `components/dashboard-tabs.tsx` |
392
431
  | Board cards | **`ListPageBoardCard`** + primitives + entity card (**§4.4**) | `components/data-views/list-page-board-card.tsx`, `board-card-primitives.tsx`, `placement-board-card.tsx` |
393
432
  | **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** |
433
+ | **Collaboration & access** (face rail + invite sheet) | **`PageHeader` `variant="collaboration"`**, **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`** | `components/page-header.tsx`, `components/invite-collaborators-drawer.tsx`, `components/question-bank-page-header.tsx`, `components/question-bank-client.tsx`, **`§4.7`**, **`docs/collaboration-access-pattern.md`** |
434
+ | **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`** |
394
435
  | Persistence (example) | Page + lifecycle keys | `lib/data-list-persistence.ts`, `DataListClient` / `DataListTable` |
395
436
  | Coach marks / tours | `CoachMark`, `useCoachMark`, coach mark registry | `components/ui/coach-mark.tsx`, `hooks/use-coach-mark.ts`, `lib/coach-mark-registry.ts` |
396
437
  | Settings page | Coach mark management | `app/(app)/settings/page.tsx`, `components/settings-client.tsx` |
@@ -403,7 +444,7 @@ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcu
403
444
 
404
445
  - **Product (Exxat One / Prism):** Use **`ExxatProductLogo`** for the header product control and **`ProductSwitcher`** — do **not** substitute logo.dev rasters unless product explicitly requests it.
405
446
  - **School logos:** Use **`logoDevUrl()`** from **`lib/logo-dev.ts`** in **`NAV_SCHOOLS`**; optional env **`NEXT_PUBLIC_LOGO_DEV_TOKEN`**.
406
- - **Team / program dropdown:** Override **`DropdownMenuContent`** default **`w-(--radix-dropdown-menu-trigger-width)`** for the school switcher (e.g. **`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so long names are not forced to wrap like the narrow sidebar trigger. **Do not truncate** school or program labels; wrap with **`items-start`**, **`break-words`**, **`whitespace-normal`**. Selected-school summary shows **school + current program**.
447
+ - **Team / program dropdown:** The shared **`DropdownMenuContent`** uses **intrinsic width** (**`min-w-52 w-max`** + viewport-capped **`max-w`**) so view menus and table actions are not squeezed to the trigger. The **school / program** switcher still passes an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) for long labels. **Do not truncate** school or program labels; wrap with **`items-start`**, **`break-words`**, **`whitespace-normal`**. Selected-school summary shows **school + current program**.
407
448
  - **Team switcher trigger:** **`SidebarMenuButton` `size="lg"`** is **`h-12`** + **`overflow-hidden`** and **clips** the program line — when expanded or mobile, use **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**; on **icon rail**, hide text with **`group-data-[collapsible=icon]:hidden`**.
408
449
  - **Nav items with children:** **Popover** on desktop **icon rail**; **Collapsible** when expanded. **MUST NOT** use **`SidebarMenuButton` `tooltip={…}`** as the **direct** child of **`CollapsibleTrigger asChild`** (extra **`Tooltip` root** breaks Radix **`Slot`** / **`React.Children.only`**).
409
450
  - **Mock profile photo:** **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs.
@@ -463,6 +504,10 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
463
504
  ## 12. Documentation
464
505
 
465
506
  - **Deep dive:** `docs/data-views-pattern.md` (includes **Page vs drawer** with **§6.4**)
507
+ - **Drawer vs dialog (same route):** `docs/drawer-vs-dialog-pattern.md` — **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**
508
+ - **Cards vs table rows:** `docs/card-vs-rows-pattern.md` — **`.cursor/rules/exxat-card-vs-list-rows.mdc`**
509
+ - **KPI strip (max four tiles):** `docs/kpi-strip-max-four-pattern.md` — **`.cursor/rules/exxat-kpi-max-four.mdc`**
510
+ - **KPI deltas & trend arrows:** `docs/kpi-trend-pattern.md` (`MetricItem.trendPolarity`, `KeyMetrics`, chart mini-metrics)
466
511
  - **Global command palette (⌘K):** `docs/command-menu-pattern.md`
467
512
  - **No toast / snackbars:** **§6.5**, root **`.cursor/rules/exxat-no-toast.mdc`**
468
513
  - **This handbook:** `exxat-ds/AGENTS.md` (keep checklist sections updated when patterns change)
@@ -479,7 +524,10 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
479
524
  | Match Placements for export + primary CTA + More menu | Outline button as the single primary CTA on exportable pages |
480
525
  | Pair `Kbd` hints with real shortcuts | Browser-reserved chords for app actions |
481
526
  | Global palette: **§7.1** — search + quick in-menu AI vs **Ask Leo**; **`dataGroups`** + **`searchOnly`** for bulky indexes | Palette as link-only dump; AI that belongs in **Ask Leo** forced into the palette; mounting full **`dataGroups`** on open when **`searchOnly`** should hide them |
482
- | **§6.4** — drawer when **page context + quick** view/actions; **new page** for primary / long / own-URL flows | Forcing **full workflows** into a drawer when a route fits; or **routing** for tasks that are only quick glances over a hub |
527
+ | **§6.4** — **drawer** when **page context + quick** view/actions; **dialog** for **blocking** confirm/alert/short choice; **new page** for primary / long / own-URL flows | Forcing **full workflows** into a drawer when a route fits; using a **dialog** when users must **reference** the grid (prefer drawer); **routing** for tasks that are only quick glances over a hub |
528
+ | **KPI strips** — **≤ 4** `MetricItem` per **`KeyMetrics`** on template metrics + Data-tab key-metrics cards (**`KEY_METRICS_KPI_COUNT_MAX`**) | Fifth+ headline tile in the same strip; duplicate tiles to pad count |
529
+ | **Cards vs rows** — **DataTable** for dense comparable hubs; **`ListPageBoardCard`** / **`ListPageViewFrame`** when visual/kanban/folder — **`docs/card-vs-rows-pattern.md`** | Card walls for **50+** homogeneous records where the product expects **sort/filter/compare** without a deliberate UX exception |
530
+ | **Reuse before custom** — scan **`components/`** + **§9**; **ask the user** before new shared primitives or large bespoke widgets — **`exxat-reuse-before-custom.mdc`** | Parallel stacks; silent new “table” or metric systems when **`DataTable`** / **`KeyMetrics`** already apply |
483
531
  | **§6.5** — feedback via **banners / inline / dialogs** — **no** toast or snackbar | **`toast()`** / **Sonner** / transient corner notifications for product messaging |
484
532
  | Meet **§8** + **`exxat-accessibility`** skill (ARIA, 24px targets, contrast, **§8.3** min **11px** text, overlay titles) | `tablist` mixing non-tabs; **16px** sole targets; dialogs without titles; text below **11px** (except legally required fine print) |
485
533
  | Use `CoachMark` + `useCoachMark` for onboarding tours (§11); register in `coach-mark-registry` | Build one-off walkthrough overlays or custom onboarding modals |
@@ -487,6 +535,8 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
487
535
  | 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) |
488
536
  | **§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**) |
489
537
  | **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav | **`secondaryPanel`** id with no panel component or activator |
538
+ | **§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
+ | **§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 |
490
540
  | **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 |
491
541
 
492
542
  ---
@@ -496,7 +546,7 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
496
546
  Copy and complete when implementing or reviewing:
497
547
 
498
548
  - [ ] **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`**.
499
- - [ ] **Reuse:** `ListPageTemplate`, `DataTable` / `useTableState`, `TablePropertiesDrawer` — no parallel bespoke tabs/filters.
549
+ - [ ] **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`**.
500
550
  - [ ] **Tabs:** Any main `DataTable` sits under `ListPageTemplate` with appropriate view tabs.
501
551
  - [ ] **Inset:** No double horizontal padding around `DataTable`.
502
552
  - [ ] **§4.5 View shells:** Folder / panel / icon views use **`ListPageViewFrame`** (or a **`data-views/`** component that uses it); no page-tied-only grid wrappers; **`DataTable`** not double-wrapped (**§5**).
@@ -508,7 +558,9 @@ Copy and complete when implementing or reviewing:
508
558
  - [ ] **Data view dashboard (Placements / Team / Compliance):** Charts use **`ChartFigure`** + **`ChartDataTable`**; **Edit layout** on toolbar; **`activeBar` / `activeShape`** keyboard styling from **`lib/chart-keyboard-selection`** — not opacity-only **`Cell`** hacks (§4.3).
509
559
  - [ ] **Dashboard layout persistence:** **`lib/data-view-dashboard-storage`** (or **`saveDashboardLayout`** / **`loadDashboardLayout`** on Placements); **`mergeDashboardLayout`** on load — no new ad-hoc storage keys for the same layout (§4.3).
510
560
  - [ ] **⌘K palette (§7.1):** If adding or changing **`dataGroups`**, map rows in **`lib/command-menu-search-data.ts`** (not `command-menu.tsx`); use **`searchOnly`** on bulky groups; keep **`docs/command-menu-pattern.md`** aligned.
511
- - [ ] **Page vs drawer (§6.4):** Quick auxiliary actions with **parent context** → drawer/sheet; primary or long flows → **new route** — see **`docs/data-views-pattern.md`**.
561
+ - [ ] **Page vs drawer vs dialog (§6.4):** Quick auxiliary with **parent context** and interactable hub **drawer/sheet**; **blocking** short confirm → **dialog**; primary or long flows → **new route** — **`docs/data-views-pattern.md`**, **`docs/drawer-vs-dialog-pattern.md`**.
562
+ - [ ] **Cards vs rows:** Primary sortable hub with many homogeneous records → **`DataTable`**; kanban / visual tiles → **`ListPageBoardCard`** — **`docs/card-vs-rows-pattern.md`**, **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
563
+ - [ ] **KPI count (max four):** **`entityKpiMetrics`** (and any static **`MetricItem[]`** for the same strip) has **≤ 4** tiles for template metrics + Data-tab key-metrics — **`docs/kpi-strip-max-four-pattern.md`**, **`.cursor/rules/exxat-kpi-max-four.mdc`**.
512
564
  - [ ] **No toast (§6.5):** No **`toast()`** / Sonner / snackbars — use banners, inline status, or dialogs.
513
565
  - [ ] **Typography (§8.3):** No visible copy below **11px** — use **`text-xs`** (`--text-xs` in **`globals.css`**); board/list cards use **`text-xs`** / **`text-sm`** for body lines.
514
566
  - [ ] **Board cards (§4.4):** **`ListPageBoardCard`** + hierarchy (title → badge row → body); **`ListPageBoardCardAvatar`** when appropriate; status via **`ListHubStatusBadge`** + **`lib/list-status-badges`** — **not** `uppercase` on labels; **`BoardCardTwoLineBlock`** for stacked facts.
@@ -517,10 +569,13 @@ Copy and complete when implementing or reviewing:
517
569
  - [ ] **Kbd:** Follow `exxat-kbd-shortcuts.mdc` if adding shortcuts or hints.
518
570
  - [ ] **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.
519
571
  - [ ] **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`**.
520
- - [ ] **Application sidebar (§9.1):** **`ExxatProductLogo`** for product; **`logoDevUrl`** for schools; team switcher **`DropdownMenuContent`** not trigger-width-only (**`!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`**.
572
+ - [ ] **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`**.
521
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`**.
574
+ - [ ] **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
+ - [ ] **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
+ - [ ] **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`**.
522
577
  - [ ] **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`**.
523
578
 
524
579
  ---
525
580
 
526
- *Last updated: §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.*
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.*
@@ -5,7 +5,8 @@ import { Button } from "@/components/ui/button"
5
5
  const LINKS = [
6
6
  { href: "/dashboard", label: "Dashboard", description: "Metrics, charts, and layout patterns." },
7
7
  { href: "/data-list", label: "List hub", description: "Table, list, board, and dashboard views on shared state." },
8
- { href: "/question-bank", label: "Question bank", description: "Folders, OS folder view, panel, and tree demos on mock items." },
8
+ { href: "/question-bank", label: "Question bank", description: "Discovery hub for browsing folders, recents, and AI-assisted create/import flows." },
9
+ { href: "/question-bank/library", label: "Question library", description: "Folders, OS folder view, panel, and tree demos on mock items." },
9
10
  { href: "/settings", label: "Settings", description: "Appearance, tours, and shell preferences." },
10
11
  { href: "/help", label: "Help", description: "Support and documentation entry points." },
11
12
  ] as const
@@ -28,6 +28,12 @@ export default function HelpPage() {
28
28
  <Link href="/settings#appearance">App settings</Link>
29
29
  </Button>
30
30
  </div>
31
+ <section id="more" className="scroll-mt-20 mt-10">
32
+ <h2 className="text-sm font-semibold text-foreground">More</h2>
33
+ <p className="mt-1 text-sm text-muted-foreground leading-relaxed">
34
+ Additional resources and shortcuts from the sidebar land here when you choose More.
35
+ </p>
36
+ </section>
31
37
  </div>
32
38
  </PrimaryPageTemplate>
33
39
  )
@@ -10,7 +10,7 @@ import { SystemBannerSlot } from "@/components/system-banner-slot"
10
10
  import { CommandMenu } from "@/components/command-menu"
11
11
  import { CommandMenuProvider } from "@/contexts/command-menu-context"
12
12
  import { buildCommandMenuConfig } from "@/lib/command-menu-config"
13
- import { getCommandMenuSearchDataGroups } from "@/lib/command-menu-search-data"
13
+ import { COMMAND_MENU_SEARCH_DATA_GROUPS } from "@/lib/command-menu-search-data"
14
14
 
15
15
  /**
16
16
  * Shared app layout:
@@ -21,14 +21,17 @@ import { getCommandMenuSearchDataGroups } from "@/lib/command-menu-search-data"
21
21
  * via SystemBannerProvider) — no hardcoded copy here.
22
22
  */
23
23
  export default function AppLayout({ children }: { children: React.ReactNode }) {
24
+ const commandMenuConfig = React.useMemo(
25
+ () => buildCommandMenuConfig({ dataGroups: COMMAND_MENU_SEARCH_DATA_GROUPS }),
26
+ [],
27
+ )
28
+
24
29
  return (
25
30
  <DashboardViewProvider>
26
31
  <ChartVariantProvider>
27
32
  <AskLeoProvider>
28
33
  <SystemBannerProvider>
29
- <CommandMenuProvider
30
- value={buildCommandMenuConfig({ dataGroups: getCommandMenuSearchDataGroups() })}
31
- >
34
+ <CommandMenuProvider value={commandMenuConfig}>
32
35
 
33
36
  <SidebarShell wrapperClassName="flex min-h-svh flex-col">
34
37
  {/* ⌘K command palette */}
@@ -0,0 +1,12 @@
1
+ import { Suspense } from "react"
2
+
3
+ import { QuestionBankClient } from "@/components/question-bank-client"
4
+
5
+ /** Discovery hub composer results — same hub chrome as the library, distinct from `/question-bank/list`. */
6
+ export default function QuestionBankHubFindPage() {
7
+ return (
8
+ <Suspense fallback={null}>
9
+ <QuestionBankClient />
10
+ </Suspense>
11
+ )
12
+ }
@@ -0,0 +1,46 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { usePathname } from "next/navigation"
5
+
6
+ import { useSecondaryPanel } from "@/components/secondary-panel"
7
+ import { QUESTION_BANK_ENTRY_PATH } from "@/lib/question-bank-nav"
8
+
9
+ /**
10
+ * Keeps the nested secondary panel open across library / list / find navigations.
11
+ * The discovery hub (`/question-bank`) is full-width — no secondary bar there.
12
+ */
13
+ export default function QuestionBankLayout({ children }: { children: React.ReactNode }) {
14
+ const pathname = usePathname()
15
+ const { openPanel, closePanel, activePanel } = useSecondaryPanel()
16
+ const closePanelRef = React.useRef(closePanel)
17
+ const openPanelRef = React.useRef(openPanel)
18
+ closePanelRef.current = closePanel
19
+ openPanelRef.current = openPanel
20
+
21
+ /** Leaving `/question-bank/*` entirely — close nested panel without forcing primary sidebar open (⌘B / cookie). */
22
+ React.useEffect(() => {
23
+ return () => {
24
+ closePanelRef.current({ mainSidebar: "leave" })
25
+ }
26
+ }, [])
27
+
28
+ /** Only react to route changes — refs carry latest open/close. */
29
+ React.useEffect(() => {
30
+ const isDiscoveryHubRoot =
31
+ pathname === QUESTION_BANK_ENTRY_PATH || pathname === `${QUESTION_BANK_ENTRY_PATH}/`
32
+
33
+ if (isDiscoveryHubRoot) {
34
+ closePanelRef.current({ mainSidebar: "leave" })
35
+ return undefined
36
+ }
37
+
38
+ if (activePanel !== "question-bank") {
39
+ openPanelRef.current("question-bank")
40
+ }
41
+ return undefined
42
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- pathname + activePanel drive when to ensure panel id
43
+ }, [pathname, activePanel])
44
+
45
+ return children
46
+ }
@@ -0,0 +1,11 @@
1
+ import { Suspense } from "react"
2
+
3
+ import { QuestionBankClient } from "@/components/question-bank-client"
4
+
5
+ export default function QuestionBankLibraryPage() {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <QuestionBankClient />
9
+ </Suspense>
10
+ )
11
+ }
@@ -0,0 +1,12 @@
1
+ import { Suspense } from "react"
2
+
3
+ import { QuestionBankClient } from "@/components/question-bank-client"
4
+
5
+ /** Question bank list surface — same hub as `/question-bank/library`, optimized for `?q=` search landings. */
6
+ export default function QuestionBankListPage() {
7
+ return (
8
+ <Suspense fallback={null}>
9
+ <QuestionBankClient />
10
+ </Suspense>
11
+ )
12
+ }
@@ -1,10 +1,11 @@
1
1
  import { Suspense } from "react"
2
- import { QuestionBankClient } from "@/components/question-bank-client"
3
2
 
4
- export default function QuestionBankPage() {
3
+ import { QuestionBankHubClient } from "@/components/question-bank-hub-client"
4
+
5
+ export default function QuestionBankHubPage() {
5
6
  return (
6
7
  <Suspense fallback={null}>
7
- <QuestionBankClient />
8
+ <QuestionBankHubClient />
8
9
  </Suspense>
9
10
  )
10
11
  }
@@ -10,9 +10,8 @@
10
10
 
11
11
  @import "tailwindcss";
12
12
  @import "tw-animate-css";
13
- @import "shadcn/tailwind.css";
14
13
 
15
- /* Ensure Tailwind scans the shared UI package for utility classes */
14
+ /* Ensure Tailwind scans the shared UI package for utility classes (repo-relative — stable with pnpm + Turbopack). */
16
15
  @source "../node_modules/@exxatdesignux/ui/src";
17
16
 
18
17
  /* RTL layout direction support */
@@ -85,6 +85,7 @@ import {
85
85
  type NavSchool,
86
86
  type NavProgram,
87
87
  } from "@/lib/mock/navigation"
88
+ import { QUESTION_BANK_ENTRY_PATH } from "@/lib/question-bank-nav"
88
89
 
89
90
  /** Path segment of a nav URL (strip `#fragment` for matching). */
90
91
  function navUrlPath(url: string): string {
@@ -93,11 +94,42 @@ function navUrlPath(url: string): string {
93
94
  return i === -1 ? url : url.slice(0, i)
94
95
  }
95
96
 
96
- function isNavActive(pathname: string, url: string): boolean {
97
+ /** Hash segment from a nav `href` (no `#`). `null` when the URL has no `#`. */
98
+ function navUrlFragment(url: string): string | null {
99
+ if (!url.includes("#")) return null
100
+ return url.slice(url.indexOf("#") + 1)
101
+ }
102
+
103
+ function normalizedLocationHash(locationHash: string): string {
104
+ if (!locationHash) return ""
105
+ return locationHash.startsWith("#") ? locationHash.slice(1) : locationHash
106
+ }
107
+
108
+ /**
109
+ * Whether `pathname` (+ optional `location.hash`) matches a sidebar `href`.
110
+ * When several links share the same path (e.g. `/settings`), disambiguate with `#fragment`
111
+ * and require an empty hash for the “default” row (`/settings` with no `#`).
112
+ */
113
+ function isNavActive(pathname: string, url: string, locationHash = ""): boolean {
97
114
  const pathOnly = navUrlPath(url)
115
+ const frag = navUrlFragment(url)
116
+ const h = normalizedLocationHash(locationHash)
117
+
98
118
  if (!pathOnly || pathOnly === "#") return false
99
- if (pathOnly === "/") return pathname === "/"
100
- if (pathname === pathOnly) return true
119
+
120
+ if (frag !== null) {
121
+ if (pathOnly === "/") return pathname === "/" && h === frag
122
+ if (pathOnly === "/library") {
123
+ return pathname.startsWith("/library/") && h === frag
124
+ }
125
+ if (pathOnly.startsWith("/library/")) {
126
+ return pathname === pathOnly && h === frag
127
+ }
128
+ return pathname === pathOnly && h === frag
129
+ }
130
+
131
+ if (pathOnly === "/") return pathname === "/" && h === ""
132
+ if (pathname === pathOnly) return h === ""
101
133
  // Design system library — active on hub and detail routes.
102
134
  if (pathOnly === "/library") {
103
135
  return pathname.startsWith("/library/")
@@ -116,11 +148,11 @@ function isCollapsibleChildActive(
116
148
  locationHash: string
117
149
  ): boolean {
118
150
  const children = parent.children
119
- if (!children?.length) return isNavActive(pathname, child.url)
151
+ if (!children?.length) return isNavActive(pathname, child.url, locationHash)
120
152
 
121
153
  const hasHashChild = children.some(c => c.url.includes("#"))
122
154
  if (hasHashChild) {
123
- const h = locationHash.startsWith("#") ? locationHash.slice(1) : locationHash
155
+ const h = normalizedLocationHash(locationHash)
124
156
  const childHash = child.url.includes("#") ? child.url.split("#")[1] : ""
125
157
  if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) {
126
158
  return h === ""
@@ -131,7 +163,7 @@ function isCollapsibleChildActive(
131
163
  return false
132
164
  }
133
165
 
134
- if (!isNavActive(pathname, child.url)) return false
166
+ if (!isNavActive(pathname, child.url, locationHash)) return false
135
167
 
136
168
  const urls = children.map(c => c.url)
137
169
  const allSameUrl = urls.length > 1 && urls.every(u => u === urls[0])
@@ -204,12 +236,12 @@ function SidebarNavChildLink({
204
236
  */
205
237
  function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: string }) {
206
238
  const locationHash = useLocationHash()
207
- const isActive = isNavActive(pathname, item.url)
239
+ const isActive = isNavActive(pathname, item.url, locationHash)
208
240
  const isAnyChildActive =
209
241
  item.children?.some(c => isCollapsibleChildActive(pathname, item, c, locationHash)) ?? false
210
242
  const { state, isMobile } = useSidebar()
211
243
  const { openPanel } = useSecondaryPanel()
212
- const [open, setOpen] = React.useState(isAnyChildActive)
244
+ const [open, setOpen] = React.useState(false)
213
245
  const [flyoutOpen, setFlyoutOpen] = React.useState(false)
214
246
  const flyoutTitleId = React.useId()
215
247
  const iconRailCollapsed = state === "collapsed" && !isMobile
@@ -368,7 +400,8 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
368
400
  }
369
401
 
370
402
  function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: string }) {
371
- const { openPanel } = useSecondaryPanel()
403
+ const { openPanel, closePanel } = useSecondaryPanel()
404
+ const locationHash = useLocationHash()
372
405
  return (
373
406
  <>
374
407
  {items.map(item => {
@@ -379,7 +412,7 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
379
412
  return <CollapsibleNavItem key={item.key} item={item} pathname={pathname} />
380
413
  }
381
414
 
382
- const isActive = isNavActive(pathname, item.url)
415
+ const isActive = isNavActive(pathname, item.url, locationHash)
383
416
  const itemPath = navUrlPath(item.url)
384
417
  return (
385
418
  <SidebarMenuItem key={item.key}>
@@ -400,7 +433,11 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
400
433
  !item.url.includes("#")
401
434
  ) {
402
435
  e.preventDefault()
403
- openPanel(item.secondaryPanel)
436
+ if (itemPath === QUESTION_BANK_ENTRY_PATH) {
437
+ closePanel({ mainSidebar: "leave" })
438
+ } else {
439
+ openPanel(item.secondaryPanel)
440
+ }
404
441
  }
405
442
  }}
406
443
  >
@@ -465,6 +502,7 @@ function SidebarNavSecondaryItems({
465
502
  pathname: string
466
503
  }) {
467
504
  const mod = useModKeyLabel()
505
+ const locationHash = useLocationHash()
468
506
  return (
469
507
  <>
470
508
  {items.map((item) => {
@@ -473,7 +511,7 @@ function SidebarNavSecondaryItems({
473
511
  !item.opensCommandMenu &&
474
512
  Boolean(pathOnly) &&
475
513
  pathOnly !== "#" &&
476
- isNavActive(pathname, item.url)
514
+ isNavActive(pathname, item.url, locationHash)
477
515
 
478
516
  return (
479
517
  <SidebarMenuItem key={item.key}>
@@ -767,7 +805,7 @@ function ProductLogoButton() {
767
805
  </TooltipContent>
768
806
  </Tooltip>
769
807
 
770
- <DropdownMenuContent className="w-52" align="start" side="right" sideOffset={8}>
808
+ <DropdownMenuContent align="start" side="right" sideOffset={8}>
771
809
  <DropdownMenuLabel className="text-xs text-muted-foreground">
772
810
  Switch product
773
811
  </DropdownMenuLabel>