@exxatdesignux/ui 0.5.10 → 0.5.12

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 (478) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/cli.mjs +70 -1
  3. package/bin/init.mjs +18 -4
  4. package/bin/sync-extras.mjs +28 -4
  5. package/consumer-extras/README.md +41 -5
  6. package/consumer-extras/cursor-rules/exxat-accessibility.mdc +2 -1
  7. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +5 -3
  8. package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +1 -0
  9. package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +1 -0
  10. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +4 -2
  11. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -0
  12. package/consumer-extras/cursor-rules/exxat-command-menu.mdc +3 -2
  13. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +5 -3
  14. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +7 -0
  15. package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +2 -1
  16. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +13 -2
  17. package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +1 -0
  18. package/consumer-extras/cursor-rules/exxat-hub-supported-views.mdc +6 -4
  19. package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +6 -5
  20. package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +1 -0
  21. package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +1 -0
  22. package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +1 -0
  23. package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +2 -1
  24. package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +6 -2
  25. package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +2 -1
  26. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +2 -1
  27. package/consumer-extras/cursor-rules/exxat-nav-single-active.mdc +4 -3
  28. package/consumer-extras/cursor-rules/exxat-no-image-pixel-copy.mdc +25 -14
  29. package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +8 -2
  30. package/consumer-extras/cursor-rules/exxat-no-toast.mdc +1 -0
  31. package/consumer-extras/cursor-rules/exxat-no-vaul.mdc +2 -1
  32. package/consumer-extras/cursor-rules/exxat-page-header-actions.mdc +6 -4
  33. package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +2 -1
  34. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -0
  35. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +2 -1
  36. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -0
  37. package/consumer-extras/cursor-rules/exxat-sidebar-shell.mdc +13 -7
  38. package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +5 -3
  39. package/consumer-extras/cursor-rules/exxat-table-row-preview.mdc +1 -0
  40. package/consumer-extras/cursor-rules/exxat-tabs-chrome.mdc +6 -4
  41. package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +6 -0
  42. package/consumer-extras/cursor-rules/exxat-ux-discovery-protocol.mdc +92 -12
  43. package/consumer-extras/cursor-rules/exxat-ux-principles.mdc +1 -0
  44. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +1 -1
  45. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +5 -3
  46. package/consumer-extras/cursor-skills/exxat-senior-ux/SKILL.md +70 -17
  47. package/consumer-extras/patterns/command-menu-pattern.md +2 -2
  48. package/consumer-extras/patterns/consumer-upgrade-checklist.md +1 -1
  49. package/consumer-extras/patterns/jobs/README.md +1 -1
  50. package/consumer-extras/patterns/perf-memory-pattern.md +115 -150
  51. package/consumer-extras/scripts/dev-guard.mjs +156 -0
  52. package/consumer-extras/templates/README.md +23 -0
  53. package/consumer-extras/templates/handoff.md +190 -0
  54. package/dist/hooks/use-app-theme.d.ts +1 -1
  55. package/package.json +2 -2
  56. package/{template → template-vite}/.claude/skills/exxat-ds-skill/SKILL.md +184 -23
  57. package/template-vite/.cursor/rules/exxat-accessibility.mdc +40 -0
  58. package/template-vite/.cursor/rules/exxat-board-cards.mdc +28 -0
  59. package/template-vite/.cursor/rules/exxat-breadcrumbs-no-back.mdc +22 -0
  60. package/template-vite/.cursor/rules/exxat-card-vs-list-rows.mdc +22 -0
  61. package/template-vite/.cursor/rules/exxat-centralized-list-dataset.mdc +46 -0
  62. package/template-vite/.cursor/rules/exxat-collaboration-access.mdc +33 -0
  63. package/{template → template-vite}/.cursor/rules/exxat-command-menu.mdc +5 -5
  64. package/template-vite/.cursor/rules/exxat-data-tables.mdc +47 -0
  65. package/template-vite/.cursor/rules/exxat-dedicated-search-surfaces.mdc +32 -0
  66. package/template-vite/.cursor/rules/exxat-drawer-vs-dialog.mdc +23 -0
  67. package/template-vite/.cursor/rules/exxat-ds-agents.mdc +87 -0
  68. package/template-vite/.cursor/rules/exxat-fontawesome-icons.mdc +32 -0
  69. package/template-vite/.cursor/rules/exxat-hub-supported-views.mdc +56 -0
  70. package/{template → template-vite}/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -0
  71. package/template-vite/.cursor/rules/exxat-kpi-flat-band.mdc +29 -0
  72. package/template-vite/.cursor/rules/exxat-kpi-max-four.mdc +22 -0
  73. package/template-vite/.cursor/rules/exxat-kpi-trends.mdc +32 -0
  74. package/template-vite/.cursor/rules/exxat-library-hub-header.mdc +29 -0
  75. package/template-vite/.cursor/rules/exxat-list-page-connected-views.mdc +28 -0
  76. package/template-vite/.cursor/rules/exxat-list-page-view-shells.mdc +32 -0
  77. package/{template → template-vite}/.cursor/rules/exxat-mono-ids.mdc +1 -0
  78. package/template-vite/.cursor/rules/exxat-nav-single-active.mdc +32 -0
  79. package/template-vite/.cursor/rules/exxat-no-image-pixel-copy.mdc +46 -0
  80. package/template-vite/.cursor/rules/exxat-no-slds-leakage.mdc +84 -0
  81. package/{template → template-vite}/.cursor/rules/exxat-no-toast.mdc +2 -2
  82. package/template-vite/.cursor/rules/exxat-no-vaul.mdc +26 -0
  83. package/template-vite/.cursor/rules/exxat-page-header-actions.mdc +33 -0
  84. package/{template → template-vite}/.cursor/rules/exxat-page-vs-drawer.mdc +5 -3
  85. package/template-vite/.cursor/rules/exxat-person-identity-display.mdc +48 -0
  86. package/template-vite/.cursor/rules/exxat-primary-nav-secondary-panel.mdc +53 -0
  87. package/template-vite/.cursor/rules/exxat-reuse-before-custom.mdc +37 -0
  88. package/template-vite/.cursor/rules/exxat-sidebar-shell.mdc +41 -0
  89. package/template-vite/.cursor/rules/exxat-table-properties-drawer.mdc +79 -0
  90. package/template-vite/.cursor/rules/exxat-table-row-preview.mdc +25 -0
  91. package/template-vite/.cursor/rules/exxat-tabs-chrome.mdc +33 -0
  92. package/template-vite/.cursor/rules/exxat-token-discipline.mdc +109 -0
  93. package/template-vite/.cursor/rules/exxat-ux-discovery-protocol.mdc +202 -0
  94. package/template-vite/.cursor/rules/exxat-ux-principles.mdc +187 -0
  95. package/template-vite/.cursor/skills/exxat-accessibility/SKILL.md +282 -0
  96. package/template-vite/.cursor/skills/exxat-board-cards/SKILL.md +68 -0
  97. package/template-vite/.cursor/skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  98. package/template-vite/.cursor/skills/exxat-centralized-list-dataset/SKILL.md +99 -0
  99. package/template-vite/.cursor/skills/exxat-collaboration-access/SKILL.md +35 -0
  100. package/template-vite/.cursor/skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  101. package/template-vite/.cursor/skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  102. package/template-vite/.cursor/skills/exxat-ds-skill/SKILL.md +893 -0
  103. package/template-vite/.cursor/skills/exxat-ds-skill/references/accessibility.md +142 -0
  104. package/template-vite/.cursor/skills/exxat-ds-skill/references/coach-marks.md +169 -0
  105. package/template-vite/.cursor/skills/exxat-ds-skill/references/data-table-pattern.md +392 -0
  106. package/template-vite/.cursor/skills/exxat-fontawesome-icons/SKILL.md +31 -0
  107. package/template-vite/.cursor/skills/exxat-kpi-flat-band/SKILL.md +38 -0
  108. package/template-vite/.cursor/skills/exxat-kpi-max-four/SKILL.md +19 -0
  109. package/template-vite/.cursor/skills/exxat-kpi-trends/SKILL.md +29 -0
  110. package/template-vite/.cursor/skills/exxat-list-page-view-shells/SKILL.md +36 -0
  111. package/template-vite/.cursor/skills/exxat-mono-ids/SKILL.md +56 -0
  112. package/template-vite/.cursor/skills/exxat-primary-nav-secondary-panel/SKILL.md +49 -0
  113. package/template-vite/.cursor/skills/exxat-senior-ux/SKILL.md +198 -0
  114. package/template-vite/.cursor/skills/exxat-token-economy/SKILL.md +287 -0
  115. package/template-vite/.cursor/skills/exxat-ux-audit/SKILL.md +303 -0
  116. package/{template → template-vite}/components/ask-leo-sidebar.tsx +10 -8
  117. package/{template → template-vite}/components/command-menu.tsx +1 -1
  118. package/{template → template-vite}/components/data-views/library-folder-tree-branch.tsx +1 -1
  119. package/{template → template-vite}/components/dedicated-search-recents.tsx +1 -1
  120. package/{template → template-vite}/components/dedicated-search-url-composer.tsx +1 -1
  121. package/{template → template-vite}/components/library-client.tsx +1 -1
  122. package/{template → template-vite}/components/library-hub-client.tsx +2 -2
  123. package/{template → template-vite}/components/library-secondary-nav.tsx +2 -2
  124. package/{template → template-vite}/components/library-table.tsx +35 -27
  125. package/{template → template-vite}/components/new-library-item-form.tsx +1 -1
  126. package/{template → template-vite}/components/page-breadcrumb-trail.tsx +1 -1
  127. package/{template → template-vite}/components/settings-client.tsx +1 -1
  128. package/{template → template-vite}/components/sidebar/app-sidebar.tsx +2 -2
  129. package/{template → template-vite}/components/sidebar/nav-main.tsx +1 -1
  130. package/{template → template-vite}/components/sidebar/nav-user.tsx +1 -1
  131. package/{template → template-vite}/components/sidebar/secondary-nav.tsx +1 -1
  132. package/{template → template-vite}/components/system-banner-slot.tsx +1 -1
  133. package/{template → template-vite}/components/templates/discovery-hub-template.tsx +2 -2
  134. package/{template → template-vite}/components/templates/new-focus-template.tsx +1 -1
  135. package/{template → template-vite}/components/tokens-secondary-nav.tsx +2 -2
  136. package/{template → template-vite}/components/tokens-themes-client.tsx +1 -1
  137. package/{template → template-vite}/hooks/use-secondary-panel-hub-nav.ts +1 -1
  138. package/template-vite/index.html +49 -0
  139. package/template-vite/lib/next-compat.tsx +98 -0
  140. package/{template → template-vite}/package.json +15 -27
  141. package/template-vite/scripts/port-next-imports.mjs +70 -0
  142. package/template-vite/src/App.tsx +103 -0
  143. package/template-vite/src/main.tsx +50 -0
  144. package/{template/app/(app)/error.tsx → template-vite/src/pages/_error.tsx} +12 -24
  145. package/{template/app/(app)/loading.tsx → template-vite/src/pages/_loading.tsx} +4 -2
  146. package/template-vite/src/pages/_not-found.tsx +17 -0
  147. package/template-vite/src/pages/dashboard.tsx +48 -0
  148. package/{template/app/(app)/help/page.tsx → template-vite/src/pages/help.tsx} +3 -2
  149. package/{template/app/(app)/library/layout.tsx → template-vite/src/pages/library/_layout.tsx} +18 -16
  150. package/{template/app/(app)/library/all/page.tsx → template-vite/src/pages/library/all.tsx} +1 -1
  151. package/{template/app/(app)/library/new/page.tsx → template-vite/src/pages/library/new.tsx} +12 -18
  152. package/template-vite/src/routes.tsx +72 -0
  153. package/{template/app → template-vite/src/styles}/globals.css +6 -2
  154. package/{template → template-vite}/tsconfig.json +5 -14
  155. package/template-vite/vite.config.ts +52 -0
  156. package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +0 -53
  157. package/template/.agents/skills/shadcn/SKILL.md +0 -242
  158. package/template/.agents/skills/shadcn/agents/openai.yml +0 -5
  159. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  160. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  161. package/template/.agents/skills/shadcn/cli.md +0 -257
  162. package/template/.agents/skills/shadcn/customization.md +0 -202
  163. package/template/.agents/skills/shadcn/evals/evals.json +0 -47
  164. package/template/.agents/skills/shadcn/mcp.md +0 -94
  165. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +0 -306
  166. package/template/.agents/skills/shadcn/rules/composition.md +0 -195
  167. package/template/.agents/skills/shadcn/rules/forms.md +0 -192
  168. package/template/.agents/skills/shadcn/rules/icons.md +0 -101
  169. package/template/.agents/skills/shadcn/rules/styling.md +0 -162
  170. package/template/.cursor/rules/exxat-accessibility.mdc +0 -33
  171. package/template/.cursor/rules/exxat-data-tables.mdc +0 -32
  172. package/template/.cursor/rules/exxat-ds-agents.mdc +0 -26
  173. package/template/.cursor/rules/exxat-list-page-connected-views.mdc +0 -16
  174. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +0 -40
  175. package/template/.nvmrc +0 -1
  176. package/template/.prettierignore +0 -7
  177. package/template/Logo/Exxat_Prism.svg +0 -39
  178. package/template/Logo/Exxat_one.svg +0 -36
  179. package/template/app/(app)/dashboard/loading.tsx +0 -18
  180. package/template/app/(app)/dashboard/page.tsx +0 -36
  181. package/template/app/(app)/layout.tsx +0 -77
  182. package/template/app/global-error.tsx +0 -63
  183. package/template/app/layout.tsx +0 -133
  184. package/template/app/page.tsx +0 -9
  185. package/template/docs/HANDBOOK.md +0 -187
  186. package/template/docs/blueprints/README.md +0 -86
  187. package/template/docs/blueprints/_template.md +0 -91
  188. package/template/docs/blueprints/board-card.md +0 -123
  189. package/template/docs/blueprints/data-table.md +0 -139
  190. package/template/docs/blueprints/key-metrics.md +0 -128
  191. package/template/docs/blueprints/list-page-template.md +0 -123
  192. package/template/docs/blueprints/page-header.md +0 -130
  193. package/template/docs/card-vs-rows-pattern.md +0 -36
  194. package/template/docs/collaboration-access-pattern.md +0 -116
  195. package/template/docs/command-menu-pattern.md +0 -45
  196. package/template/docs/component-selection-guide.md +0 -224
  197. package/template/docs/components-audit-2026-05.md +0 -158
  198. package/template/docs/consumer-upgrade-checklist.md +0 -52
  199. package/template/docs/data-views-pattern.md +0 -185
  200. package/template/docs/drawer-vs-dialog-pattern.md +0 -50
  201. package/template/docs/glossary.md +0 -59
  202. package/template/docs/hub-supported-views-pattern.md +0 -53
  203. package/template/docs/jobs/README.md +0 -59
  204. package/template/docs/jobs/record-detail.md +0 -177
  205. package/template/docs/kpi-flat-band-pattern.md +0 -57
  206. package/template/docs/kpi-strip-max-four-pattern.md +0 -30
  207. package/template/docs/kpi-trend-pattern.md +0 -58
  208. package/template/docs/large-dataset-strategy.md +0 -155
  209. package/template/docs/library-hub-header-pattern.md +0 -25
  210. package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +0 -95
  211. package/template/docs/migrations/0002-exxat-token-namespace.md +0 -154
  212. package/template/docs/migrations/0003-globals-css-canonical.md +0 -110
  213. package/template/docs/migrations/README.md +0 -100
  214. package/template/docs/migrations/_template.md +0 -64
  215. package/template/docs/modern-saas-patterns.md +0 -165
  216. package/template/docs/perf-memory-pattern.md +0 -206
  217. package/template/docs/reference-implementations.md +0 -153
  218. package/template/docs/shell-surface-elevation-pattern.md +0 -52
  219. package/template/docs/token-taxonomy.md +0 -416
  220. package/template/docs/voice-and-tone.md +0 -262
  221. package/template/ecosystem.config.cjs +0 -32
  222. package/template/next.config.mjs +0 -216
  223. package/template/postcss.config.mjs +0 -8
  224. package/template/public/favicon/favicon.ico +0 -0
  225. package/template/tests/setup.ts +0 -26
  226. package/template/vitest.config.ts +0 -18
  227. /package/{template → template-vite}/.cursor/rules/exxat-dashboard-view-charts.mdc +0 -0
  228. /package/{template → template-vite}/.prettierrc +0 -0
  229. /package/{template → template-vite}/AGENTS.md +0 -0
  230. /package/{template → template-vite}/README.md +0 -0
  231. /package/{template → template-vite}/components/.gitkeep +0 -0
  232. /package/{template → template-vite}/components/ask-leo-composer.tsx +0 -0
  233. /package/{template → template-vite}/components/brand-color-picker.tsx +0 -0
  234. /package/{template → template-vite}/components/chart-area-interactive.tsx +0 -0
  235. /package/{template → template-vite}/components/charts-overview.tsx +0 -0
  236. /package/{template → template-vite}/components/collaboration-access-flow.tsx +0 -0
  237. /package/{template → template-vite}/components/columns-client.tsx +0 -0
  238. /package/{template → template-vite}/components/columns-showcase.tsx +0 -0
  239. /package/{template → template-vite}/components/dashboard-promo-banner.tsx +0 -0
  240. /package/{template → template-vite}/components/dashboard-quota-progress-card.tsx +0 -0
  241. /package/{template → template-vite}/components/dashboard-report-charts.tsx +0 -0
  242. /package/{template → template-vite}/components/dashboard-section-heading.tsx +0 -0
  243. /package/{template → template-vite}/components/dashboard-tabs.tsx +0 -0
  244. /package/{template → template-vite}/components/data-table/filter-date-calendar.tsx +0 -0
  245. /package/{template → template-vite}/components/data-table/filter-text-value-input.tsx +0 -0
  246. /package/{template → template-vite}/components/data-table/index.tsx +0 -0
  247. /package/{template → template-vite}/components/data-table/pagination.tsx +0 -0
  248. /package/{template → template-vite}/components/data-table/types.ts +0 -0
  249. /package/{template → template-vite}/components/data-table/use-table-state.test.ts +0 -0
  250. /package/{template → template-vite}/components/data-table/use-table-state.ts +0 -0
  251. /package/{template → template-vite}/components/data-views/board-card-primitives.tsx +0 -0
  252. /package/{template → template-vite}/components/data-views/data-row-list.tsx +0 -0
  253. /package/{template → template-vite}/components/data-views/finder-panel-view.tsx +0 -0
  254. /package/{template → template-vite}/components/data-views/folder-grid-view.tsx +0 -0
  255. /package/{template → template-vite}/components/data-views/hub-table.tsx +0 -0
  256. /package/{template → template-vite}/components/data-views/index.ts +0 -0
  257. /package/{template → template-vite}/components/data-views/list-page-board-card.tsx +0 -0
  258. /package/{template → template-vite}/components/data-views/list-page-board-template.tsx +0 -0
  259. /package/{template → template-vite}/components/data-views/list-page-connected-view-body.tsx +0 -0
  260. /package/{template → template-vite}/components/data-views/list-page-split-details-placeholder.tsx +0 -0
  261. /package/{template → template-vite}/components/data-views/list-page-split-hub-chrome.tsx +0 -0
  262. /package/{template → template-vite}/components/data-views/list-page-split-hub-tokens.ts +0 -0
  263. /package/{template → template-vite}/components/data-views/list-page-tree-column-header.tsx +0 -0
  264. /package/{template → template-vite}/components/data-views/list-page-tree-panel-shell.tsx +0 -0
  265. /package/{template → template-vite}/components/data-views/list-page-view-frame.tsx +0 -0
  266. /package/{template → template-vite}/components/data-views/os-folder-glyph.tsx +0 -0
  267. /package/{template → template-vite}/components/data-views/outline-tree-menu.tsx +0 -0
  268. /package/{template → template-vite}/components/data-views/table-cells.tsx +0 -0
  269. /package/{template → template-vite}/components/dev-chunk-load-recovery.tsx +0 -0
  270. /package/{template → template-vite}/components/export-drawer.test.tsx +0 -0
  271. /package/{template → template-vite}/components/export-drawer.tsx +0 -0
  272. /package/{template → template-vite}/components/exxat-product-logo.tsx +0 -0
  273. /package/{template → template-vite}/components/folder-details-shell.tsx +0 -0
  274. /package/{template → template-vite}/components/form-layout-01.tsx +0 -0
  275. /package/{template → template-vite}/components/hub-tree-panel-view.tsx +0 -0
  276. /package/{template → template-vite}/components/invite-collaborators-drawer.tsx +0 -0
  277. /package/{template → template-vite}/components/key-metrics-ask-leo-bridge.tsx +0 -0
  278. /package/{template → template-vite}/components/key-metrics.tsx +0 -0
  279. /package/{template → template-vite}/components/leo-insight-indicator.tsx +0 -0
  280. /package/{template → template-vite}/components/leo-typing-dots.tsx +0 -0
  281. /package/{template → template-vite}/components/library-board-view.tsx +0 -0
  282. /package/{template → template-vite}/components/library-dashboard-charts.tsx +0 -0
  283. /package/{template → template-vite}/components/library-favorite-button.tsx +0 -0
  284. /package/{template → template-vite}/components/library-new-folder-sheet.tsx +0 -0
  285. /package/{template → template-vite}/components/library-os-folder-view.tsx +0 -0
  286. /package/{template → template-vite}/components/library-page-header.tsx +0 -0
  287. /package/{template → template-vite}/components/library-panel-activator.tsx +0 -0
  288. /package/{template → template-vite}/components/list-hub-status-badge.tsx +0 -0
  289. /package/{template → template-vite}/components/list-page-dashboard-charts.tsx +0 -0
  290. /package/{template → template-vite}/components/onboarding/getting-started.tsx +0 -0
  291. /package/{template → template-vite}/components/onboarding/index.ts +0 -0
  292. /package/{template → template-vite}/components/onboarding/onboarding-01.tsx +0 -0
  293. /package/{template → template-vite}/components/onboarding/onboarding-02.tsx +0 -0
  294. /package/{template → template-vite}/components/onboarding/onboarding-03.tsx +0 -0
  295. /package/{template → template-vite}/components/onboarding/onboarding-04.tsx +0 -0
  296. /package/{template → template-vite}/components/page-header.tsx +0 -0
  297. /package/{template → template-vite}/components/product-switcher.tsx +0 -0
  298. /package/{template → template-vite}/components/product-wordmark.tsx +0 -0
  299. /package/{template → template-vite}/components/settings-appearance-card.tsx +0 -0
  300. /package/{template → template-vite}/components/settings-form-row.tsx +0 -0
  301. /package/{template → template-vite}/components/sidebar/app-sidebar-dynamic.tsx +0 -0
  302. /package/{template → template-vite}/components/sidebar/index.ts +0 -0
  303. /package/{template → template-vite}/components/sidebar/nav-documents.tsx +0 -0
  304. /package/{template → template-vite}/components/sidebar/nav-secondary.tsx +0 -0
  305. /package/{template → template-vite}/components/sidebar/secondary-panel.tsx +0 -0
  306. /package/{template → template-vite}/components/sidebar/sidebar-auto-collapse.tsx +0 -0
  307. /package/{template → template-vite}/components/sidebar/sidebar-auto-open.tsx +0 -0
  308. /package/{template → template-vite}/components/sidebar/sidebar-shell.tsx +0 -0
  309. /package/{template → template-vite}/components/site-header.tsx +0 -0
  310. /package/{template → template-vite}/components/table-properties/column-row.tsx +0 -0
  311. /package/{template → template-vite}/components/table-properties/draggable-list.ts +0 -0
  312. /package/{template → template-vite}/components/table-properties/drawer-button.tsx +0 -0
  313. /package/{template → template-vite}/components/table-properties/drawer.tsx +0 -0
  314. /package/{template → template-vite}/components/table-properties/filter-card.tsx +0 -0
  315. /package/{template → template-vite}/components/table-properties/index.ts +0 -0
  316. /package/{template → template-vite}/components/table-properties/sort-card.tsx +0 -0
  317. /package/{template → template-vite}/components/table-properties/types.ts +0 -0
  318. /package/{template → template-vite}/components/task-list-panel.tsx +0 -0
  319. /package/{template → template-vite}/components/task-priority-badge.tsx +0 -0
  320. /package/{template → template-vite}/components/templates/dedicated-search-landing-template.tsx +0 -0
  321. /package/{template → template-vite}/components/templates/dedicated-search-results-template.tsx +0 -0
  322. /package/{template → template-vite}/components/templates/list-page.tsx +0 -0
  323. /package/{template → template-vite}/components/templates/nested-secondary-panel-shell.tsx +0 -0
  324. /package/{template → template-vite}/components/templates/primary-page-template.tsx +0 -0
  325. /package/{template → template-vite}/components/templates/secondary-panel-hub-template.tsx +0 -0
  326. /package/{template → template-vite}/components/theme-color-sync.tsx +0 -0
  327. /package/{template → template-vite}/components/theme-provider.tsx +0 -0
  328. /package/{template → template-vite}/components/tinted-icon-disc.tsx +0 -0
  329. /package/{template → template-vite}/components/tokens-hub-auxiliary-views.tsx +0 -0
  330. /package/{template → template-vite}/components/tokens-themes-section.tsx +0 -0
  331. /package/{template → template-vite}/components/ui/accordion.tsx +0 -0
  332. /package/{template → template-vite}/components/ui/ai-thinking-surface.tsx +0 -0
  333. /package/{template → template-vite}/components/ui/alert-dialog.tsx +0 -0
  334. /package/{template → template-vite}/components/ui/avatar.tsx +0 -0
  335. /package/{template → template-vite}/components/ui/badge.tsx +0 -0
  336. /package/{template → template-vite}/components/ui/banner.tsx +0 -0
  337. /package/{template → template-vite}/components/ui/breadcrumb.tsx +0 -0
  338. /package/{template → template-vite}/components/ui/button.tsx +0 -0
  339. /package/{template → template-vite}/components/ui/calendar.tsx +0 -0
  340. /package/{template → template-vite}/components/ui/card.tsx +0 -0
  341. /package/{template → template-vite}/components/ui/chart.tsx +0 -0
  342. /package/{template → template-vite}/components/ui/checkbox.tsx +0 -0
  343. /package/{template → template-vite}/components/ui/coach-mark.tsx +0 -0
  344. /package/{template → template-vite}/components/ui/collapsible.tsx +0 -0
  345. /package/{template → template-vite}/components/ui/command.tsx +0 -0
  346. /package/{template → template-vite}/components/ui/context-menu.tsx +0 -0
  347. /package/{template → template-vite}/components/ui/date-picker-field.tsx +0 -0
  348. /package/{template → template-vite}/components/ui/dialog.tsx +0 -0
  349. /package/{template → template-vite}/components/ui/dot-pattern.tsx +0 -0
  350. /package/{template → template-vite}/components/ui/drag-handle-grip.tsx +0 -0
  351. /package/{template → template-vite}/components/ui/dropdown-menu.tsx +0 -0
  352. /package/{template → template-vite}/components/ui/field.tsx +0 -0
  353. /package/{template → template-vite}/components/ui/form.tsx +0 -0
  354. /package/{template → template-vite}/components/ui/hover-card.tsx +0 -0
  355. /package/{template → template-vite}/components/ui/input-group.tsx +0 -0
  356. /package/{template → template-vite}/components/ui/input-mask.tsx +0 -0
  357. /package/{template → template-vite}/components/ui/input.tsx +0 -0
  358. /package/{template → template-vite}/components/ui/kbd.tsx +0 -0
  359. /package/{template → template-vite}/components/ui/label.tsx +0 -0
  360. /package/{template → template-vite}/components/ui/leo-icon.tsx +0 -0
  361. /package/{template → template-vite}/components/ui/payment-card-fields.tsx +0 -0
  362. /package/{template → template-vite}/components/ui/popover.tsx +0 -0
  363. /package/{template → template-vite}/components/ui/radio-group.tsx +0 -0
  364. /package/{template → template-vite}/components/ui/resizable.tsx +0 -0
  365. /package/{template → template-vite}/components/ui/scroll-area.tsx +0 -0
  366. /package/{template → template-vite}/components/ui/select.tsx +0 -0
  367. /package/{template → template-vite}/components/ui/selection-tile-grid.tsx +0 -0
  368. /package/{template → template-vite}/components/ui/separator.tsx +0 -0
  369. /package/{template → template-vite}/components/ui/sheet.tsx +0 -0
  370. /package/{template → template-vite}/components/ui/sidebar.tsx +0 -0
  371. /package/{template → template-vite}/components/ui/skeleton.tsx +0 -0
  372. /package/{template → template-vite}/components/ui/slider.tsx +0 -0
  373. /package/{template → template-vite}/components/ui/sonner.tsx +0 -0
  374. /package/{template → template-vite}/components/ui/status-badge.tsx +0 -0
  375. /package/{template → template-vite}/components/ui/table.tsx +0 -0
  376. /package/{template → template-vite}/components/ui/tabs.tsx +0 -0
  377. /package/{template → template-vite}/components/ui/textarea.tsx +0 -0
  378. /package/{template → template-vite}/components/ui/tip.tsx +0 -0
  379. /package/{template → template-vite}/components/ui/toggle-group.tsx +0 -0
  380. /package/{template → template-vite}/components/ui/toggle-switch.tsx +0 -0
  381. /package/{template → template-vite}/components/ui/toggle.tsx +0 -0
  382. /package/{template → template-vite}/components/ui/tooltip.tsx +0 -0
  383. /package/{template → template-vite}/components/ui/view-segmented-control.tsx +0 -0
  384. /package/{template → template-vite}/components.json +0 -0
  385. /package/{template → template-vite}/contexts/chart-variant-context.tsx +0 -0
  386. /package/{template → template-vite}/contexts/command-menu-context.tsx +0 -0
  387. /package/{template → template-vite}/contexts/dashboard-view-context.tsx +0 -0
  388. /package/{template → template-vite}/contexts/product-context.tsx +0 -0
  389. /package/{template → template-vite}/contexts/system-banner-context.tsx +0 -0
  390. /package/{template → template-vite}/eslint.config.mjs +0 -0
  391. /package/{template → template-vite}/fontawesome-subset.manifest.json +0 -0
  392. /package/{template → template-vite}/hooks/.gitkeep +0 -0
  393. /package/{template → template-vite}/hooks/use-app-theme.ts +0 -0
  394. /package/{template → template-vite}/hooks/use-coach-mark.ts +0 -0
  395. /package/{template → template-vite}/hooks/use-location-hash.ts +0 -0
  396. /package/{template → template-vite}/hooks/use-mobile.ts +0 -0
  397. /package/{template → template-vite}/hooks/use-mod-key-label.ts +0 -0
  398. /package/{template → template-vite}/hooks/use-sidebar-reflow-zoom.ts +0 -0
  399. /package/{template → template-vite}/lib/.gitkeep +0 -0
  400. /package/{template → template-vite}/lib/ask-leo-route-context.ts +0 -0
  401. /package/{template → template-vite}/lib/chart-keyboard-selection.test.ts +0 -0
  402. /package/{template → template-vite}/lib/chart-keyboard-selection.ts +0 -0
  403. /package/{template → template-vite}/lib/chart-line-dash.ts +0 -0
  404. /package/{template → template-vite}/lib/chunk-load-error.ts +0 -0
  405. /package/{template → template-vite}/lib/coach-mark-registry.ts +0 -0
  406. /package/{template → template-vite}/lib/collaborator-access.ts +0 -0
  407. /package/{template → template-vite}/lib/command-menu-config.ts +0 -0
  408. /package/{template → template-vite}/lib/command-menu-search-data.ts +0 -0
  409. /package/{template → template-vite}/lib/conditional-rule-match.ts +0 -0
  410. /package/{template → template-vite}/lib/dashboard-customize-coach-mark.ts +0 -0
  411. /package/{template → template-vite}/lib/dashboard-layout-merge.ts +0 -0
  412. /package/{template → template-vite}/lib/data-list-display-options.ts +0 -0
  413. /package/{template → template-vite}/lib/data-list-persistence.ts +0 -0
  414. /package/{template → template-vite}/lib/data-list-view-registry.ts +0 -0
  415. /package/{template → template-vite}/lib/data-list-view-surface.ts +0 -0
  416. /package/{template → template-vite}/lib/data-list-view.ts +0 -0
  417. /package/{template → template-vite}/lib/data-view-dashboard-storage.ts +0 -0
  418. /package/{template → template-vite}/lib/date-filter.ts +0 -0
  419. /package/{template → template-vite}/lib/dedicated-search-recents.ts +0 -0
  420. /package/{template → template-vite}/lib/dedicated-search-url.ts +0 -0
  421. /package/{template → template-vite}/lib/dev-log.test.ts +0 -0
  422. /package/{template → template-vite}/lib/dev-log.ts +0 -0
  423. /package/{template → template-vite}/lib/discovery-hub.ts +0 -0
  424. /package/{template → template-vite}/lib/editable-target.ts +0 -0
  425. /package/{template → template-vite}/lib/exxat-palette.json +0 -0
  426. /package/{template → template-vite}/lib/exxat-palette.ts +0 -0
  427. /package/{template → template-vite}/lib/floating-sheet-panel.ts +0 -0
  428. /package/{template → template-vite}/lib/full-hub-supported-views.ts +0 -0
  429. /package/{template → template-vite}/lib/hub-connected-view-renderers.ts +0 -0
  430. /package/{template → template-vite}/lib/initials-from-name.ts +0 -0
  431. /package/{template → template-vite}/lib/library-authoring.ts +0 -0
  432. /package/{template → template-vite}/lib/library-dedicated-search.ts +0 -0
  433. /package/{template → template-vite}/lib/library-hub-search.ts +0 -0
  434. /package/{template → template-vite}/lib/library-nav.ts +0 -0
  435. /package/{template → template-vite}/lib/library-recent-searches.ts +0 -0
  436. /package/{template → template-vite}/lib/library-supported-views.ts +0 -0
  437. /package/{template → template-vite}/lib/list-hub-supported-views.ts +0 -0
  438. /package/{template → template-vite}/lib/list-page-table-properties.ts +0 -0
  439. /package/{template → template-vite}/lib/list-status-badges.ts +0 -0
  440. /package/{template → template-vite}/lib/logo-dev.ts +0 -0
  441. /package/{template → template-vite}/lib/mailto.ts +0 -0
  442. /package/{template → template-vite}/lib/mock/dashboard.ts +0 -0
  443. /package/{template → template-vite}/lib/mock/library-folders.ts +0 -0
  444. /package/{template → template-vite}/lib/mock/library-header-collaborators.ts +0 -0
  445. /package/{template → template-vite}/lib/mock/library-inspector.ts +0 -0
  446. /package/{template → template-vite}/lib/mock/library-kpi.ts +0 -0
  447. /package/{template → template-vite}/lib/mock/library.ts +0 -0
  448. /package/{template → template-vite}/lib/mock/navigation.tsx +0 -0
  449. /package/{template → template-vite}/lib/motion-ui.ts +0 -0
  450. /package/{template → template-vite}/lib/product-brand.ts +0 -0
  451. /package/{template → template-vite}/lib/raf-throttle.ts +0 -0
  452. /package/{template → template-vite}/lib/row-height.ts +0 -0
  453. /package/{template → template-vite}/lib/sidebar-state-cookie.ts +0 -0
  454. /package/{template → template-vite}/lib/stock-portrait.ts +0 -0
  455. /package/{template → template-vite}/lib/table-state-lifecycle.ts +0 -0
  456. /package/{template → template-vite}/lib/utils.test.ts +0 -0
  457. /package/{template → template-vite}/lib/utils.ts +0 -0
  458. /package/{template → template-vite}/public/.gitkeep +0 -0
  459. /package/{template → template-vite}/public/Illustration/Rotation.svg +0 -0
  460. /package/{template → template-vite}/public/avatars/user.svg +0 -0
  461. /package/{template/public → template-vite/public/favicon}/favicon.ico +0 -0
  462. /package/{template/app → template-vite/public}/favicon.ico +0 -0
  463. /package/{template → template-vite}/public/folders/icons8-folder-windows-11.svg +0 -0
  464. /package/{template → template-vite}/public/logos/exxat-one.svg +0 -0
  465. /package/{template → template-vite}/public/logos/exxat-prism.svg +0 -0
  466. /package/{template → template-vite}/public/mock-schools/emory.svg +0 -0
  467. /package/{template → template-vite}/public/mock-schools/rush.svg +0 -0
  468. /package/{template → template-vite}/scripts/fontawesome-subset-audit.mjs +0 -0
  469. /package/{template → template-vite}/scripts/pm2-startup-macos.sh +0 -0
  470. /package/{template → template-vite}/skills-lock.json +0 -0
  471. /package/{template/app/(app)/columns/page.tsx → template-vite/src/pages/columns.tsx} +0 -0
  472. /package/{template/app/(app)/library/find/page.tsx → template-vite/src/pages/library/find.tsx} +0 -0
  473. /package/{template/app/(app)/library/page.tsx → template-vite/src/pages/library/index.tsx} +0 -0
  474. /package/{template/app/(app)/library/list/page.tsx → template-vite/src/pages/library/list.tsx} +0 -0
  475. /package/{template/app/(app)/settings/page.tsx → template-vite/src/pages/settings.tsx} +0 -0
  476. /package/{template/app/(app)/tokens-themes/page.tsx → template-vite/src/pages/tokens-themes.tsx} +0 -0
  477. /package/{template → template-vite}/stores/app-store.ts +0 -0
  478. /package/{template → template-vite}/types/react-payment-inputs.d.ts +0 -0
