@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
@@ -19,9 +19,11 @@ description: >
19
19
  ## 1. Project Overview
20
20
 
21
21
  - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
- - **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
23
- - **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
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
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.
25
27
 
26
28
  ---
27
29
 
@@ -87,16 +89,161 @@ To add a primary nav item, append to `NAV_PRIMARY`:
87
89
 
88
90
  | Concern | Pattern |
89
91
  |--------|---------|
90
- | **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. |
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. |
91
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. |
92
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**. |
93
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. |
94
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. |
95
- | **Nav items with children** | **Expanded:** **`Collapsible`**. **Desktop icon rail:** **`Popover`** listing child links. **Do not** pass **`tooltip={…}`** on **`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. |
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). |
96
98
  | **Profile (mock)** | **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs. |
97
99
 
98
100
  **Reference:** `components/app-sidebar.tsx`, `components/nav-user.tsx`, `components/product-switcher.tsx`.
99
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
+
100
247
  ---
101
248
 
102
249
  ## 4. Primary Hub Pages — Mandatory Pattern
@@ -104,37 +251,41 @@ To add a primary nav item, append to `NAV_PRIMARY`:
104
251
  Any **primary nav destination** that shows a list of records **must** use this composition (same as Placements / Team):
105
252
 
106
253
  ```
107
- ListPageTemplate
254
+ ListPageTemplate (supportedViewTypes = FULL_HUB_SUPPORTED_VIEWS — seven views)
108
255
  ├── PageHeader (title, subtitle with count, primary CTA, ⋯ more menu)
109
256
  ├── KeyMetrics (flat variant, single row)
110
257
  └── renderContent()
111
- └── DataTable + useTableState + TablePropertiesDrawer
258
+ └── HubTable + useTableState + TablePropertiesDrawer + renderers per view
112
259
  ```
113
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
+
114
263
  **Reference implementations:**
115
- - `components/library-table.tsx` + `library-client.tsx` — canonical seven-view hub (start here)
116
- - `components/columns-showcase.tsx` — cell catalog via `LibraryTable` (custom `columnDefs`, same Add view as Library)
117
- - `components/tokens-themes-client.tsx` + `components/tokens-secondary-nav.tsx` — hub with secondary panel + URL-driven scope + built-in pagination chrome
118
- - `components/library-table.tsx` + `components/library-hub-client.tsx` — full multi-view hub (table, board, dashboard)
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)
119
269
 
120
270
  **Files to create for a new hub page `Foo`:**
121
271
  | File | Purpose |
122
272
  |------|---------|
123
273
  | `lib/mock/foo.ts` | Mock data + TypeScript interface (12+ rows) |
124
- | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` (≤ 4 `MetricItem`s, each with `onClick` or `href`) + `fooKpiInsight()` returning a single `MetricInsight` |
274
+ | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` + `fooKpiInsight()` |
125
275
  | `components/foo-page-header.tsx` | `PageHeader` + primary CTA + ⋯ menu |
126
- | `components/foo-showcase.tsx` *or* `components/foo-table.tsx` | `HubTable` (NOT raw `DataTable`) pass `pagination` / `onPaginationChange` if the hub needs pagination chrome |
127
- | `components/foo-client.tsx` | `ListPageTemplate` orchestrator, mounts `KeyMetrics` with `items` + `insight` |
276
+ | `components/foo-table.tsx` | `DataTable` + `useTableState` + `TablePropertiesDrawer` |
277
+ | `components/foo-client.tsx` | `ListPageTemplate` orchestrator |
128
278
  | `app/(app)/foo/page.tsx` | Thin server component |
129
279
 
130
- **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 **`exxat-ds/AGENTS.md` §4.1**) unless the product explicitly defines a non-data shell.
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.
131
281
 
132
282
  ### Page vs drawer (actions)
133
283
 
134
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`**.
135
286
  - **New page** — Use **otherwise**: **primary**, **long-form**, **multi-step**, or flows that need their **own URL** without the hub visible.
