@exxatdesignux/ui 0.0.5 → 0.0.7

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 (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
package/bin/init.mjs ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { cpSync, existsSync, readdirSync } from 'fs'
3
+ import { resolve, dirname } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { execSync } from 'child_process'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+ const templateDir = resolve(__dirname, '../template')
9
+ const targetDir = process.cwd()
10
+
11
+ const files = readdirSync(targetDir)
12
+ const isEmpty = files.length === 0 || (files.length === 1 && files[0] === '.git')
13
+
14
+ if (!isEmpty) {
15
+ console.error('❌ Target directory is not empty. Run this in a new empty folder.')
16
+ process.exit(1)
17
+ }
18
+
19
+ console.log('📦 Copying Exxat DS starter app...')
20
+ cpSync(templateDir, targetDir, { recursive: true })
21
+
22
+ console.log('📥 Installing dependencies...')
23
+ execSync('npm install', { stdio: 'inherit', cwd: targetDir })
24
+
25
+ console.log('')
26
+ console.log('✅ Done! Your Exxat DS app is ready.')
27
+ console.log('')
28
+ console.log(' npm run dev → start dev server')
29
+ console.log('')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,8 +29,13 @@
29
29
  },
30
30
  "main": "./src/index.ts",
31
31
  "types": "./src/index.ts",
32
+ "bin": {
33
+ "create-exxat-app": "./bin/init.mjs"
34
+ },
32
35
  "files": [
33
- "src"
36
+ "src",
37
+ "bin",
38
+ "template"
34
39
  ],