@@ -0,0 +1,893 @@
1
+ ---
2
+ name: exxat-ds
3
+ description: >
4
+ Complete rules, patterns, and architecture guide for the Exxat DS Next.js design system.
5
+ Use this skill whenever working on any feature, page, component, or nav item in the Exxat DS
6
+ codebase — including adding sidebar items, creating list pages, building data tables,
7
+ wiring navigation, writing accessible UI, handling dates, adding tooltips, using icons,
8
+ or adding charts, graphs, KPI cards, or any data visualization.
9
+ Also apply whenever the user asks about Exxat patterns, component reuse, WCAG compliance
10
+ for this project, or asks "how do I build X" in the Exxat DS context.
11
+ ---
12
+
13
+ # Exxat DS — Patterns & Rules Handbook
14
+
15
+ > **Read this before writing any code.** Every section below is binding. "Done" means passing all applicable rules here.
16
+
17
+ ---
18
+
19
+ ## 1. Project Overview
20
+
21
+ - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
+ - **App root:** `apps/web/app/(app)/` — route group that wraps all authenticated pages
23
+ - **Single source of truth:** `apps/web/AGENTS.md` for full prose explanations; this skill is the actionable summary
24
+ - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-mono-ids`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
25
+ - **Library folder-scoped header (rule + doc):** **`.cursor/rules/exxat-library-hub-header.mdc`** and **`docs/library-hub-header-pattern.md`** — pair with **`exxat-primary-nav-secondary-panel`** when URL **`scope=folder`** drives the hub title.
26
+ - **Consumer repos (npm install of `@exxatdesignux/ui`):** After a version bump, read **`node_modules/@exxatdesignux/ui/CHANGELOG.md`**, run **`npx --package=@exxatdesignux/ui@latest exxat-ui sync-extras`** so **`docs/exxat-ds/consumer-upgrade-checklist.md`** and Cursor skills match the tarball, and diff the host app against **`node_modules/@exxatdesignux/ui/template/`** for anything new to port (routes, re-exports, AGENTS). Use **`exxat-ui changelog`**, **`exxat-ui update`**, and **`exxat-ui doctor`** for CLI guidance.
27
+
28
+ ---
29
+
30
+ ## 2. Page Architecture
31
+
32
+ Every page inside `app/(app)/` uses this exact shell:
33
+
34
+ ```tsx
35
+ // app/(app)/my-feature/page.tsx
36
+ import { SiteHeader } from "@/components/site-header"
37
+ import { MyFeatureClient } from "@/components/my-feature-client"
38
+ import { SidebarInset } from "@/components/ui/sidebar"
39
+
40
+ export default function MyFeaturePage() {
41
+ return (
42
+ <SidebarInset>
43
+ <SiteHeader title="My Feature" />
44
+ <main id="main-content" tabIndex={-1} className="flex flex-1 flex-col outline-none">
45
+ <div className="@container/main flex flex-1 flex-col w-full max-w-[1440px] mx-auto">
46
+ <MyFeatureClient />
47
+ </div>
48
+ </main>
49
+ </SidebarInset>
50
+ )
51
+ }
52
+ ```
53
+
54
+ **Rules:**
55
+ - `SidebarInset` is always the outermost wrapper
56
+ - `SiteHeader` always goes directly inside it, before `<main>`
57
+ - `<main id="main-content" tabIndex={-1}>` is required — it's the skip-link target
58
+ - Move all interactive/stateful logic into a `"use client"` component (e.g. `MyFeatureClient`) — keep page.tsx as a server component
59
+
60
+ ---
61
+
62
+ ## 3. Adding a Sidebar Nav Item
63
+
64
+ All navigation lives in **`lib/mock/navigation.tsx`** — it is the single source of truth.
65
+
66
+ To add a primary nav item, append to `NAV_PRIMARY`:
67
+
68
+ ```tsx
69
+ {
70
+ key: "my-feature",
71
+ title: "My Feature",
72
+ url: "/my-feature",
73
+ icon: <i className="fa-light fa-<icon-name>" aria-hidden="true" />,
74
+ iconActive: <i className="fa-solid fa-<icon-name>" aria-hidden="true" />,
75
+ }
76
+ ```
77
+
78
+ - `icon` uses `fa-light` weight; `iconActive` uses `fa-solid` — always pair them
79
+ - All icons must have `aria-hidden="true"` (decorative)
80
+ - Optional `badge?: number | string` — `"New"` → green, `"Beta"` → amber, other strings → brand color
81
+ - For document-section items add to `NAV_DOCUMENTS` instead
82
+ - For utility links (Settings, Search, Help) add to `NAV_SECONDARY`
83
+
84
+ **Routing:** create the page at `app/(app)/<key>/page.tsx` — the url must match the key.
85
+
86
+ ### 3.1 Application sidebar shell (`app-sidebar.tsx`)
87
+
88
+ **Data:** `lib/mock/navigation.tsx` also holds **`NAV_SCHOOLS`**, **`NAV_USER`**, and related defaults. School marks use **`logoDevUrl()`** from **`lib/logo-dev.ts`** (publishable token; optional **`NEXT_PUBLIC_LOGO_DEV_TOKEN`**).
89
+
90
+ | Concern | Pattern |
91
+ |--------|---------|
92
+ | **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. The logo is now **generated from a config**, not hand-built SVG paths — see **§3.4** to add a new product. |
93
+ | **School/program menu width** | **`DropdownMenuContent`** defaults to **intrinsic width** (**`min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]`** via **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** in **`@exxatdesignux/ui/lib/dropdown-menu-surface`**) — pure CSS, no **`ResizeObserver`**. The **school / program** switcher still uses an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so dense rows stay readable. |
94
+ | **School/program copy** | **Do not truncate** school or program names in the switcher; wrap (**`break-words`**, **`whitespace-normal`**, **`items-start`** on multi-line rows). The selected-school summary shows **school name + current program**. |
95
+ | **Team switcher trigger** | **`SidebarMenuButton` `size="lg"`** uses **`h-12`** + **`overflow-hidden`**, which **clips** a second line (program). When the sidebar is **expanded** or **mobile**, add **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**. On **icon rail**, hide label rows with **`group-data-[collapsible=icon]:hidden`** (tooltip still exposes the full string). Icon mode defaults **`size-8` + `p-2`** (~16px inner) **clips** school logos — override **`!size-9`**, **`!p-0`**, **`overflow-visible`**. Omit header **chevrons** next to logos if they look like stray chrome. |
96
+ | **Motion / Animate UI** | [Animate UI](https://animate-ui.com/docs) — open **copy-first** animated components (Motion + Tailwind). This repo uses **`motion/react`** + **`lib/motion-ui.ts`** presets; pull more animations from their registry into `components/` when needed. |
97
+ | **Nav items with children** | See **§3.2** below for the full parent ↔ children pattern (collapsible vs popover, active-state rules, chevron + slide animation, reduced motion). |
98
+ | **Profile (mock)** | **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs. |
99
+
100
+ **Reference:** `components/app-sidebar.tsx`, `components/nav-user.tsx`, `components/product-switcher.tsx`.
101
+
102
+ ### 3.2 Parent ↔ children nav (collapsible vs popover vs secondary panel)
103
+
104
+ A primary nav row that owns sub-routes has **three possible shapes** — pick exactly one per item:
105
+
106
+ | Shape | When to use | Where it lives |
107
+ |---|---|---|
108
+ | **A. Collapsible children** (e.g. Library → All / My / Favorites / Folders) | Small finite child list that benefits from inline browsing (≤ 40 items, no extra page chrome). | `NavLinkItem.children` rendered by **`CollapsibleNavItem`** in `app-sidebar.tsx`. |
109
+ | **B. Secondary panel** (separate nested rail) | Same nav row needs **scoped search / tree / metrics** alongside the hub content. | `NavLinkItem.secondaryPanel = "<id>"` + `PANELS[id]` — see companion skill `exxat-primary-nav-secondary-panel`. |
110
+ | **C. Both A + B on one row** (Library does this) | Most cases when a hub has a sub-list AND a rich rail. The sidebar still shows the collapsible children; clicking the parent route also opens the secondary panel via `useAutoPanel`. | Combine A + B; the active-state and animation rules in §3.2 still apply to the sidebar children. |
111
+
112
+ #### Active-state rules — **the single biggest mistake to avoid**
113
+
114
+ 1. **Expanded sidebar (full rail):** parent stays **visually neutral** when a child is active — **never double-highlight**. `isCollapsibleParentMenuButtonActive` returns `false` if `anyChildActive` so the active child row carries `data-active` alone.
115
+ 2. **Collapsed sidebar (icon rail):** the parent icon is the **only** affordance, so it lights up when **any child** route is active. Implementation: `iconRailActive = isAnyChildActive` is fed into `SidebarMenuButton.isActive` and selects `item.iconActive` (`fa-solid`) over `item.icon` (`fa-light`).
116
+ 3. **Tooltip / aria copy** in icon mode names the parent (e.g. "Library — open subpages"); the **popover** content lists children with their own active state so users see which sub-route is selected.
117
+
118
+ ```tsx
119
+ // Inside CollapsibleNavItem
120
+ const isAnyChildActive = item.children?.some(c => isCollapsibleChildActive(...))
121
+ const parentMenuButtonActive = isCollapsibleParentMenuButtonActive(pathname, item) // false when anyChildActive
122
+ const iconRailActive = isAnyChildActive
123
+ ```
124
+
125
+ #### Chevron + content animation (shadcn / Radix collapsible)
126
+
127
+ The collapsible rotates a chevron and slides the children in/out, mirroring shadcn's `Accordion`/`Collapsible` motion. **Three pieces wired together:**
128
+
129
+ 1. **`group/collapsible`** on the **`SidebarMenuItem`** that wraps the trigger — opts the chevron into `group-data-[state=open]/collapsible:rotate-90` (or whatever direction the icon needs). Without `group/collapsible` the chevron never rotates.
130
+ 2. **`CollapsibleContent`** uses Radix's `--radix-collapsible-content-height` CSS var via the shared keyframes:
131
+ ```tsx
132
+ <CollapsibleContent className="overflow-hidden
133
+ data-[state=open]:[animation:collapsible-down_200ms_ease-out]
134
+ data-[state=closed]:[animation:collapsible-up_200ms_ease-out]
135
+ motion-reduce:animate-none
136
+ group-data-[collapsible=icon]:hidden">
137
+ <SidebarMenuSub>...</SidebarMenuSub>
138
+ </CollapsibleContent>
139
+ ```
140
+ `overflow-hidden` is **required** so the height clip is visible during the animation. `motion-reduce:animate-none` honours `prefers-reduced-motion` (WCAG 2.3.3).
141
+ 3. **Keyframes in `app/globals.css`** (shared, reused by `Accordion` too):
142
+ ```css
143
+ @keyframes collapsible-down { from { height: 0 } to { height: var(--radix-collapsible-content-height) } }
144
+ @keyframes collapsible-up { from { height: var(--radix-collapsible-content-height) } to { height: 0 } }
145
+ ```
146
+
147
+ #### Icon-rail popover (collapsed sidebar)
148
+
149
+ When `state === "collapsed"` (or `isMobile === false` on a tablet icon rail), `CollapsibleNavItem` renders a **`Popover`** anchored to the parent icon, listing children as full rows. **Do not** pass `tooltip={…}` to a `SidebarMenuButton` that is the **direct** child of `CollapsibleTrigger asChild` — the tooltip wrapper inserts an extra `Tooltip` root and breaks Radix `Slot` (`React.Children.only`). Compose `Tooltip > TooltipTrigger > CollapsibleTrigger > SidebarMenuButton` without the `tooltip` prop, or use the popover branch only.
150
+
151
+ #### Hydration
152
+
153
+ `CollapsibleNavItem` is an isolated component with its own controlled `open` state initialised in `useEffect`. **Do not** pass `defaultOpen` based on pathname at render time — server and client resolve it differently and Radix throws a hydration mismatch.
154
+
155
+ #### Cap
156
+
157
+ The data shape supports any number of children, but the collapsible variant is rendered only when `childCount <= 40`. Beyond that, model the children as a **secondary panel** (shape B) so the user gets search + scroll.
158
+
159
+ **Reference:** `components/app-sidebar.tsx` (`CollapsibleNavItem`, `isCollapsibleParentMenuButtonActive`, `isCollapsibleChildActive`), `app/globals.css` (`@keyframes collapsible-down/up`), `lib/mock/navigation.tsx` (`NavLinkItem.children`).
160
+
161
+ ### 3.3 Secondary panel auto-collapse on high zoom
162
+
163
+ `SecondaryPanelProvider` (`components/secondary-panel.tsx`) reads **`useSidebarReflowZoom()`** (browser zoom ≥ 200% **or** very short viewport — same WCAG 1.4.10 signal the primary sidebar uses) and **auto-collapses the nested rail to its icon variant on entering high zoom**. The user can re-expand once collapsed; the next zoom-out → zoom-in cycle re-collapses. `openPanel` also opens directly in compact mode when high zoom is active so freshly-navigated panels don't briefly flash expanded.
164
+
165
+ Any future secondary-panel-like rail should reuse `useSidebarReflowZoom` rather than inventing a parallel zoom hook.
166
+
167
+ **Reference:** `components/secondary-panel.tsx` (`SecondaryPanelProvider`), `hooks/use-sidebar-reflow-zoom.ts`.
168
+
169
+ ### 3.4 Product wordmark + brand registry
170
+
171
+ The product logo ("**Exxat** *One*" / "**Exxat** *Prism*") is **generated from a `ProductBrandConfig`** in `lib/product-brand.ts`, not from hand-built SVG paths. The suffix word renders as real **Ivy Presto** italic text (`var(--font-heading)`, Adobe Fonts kit `wuk5wqn` preloaded in `app/layout.tsx`); the circular mark is the same Exxat "E" geometry, recolored from the brand's gradient.
172
+
173
+ **To add a new product:**
174
+
175
+ ```ts
176
+ // lib/my-product-brand.ts
177
+ import { defineProductBrand, registerProductBrand } from "@/lib/product-brand"
178
+
179
+ export const EXXAT_PULSE = defineProductBrand({
180
+ id: "exxat-pulse",
181
+ prefix: "Exxat", // optional, defaults to "Exxat"
182
+ suffix: "Pulse", // rendered in Ivy Presto italic
183
+ brandColor: "#00A8E8", // any CSS color — drives suffix text + mark fill
184
+ markGradient: ["#0083C7", "#3FC6FF"], // optional 2-stop linear gradient on the mark
185
+ markShadow: "#006FAA", // optional inner shadow behind the "E" cut-out
186
+ })
187
+
188
+ registerProductBrand(EXXAT_PULSE)
189
+ ```
190
+
191
+ Then render anywhere via either the **generic** primitives or the **Exxat** convenience wrappers:
192
+
193
+ ```tsx
194
+ import { ProductWordmark, ProductMark, ProductLogo } from "@/components/product-wordmark"
195
+ import { ExxatProductLogo, ExxatProductMark } from "@/components/exxat-product-logo"
196
+
197
+ // Generic — works for any registered brand
198
+ <ProductLogo config={EXXAT_PULSE} className="h-7" variant="mutedSuffix" />
199
+ <ProductMark config={EXXAT_PULSE} className="size-7" />
200
+ <ProductWordmark config={EXXAT_PULSE} className="h-7" />
201
+
202
+ // Existing Exxat call-sites unchanged
203
+ <ExxatProductLogo product="exxat-one" variant="mutedSuffix" className="h-7" />
204
+ <ExxatProductMark product="exxat-prism" className="size-7" />
205
+ ```
206
+
207
+ **Visual contract (so new brands look like real logos, not styled text):**
208
+
209
+ 1. **Prefix** uses Inter `font-extrabold` (800) `tracking-tight` in deep slate (`#273441`) / soft grey on dark (`#A8B2BA`). Always.
210
+ 2. **Suffix** — **official Exxat brand spec from Figma** — `var(--font-heading)` (IvyPresto Text), **upright `font-semibold` (600)** with `tracking-[-0.03em]` (Figma "letter spacing -3 %"), tinted with `brandColor`. **Not italic**, **not Bold/ExtraBold** — IvyPresto's Bodoni-lineage SemiBold already has the thick verticals that read as a logo, and pushing to 700/800 makes the letterforms visually heavier than the brand asset (`ExxatOne_WordmarkLogo_WithMinClearSpace.png`). Avoid `medium` (500) / `regular` (400) — those read as inline text, not as a wordmark.
211
+ 3. **Size relationship** — the `ExxatProductLogo` outer span pins `text-base leading-none` (16 px inherited) so the wordmark's `text-[1.78em]` resolves to ~28 px font / ~20 px cap-height regardless of host surface (sidebar `text-sm`, dropdown `text-base`, etc.). The mark renders at **`h-full`** of the outer height — the original hand-built `viewBox="0 0 766 164"` already bakes breathing room around the circle artwork, so giving the mark the full outer height reproduces the brand asset's 1.56:1 mark-to-cap ratio (caps ≈ 20 px against a 32 px mark at `h-8`). Shrinking the mark (e.g. `h-[88%]`) makes it visually smaller than the wordmark span and inverts the intended hierarchy. Use **`h-8`** (32 px) as the default sidebar / dropdown / switcher height; `h-7` is too tight for the new wordmark scale and produces a sub-30 px mark.
212
+ 4. **Cap-to-mark centring** — the wordmark adds `translate-y-[0.09em]` to compensate for the cap-midpoint vs em-box-midpoint offset (Inter / Ivy Presto put cap glyphs in the upper portion of the line box). Without this, `items-center` aligns the *spans* but not the *visible cap* and mark — the wordmark visually rides ~3 px above the mark. Keep the translate when forking.
213
+ 5. **`variant="mutedSuffix"`** (sidebar / switcher) keeps the brand color in **light** mode and only tints to `--muted-foreground` in **dark** mode. Don't mute the suffix in light mode — it loses brand recognition.
214
+ 6. **Mark** stays the canonical Exxat "E" geometry so existing pixel-aligned layouts (sidebar header avatar slot, switcher dropdown rows) keep working when you swap brands; only colors change.
215
+ 7. **Host span** uses `overflow-visible` because the wordmark's `1.78em` line-box can overshoot the parent `h-X` by ~1 px — let it render, don't clip.
216
+ 8. **Don't pass `object-*` classes** to the logo `className` — `ExxatProductLogo` is a `<span>`, not a replaced element, so `object-contain` / `object-left` are silently dropped. Use width / max-width constraints instead.
217
+ 9. **`ProductMark` has no size default** — callers MUST set explicit dimensions (`size-7`, `h-full w-auto`, etc.). A `size-*` default would silently lose against a downstream `h-full / w-auto` whenever `tailwind-merge` failed to recognise the shorthand-to-pair equivalence, and the mark would render at the default size (28 px) instead of the parent height (32 px in h-8). `ExxatProductLogo` passes `h-full w-auto`; the icon-rail / collapsed-sidebar usages pass `size-7`. Keep the explicit size.
218
+
219
+ **Where to extend the registry:** brand configs live alongside `lib/product-brand.ts` for the two built-ins. Co-locate new product configs near their feature (e.g. `app/(app)/<product>/_lib/brand.ts`) and call `registerProductBrand` at module import time so `ProductSwitcher` / `getProductBrand(id)` resolve it without ordering issues.
220
+
221
+ **MUST NOT:** add new hand-traced SVG paths for an additional product. Author a `ProductBrandConfig` instead.
222
+
223
+ **Reference:** `lib/product-brand.ts`, `components/product-wordmark.tsx`, `components/exxat-product-logo.tsx`, `components/product-switcher.tsx`.
224
+
225
+ ### 3.5 Appearance preview tiles (Theme / Contrast)
226
+
227
+ `components/settings-appearance-card.tsx` renders the Theme (Light / Dark / System) and Contrast (Normal / High / Windows / System) pickers using a shared **`ChromeIllustration`** SVG helper — a polished mini browser window (Mac-style traffic lights, sidebar with brand-tinted mark + 5 nav rows, header bar with search pill + avatar, KPI card + mini bar-chart card + list rows).
228
+
229
+ Two knobs to make new variants without forking the geometry:
230
+
231
+ - **`tokens: ChromeTokens`** — palette per labeled mode. Override individual fills via `{...CHROME_LIGHT, shellStroke: "..."}`. Don't reach for `var(--background)` — preview tiles must show their target mode regardless of the active theme so users can compare before committing.
232
+ - **`strokeBoost: number`** — multiplier applied to every border weight. `1.8` is the high-contrast value used by both **High** and **Windows** tiles.
233
+
234
+ For "System" variants, use the **`SplitSystemSvg`** helper. It renders `ChromeIllustration` twice in the **same** 96 × 56 viewBox, each clipped to a triangular half (top-left triangle = "light", bottom-right triangle = "dark") via SVG `<clipPath>` polygons. The result is **one window with a diagonal theme split** — the macOS / iOS "Auto" pattern — not two adjacent windows.
235
+
236
+ ```tsx
237
+ <SplitSystemSvg
238
+ light={{ tokens: CHROME_LIGHT, sidebar: split.light, sidebarMark: split.markLight }}
239
+ dark={{ tokens: CHROME_DARK, sidebar: split.dark, sidebarMark: split.markDark }}
240
+ />
241
+ ```
242
+
243
+ **MUST NOT** invent ad-hoc rect-stacks for new appearance options, or render two side-by-side mini-windows for a single "System" tile — extend `ChromeIllustration` or compose `SplitSystemSvg`.
244
+
245
+ **Reference:** `components/settings-appearance-card.tsx` (`ChromeIllustration`, `SplitSystemSvg`, `CHROME_LIGHT`, `CHROME_DARK`, `SPLIT_SIDEBAR`).
246
+
247
+ ---
248
+
249
+ ## 4. Primary Hub Pages — Mandatory Pattern
250
+
251
+ Any **primary nav destination** that shows a list of records **must** use this composition (same as Placements / Team):
252
+
253
+ ```
254
+ ListPageTemplate (supportedViewTypes = FULL_HUB_SUPPORTED_VIEWS — seven views)
255
+ ├── PageHeader (title, subtitle with count, primary CTA, ⋯ more menu)
256
+ ├── KeyMetrics (flat variant, single row)
257
+ └── renderContent()
258
+ └── HubTable + useTableState + TablePropertiesDrawer + renderers per view
259
+ ```
260
+
261
+ **Add view parity (binding):** `.cursor/rules/exxat-hub-supported-views.mdc`, `apps/web/docs/hub-supported-views-pattern.md`. **MUST NOT** use `supportedViewTypes={["table"]}` or four-view-only allowlists without a documented exception. List view **MUST** use **`ListPageBoardCard`** (`library-table.tsx`).
262
+
263
+ **Reference implementations:**
264
+ - `components/library-client.tsx` + `components/library-table.tsx` — **canonical seven-view hub** (All questions)
265
+ - `components/columns-showcase.tsx` — custom table via **`LibraryTable`** + same seven views
266
+ - `components/tokens-themes-client.tsx` + `components/tokens-hub-auxiliary-views.tsx`
267
+ - `components/team-client.tsx` + `components/team-table.tsx` — entity hub pattern
268
+ - `components/placements-client.tsx` + `components/placements-table.tsx` — Placements (most complete)
269
+
270
+ **Files to create for a new hub page `Foo`:**
271
+ | File | Purpose |
272
+ |------|---------|
273
+ | `lib/mock/foo.ts` | Mock data + TypeScript interface (12+ rows) |
274
+ | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` + `fooKpiInsight()` |
275
+ | `components/foo-page-header.tsx` | `PageHeader` + primary CTA + ⋯ menu |
276
+ | `components/foo-table.tsx` | `DataTable` + `useTableState` + `TablePropertiesDrawer` |
277
+ | `components/foo-client.tsx` | `ListPageTemplate` orchestrator |
278
+ | `app/(app)/foo/page.tsx` | Thin server component |
279
+
280
+ **Do not** ship a **nav-linked hub** as an **empty page** or a single “replace this later” paragraph. If the route appears in **`lib/mock/navigation.tsx`**, implement the full hub (mock rows, **`ListPageTemplate`**, connected views per **`apps/web/AGENTS.md` §4.1**) unless the product explicitly defines a non-data shell.
281
+
282
+ ### Page vs drawer (actions)
283
+
284
+ - **Drawer / sheet** — Use when the user needs **the current page behind them** *and* a **quick view**, **quick actions**, or a **short step** (e.g. properties, export, glance at a row).
285
+ - **Dialog** — **Blocking** confirm/alert/short choice — **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
286
+ - **New page** — Use **otherwise**: **primary**, **long-form**, **multi-step**, or flows that need their **own URL** without the hub visible.
287
+
288
+ Align with **`apps/web/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
289
+
290
+ ---
291
+
292
+ ## 5. Data Table Stack
293
+
294
+ **Always use these — never raw `<table>` or shadcn's `ui/table` for product data lists.**
295
+
296
+ | Import | Purpose |
297
+ |--------|---------|
298
+ | `DataTable` from `@/components/data-table` | Base table |
299
+ | `useTableState` from `@/components/data-table/use-table-state` | Sort, filter, column state |
300
+ | `TablePropertiesDrawer` from `@/components/table-properties` | Columns, density, filters, sort, conditional rules |
301
+ | `ColumnDef` from `@/components/data-table/types` | Column type |
302
+ | `FilterFieldDef`, `FilterOperator`, `ConditionalRule` from `@/components/table-properties/types` | Filter types |
303
+
304
+ **Board (kanban) cards:** Use **`ListPageBoardCard`** and related parts from **`components/data-views/list-page-board-card.tsx`**; **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** from **`board-card-primitives.tsx`**. **List hub** status (Team, Compliance, Library, …): maps in **`lib/list-status-badges.ts`**; render with **`ListHubStatusBadge`** (**`surface="table"`** in table/list, **`surface="board"`** on cards); semantic tints **`LIST_HUB_STATUS_TINT_*`** for new domains; no **`uppercase`**. **Placements** uses **`StatusBadge`** in **`placements-table-cells.tsx`** (wrapper over **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`**). **Full rules:** **`apps/web/AGENTS.md` §4.4**, **`.cursor/rules/exxat-board-cards.mdc`**, **`.cursor/skills/exxat-board-cards/SKILL.md`**.
305
+
306
+ **Minimum required features on any data list page:**
307
+ - Search (wire `searchable={displayOptions.showToolbarSearch}`)
308
+ - Filters (via `TablePropertiesDrawer` filter fields)
309
+ - Table properties drawer (Properties button with `fa-light fa-sliders`)
310
+ - `selectable={true}` with bulk-actions slot
311
+ - `emptyState` prop with helpful message
312
+
313
+ **Column definition pattern:**
314
+ ```ts
315
+ {
316
+ key: "name",
317
+ label: "Name",
318
+ width: 240,
319
+ minWidth: 160,
320
+ sortable: true,
321
+ sortKey: "name",
322
+ filter: {
323
+ type: "text", // "text" | "select" | "date"
324
+ icon: "fa-user",
325
+ operators: ["contains", "not_contains"],
326
+ },
327
+ cell: row => <span className="text-sm font-medium text-foreground">{row.name}</span>,
328
+ }
329
+ ```
330
+
331
+ **Pin conventions:**
332
+ - `select` column: `defaultPin: "left"`, `lockPin: true`
333
+ - `actions` column: `defaultPin: "right"`, `lockPin: true`
334
+
335
+ ### 5.1 Data table and view-toolbar menus
336
+
337
+ **`DropdownMenuContent`** (from **`@/components/ui/dropdown-menu`**, backed by **`@exxatdesignux/ui`**) applies a **default surface** so **view settings**, **Add view**, **row ⋯**, **column ⋯**, and **filter field** menus get **`min-w-52`**, grow with **`w-max`**, and cap at **`max-w-[min(24rem,calc(100vw-2rem))]`** — all **static Tailwind** (no **`ResizeObserver`** / layout measurement).
338
+
339
+ - **Override** only when the UX needs a fixed rail (e.g. **`className="w-20"`** on the pagination page-size menu, **`w-(--radix-dropdown-menu-trigger-width) min-w-60`** on **`NavUser`**, **`!w-max min-w-72 …`** on the school/program switcher).
340
+ - **Reuse** **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** if you build a custom menu primitive that does not wrap **`DropdownMenuContent`**.
341
+
342
+ ### 5.2 KPI trends (`KeyMetrics`, `*-kpi.ts`)
343
+
344
+ **`MetricItem.trend`** must match the **signed change** (arrow direction = truth). **`trendPolarity`** (`higher_is_better` default, **`lower_is_better`**, **`informational`**) controls **tints** and **`aria-label`** — e.g. **low PBI / review flags** rising → `trend: "up"` + **`lower_is_better`** → unfavourable (red), not green. **Doc:** **`docs/kpi-trend-pattern.md`** · **Rule:** **`.cursor/rules/exxat-kpi-trends.mdc`** · **Skill:** **`.cursor/skills/exxat-kpi-trends/SKILL.md`**.
345
+
346
+ ### 5.3 KPI count (max four)
347
+
348
+ **`ListPageTemplate`** metrics and **Data tab** key-metrics cards: **≤ 4** `MetricItem` — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`**, **`.cursor/rules/exxat-kpi-max-four.mdc`**, **`.cursor/skills/exxat-kpi-max-four/SKILL.md`**.
349
+
350
+ ### 5.4 Cards vs table rows
351
+
352
+ Dense comparable hub → **`DataTable`**. Boards / folders / visual browse → **`ListPageBoardCard`** + **`ListPageViewFrame`**. **Doc:** **`docs/card-vs-rows-pattern.md`** · **Rule:** **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
353
+
354
+ **DataTable must wrap in `<div className="pb-6">`.**
355
+
356
+ ---
357
+
358
+ ## 6. Page Header Pattern
359
+
360
+ Use `PageHeader` from `@/components/page-header` for the content-area header (below SiteHeader):
361
+
362
+ ```tsx
363
+ <PageHeader
364
+ title="Foo"
365
+ subtitle={`${count} items · Last updated now`}
366
+ actions={
367
+ <div className="flex items-center gap-2" role="group" aria-label="Foo actions">
368
+ <Tip side="bottom" label="Add a new foo">
369
+ <Button type="button" size="lg" onClick={onAdd}>
370
+ <i className="fa-light fa-plus" aria-hidden="true" />
371
+ Add foo
372
+ </Button>
373
+ </Tip>
374
+ <DropdownMenu ...>
375
+ <Tip side="bottom" label="More actions">
376
+ <DropdownMenuTrigger asChild>
377
+ <Button type="button" size="lg" variant="outline" className="aspect-square px-0" aria-label="More actions">
378
+ <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
379
+ </Button>
380
+ </DropdownMenuTrigger>
381
+ </Tip>
382
+ <DropdownMenuContent align="end">
383
+ <DropdownMenuItem onClick={onExport}>
384
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
385
+ Export
386
+ </DropdownMenuItem>
387
+ <DropdownMenuSeparator />
388
+ <DropdownMenuItem onClick={onToggleMetrics}>
389
+ <i className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`} aria-hidden="true" />
390
+ {showMetrics ? "Hide metric section" : "Show metric section"}
391
+ </DropdownMenuItem>
392
+ </DropdownMenuContent>
393
+ </DropdownMenu>
394
+ </div>
395
+ }
396
+ />
397
+ ```
398
+
399
+ **Rules:**
400
+ - Primary CTA: `Button size="lg"` (filled/default) — never `variant="outline"` as the sole primary action
401
+ - More (⋯): `variant="outline"` icon-only button → dropdown with Export → `ExportDrawer`
402
+ - Subtitle: `"{count} items · Last updated now"` format
403
+ - Title uses Ivy Presto (`font-heading` variable) — applied automatically by `PageHeader`
404
+
405
+ ### 6.1 Collaboration & access (shared hubs)
406
+
407
+ When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
408
+
409
+ **Library library — folder URL scope:** When **`?scope=folder&folderId=`** applies, **⋯ More** must also offer **Customize folder** (**`LibraryPageHeader`** **`onCustomizeFolder`**) and the **`LibraryNewFolderSheet`** must be mounted on **`LibraryClient`** so it works on every **`ListPageTemplate`** view tab. **`.cursor/rules/exxat-library-hub-header.mdc`** · **`docs/library-hub-header-pattern.md`** (app: **`apps/web/docs/...`**).
410
+
411
+ **Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Library header + client.
412
+
413
+ ---
414
+
415
+ ## 7. Navigation: Breadcrumbs vs Back Link
416
+
417
+ **Never use both on the same page. Pick one:**
418
+
419
+ | Page type | Use |
420
+ |-----------|-----|
421
+ | Detail page (child of a list) | **Breadcrumbs** via `SiteHeader` `breadcrumbs` prop |
422
+ | Full-page form / wizard | **Back link** only (no breadcrumbs) |
423
+
424
+ ```tsx
425
+ // Detail page — breadcrumbs
426
+ <SiteHeader
427
+ title="Sarah Johnson"
428
+ breadcrumbs={[
429
+ { label: "Placements", href: "/data-list" },
430
+ { label: "Placement Details" },
431
+ ]}
432
+ />
433
+
434
+ // Form page — back link + SidebarAutoCollapse
435
+ <SidebarAutoCollapse />
436
+ <Link href="/data-list">← Back</Link>
437
+ ```
438
+
439
+ Breadcrumb separator: `fa-light fa-chevron-right text-[8px]`. Last segment is `font-medium text-foreground`, parent segments are `text-muted-foreground`.
440
+
441
+ ---
442
+
443
+ ## 8. Component Reuse — Hard Rules
444
+
445
+ Never install new packages or create parallel components. Always use what exists.
446
+
447
+ | Need | Use |
448
+ |------|-----|
449
+ | Any button | `Button` from `@/components/ui/button` |
450
+ | Chart / graph card | `ChartCard` from `@/components/charts-overview` |
451
+ | Drawer / panel | `Sheet` from `@/components/ui/sheet` (floating style — see memory) |
452
+ | Tooltip | `Tip` from `@/components/ui/tip` — never `title` attribute |
453
+ | Keyboard hint | `Kbd` / `KbdGroup` from `@/components/ui/kbd` |
454
+ | Badge | `Badge` from `@/components/ui/badge` |
455
+ | Tabs | `Tabs`/`TabsList`/`TabsTrigger` from `@/components/ui/tabs` |
456
+ | Banner (page-level) | `SystemBanner` / `LocalBanner` from `@/components/ui/banner` |
457
+ | Success / error feedback | Inline copy, `LocalBanner`, or dialog — **never** `toast()` / Sonner / snackbars (**`AGENTS.md` §6.5**, **`.cursor/rules/exxat-no-toast.mdc`**) |
458
+ | Date formatting | `formatDateUS()` / `formatDateTimeUS()` from `@/lib/date-filter` |
459
+ | Modifier key label | `useModKeyLabel()` from `@/hooks/use-mod-key-label` |
460
+ | Class merging | `cn()` from `@/lib/utils` |
461
+ | Color | CSS design tokens only — no hardcoded hex/rgb |
462
+ | Minimum font size | **`text-xs`** (11px at 16px root via `--text-xs`) or larger — never arbitrary classes below 11px (`AGENTS.md` §8.3) |
463
+
464
+ Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one. **If nothing fits** and you need a **new shared primitive or large bespoke widget**, **ask the user** before new files — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already approved greenfield).
465
+
466
+ ---
467
+
468
+ ## 9. Date & Time Format
469
+
470
+ Mandatory across the **entire** app — tables, filters, tooltips, forms, everywhere:
471
+
472
+ | Type | Format | Example |
473
+ |------|--------|---------|
474
+ | Date only | `MM/DD/YYYY` | `03/15/2026` |
475
+ | Date + Time | `MM/DD/YYYY hh:mm AM/PM EST` | `03/15/2026 09:30 AM EST` |
476
+
477
+ - Zero-pad month and day: `03` not `3`
478
+ - 4-digit year always
479
+ - 12-hour clock, uppercase AM/PM, append `EST`
480
+ - Never ISO (`2026-03-15`) or verbose (`Mar 15, 2026`) in the UI
481
+ - Date picker: Calendar + Popover component; format displayed as `MM/DD/YYYY`
482
+
483
+ ### 9.1 Format hints MUST be persistent (WCAG 3.3.2)
484
+
485
+ Any input whose value must follow a specific format — **dates, times, phone, currency, GPA, Student IDs, URLs, unit-bearing numbers** — MUST render the format as **persistent helper text** via `FormDescription` (or any element tied to the input via `aria-describedby`). Placeholders disappear on focus and are not reliably announced — they **MUST NOT** be the sole carrier of the format.
486
+
487
+ ```tsx
488
+ <FormField name="startDate" render={({ field }) => (
489
+ <FormItem>
490
+ <FormLabel>Start date<span aria-hidden="true"> *</span></FormLabel>
491
+ <FormControl><DatePickerField value={field.value} onChange={field.onChange} /></FormControl>
492
+ <FormDescription>MM/DD/YYYY</FormDescription>
493
+ <FormMessage />
494
+ </FormItem>
495
+ )} />
496
+ ```
497
+
498
+ Units belong in `FormDescription` (e.g. GPA → "Out of 4.0"; Hours/week → "hrs/wk"), not hidden in the placeholder. Prefer picker primitives (`DatePickerField`, `Select`) over free-text wherever one exists. Full checklist: `.cursor/skills/exxat-accessibility/SKILL.md` — *Form fields — format hints*.
499
+
500
+ ---
501
+
502
+ ## 10. Ask Leo Icon
503
+
504
+ Every Ask Leo / AI surface uses this exact class pattern:
505
+
506
+ ```tsx
507
+ <i className="fa-duotone fa-solid fa-star-christmas text-brand" aria-hidden="true" />
508
+ ```
509
+
510
+ - `fa-duotone fa-solid fa-star-christmas` — must be duotone solid weight
511
+ - `text-brand` — resolves to `var(--brand-color-dark)`, passes WCAG AA 4.5:1 on white
512
+ - Applies to: Ask Leo toggle (site header), sidebar, chart cards, KPI insight cards, promo banners
513
+ - Never: `fa-light fa-sparkles`, `fa-solid` without `fa-duotone`, or any other star/sparkle variant
514
+
515
+ ---
516
+
517
+ ## 11. Keyboard Shortcuts
518
+
519
+ Show `Kbd` / `KbdGroup` on: primary CTAs, secondary/overflow actions, Search, Ask Leo toggle, Sidebar toggle.
520
+
521
+ ```tsx
522
+ <Tip side="bottom" label={<span className="flex items-center gap-1.5">New <KbdGroup><Kbd>{mod}</Kbd><Kbd>⌥</Kbd><Kbd>N</Kbd></KbdGroup></span>}>
523
+ <Button ...>Add foo</Button>
524
+ </Tip>
525
+ ```
526
+
527
+ **If you show a shortcut, implement it.** Use the shared primitives from `@/components/ui/dropdown-menu`:
528
+
529
+ ```tsx
530
+ import { DropdownMenuItem, Shortcut } from "@/components/ui/dropdown-menu"
531
+
532
+ // Visual hint on the menu item (renders a DropdownMenuShortcut automatically).
533
+ <DropdownMenuItem shortcut="⌘⇧E" onSelect={onExport}>Export</DropdownMenuItem>
534
+
535
+ // Global binding — render in a parent that stays mounted (menu items unmount when closed).
536
+ <Shortcut keys="⌘⇧E" onInvoke={onExport} />
537
+ ```
538
+
539
+ - `shortcut` prop = visual only. `<Shortcut>` = actual key binding.
540
+ - Accepts symbols (`⌘⇧⌥⌃⌫⌦⏎↑↓←→`) or words (`Cmd+Shift+D`, `Alt+P`).
541
+ - The hook automatically skips input/textarea/contenteditable targets and any open modal dialog.
542
+ - Prefer this over ad-hoc `document.addEventListener("keydown", …)` + `isEditableTarget` — one source of truth, fewer stale refs.
543
+
544
+ **Every action menu should carry shortcuts.** Standard bindings across the app:
545
+
546
+ | Action | Shortcut |
547
+ |--------|----------|
548
+ | Toggle sidebar | ⌘/Ctrl + B |
549
+ | Table search | ⌘/Ctrl + K |
550
+ | Ask Leo | ⌘/Ctrl + ⌥/Alt + K |
551
+ | New (primary CTA) | ⌘/Ctrl + ⌥/Alt + N |
552
+ | More (⋯ menu open) | ⌘/Ctrl + ⌥/Alt + M |
553
+ | Export | ⌘/Ctrl + ⇧/Shift + E |
554
+ | Hide/Show metric section | ⌘/Ctrl + ⌥/Alt + H |
555
+ | Rename (view, tab) | F2 |
556
+ | Duplicate | ⌘/Ctrl + D |
557
+ | Review / Info | ⌘/Ctrl + I |
558
+ | Remove / Delete item | ⌘/Ctrl + ⌫ |
559
+ | Add view (1..n) | **1..9** (plain digit; `dataListViewAddShortcut`) |
560
+ | **Submit a workflow** (Create, Save, Export, Apply) | **Enter** ⏎ — scoped to the open form/drawer/dialog |
561
+ | **Cancel / dismiss** a workflow | **Esc** (Radix Dialog/Sheet/AlertDialog already bind this) |
562
+ | **Advance a multi-step wizard** | ⌘/Ctrl + Enter (plain Enter must not submit mid-flow) |
563
+ | **Back in a wizard** | ⌘/Ctrl + ⌥/Alt + ← |
564
+
565
+ **Avoid browser-reserved chords:** ⌘⇧N, ⌘⇧T, ⌘⇧O, ⌘⇧B, ⌘L.
566
+
567
+ ### 11.0 Every workflow primary/secondary action MUST carry Enter / Esc
568
+
569
+ Every form, dialog, drawer, sheet, or wizard MUST show and bind:
570
+
571
+ - **Primary (submit/commit)** → **Enter** ⏎. Render the `<Kbd>⏎</Kbd>` **inline inside the button** (after the label, wrapped in `<KbdGroup className="ml-1.5">`) — NOT in a hover Tip. Workflow buttons must expose the shortcut at rest. Pair with a `<Shortcut keys="Enter" onInvoke={submit} />` mounted while the surface is open. `useShortcut` skips input/textarea/contenteditable events, so Enter in a text field still types — it only fires on surface chrome.
572
+ - **Secondary (Cancel/Dismiss)** → **Esc**. Inline `<Kbd>Esc</Kbd>` in the Cancel button (same `ml-1.5` pattern). Radix `Dialog` / `Sheet` / `AlertDialog` bind Esc natively.
573
+
574
+ Inside a button, use **`<Kbd variant="bare">`** — no background, no border, inherits `currentColor` at 70% opacity — so the hint reads as part of the button label, not a pasted-on tile. The default `variant="tile"` (filled, bordered) is for tooltips, menus, docs, and standalone contexts.
575
+
576
+ ```tsx
577
+ <Button type="submit">
578
+ <i className="fa-light fa-check" aria-hidden="true" />
579
+ Create placement
580
+ <KbdGroup className="ml-1.5"><Kbd variant="bare">⏎</Kbd></KbdGroup>
581
+ </Button>
582
+ <Button type="button" variant="outline" onClick={onCancel}>
583
+ Cancel
584
+ <KbdGroup className="ml-1.5"><Kbd variant="bare">Esc</Kbd></KbdGroup>
585
+ </Button>
586
+ ```
587
+
588
+ Glue multi-key chords into a single `<Kbd variant="bare">` (`⌘⌥←`, `⌘⏎`) rather than one tile per key — otherwise the gap between tiles reintroduces the "patch" look.
589
+
590
+ Hover Tips remain correct for **page-level** actions (e.g. page header "New", ⋯ overflow trigger) where inline Kbd would crowd dense chrome. Inline Kbd is for **workflow surfaces** — forms, dialogs, drawers, wizard footers.
591
+ - **Multi-step wizard safety**: plain Enter MUST NOT submit on intermediate steps, or the final review auto-closes when the user hits Enter inside an input. Gate `form.onSubmit` on `step === lastStep`:
592
+
593
+ ```tsx
594
+ <form onSubmit={(e) => {
595
+ if (step !== LAST) { e.preventDefault(); return }
596
+ form.handleSubmit(onSubmit)(e)
597
+ }}>
598
+ ```
599
+
600
+ Use `⌘Enter` via `<Shortcut>` to advance intermediate steps. On the final step, plain Enter submits and the Kbd hint shows **⏎**.
601
+
602
+ Reference implementations: `new-placement-form.tsx` (Create placement = Enter on step 5, Back = ⌘⌥←), `export-drawer.tsx` (Export = Enter, Cancel = Esc).
603
+
604
+ ### 11.1 Global command palette (⌘K)
605
+
606
+ **`CommandMenu`** is **global search** and the main **AI entry** (not a second nav). Config: **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**, **`CommandMenuProvider`** in **`app/(app)/layout.tsx`**.
607
+
608
+ - **Navigation / library / patterns:** Search and pick a row—**Enter** to go.
609
+ - **Searchable row data:** Pass **`dataGroups`** into **`buildCommandMenuConfig`**. Map mocks/API rows in **`lib/command-menu-search-data.ts`** (e.g. **`getCommandMenuSearchDataGroups()`** from placements / student fields). Do **not** embed data mapping inside **`command-menu.tsx`**.
610
+ - **`searchOnly` groups:** For large indexes, set **`searchOnly: true`** on **`CommandMenuGroup`** so that group is **not rendered** until the user types (cmdk otherwise shows every item when the query is empty). Static groups stay visible on open.
611
+ - **Natural language / AI:** Product **SHOULD** show **quick results in the palette** when the response fits; use **Ask Leo** (**⌘⌥K**) for **longer or complex** answers.
612
+ - **Do not** treat the palette as a static link list only—leave room for inline AI results as they ship.
613
+
614
+ **Details:** `apps/web/docs/command-menu-pattern.md`, **`apps/web/AGENTS.md` §7.1** (or `./` when the app folder is the workspace root).
615
+
616
+ ---
617
+
618
+ ## 12. Accessibility — WCAG 2.1 AA (Non-Negotiable)
619
+
620
+ Full checklist in `references/accessibility.md`. Summary of the most-violated rules:
621
+
622
+ ### Structure
623
+ - One `<main id="main-content" tabIndex={-1}>` per page
624
+ - One `<h1>` per page (via `PageHeader`) — `SiteHeader` title is NOT an h1
625
+ - `DialogTitle` / `SheetTitle` always present (use `className="sr-only"` if visually hidden)
626
+
627
+ ### ARIA roles
628
+ - `role="tablist"` → only `role="tab"` children. **Never** put buttons, menus, or other controls inside `tablist`
629
+ - View switchers with extra controls (tabs + remove + settings): use `role="toolbar"` + `aria-label`
630
+
631
+ ### Icons that communicate information — always have a text alternative
632
+
633
+ This rule covers **every icon that carries meaning**, not only icon-only buttons. FA glyphs, inline SVGs, avatar placeholders, trend arrows, status dots, chart-legend squares, calendar/clock/pin icons in cells — if the icon **tells the user something**, that something MUST be reachable by screen readers AND discoverable to sighted users who don't recognise the glyph. SC 1.1.1, 3.3.2, 2.4.6.
634
+
635
+ **Case A — Decorative icon next to text that already names it** → icon is `aria-hidden`, no `aria-label`, no tooltip. The text is the alt.
636
+
637
+ ```tsx
638
+ <span className="flex items-center gap-1.5">
639
+ <i className="fa-light fa-calendar-days" aria-hidden />
640
+ <span>12/14/2025 – 12/20/2025</span>
641
+ </span>
642
+ ```
643
+
644
+ **Case B — Informational icon standing alone** (calendar = "date range", clock = "updated at", pin = "site", cap = "student", trend arrow, status dot, icon-only chart legend) → MUST pair `role="img"` + `aria-label` with a visible `Tooltip`. Wrapper MUST be keyboard-focusable (`tabIndex={0}`).
645
+
646
+ ```tsx
647
+ <Tooltip>
648
+ <TooltipTrigger asChild>
649
+ <span role="img" aria-label="Placement date range" tabIndex={0}
650
+ 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">
651
+ <i className="fa-light fa-calendar-days" aria-hidden />
652
+ </span>
653
+ </TooltipTrigger>
654
+ <TooltipContent side="top">Placement date range</TooltipContent>
655
+ </Tooltip>
656
+ ```
657
+
658
+ **Case C — Interactive icon-only button / link** (close `×`, chevron, overflow `⋯`, sort, filter-dismiss, copy, Ask Leo toggle, row actions) → MUST pair `aria-label` on the `<button>` with a wrapping `Tooltip`. Inner `<i>` / `<svg>` is `aria-hidden`. Target ≥ 24×24.
659
+
660
+ ```tsx
661
+ <Tooltip>
662
+ <TooltipTrigger asChild>
663
+ <button type="button" aria-label="Close insight" className="size-7 …">
664
+ <i className="fa-solid fa-xmark" aria-hidden />
665
+ </button>
666
+ </TooltipTrigger>
667
+ <TooltipContent side="top" className="flex items-center gap-1.5">
668
+ <span>Close</span>
669
+ <Kbd>Esc</Kbd>
670
+ </TooltipContent>
671
+ </Tooltip>
672
+ ```
673
+
674
+ **Decision tree:** adjacent text label? → A. Else interactive? → C. Else → B. When in doubt: add the accessible name + tooltip. Narrow exception for all cases: a chevron inside a labelled composite (`Select`, `Combobox`) where the parent already carries the name.
675
+
676
+ ### Touch targets
677
+ - Minimum **24×24 CSS px** for all interactive controls
678
+ - Icon-only: `size-6` or `min-h-6 min-w-6` — never `size-4` as sole target
679
+
680
+ ### Color & contrast
681
+ - Normal text ≥ 4.5:1; UI components ≥ 3:1
682
+ - Status never by color alone — always include text label or icon
683
+ - Decorative icons: `aria-hidden="true"`
684
+
685
+ ### Dynamic content
686
+ - Filter/result count changes: `aria-live="polite"`
687
+ - Loading states: `aria-busy="true"`
688
+ - Toasts: `role="status"` or `aria-live="polite"`
689
+
690
+ ---
691
+
692
+ ## 13. Charts & Graphs — Use Existing Cards
693
+
694
+ **Never create a custom card shell for a chart. Always use the existing components.**
695
+
696
+ ### ChartCard — the only chart wrapper
697
+
698
+ Use `ChartCard` from `@/components/charts-overview` for every chart/graph in the app.
699
+
700
+ ```tsx
701
+ import { ChartCard } from "@/components/charts-overview"
702
+
703
+ <ChartCard
704
+ title="Placements Over Time"
705
+ description="Monthly placement activity for the current academic year"
706
+ variant="normal" // see variants below
707
+ >
708
+ {/* Recharts chart goes here */}
709
+ <ChartContainer config={chartConfig} className="h-[200px] w-full">
710
+ <AreaChart data={data}>...</AreaChart>
711
+ </ChartContainer>
712
+ </ChartCard>
713
+ ```
714
+
715
+ ### ChartCard variants — pick the right one
716
+
717
+ | `variant` | When to use |
718
+ |-----------|-------------|
719
+ | `"normal"` | Single chart with Ask Leo button in the header |
720
+ | `"tabs"` | Chart view + Trend (line) toggle, or any custom tab pair |
721
+ | `"selector"` | Dropdown filter (period, program, cohort) above the chart |
722
+ | `"metrics-tabs"` | KPI strip whose tabs drive the chart (metric cells ARE the tab triggers) |
723
+ | `"kpi-chart"` | Hero chart card with prominent KPI number + mini chart |
724
+
725
+ ```tsx
726
+ // selector variant — adds a dropdown filter
727
+ <ChartCard
728
+ variant="selector"
729
+ title="Placements by Program"
730
+ description="Filter by program to compare activity"
731
+ filterOptions={[
732
+ { value: "all", label: "All programs" },
733
+ { value: "nursing", label: "Nursing" },
734
+ { value: "pt", label: "PT" },
735
+ ]}
736
+ defaultFilter="all"
737
+ onFilterChange={setFilter}
738
+ >
739
+ {(filter) => <MyChart program={filter} />}
740
+ </ChartCard>
741
+
742
+ // metrics-tabs variant — KPI cells drive chart
743
+ <ChartCard
744
+ variant="metrics-tabs"
745
+ title="Compliance Trend"
746
+ description="Select a metric to view its trend"
747
+ miniMetrics={[
748
+ { label: "Completed", value: "84%", trend: "up" },
749
+ { label: "Pending", value: "12", trend: "neutral" },
750
+ { label: "Overdue", value: "3", trend: "down" },
751
+ ]}
752
+ >
753
+ {(activeMetric) => <MetricChart metric={activeMetric} />}
754
+ </ChartCard>
755
+ ```
756
+
757
+ ### ChartFigure — accessibility wrapper inside ChartCard
758
+
759
+ Wrap the Recharts chart inside `ChartFigure` to get keyboard navigation (arrow keys through data points) and screen-reader announcements:
760
+
761
+ ```tsx
762
+ import { ChartFigure } from "@/components/charts-overview" // internal export
763
+
764
+ // ChartFigure is used inside ChartCard's children — it handles:
765
+ // - role="application" with aria-label
766
+ // - ArrowLeft/Right to cycle data points
767
+ // - Escape to exit chart navigation
768
+ // - Live region announcements
769
+ ```
770
+
771
+ ### ChartContainer + color tokens
772
+
773
+ ```tsx
774
+ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
775
+ import type { ChartConfig } from "@/components/ui/chart"
776
+
777
+ const chartConfig: ChartConfig = {
778
+ placements: { label: "Placements", color: "var(--color-chart-1)" },
779
+ compliance: { label: "Compliance", color: "var(--color-chart-2)" },
780
+ }
781
+
782
+ // Always use CSS chart color tokens — never hardcoded hex/rgb:
783
+ // var(--brand-color) — primary brand
784
+ // var(--color-chart-1) through var(--color-chart-5) — series colors
785
+ // var(--chart-2) — success/positive
786
+ // var(--chart-4) — warning
787
+ // var(--destructive) — error/negative
788
+ ```
789
+
790
+ ### Chart accessibility rules
791
+
792
+ These are non-negotiable (built into `ChartCard`/`ChartFigure` when used correctly):
793
+
794
+ 1. **Accessible data table** — add `ChartDataTable` (sr-only) after every chart so screen-reader users can navigate data as a table
795
+ 2. **Color is never the only differentiator** — pair series colors with dashed vs solid lines, shape markers, or inline labels
796
+ 3. **Chart series colors ≥ 3:1** contrast against card background
797
+ 4. **Text inside charts ≥ 4.5:1** on their local background
798
+ 5. **Tooltips on keyboard focus**, not hover only — `ChartTooltipContent` handles this automatically
799
+
800
+ ### KeyMetrics — for KPI strips
801
+
802
+ Use `KeyMetrics` from `@/components/key-metrics` for metric/KPI strips. Do not build a custom metric grid.
803
+
804
+ ```tsx
805
+ <KeyMetrics
806
+ variant="flat" // "card" | "flat" | "compact"
807
+ metrics={metrics} // MetricItem[]
808
+ insight={insight} // MetricInsight
809
+ showHeader={false}
810
+ metricsSingleRow
811
+ />
812
+ ```
813
+
814
+ ### What NOT to do
815
+
816
+ - Do not use raw `Card` + `CardHeader` + a Recharts chart without `ChartCard`
817
+ - Do not install new charting libraries (`react-chartjs-2`, `victory`, `nivo`, etc.) — Recharts is the only chart library
818
+ - Do not hardcode chart colors — use CSS tokens only
819
+ - Do not build a custom KPI/metric row — use `KeyMetrics`
820
+ - Do not add an Ask Leo button manually to chart cards — `ChartCard` includes it automatically
821
+
822
+ ---
823
+
824
+ `DataTable` already applies its own horizontal inset. Do not wrap it in extra `px-*` / `mx-*` — that creates staggered margins between the filter bar and table vs tabs.
825
+
826
+ Follow `ListPageTemplate` → `DataTable`'s own inset — one horizontal rhythm only.
827
+
828
+ ---
829
+
830
+ ## 14. KPI Metrics Pattern
831
+
832
+ Every primary hub page has a collapsible metrics strip:
833
+
834
+ ```tsx
835
+ // lib/mock/foo-kpi.ts
836
+ export function fooKpiMetrics(items: Foo[]): MetricItem[] {
837
+ return [
838
+ { id: "total", label: "Total", value: items.length, delta: "+1", trend: "up", href: "#", metricVariant: "hero" },
839
+ { id: "active", label: "Active", value: activeCount, delta: "—", trend: "neutral", href: "#" },
840
+ // ...more metrics
841
+ ]
842
+ }
843
+
844
+ export function fooKpiInsight(items: Foo[]): MetricInsight {
845
+ return {
846
+ title: "Insight title",
847
+ description: "Short actionable insight based on the data.",
848
+ severity: "info" | "warning",
849
+ actionLabel: "Ask Leo",
850
+ }
851
+ }
852
+ ```
853
+
854
+ In `FooClient`:
855
+ ```tsx
856
+ <KeyMetrics variant="flat" metrics={metrics} insight={insight} showHeader={false} metricsSingleRow />
857
+ ```
858
+
859
+ ---
860
+
861
+ ## 15. AI Execution Checklist
862
+
863
+ Copy and complete for every list/table/hub page:
864
+
865
+ - [ ] Page shell: `SidebarInset` → `SiteHeader` → `<main id="main-content" tabIndex={-1}>` → `@container/main div`
866
+ - [ ] Sidebar item added to `lib/mock/navigation.tsx` with light/solid icon pair
867
+ - [ ] **Shell sidebar:** Product header uses **`ExxatProductLogo`**; school **`logoDevUrl`** + **`lib/logo-dev`**; team switcher menu **`!w-max`** (not trigger-width-only); expanded switcher **`h-auto min-h-12`** so school + program lines are not clipped; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child nav uses **popover** on icon rail per **§3.1**
868
+ - [ ] Hub pages: `ListPageTemplate` + `DataTable` + `useTableState` + `TablePropertiesDrawer`
869
+ - [ ] Board view: `ListPageBoardCard` shell + `ListHubStatusBadge` + `list-status-badges` when applicable (`apps/web/AGENTS.md` §4.4)
870
+ - [ ] New primary hubs: not placeholder-only — full template + data + views (`apps/web/AGENTS.md` §4.1)
871
+ - [ ] **§6.4:** Parent **context** + quick view/actions → drawer/sheet; primary or long flows → **new page** (`AGENTS.md`, `docs/data-views-pattern.md`)
872
+ - [ ] No raw `<table>` or `ui/table` for product data lists
873
+ - [ ] No double horizontal padding around `DataTable`
874
+ - [ ] Primary CTA: filled `Button size="lg"`; ⋯ more menu with Export + toggle metrics
875
+ - [ ] Breadcrumbs OR back link — never both
876
+ - [ ] Charts: wrapped in `ChartCard` with correct `variant`; no raw `Card` + Recharts combos; color tokens only
877
+ - [ ] All dates: `MM/DD/YYYY` / `MM/DD/YYYY hh:mm AM/PM EST`
878
+ - [ ] All tooltips via `<Tip>` — no `title` attribute
879
+ - [ ] All icons: `aria-hidden="true"`; Ask Leo: `fa-duotone fa-solid fa-star-christmas text-brand`
880
+ - [ ] **Every icon that communicates info has a text alternative** — Case A adjacent label (preferred), Case B `role="img"` + `aria-label` + `Tooltip` (calendar-for-date, status dot, trend arrow, icon-only legend), Case C `aria-label` + `Tooltip` on icon-only buttons; target ≥ 24×24 px. See §12 *Icons that communicate information*.
881
+ - [ ] **`Kbd` inside a `Button` uses `variant="bare"`** (glue chords into one bare kbd); **`Kbd` inside `TooltipContent` uses the default tile** — see §11 Keyboard shortcuts
882
+ - [ ] `DialogTitle`/`SheetTitle` present on every overlay
883
+ - [ ] `role="tablist"` contains only tab-role children
884
+ - [ ] No new shadcn components, no hardcoded colors, no duplicate component abstractions
885
+
886
+ ---
887
+
888
+ ## Reference Files
889
+
890
+ - `references/accessibility.md` — Full WCAG 2.1 AA checklist (interactive elements, keyboard, forms, semantics, contrast, dynamic content, component-specific rules)
891
+ - `references/data-table-pattern.md` — Complete data table implementation guide with full column/filter/drawer wiring
892
+
893
+ Read the relevant reference file when implementing the corresponding feature.