136
287
 
137
- Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
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`**.
138
289
 
139
290
  ---
140
291
 
@@ -150,7 +301,7 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
150
301
  | `ColumnDef` from `@/components/data-table/types` | Column type |
151
302
  | `FilterFieldDef`, `FilterOperator`, `ConditionalRule` from `@/components/table-properties/types` | Filter types |
152
303
 
153
- **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 **`data-list-table-cells.tsx`** (wrapper over **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`**). **Full rules:** **`exxat-ds/AGENTS.md` §4.4**, **`.cursor/rules/exxat-board-cards.mdc`**, **`.cursor/skills/exxat-board-cards/SKILL.md`**.
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`**.
154
305
 
155
306
  **Minimum required features on any data list page:**
156
307
  - Search (wire `searchable={displayOptions.showToolbarSearch}`)
@@ -192,6 +343,14 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
192
343
 
193
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`**.
194
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
+
195
354
  **DataTable must wrap in `<div className="pb-6">`.**
196
355
 
197
356
  ---
@@ -247,6 +406,8 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
247
406
 
248
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`**.
249
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
+
250
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.
251
412
 
252
413
  ---
@@ -300,7 +461,7 @@ Never install new packages or create parallel components. Always use what exists
300
461
  | Color | CSS design tokens only — no hardcoded hex/rgb |
301
462
  | Minimum font size | **`text-xs`** (11px at 16px root via `--text-xs`) or larger — never arbitrary classes below 11px (`AGENTS.md` §8.3) |
302
463
 
303
- Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one.
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).
304
465
 
305
466
  ---
306
467
 
@@ -395,7 +556,7 @@ import { DropdownMenuItem, Shortcut } from "@/components/ui/dropdown-menu"
395
556
  | Duplicate | ⌘/Ctrl + D |
396
557
  | Review / Info | ⌘/Ctrl + I |
397
558
  | Remove / Delete item | ⌘/Ctrl + ⌫ |
398
- | Add view (1..n) | ⌘/Ctrl + ⇧/Shift + 1..9 |
559
+ | Add view (1..n) | **1..9** (plain digit; `dataListViewAddShortcut`) |
399
560
  | **Submit a workflow** (Create, Save, Export, Apply) | **Enter** ⏎ — scoped to the open form/drawer/dialog |
400
561
  | **Cancel / dismiss** a workflow | **Esc** (Radix Dialog/Sheet/AlertDialog already bind this) |
401
562
  | **Advance a multi-step wizard** | ⌘/Ctrl + Enter (plain Enter must not submit mid-flow) |
@@ -450,7 +611,7 @@ Reference implementations: `new-placement-form.tsx` (Create placement = Enter on
450
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.
451
612
  - **Do not** treat the palette as a static link list only—leave room for inline AI results as they ship.
452
613
 
453
- **Details:** `exxat-ds/docs/command-menu-pattern.md`, **`exxat-ds/AGENTS.md` §7.1** (or `./` when the app folder is the workspace root).
614
+ **Details:** `apps/web/docs/command-menu-pattern.md`, **`apps/web/AGENTS.md` §7.1** (or `./` when the app folder is the workspace root).
454
615
 
455
616
  ---
456
617
 
@@ -461,7 +622,7 @@ Full checklist in `references/accessibility.md`. Summary of the most-violated ru
461
622
  ### Structure
462
623
  - One `<main id="main-content" tabIndex={-1}>` per page
463
624
  - One `<h1>` per page (via `PageHeader`) — `SiteHeader` title is NOT an h1
464
- - `DialogTitle` / `SheetTitle` / `DrawerTitle` always present (use `className="sr-only"` if visually hidden)
625
+ - `DialogTitle` / `SheetTitle` always present (use `className="sr-only"` if visually hidden)
465
626
 
466
627
  ### ARIA roles
467
628
  - `role="tablist"` → only `role="tab"` children. **Never** put buttons, menus, or other controls inside `tablist`
@@ -705,8 +866,8 @@ Copy and complete for every list/table/hub page:
705
866
  - [ ] Sidebar item added to `lib/mock/navigation.tsx` with light/solid icon pair
706
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**
707
868
  - [ ] Hub pages: `ListPageTemplate` + `DataTable` + `useTableState` + `TablePropertiesDrawer`
708
- - [ ] Board view: `ListPageBoardCard` shell + `ListHubStatusBadge` + `list-status-badges` when applicable (`exxat-ds/AGENTS.md` §4.4)
709
- - [ ] New primary hubs: not placeholder-only — full template + data + views (`exxat-ds/AGENTS.md` §4.1)
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)
710
871
  - [ ] **§6.4:** Parent **context** + quick view/actions → drawer/sheet; primary or long flows → **new page** (`AGENTS.md`, `docs/data-views-pattern.md`)
711
872
  - [ ] No raw `<table>` or `ui/table` for product data lists
712
873
  - [ ] No double horizontal padding around `DataTable`
@@ -718,7 +879,7 @@ Copy and complete for every list/table/hub page:
718
879
  - [ ] All icons: `aria-hidden="true"`; Ask Leo: `fa-duotone fa-solid fa-star-christmas text-brand`
719
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*.
720
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
721
- - [ ] `DialogTitle`/`SheetTitle`/`DrawerTitle` present on every overlay
882
+ - [ ] `DialogTitle`/`SheetTitle` present on every overlay
722
883
  - [ ] `role="tablist"` contains only tab-role children
723
884
  - [ ] No new shadcn components, no hardcoded colors, no duplicate component abstractions
724
885
 
@@ -0,0 +1,40 @@
1
+ ---
2
+ description: Exxat DS — WCAG 2.1 AA, ARIA tablists, 24px targets, contrast; see AGENTS.md §8 and exxat-accessibility skill
3
+ alwaysApply: true
4
+ appliesTo: [universal]
5
+ ---
6
+
7
+ # Exxat DS — accessibility (binding summary)
8
+
9
+ **Full checklist (ARIA, touch targets, color, sidebar badges, audit follow-ups):** repo **`.cursor/skills/exxat-accessibility/SKILL.md`**.
10
+
11
+ **Product rules in prose + checklist:** **`apps/web/AGENTS.md` §8**.
12
+
13
+ ## Non‑negotiables
14
+
15
+ 1. **Target:** **WCAG 2.1 Level AA** (2.2 where noted — e.g. target size).
16
+ 2. **`role="tablist"`** — only **`role="tab"`** (or equivalent) as direct tab semantics. **MUST NOT** put `role="button"`, menus (`aria-haspopup`), or other controls **inside** the same `tablist` container.
17
+ 3. **Composite view switchers** (tabs + per-tab menu + remove): use **`role="toolbar"`** + **`aria-label`**; **`aria-pressed`** on toggles — **MUST NOT** misuse `tab`/`tablist` for those controls.
18
+ 4. **Touch targets (2.5.8):** interactive controls **≥ 24×24 CSS px** or **24px** spacing so hit areas do not overlap — use **`min-h-6 min-w-6` / `size-6`** for icon-only targets; avoid **`size-4`** as the sole target.
19
+ 5. **Contrast:** normal text **≥ 4.5:1**; UI components / focus where required **≥ 3:1**; muted text on tinted surfaces use tokens against the correct surface (e.g. sidebar), not only `--background`.
20
+ 6. **Minimum text size:** visible product copy **≥ 11px** — use **`text-xs`** or larger; **MUST NOT** use arbitrary Tailwind font sizes below that (see **`apps/web/AGENTS.md` §8.3**, **`apps/web/app/globals.css`** `--text-xs`).
21
+ 7. **Dialogs / sheets:** must expose a **Title** (`DialogTitle` / `SheetTitle`); use **`sr-only`** if visually hidden (shadcn pattern).
22
+ 8. **Format hints are persistent, not placeholders (SC 3.3.2 Labels or Instructions, 1.3.1).** Any field that expects a specific format — **date, time, phone, currency, ID pattern, GPA scale, URL, hours, unit-bearing numbers** — MUST show the format as **persistent helper text** via **`FormDescription`** (or the field's description slot). Placeholders disappear on focus and are unreliable for AT, so **MUST NOT** be the sole carrier of the format. Example: GPA → "Out of 4.0"; Date → "MM/DD/YYYY"; Phone → "+1 (555) 555-0100"; Student ID → "STU-YYYY-####". Pair with `inputMode`, `pattern`, or a picker primitive (e.g. `DatePickerField`) where applicable; never rely on a free-text date input.
23
+ 9. **Every icon that communicates information MUST have a text alternative** — not just icon-only buttons. Three cases (SC 1.1.1 Non-text Content, 3.3.2 Labels or Instructions, 2.4.6 Headings and Labels):
24
+
25
+ - **A. Decorative icon next to text that already names it** (e.g. `<i class="fa-light fa-calendar-days" aria-hidden /> 12/14/2025 – 12/20/2025`) → icon MUST be **`aria-hidden`**; MUST NOT add `aria-label` (screen readers would announce the meaning twice). No tooltip needed.
26
+ - **B. Informational icon standing alone** — a calendar glyph used to mean "date range", clock for "updated at", pin for "site", graduation cap for "student", trending arrow for direction, status dot, icon-only chart legend — MUST pair **`role="img"` + `aria-label`** (or `aria-labelledby` on a wrapping element) with a visible **`Tooltip`** so sighted users who don't recognise the glyph learn the meaning. The icon wrapper MUST be keyboard-focusable (`tabIndex={0}` on the `span[role="img"]`) so the tooltip opens on focus.
27
+ - **C. Interactive icon-only button/link** (close `×`, chevron, overflow `⋯`, sort direction, filter chip dismiss, copy, Ask Leo toggle, row actions) → MUST pair **`aria-label`** on the `<button>` with a wrapping **`Tooltip`**. `aria-label` alone is NOT enough — sighted mouse users and keyboard users rely on the tooltip to discover what a bare icon does.
28
+
29
+ In all three cases, the inner `<i>` / `<svg>` MUST be `aria-hidden`; the accessible name lives on the wrapping element. Tooltip text MUST match the accessible name. Narrow exception: a chevron inside a labelled composite (`Select`, `Combobox`) where the parent control already names the whole thing. See **§8.6 (Case A/B/C)** in `AGENTS.md` and the accessibility skill.
30
+ 10. **Keyboard shortcut hints inside buttons** MUST use **`<Kbd variant="bare">`** (no background, no border, inherits `currentColor` at 70%). The default `tile` variant is reserved for **tooltips** and **menu `shortcut=` slots**. Glue multi-key chords into one bare kbd (e.g. `<Kbd variant="bare">⌘⌥K</Kbd>`), not one tile per key. Reference: `Next` / `Back` buttons in `new-library-item-form.tsx`; see `.cursor/rules/exxat-kbd-shortcuts.mdc`.
31
+
32
+ After changing **views toolbar** or **tab** UIs, re-run **axe** (or equivalent) on **Placements** (or the affected page).
33
+
34
+ **Charts:** Keyboard exploration uses **`ChartFigure`**; selected data points should have **visible** focus feedback — see skill **§ Charts (keyboard exploration)** and **`AGENTS.md` §4.3** (`chart-keyboard-selection`).
35
+
36
+ ## See also
37
+
38
+ - **`apps/web/AGENTS.md`** §8 — accessibility in project context.
39
+ - **`.cursor/rules/exxat-kbd-shortcuts.mdc`** — shortcuts paired with `Kbd` hints.
40
+ - **`apps/web/.cursor/rules/exxat-dashboard-view-charts.mdc`** — Data view chart keyboard parity.
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: Exxat DS — board (kanban) cards via ListPageBoardCard, badges, primitives. Auto-attaches when editing React board/list files; ask explicitly when designing kanban surfaces.
3
+ globs: apps/web/components/**/*.{tsx,ts}
4
+ alwaysApply: false
5
+ appliesTo: [react]
6
+ ---
7
+
8
+ # Exxat DS — board cards
9
+
10
+ **Authoritative detail:** **`apps/web/AGENTS.md` §4.4** and **`.cursor/skills/exxat-board-cards/SKILL.md`**.
11
+
12
+ ## MUST
13
+
14
+ 1. **Shell** — Use **`ListPageBoardCard`** from **`components/data-views/list-page-board-card.tsx`** for product board cards (same **`Card` `size="sm"`** treatment as Placements).
15
+ 2. **Hierarchy** — **Title** (`ListPageBoardCardTitleRow`) → optional **avatar** (`ListPageBoardCardAvatar` on `trailing`) → **status row** (`ListPageBoardCardBadgeRow` + **`ListHubStatusBadge`** **`surface="board"`**) when the entity has status → **body** (`ListPageBoardCardBody`) with **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** (**`components/data-views/board-card-primitives.tsx`**).
16
+ 3. **Status** — Use **`ListHubStatusBadge`** with **`surface="table"`** in grid / list rows and **`surface="board"`** on kanban cards. Generic semantic tints live in **`lib/list-status-badges.ts`** (**`LIST_HUB_STATUS_TINT_SUCCESS / WARNING / INFO / NEUTRAL / DANGER`**); compose per-domain label / icon maps next to the entity's mock data. Reference: **`library-board-view.tsx`** + **`lib/mock/library.ts`**. **MUST NOT** use **`uppercase`** on those chips.
17
+ 4. **Simple column boards** — **`ListPageBoardTemplate`** + **`renderCard`**; compose **`ListPageBoardCard`** inside **`renderCard`**.
18
+
19
+ ## MUST NOT
20
+
21
+ - Duplicate **`ListPageBoardCard`** with ad-hoc **`<button>`** + border/padding classes for the same hub pattern.
22
+ - Show **status** only as plain text in the **body** when the hub uses **status** elsewhere — use the **badge row** + shared maps.
23
+
24
+ ## See also
25
+
26
+ - **`apps/web/AGENTS.md` §4.4**, **§13** checklist
27
+ - **`apps/web/docs/data-views-pattern.md`** — Board UI reuse
28
+ - **`lib/initials-from-name.ts`** — owner initials when mock has no `initials` field
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Do not duplicate parent navigation with a Back control when breadcrumbs exist
3
+ globs: apps/web/**/*.tsx
4
+ alwaysApply: false
5
+ appliesTo: [react]
6
+ ---
7
+
8
+ # Exxat DS — breadcrumbs and back navigation
9
+
10
+ ## MUST NOT
11
+
12
+ When a page uses **`SiteHeader`** with **`breadcrumbs`** (a visible trail such as Patterns → Library → current title), **do not** add a **“Back to …”** link or button in the page body that goes to the same parent as the breadcrumb segment. Breadcrumbs already provide hierarchy and one-click navigation up the tree.
13
+
14
+ ## MAY
15
+
16
+ - Rely on **`SiteHeader`** breadcrumbs only for returning to parent routes.
17
+ - Use a **single** explicit back affordance on flows that **omit** breadcrumbs by design (e.g. full-screen step, modal route) where product copy requires it.
18
+
19
+ ## See also
20
+
21
+ - `components/site-header.tsx`
22
+ - `components/templates/primary-page-template.tsx`
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Exxat DS — when to use cards vs DataTable rows vs simple list rows.
3
+ alwaysApply: true
4
+ appliesTo: [universal]
5
+ ---
6
+
7
+ # Exxat DS — cards vs rows vs lists
8
+
9
+ ## MUST
10
+
11
+ 1. **Dense, comparable records (10+)** — **`DataTable`** + **`ListPageTemplate`** + **`useTableState`** for primary hubs (**`exxat-data-tables.mdc`**).
12
+ 2. **Board / tiles / visual browse** — **`ListPageBoardCard`** (and related shells), **`ListPageViewFrame`** for non-table bodies (**`exxat-board-cards.mdc`**, **`exxat-list-page-view-shells.mdc`**).
13
+ 3. **One dataset** — Cards and tables read the **same** **`tableState.rows`**; no forked mock arrays (**`exxat-centralized-list-dataset.mdc`**).
14
+
15
+ ## MUST NOT
16
+
17
+ - Replace a **primary data hub grid** with a **card wall** when users need column sort, filter chips, and export parity.
18
+ - Introduce a **second table stack** for the same entity.
19
+
20
+ ## See also
21
+
22
+ - **`docs/card-vs-rows-pattern.md`** · **`.cursor/skills/exxat-card-vs-list-rows/SKILL.md`**
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Single source of truth for hub rows, KPIs, table properties, detail views, and shared view chrome — dataset + presentation consistency across DataListViewType surfaces. Auto-attaches when editing list-hub React files.
3
+ globs: apps/web/{components,lib}/**/*.{tsx,ts}
4
+ alwaysApply: false
5
+ appliesTo: [react]
6
+ ---
7
+
8
+ # Exxat DS — centralized list hub dataset + presentation
9
+
10
+ **Authoritative detail:** **`apps/web/AGENTS.md` §4.1** and **§4.5** (connected views + view shells), **`docs/data-views-pattern.md`**, **`.cursor/rules/exxat-list-page-connected-views.mdc`**, and **`.cursor/rules/exxat-list-page-view-shells.mdc`**. This rule tightens **where data lives** and **where reusable view chrome lives** so **table, list, board, dashboard, folder, panel, tree, and inspectors** never drift.
11
+
12
+ ## Single dataset
13
+
14
+ 1. **Rows** — One typed collection per entity (**`lib/mock/<entity>.ts`** until API wiring). **MUST NOT** maintain a second parallel array (e.g. “tree only” or “panel only”) with overlapping IDs unless it is a **derived view** (computed from the same source via selectors).
15
+
16
+ 2. **`useTableState`** — One instance per tab table; **`tableState.rows`** is the **only** filtered/sorted row bag that **list / board / dashboard / folder / panel / tree** surfaces read. **MUST NOT** bypass filters by importing raw mock arrays into alternate views.
17
+
18
+ 3. **Detail / inspector / finder / split-panel bodies** — Resolve the active record by **`id`** (or stable key) against **`tableState.rows`** (or the same upstream state that feeds **`useTableState`**). **MUST NOT** hydrate inspector fields from ad-hoc literals when the row already exists on the dataset.
19
+
20
+ 4. **Columns & table “properties”** — Column defs **`accessorKey` / cell renderers** map to the **same** row interface as KPI helpers and board cards. **MUST NOT** introduce incompatible duplicate TypeScript shapes per view.
21
+
22
+ 5. **`TablePropertiesDrawer`** — Stays wired to the **`DataTable`** that owns **`useTableState`** (**§4.2**): **`currentView`**, **`onViewChange`**, column visibility/density — all reflect manipulation of the **same** underlying rows.
23
+
24
+ 6. **Labels / status / chips** — Prefer shared maps (**`lib/list-status-badges.ts`**, **`lib/data-list-view.ts`** tiles, entity-specific maps **next to** **`lib/mock/<entity>.ts`**). **MUST NOT** fork label strings or badge colors per view file.
25
+
26
+ 7. **KPIs & charts** — **`MetricItem`** / **`MetricInsight`** builders take **`tableState.rows`** (or equivalent filtered list). Same inputs as the grid after search/filters.
27
+
28
+ ## Centralized presentation (layout + components)
29
+
30
+ 8. **`ListPageViewFrame`** — Non-**`DataTable`** view bodies (**folder**, **panel**, icon grids, comparable dashboard sections) **MUST** use **`ListPageViewFrame`** (and exported max-width constants) instead of copy-pasted **`mx-*` / `max-w-*`** per hub (**`exxat-list-page-view-shells.mdc`**).
31
+
32
+ 9. **`components/data-views/`** — New **record-bearing** view layouts (**grids**, **OS folder**, **finder split**) **MUST** land as **generic** building blocks under **`data-views/`** with **`rows`**, **`getRowId`**, render props — hub **`TeamTable`** / **`LibraryTable`** **only** wires props and branch logic (**`AGENTS.md` §4.5**).
33
+
34
+ 10. **Hub client composition** — One **`*-client.tsx`** owns **`useTableState`**, passes **`tableState.rows`** into every **`viewType`** branch, and mounts **template** slots (**metrics**, **export**, **`beforeSiteHeader`**) — **MUST NOT** split the same hub across multiple clients with different row sources.
35
+
36
+ ## MUST NOT
37
+
38
+ - Ship alternate mock datasets per **`DataListViewType`** for the same hub without documenting them as **computed derivatives** of one canonical list.
39
+ - Duplicate entity fields in inspector-only types that contradict **`LibraryItem`** / **`Placement`** / etc.; extend the shared interface **once**.
40
+
41
+ ## See also
42
+
43
+ - **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** — workflow + checklist.
44
+ - **`.cursor/rules/exxat-table-properties-drawer.mdc`** — **`currentView`** / **`onViewChange`**.
45
+ - **`exxat-list-page-connected-views.mdc`** — toolbar + **`tableState.rows`**.
46
+ - **`exxat-list-page-view-shells.mdc`** — **`ListPageViewFrame`** + **`data-views/`** extraction.
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: Shared hubs — collaboration header, invite sheet, library access vs directory roles
3
+ globs: apps/web/components/**/*.{tsx,ts},apps/web/lib/**/*.{tsx,ts}
4
+ alwaysApply: false
5
+ appliesTo: [react]
6
+ ---
7
+
8
+ # Exxat DS — collaboration & access
9
+
10
+ **Authoritative detail:** **`apps/web/AGENTS.md` §4.7** and **`apps/web/docs/collaboration-access-pattern.md`**.
11
+
12
+ ## MUST
13
+
14
+ 1. **Shared hubs** — Use **`PageHeader`** **`variant="collaboration"`** with **`collaborators`** (`PageHeaderCollaborator[]`) and optional **`accessInfo`**.
15
+ 2. **Header chrome** — **Empty roster:** outline **Add collaborator** (`COLLABORATION_HEADER_ADD_LABEL` / **`addCollaboratorLabel`**) opens the invite sheet. **Non-empty roster:** face rail only; faces and **`+N`** open the same sheet via **`onCollaboratorsOpen`**.
16
+ 3. **Invite entry** — **⋯ More** → **Invite people** on the entity page header; opens **`InviteCollaboratorsDrawer`** (floating sheet, same family as **`ExportDrawer`**).
17
+ 4. **State** — Prefer **`CollaborationAccessFlow`** on hub **`*-client.tsx`**; otherwise own **`collaborators`** + **`inviteOpen`**. Successful invite updates **`collaborators`** so the header and sheet roster match.
18
+ 5. **Library access** — Labels and invite options from **`lib/collaborator-access.ts`**; trailing roster badge = access (Owner / Editor / Commenter / Viewer).
19
+ 6. **Directory roles** — Optional **`roles`** on **`PageHeaderCollaborator`** as **`Badge variant="outline"`** chips (Faculty, Program coordinator, Director) — **not** library access.
20
+ 7. **Roster layout** — One bordered list with row dividers; per row: **name**, **email**, role tags, access badge — **not** one card per person.
21
+ 8. **Invite field** — **`FieldGroup`** + **`Field`**; combined email + access row uses **`InputGroup`** + **`InputGroupInput`** + **`InputGroupAddon`** with shadcn **`Select`** (**`SelectGroup`** / **`SelectItem`**, **`position="popper"`**); persistent **`FieldDescription`** for email format; **no** **`toast()`** (**`exxat-no-toast.mdc`**).
22
+
23
+ ## MUST NOT
24
+
25
+ - A second invite control **beside** a populated face rail.
26
+ - Fork access enums/labels per hub — extend **`collaborator-access.ts`** once.
27
+ - Use **`Select`** inside **`InputGroupAddon`** without **`InputGroupInput`** / **`SelectGroup`**; put email above name in the roster (order is **name → email → role tags**).
28
+
29
+ ## See also
30
+
31
+ - **`exxat-page-vs-drawer.mdc`** — invite is a **sheet**, not a new route.
32
+ - **`exxat-kbd-shortcuts.mdc`** — workflow **Cancel** / **Send invite** shortcuts on the sheet.
33
+ - **`exxat-library-hub-header.mdc`** — Library library: when URL is **folder-scoped**, **⋯ More** also includes **Customize folder** (hub client hosts **`LibraryNewFolderSheet`**).
@@ -1,23 +1,23 @@
1
1
  ---
2
2
  description: Exxat DS — global command palette (⌘K) as search + quick AI vs Ask Leo for long answers.
3
3
  alwaysApply: true
4
+ appliesTo: [universal]
4
5
  ---
5
6
 
6
7
  # Exxat DS — global command palette (`CommandMenu`)
7
8
 
8
9
  ## Intent
9
10
 
10
- - **`CommandMenu`** (**⌘K** / **Ctrl+K**) is **global search** (routes, library, patterns, AI starters, optional row data such as student names / question stems) — see **`AGENTS.md` §7.1** and **`docs/command-menu-pattern.md`**.
11
+ - **`CommandMenu`** (**⌘K** / **Ctrl+K**) is **global search** (routes, library, patterns, AI starters, optional row data such as student names / question stems) — see **`apps/web/AGENTS.md` §7.1** and **`apps/web/docs/command-menu-pattern.md`**.
11
12
  - **Quick / lookup / short AI:** Prefer **results inside the palette** when the product can return compact answers or lightweight “research” without leaving the flow.
12
13
  - **Long or complex answers:** **Ask Leo** side panel (**⌘⌥K** / **Ctrl+Alt+K**)—not forced into the palette.
13
14
 
14
15
  ## Implementation pointers
15
16
 
16
- - Shell: `components/command-menu.tsx`; config **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**; optional **`dataGroups`** from **`lib/command-menu-search-data.ts`** (e.g. **`getCommandMenuSearchDataGroups()`**), wired in **`app/(app)/layout.tsx`**. Keep domain mapping out of the shell.
17
- - Large indexes: set **`searchOnly: true`** on **`CommandMenuGroup`** so **`command-menu.tsx`** skips the group until the user types (avoids listing every row on open).
18
- - Sidebar **“Search or ask Leo”** opens the same palette.
17
+ - Shell: `apps/web/components/command-menu.tsx`; config **`buildCommandMenuConfig()`** in **`apps/web/lib/command-menu-config.ts`**; optional **`dataGroups`** from **`apps/web/lib/command-menu-search-data.ts`** (e.g. **`getCommandMenuSearchDataGroups()`**), wired in **`apps/web/app/(app)/layout.tsx`**. Keep domain mapping out of the shell.
18
+ - Large indexes: set **`searchOnly: true`** on **`CommandMenuGroup`** so **`command-menu.tsx`** skips the group until the user types (avoids listing every row on open; cmdk shows all items when the search string is empty).
19
19
 
20
20
  ## See also
21
21
 
22
22
  - **`apps/web/AGENTS.md` §7.1**
23
- - **`.cursor/rules/exxat-kbd-shortcuts.mdc`** (⌘K vs ⌘⌥K)
23
+ - **`exxat-kbd-shortcuts.mdc`** (⌘K vs ⌘⌥K)