35
40
  "dependencies": {
36
41
  "@hookform/resolvers": "^5.2.2",
@@ -0,0 +1 @@
1
+ 25
@@ -0,0 +1,7 @@
1
+ dist/
2
+ node_modules/
3
+ .next/
4
+ .turbo/
5
+ coverage/
6
+ pnpm-lock.yaml
7
+ .pnpm-store/
@@ -0,0 +1,11 @@
1
+ {
2
+ "endOfLine": "lf",
3
+ "semi": false,
4
+ "singleQuote": false,
5
+ "tabWidth": 2,
6
+ "trailingComma": "es5",
7
+ "printWidth": 80,
8
+ "plugins": ["prettier-plugin-tailwindcss"],
9
+ "tailwindStylesheet": "app/globals.css",
10
+ "tailwindFunctions": ["cn", "cva"]
11
+ }
@@ -0,0 +1,485 @@
1
+ # Exxat DS — agent handbook (humans & AI)
2
+
3
+ **Purpose:** One place for product patterns so tools (Cursor, Codex, etc.) and contributors apply the same rules. **Imperative sections** use MUST / MUST NOT / SHOULD so they are easy to parse.
4
+
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 repo, use **`exxat-ds/AGENTS.md`**.
6
+
7
+ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tables, keyboard hints, accessibility) when the parent repo is open.
8
+
9
+ ---
10
+
11
+ ## 1. How to use this file (for AI agents)
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**.
14
+ 2. **Before** changing **keyboard hints or shortcuts**, read **§7** and root `.cursor/rules/exxat-kbd-shortcuts.mdc`.
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
+ 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).
17
+ 5. **Before** adding or changing **Data view charts** (dashboard tab on list hubs) or **graph keyboard styling**, read **§4.3** and **`exxat-ds/.cursor/rules/exxat-dashboard-view-charts.mdc`**.
18
+ 6. **Before** adding or changing **board (kanban) cards** on list hubs, read **§4.4** and the **`exxat-board-cards`** skill (**`.cursor/skills/`** or **`.claude/skills/`** at repo root — same content).
19
+ 7. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
20
+ 8. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
21
+ 9. **Before** choosing **drawer vs new page** for a task flow, read **§6.4** and **`docs/data-views-pattern.md`** (Page vs drawer).
22
+ 10. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
23
+ 11. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
24
+ - **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.
25
+ - **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`.
26
+ 12. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
27
+ 13. **Before** changing the **application sidebar** (school/program switcher, product logo, profile block, or collapsible nav with children), read **§9.1** and **exxat-ds-skill §3.1**.
28
+
29
+ **Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/command-menu-pattern.md` (keep in sync with this handbook for big refactors).
30
+
31
+ ---
32
+
33
+ ## 2. Rule precedence
34
+
35
+ 1. **User / task instructions** in the current session (highest).
36
+ 2. This **`AGENTS.md`** for Exxat DS product patterns.
37
+ 3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
38
+ 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).
39
+
40
+ If two documents conflict, prefer the **more specific** rule for the file type, then **newer** team decisions captured in `AGENTS.md`.
41
+
42
+ ---
43
+
44
+ ## 3. Data tables (product lists)
45
+
46
+ **MUST** for any screen that is a **browsable, filterable grid of records** (lists, directories, placements, team roster, etc.):
47
+
48
+ | Requirement | Action |
49
+ |-------------|--------|
50
+ | Base table | Use **`DataTable`** from `@/components/data-table` (and **`DataTablePaginated`** when pagination is required). |
51
+ | Search | Wire **find-in-list** search (toolbar or equivalent). Do not ship a bare table with no search on a data-list page. |
52
+ | Filters | Use the **shared filter model** (`FilterFieldDef`, operators, chips) consistent with existing list pages. |
53
+ | Table properties | Expose **Table properties** via **`TablePropertiesDrawer`** (`@/components/table-properties`) — columns, density, related options — same pattern as Placements / data list. |
54
+
55
+ **Reference:** `components/data-list-table.tsx`, `components/data-table/`.
56
+
57
+ **MUST NOT:** Build product list pages with only `@/components/ui/table`, raw `<table>`, or a third-party grid that bypasses this stack.
58
+
59
+ **Exception:** Tiny read-only tables **inside** charts or analytics cards (not primary data-list experiences) may use minimal markup; still use design tokens and accessibility.
60
+
61
+ ---
62
+
63
+ ## 4. View tabs + `DataTable`
64
+
65
+ **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.
66
+
67
+ **Reference implementations:** `components/data-list-client.tsx` (Placements), `components/team-client.tsx` (Team).
68
+
69
+ **Rationale:** Consistent navigation, saved views, per-tab view type (table / list / board / dashboard), export at template level.
70
+
71
+ ### 4.1 Connected views + mock data
72
+
73
+ **MUST** wire **every** view type the template exposes (table, list, board, dashboard) to the **same** `useTableState` instance: non-table surfaces read **`tableState.rows`** (filtered/sorted like the grid). **MUST NOT** ship placeholder copy such as “not wired for this demo” for those views when the entity has a table stack.
74
+
75
+ **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).
76
+
77
+ **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.
78
+
79
+ **Dashboard view tab:** **MUST** reuse **`KeyMetrics`** (same component as the optional template metrics strip) and the same KPI helpers — **MUST NOT** introduce bespoke `Card`-only metric grids for the same numbers. Full-page dashboards may also use **`DashboardTabs`**, **`ChartsOverview`**, etc. (`app/(app)/dashboard/page.tsx`); use those **shared** dashboard components when charts or multi-section layouts are product-appropriate, not one-off duplicates.
80
+
81
+ **Details:** `docs/data-views-pattern.md` (mock data, connected views, dashboard view).
82
+
83
+ ### 4.2 `TablePropertiesDrawer` and the active view
84
+
85
+ **MUST:** Any page that uses **`ListPageTemplate`** with **`tab.viewType`** (table / list / board / dashboard) and renders **`TablePropertiesDrawer`** **MUST** pass:
86
+
87
+ | Prop | Source |
88
+ |------|--------|
89
+ | **`currentView`** | The same **`DataListViewType`** as the tab’s active view (e.g. **`view={tab.viewType}`** on the table component). |
90
+ | **`onViewChange`** | From **`renderContent={(tab, updateTab) => ...}`**: **`(v) => updateTab({ viewType: v, icon: dataListViewIcon(v) })`** — import **`dataListViewIcon`** from **`@/lib/data-list-view`**. |
91
+
92
+ 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.
93
+
94
+ **Reference:** `components/data-list-table.tsx`, `components/team-table.tsx`, `components/compliance-table.tsx`. Root **`.cursor/rules/exxat-table-properties-drawer.mdc`**.
95
+
96
+ ### 4.3 Data view dashboard — charts, customisation, and parity with the gallery
97
+
98
+ **MUST** for the **dashboard** view tab on **Placements, Team, Compliance** (and any page that copies this pattern):
99
+
100
+ | Topic | Rule |
101
+ |-------|------|
102
+ | **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. |
103
+ | **Two “dashboard” surfaces** | The **`/dashboard`** route uses **`DashboardTabs`** + **`ChartsOverview`** (gallery / demos). The **Data** tab on list hubs uses **`*DashboardChartsSection`** (`PlacementsDashboardChartsSection`, Team, Compliance) and **`data-view-dashboard-charts*.tsx`**. Both share **`ChartFigure`**, **`ChartCard`**, and **`useChartVariant()`**; they are **not** duplicate chart engines — product charts belong in the shared components above. |
104
+ | **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. |
105
+ | **Customise UI** | Toggle **Edit layout** on the hub dashboard toolbar (`DataListDashboardShell` / Team / Compliance). **`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']`**. |
106
+ | **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. |
107
+ | **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`**. |
108
+ | **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]`). |
109
+ | **Persistence (centralized)** | Layout for all three hubs is stored in one bundle: **`lib/data-view-dashboard-storage.ts`** (`exxat-ds:data-view-dashboards:v1`, scopes **`placements` \| `team` \| `compliance`**). Placements wrappers: **`loadDashboardLayout`** / **`saveDashboardLayout`**; generic API: **`loadDataViewLayout`** / **`saveDataViewLayout`**. Legacy per-hub keys are migrated when a scope is missing. **MUST NOT** add a fourth localStorage key pattern for the same layout shape without extending this module. |
110
+
111
+ **Reference:** `components/data-view-dashboard-charts.tsx`, `data-view-dashboard-charts-team.tsx`, `data-view-dashboard-charts-compliance.tsx`, `components/data-list-table.tsx` (`DataListDashboardShell`), `lib/chart-keyboard-selection.ts`, `lib/data-view-dashboard-storage.ts`, **`exxat-ds/.cursor/rules/exxat-dashboard-view-charts.mdc`**.
112
+
113
+ ### 4.4 Board cards (kanban)
114
+
115
+ **MUST** for **product board views** on list hubs (Team, Compliance, Placements, and any new hub with **`viewType === "board"`**):
116
+
117
+ | Topic | Rule |
118
+ |-------|------|
119
+ | **Shell** | Compose **`ListPageBoardCard`** from **`components/data-views/list-page-board-card.tsx`** — same **`Card` `size="sm"`** ring/hover/`isNew` pattern as **`BoardPlacementCard`**. **MUST NOT** hand-roll alternate card chrome (one-off `<button>` + border classes) for the same surfaces. |
120
+ | **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. |
121
+ | **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). |
122
+ | **Avatar** | Use **`ListPageBoardCardAvatar`** with entity **`initials`** when present; otherwise derive with **`initialsFromDisplayName`** from **`lib/initials-from-name.ts`** (e.g. compliance owner name). |
123
+ | **Status labels + colors** | **All list hubs** (**Placements**, **Team**, **Compliance**, **Question bank**, …) **MUST** define status **labels**, **tint classes**, and **icons** in **`lib/list-status-badges.ts`**. Render with **`ListHubStatusBadge`**, or **`StatusBadge`** from **`components/data-list-table-cells.tsx`** for Placements (wrapper over **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`**). **`surface="table"`** for **`DataTable`** / **list** rows; **`surface="board"`** in **`ListPageBoardCardBadgeRow`**. **SHOULD** map values onto **`LIST_HUB_STATUS_TINT_*`** (success / warning / neutral / danger / **info**) before inventing new palettes. **MUST NOT** duplicate maps in feature files or add **`uppercase`** / **`tracking-wide`**. |
124
+ | **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/data-list-table-cells.tsx`**, which wraps **`ListHubStatusBadge`** with **`PLACEMENT_STATUS_*`** maps in **`lib/list-status-badges.ts`** (same system as other hubs). |
125
+
126
+ **Reference:** **`components/data-views/placement-board-card.tsx`**, **`components/team-board-view.tsx`**, **`components/compliance-board-view.tsx`**, **`components/question-bank-board-view.tsx`**, **`components/list-hub-status-badge.tsx`**, **`lib/list-status-badges.ts`**, **`components/data-views/list-page-board-template.tsx`**. **Skill (Cursor + Claude):** **`.cursor/skills/exxat-board-cards/SKILL.md`** and **`.claude/skills/exxat-board-cards/SKILL.md`** (duplicate for Claude Code).
127
+
128
+ ---
129
+
130
+ ## 5. Layout alignment (avoid double inset)
131
+
132
+ **MUST NOT** wrap `DataTable` in **extra** horizontal padding (`px-*` / `mx-*`) if `DataTable` already applies margin/padding on its shell or toolbar — that **staircases** the filter bar and table vs tabs.
133
+
134
+ **SHOULD:** Follow Placements / Team: one horizontal rhythm from `ListPageTemplate` + `DataTable`’s own inset.
135
+
136
+ ---
137
+
138
+ ## 6. Dense lists, export, primary hubs
139
+
140
+ ### 6.1 Dense lists (more than ~10 rows/cards)
141
+
142
+ **SHOULD** provide **search**, **filter**, **user-visible sorting**, and a **properties** entry point (drawer/sheet) appropriate to the surface. **Table/list/board:** use `TablePropertiesDrawer` / toolbar patterns. **Card-only pages:** a lighter properties sheet is OK if there is no `DataTable`.
143
+
144
+ Below the threshold, these MAY be omitted unless the page is a **primary hub** (§6.3).
145
+
146
+ ### 6.2 Pages with exportable data
147
+
148
+ Match **Placements**:
149
+
150
+ - **Primary CTA:** one **default (filled)** `Button`, often `size="lg"` — e.g. New placement, Invite member. **MUST NOT** use `variant="outline"` for that primary action.
151
+ - **More (⋯):** outline **icon** button → menu including **Export** → **`ExportDrawer`** (or same pattern).
152
+
153
+ **Subtitle:** Short line with **count + freshness** (e.g. `24 records · Last updated now`) when useful — see `PlacementsPageHeader` / `TeamPageHeader`.
154
+
155
+ ### 6.3 Primary pages with large or complex data
156
+
157
+ **Primary nav destinations** that show **large or highly interactive** datasets **MUST** use the **primary page template**:
158
+
159
+ - **`ListPageTemplate`** + **`KeyMetrics`** (when metrics apply) + export wiring + the same **client composition** as **`DataListClient`** / **`TeamClient`** — not a minimal `PageHeader`-only layout for that hub.
160
+
161
+ **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).
162
+
163
+ ### 6.4 Page vs drawer (actions and auxiliary views)
164
+
165
+ **SHOULD** choose the surface by whether the user must keep **page context** while acting:
166
+
167
+ | Use a **drawer / sheet** (side panel) | Use a **new page** (dedicated route) |
168
+ |--------------------------------------|----------------------------------------|
169
+ | 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 |
170
+ | 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 |
171
+
172
+ **Rationale:** Drawers preserve **spatial context** and reduce navigation churn; full pages avoid cramming complex work into a narrow overlay.
173
+
174
+ **Details:** `docs/data-views-pattern.md` (Page vs drawer). Root **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
175
+
176
+ ### 6.5 Messaging — no toast
177
+
178
+ **MUST NOT** use **toast** APIs (e.g. **Sonner** `toast()`), **snackbars**, or other **transient corner notifications** for product feedback.
179
+
180
+ **SHOULD** use **`LocalBanner`** / **`SystemBanner`**, **inline status** next to the control (e.g. saved state on a button row), or **dialog / drawer** when acknowledgment matters.
181
+
182
+ **Rationale:** Toasts are easy to miss, compete with dense app chrome, and are inconsistent for accessibility (focus, announcements). Root **`.cursor/rules/exxat-no-toast.mdc`**.
183
+
184
+ ---
185
+
186
+ ## 7. Keyboard shortcuts (`Kbd`)
187
+
188
+ Follow root **`.cursor/rules/exxat-kbd-shortcuts.mdc`**. Summary:
189
+
190
+ - Show **`Kbd`** / **`KbdGroup`** where users discover actions (primary/secondary CTAs, search, Ask Leo, sidebar) — not on every control.
191
+ - If a tooltip shows a chord, **implement** it (respect inputs / `contenteditable` via `@/lib/editable-target`).
192
+ - Use **`useModKeyLabel`** / **`useAltKeyLabel`** for correct OS labels.
193
+ - **Avoid** browser-reserved chords; prefer **⌘⌥** / **Ctrl+Alt** + letter for app actions; table search stays **⌘K** / **Ctrl+K** without Alt.
194
+
195
+ ### 7.1 Global command palette (⌘K)
196
+
197
+ **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. placements / student names) come from **`dataGroups`**, typically via **`getCommandMenuSearchDataGroups()`** in **`lib/command-menu-search-data.ts`**.
198
+
199
+ | SHOULD | Rationale |
200
+ |--------|-----------|
201
+ | Treat the palette as **global search** for routes, library, patterns, and AI suggestion starters | One mental model: ⌘K finds things and starts tasks. |
202
+ | For **natural language / AI**, prefer **quick results in the palette** when answers are short or lookup-style (inline snippets, citations, lightweight “research”) | Keeps users in flow; matches “search → pick result”. |
203
+ | Route **longer, exploratory, or multi-step** answers to **Ask Leo** (`AskLeoSidebar`) | Side panel fits long-form chat and complex help. |
204
+ | For **large row indexes** in **`dataGroups`**, set **`searchOnly: true`** on the group so users are not shown every record before they type (cmdk shows all items when the query is empty). | Keeps the first-open palette usable; matches “type to find a student / row”. |
205
+
206
+ **MUST NOT** implement the palette as **only** static links without room for AI/search evolution. **SHOULD** keep **`docs/command-menu-pattern.md`** updated as inline AI or search behavior ships.
207
+
208
+ **Reference:** `components/command-menu.tsx`, `lib/command-menu-search-data.ts`, `docs/command-menu-pattern.md`.
209
+
210
+ ---
211
+
212
+ ## 8. Accessibility (WCAG / ARIA)
213
+
214
+ **Standard:** **WCAG 2.1 Level AA** (and **2.2** where noted, e.g. target size).
215
+
216
+ **Authoritative detail (badges, placement 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`**.
217
+
218
+ ### 8.1 ARIA roles & structure (SC 1.3.1)
219
+
220
+ | MUST | MUST NOT |
221
+ |------|----------|
222
+ | Keep **`role="tablist"`** for **tabs only** — children resolve to **`role="tab"`** | Put **`role="button"`**, menus (`aria-haspopup`), or unrelated controls **inside** the same **`tablist`** container |
223
+ | For **composite view switchers** (tabs + per-tab settings + remove): use **`role="toolbar"`** + **`aria-label`**, **`aria-pressed`** on toggles where appropriate | Misuse **`tab` / `tablist`** for mixed toolbars |
224
+ | Prefer **`<button type="button">`** over **`span role="button"`** for icon actions | Sole click targets at **`size-4`** (16px) |
225
+
226
+ ### 8.2 Touch targets (WCAG 2.2 — 2.5.8)
227
+
228
+ **MUST:** Interactive controls (including icon-only chevrons and close icons) are at least **24×24 CSS pixels**, or have **24px** spacing so **24px** hit circles do not overlap.
229
+
230
+ **SHOULD:** **`min-h-6 min-w-6`** or **`size-6`** with centered icons for icon-only controls.
231
+
232
+ ### 8.3 Color (SC 1.4.3 / 1.4.11)
233
+
234
+ - **Minimum text size** for visible product UI: **11px** — use **`text-xs`** or larger; **MUST NOT** use arbitrary Tailwind classes below that (e.g. `text-[10px]`, `text-[0.65rem]` when it resolves under 11px). Theme tokens: **`app/globals.css`** (`@theme` `--text-xs` = `0.6875rem` at 16px root).
235
+ - **Normal text** (including small badge labels): **≥ 4.5:1** against its background.
236
+ - **UI components** (borders, focus rings where required): **≥ 3:1**.
237
+ - **Muted text on tinted surfaces** (e.g. sidebar): use tokens mixed against the **correct surface** (e.g. **`--sidebar`** / `--sidebar-section-label-foreground`), not only `--background`.
238
+
239
+ ### 8.4 Overlays (Dialog / Sheet / Drawer)
240
+
241
+ **MUST:** Provide an accessible **title** — `DialogTitle` / `SheetTitle` / `DrawerTitle`; use **`className="sr-only"`** when the title is visually hidden (align with shadcn patterns in this repo).
242
+
243
+ ### 8.5 Verification
244
+
245
+ **SHOULD** re-run **axe** (or your checker) on **Placements** (or the page you changed) after editing **views toolbar**, **tabs**, or **primary list** surfaces.
246
+
247
+ ### 8.6 Icons that communicate information MUST be accessible (SC 1.1.1, 3.3.2, 2.4.6)
248
+
249
+ Any icon (FA glyph, inline SVG, avatar placeholder) that carries **information** — not just icon-only buttons — MUST be accessible to screen readers AND to sighted users who may not recognise the glyph. Three cases, each with a required pattern:
250
+
251
+ #### Case A — Decorative icon next to text that already names it
252
+
253
+ When the icon sits adjacent to a visible text label that already carries the meaning, the icon is **decorative**. It MUST be `aria-hidden` and MUST NOT repeat the label via `aria-label` (screen readers would announce it twice).
254
+
255
+ ```tsx
256
+ <span className="flex items-center gap-1.5">
257
+ <i className="fa-light fa-calendar-days" aria-hidden />
258
+ <span>12/14/2025 – 12/20/2025</span>
259
+ </span>
260
+ ```
261
+
262
+ No tooltip needed — the text is the alt. This is the default when icons prefix/suffix a label in a cell, button with text, badge, breadcrumb, etc.
263
+
264
+ #### Case B — Informational icon standing alone (no adjacent text label)
265
+
266
+ When the icon is the **only** visible carrier of information — e.g. a `fa-calendar-days` in a compact table column header meaning "date range", `fa-clock` meaning "updated at", `fa-location-dot` meaning "site", `fa-graduation-cap` meaning "student", trending arrow in a KPI card, chart kind indicator, status dot, icon-only chart legend key — the icon MUST:
267
+
268
+ 1. Announce itself to AT via **`role="img"` + `aria-label`** (non-interactive) **OR** live inside a labelled parent (`aria-labelledby`).
269
+ 2. Show a visible **`Tooltip`** on hover/focus so sighted users who don't recognise the icon learn the meaning.
270
+
271
+ ```tsx
272
+ <Tooltip>
273
+ <TooltipTrigger asChild>
274
+ {/* non-interactive icon: span with role="img" is focusable via tabIndex={0} so
275
+ the tooltip opens on keyboard focus too */}
276
+ <span
277
+ role="img"
278
+ aria-label="Placement date range"
279
+ tabIndex={0}
280
+ className="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
281
+ >
282
+ <i className="fa-light fa-calendar-days" aria-hidden />
283
+ </span>
284
+ </TooltipTrigger>
285
+ <TooltipContent side="top">Placement date range</TooltipContent>
286
+ </Tooltip>
287
+ ```
288
+
289
+ Rules:
290
+ 1. `TooltipContent` text MUST match the `aria-label`.
291
+ 2. The inner `<i>` / `<svg>` still has `aria-hidden` — the accessible name lives on the wrapping element.
292
+ 3. If a visible text label could fit, **prefer adding the label** (Case A) over tooltip-only.
293
+ 4. Target/focus size still **≥ 24×24 CSS px** (§8.2) so keyboard users can focus the icon.
294
+
295
+ #### Case C — Interactive icon-only button / link
296
+
297
+ Any button or link whose visible content is a **single icon** — close (`×`), chevron, overflow (`⋯`), sort direction, filter chip dismiss, copy-to-clipboard, Ask Leo toggle, expand/collapse, row actions — MUST pair **`aria-label`** with a wrapping **`Tooltip`**.
298
+
299
+ ```tsx
300
+ <Tooltip>
301
+ <TooltipTrigger asChild>
302
+ <button
303
+ type="button"
304
+ aria-label="Close insight"
305
+ onClick={onClose}
306
+ className="inline-flex size-7 min-h-7 min-w-7 items-center justify-center rounded-md …"
307
+ >
308
+ <i className="fa-solid fa-xmark" aria-hidden />
309
+ </button>
310
+ </TooltipTrigger>
311
+ <TooltipContent side="top" className="flex items-center gap-1.5">
312
+ <span>Close</span>
313
+ <Kbd>Esc</Kbd>
314
+ </TooltipContent>
315
+ </Tooltip>
316
+ ```
317
+
318
+ Rules:
319
+ 1. `TooltipContent` MUST match or extend the `aria-label`.
320
+ 2. Inside tooltips use the default **tile** `<Kbd>` — NOT `variant="bare"`.
321
+ 3. The inner `<i>` / `<svg>` MUST be `aria-hidden`.
322
+ 4. Target size **≥ 24×24 CSS px** (§8.2).
323
+
324
+ Narrow exceptions (all cases): a chevron inside a labelled composite (`Select`, `Combobox`) where the parent control already names the whole thing; drag handles that reference a labelled ancestor via `aria-describedby`.
325
+
326
+ **When in doubt: add the accessible name + tooltip.** Silence is never the right answer for an icon that means something.
327
+
328
+ ### 8.7 `Kbd` variant inside buttons MUST be `"bare"` (no background)
329
+
330
+ Keyboard shortcut hints rendered **inline inside a `Button`** (primary, secondary, wizard Next/Back/Submit) **MUST** use **`<Kbd variant="bare">`**. The default `tile` variant is reserved for **tooltip content** and menu `shortcut=` slots. Glue multi-key chords into a single bare kbd (e.g. `<Kbd variant="bare">⌘⌥K</Kbd>`), not one tile per key.
331
+
332
+ ```tsx
333
+ // ✅ inside a button (primary/secondary/wizard)
334
+ <Button>Next <KbdGroup className="ml-1.5"><Kbd variant="bare">⌘⏎</Kbd></KbdGroup></Button>
335
+
336
+ // ✅ inside a tooltip (icon-only button above)
337
+ <TooltipContent><span>Close</span><Kbd>Esc</Kbd></TooltipContent>
338
+ ```
339
+
340
+ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcut table in **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
341
+
342
+ ---
343
+
344
+ ## 9. Architecture pointers (reuse, don’t fork)
345
+
346
+ | Need | Reuse | Where |
347
+ |------|--------|--------|
348
+ | View tabs + shell | `ListPageTemplate` | `components/templates/list-page.tsx` |
349
+ | Table + toolbar | `DataTable`, `DataTableToolbar`, `useTableState` | `components/data-table/` |
350
+ | Properties | `TablePropertiesDrawer` (+ **`currentView`** / **`onViewChange`** when using view tabs — §4.2) | `@/components/table-properties` |
351
+ | Placements flow | `DataListClient`, `DataListTable` | `components/data-list-client.tsx`, `components/data-list-table.tsx` |
352
+ | Team flow | `TeamClient`, `TeamTable`, `TeamPageHeader` | `components/team-client.tsx`, etc. |
353
+ | Dashboard view tab (KPIs + charts) | **`DashboardReportCharts`**; default **`ChartsOverview`** (placement demo). **Team** passes **`chartsSection`** (`TeamDashboardChartsSection`) so graphs match roster rows. KPIs from **`tableState.rows`** | `components/dashboard-report-charts.tsx`, `data-view-dashboard-charts-team.tsx`, `data-list-table.tsx` |
354
+ | 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` |
355
+ | Customize dashboard coach marks | Shared steps in **`lib/dashboard-customize-coach-mark.ts`**; flows **`placements-dashboard-customize`**, **`team-dashboard-customize`**, **`compliance-dashboard-customize`** | `hooks/use-coach-mark.ts` (`enabled`, `dependsOnDismissedFlowId`), `data-list-table.tsx`, `team-table.tsx`, `compliance-table.tsx` |
356
+ | 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`, `team-board-view.tsx`, **`§4.4`** |
357
+ | Full dashboard route | `DashboardTabs`, `KeyMetrics`, `ChartsOverview` | `app/(app)/dashboard/page.tsx`, `components/dashboard-tabs.tsx` |
358
+ | Board cards | **`ListPageBoardCard`** + primitives + entity card (**§4.4**) | `components/data-views/list-page-board-card.tsx`, `board-card-primitives.tsx`, `placement-board-card.tsx` |
359
+ | **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** |
360
+ | Persistence (example) | Page + lifecycle keys | `lib/data-list-persistence.ts`, `DataListClient` / `DataListTable` |
361
+ | Coach marks / tours | `CoachMark`, `useCoachMark`, coach mark registry | `components/ui/coach-mark.tsx`, `hooks/use-coach-mark.ts`, `lib/coach-mark-registry.ts` |
362
+ | Settings page | Coach mark management | `app/(app)/settings/page.tsx`, `components/settings-client.tsx` |
363
+
364
+ **MUST:** One **`useTableState` per logical table**; remount with **`key`** when column set or entity context changes.
365
+
366
+ ### 9.1 Application sidebar shell
367
+
368
+ **MUST:**
369
+
370
+ - **Product (Exxat One / Prism):** Use **`ExxatProductLogo`** for the header product control and **`ProductSwitcher`** — do **not** substitute logo.dev rasters unless product explicitly requests it.
371
+ - **School logos:** Use **`logoDevUrl()`** from **`lib/logo-dev.ts`** in **`NAV_SCHOOLS`**; optional env **`NEXT_PUBLIC_LOGO_DEV_TOKEN`**.
372
+ - **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**.
373
+ - **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`**.
374
+ - **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`**).
375
+ - **Mock profile photo:** **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs.
376
+ - **Icon rail layout:** Default **`SidebarMenuButton`** icon mode is **`size-8` + `p-2`** (~16px inner width), which **clips** 32px avatars/logos. Override with **`!size-9`**, **`!p-0`**, and **`overflow-visible`** on product/school header controls so marks stay centered and uncropped. **Chevrons** on those header triggers are optional — omit if they read as decoration next to logos.
377
+ - **Motion (Animate UI–style):** [Animate UI](https://animate-ui.com/docs) is an **open component distribution** (copy/tweak, Motion + Tailwind — not a single NPM UI package). This app uses **`motion/react`** with small presets in **`lib/motion-ui.ts`**; add more patterns by porting pieces from their registry as needed.
378
+
379
+ **Full detail:** **`.cursor/skills/exxat-ds-skill/SKILL.md`** (or **`.claude/skills/…`**) **§3.1**.
380
+
381
+ ---
382
+
383
+ ## 10. Persistence (when copying Placements behavior)
384
+
385
+ - **Page-level:** tabs, `showMetrics`, `displayOptions`, `activeTabId` — see `lib/data-list-persistence.ts` and `DataListClient`.
386
+ - **Per-lifecycle / tab:** sort, filters, columns, etc. — see `DataListTable` and `scheduleLifecycleSave`.
387
+
388
+ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrations.
389
+
390
+ ---
391
+
392
+ ## 11. Coach Marks (onboarding tours)
393
+
394
+ **MUST:** Use the **coach mark system** for all onboarding, feature discovery, and guided tours. Do **not** build one-off walkthrough overlays.
395
+
396
+ | Component | Location |
397
+ |-----------|----------|
398
+ | `CoachMark` | `@/components/ui/coach-mark` |
399
+ | `useCoachMark` hook | `@/hooks/use-coach-mark` |
400
+ | Coach mark registry | `@/lib/coach-mark-registry` |
401
+ | Settings page | `app/(app)/settings/page.tsx` + `@/components/settings-client` |
402
+
403
+ ### How to add a tour
404
+
405
+ 1. Define steps as `CoachMarkStep[]` — each with a `target` CSS selector (prefer `aria-label`, `role`, or `data-coach-mark` attributes), `title`, `description`, optional `side`/`align`/`image`.
406
+ 2. Call `useCoachMark({ flowId, steps, delay })` and render `<CoachMark state={tour} />` anywhere (it targets by selector, no child wrapping).
407
+ 3. Register the flow in `lib/coach-mark-registry.ts` so it appears in the Settings page.
408
+
409
+ ### Key behaviors
410
+
411
+ - **Selector-based:** each step finds its element by CSS selector, scrolls to it, and positions a Radix popover with a spotlight overlay.
412
+ - **Brand-colored:** popover background is `bg-brand-deep text-white` — not `bg-popover`.
413
+ - **Persistent:** once completed/skipped, the flow is dismissed via `localStorage` and won't reshow until reset from Settings.
414
+ - **Settings page:** `/settings` lists all registered flows with reset/preview controls.
415
+ - **Sequencing / gating:** `useCoachMark` supports **`enabled`** (e.g. only when **`view === "dashboard"`**) and **`dependsOnDismissedFlowId`** (e.g. customize-dashboard after **`placements-views-tour`**). Completed flows dispatch **`COACH_MARK_FLOW_COMPLETED_EVENT`** on `window` so follow-up tours can open in the same tab.
416
+ - **Customize Data dashboard:** registered flows target **`[aria-label='Edit dashboard layout']`**; shared step copy lives in **`lib/dashboard-customize-coach-mark.ts`**.
417
+
418
+ ### Variants
419
+
420
+ - **Single** (1-step array) — standalone tip, "Got it" button
421
+ - **Flow** (2+ steps) — step dots, Skip, Back, Next
422
+ - **With image** — set `image` + `imageAlt` on the step
423
+ - **Without image** — text-only
424
+
425
+ **Reference:** `references/coach-marks.md` in the skill, `components/dashboard-tabs.tsx` (dashboard tour), `components/data-list-client.tsx` (views tour).
426
+
427
+ ---
428
+
429
+ ## 12. Documentation
430
+
431
+ - **Deep dive:** `docs/data-views-pattern.md` (includes **Page vs drawer** with **§6.4**)
432
+ - **Global command palette (⌘K):** `docs/command-menu-pattern.md`
433
+ - **No toast / snackbars:** **§6.5**, root **`.cursor/rules/exxat-no-toast.mdc`**
434
+ - **This handbook:** `exxat-ds/AGENTS.md` (keep checklist sections updated when patterns change)
435
+
436
+ ---
437
+
438
+ ## 12. Summary — MUST / MUST NOT
439
+
440
+ | MUST | MUST NOT |
441
+ |------|----------|
442
+ | Use `DataTable` + search + filters + `TablePropertiesDrawer` for product data lists; with **`ListPageTemplate`** view tabs, pass **`currentView`** + **`onViewChange`** (§4.2) | Introduce a second table stack for the same surfaces; omit **`currentView`** on multi-view pages |
443
+ | Wrap main `DataTable` in `ListPageTemplate` | `DataTable` only under `PageHeader` without view tabs |
444
+ | Use primary template (`ListPageTemplate` + metrics + export pattern) for primary hubs with large data | Hub pages that look like “nested cards” with staggered margins |
445
+ | Match Placements for export + primary CTA + More menu | Outline button as the single primary CTA on exportable pages |
446
+ | Pair `Kbd` hints with real shortcuts | Browser-reserved chords for app actions |
447
+ | 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 |
448
+ | **§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 |
449
+ | **§6.5** — feedback via **banners / inline / dialogs** — **no** toast or snackbar | **`toast()`** / **Sonner** / transient corner notifications for product messaging |
450
+ | 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) |
451
+ | Use `CoachMark` + `useCoachMark` for onboarding tours (§11); register in `coach-mark-registry` | Build one-off walkthrough overlays or custom onboarding modals |
452
+ | 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` |
453
+ | 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) |
454
+
455
+ ---
456
+
457
+ ## 13. AI execution checklist (list / table / board page)
458
+
459
+ Copy and complete when implementing or reviewing:
460
+
461
+ - [ ] **Reuse:** `ListPageTemplate`, `DataTable` / `useTableState`, `TablePropertiesDrawer` — no parallel bespoke tabs/filters.
462
+ - [ ] **Tabs:** Any main `DataTable` sits under `ListPageTemplate` with appropriate view tabs.
463
+ - [ ] **Inset:** No double horizontal padding around `DataTable`.
464
+ - [ ] **> ~10 items:** Search, filter, sort, properties (per surface type in §6.1).
465
+ - [ ] **Exportable data:** Filled primary CTA; **⋯** menu with Export → `ExportDrawer`.
466
+ - [ ] **Primary hub + large data:** Same composition as `DataListClient` / `TeamClient` (template + metrics when applicable).
467
+ - [ ] **All view tabs:** List/board/dashboard use **`tableState.rows`**; dashboard view uses **`KeyMetrics`** + shared KPI helpers — no “not wired” placeholders or duplicate metric cards.
468
+ - [ ] **Properties drawer:** **`TablePropertiesDrawer`** receives **`currentView`** and **`onViewChange`** from **`renderContent`** / **`updateTab`** + **`dataListViewIcon`** (§4.2) — not table-only copy on Board/List/Dashboard.
469
+ - [ ] **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).
470
+ - [ ] **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).
471
+ - [ ] **⌘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.
472
+ - [ ] **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`**.
473
+ - [ ] **No toast (§6.5):** No **`toast()`** / Sonner / snackbars — use banners, inline status, or dialogs.
474
+ - [ ] **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.
475
+ - [ ] **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.
476
+ - [ ] **New primary hub routes:** **Not** placeholder-only pages — full **`ListPageTemplate`** stack + mock rows + connected views (**§4.1**).
477
+ - [ ] **List hub status (§4.4):** **`ListHubStatusBadge`** or Placements **`StatusBadge`**; maps only in **`lib/list-status-badges.ts`**; prefer **`LIST_HUB_STATUS_TINT_*`** for new entities.
478
+ - [ ] **Kbd:** Follow `exxat-kbd-shortcuts.mdc` if adding shortcuts or hints.
479
+ - [ ] **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.
480
+ - [ ] **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`**.
481
+ - [ ] **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`**.
482
+
483
+ ---
484
+
485
+ *Last updated: §9.1 application sidebar shell; exxat-ds-skill §3.1; §4.1 no empty hubs; §4.4 board cards + `ListHubStatusBadge`; §6.5 no toast; §6.4 page vs drawer; §7.1 command palette; §13 checklist.*