@exxatdesignux/ui 0.2.19 → 0.3.0

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 (688) hide show
  1. package/CHANGELOG.md +60 -7
  2. package/bin/sync-extras.mjs +116 -29
  3. package/consumer-extras/README.md +42 -7
  4. package/consumer-extras/cursor-rules/exxat-accessibility.mdc +39 -0
  5. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +26 -0
  6. package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +21 -0
  7. package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +21 -0
  8. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +44 -0
  9. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +32 -0
  10. package/consumer-extras/cursor-rules/exxat-command-menu.mdc +22 -0
  11. package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +53 -0
  12. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +41 -0
  13. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +25 -0
  14. package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +22 -0
  15. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +56 -0
  16. package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +31 -0
  17. package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +100 -0
  18. package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +28 -0
  19. package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +21 -0
  20. package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +31 -0
  21. package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +24 -0
  22. package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +31 -0
  23. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +30 -0
  24. package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +78 -0
  25. package/consumer-extras/cursor-rules/exxat-no-toast.mdc +25 -0
  26. package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +23 -0
  27. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +47 -0
  28. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +52 -0
  29. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +28 -0
  30. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +34 -0
  31. package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +77 -0
  32. package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +103 -0
  33. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +1 -1
  34. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  35. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +4 -15
  36. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +13 -28
  37. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +1 -1
  38. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +2 -4
  39. package/consumer-extras/handbook/HANDBOOK.md +185 -0
  40. package/consumer-extras/handbook/glossary.md +57 -0
  41. package/consumer-extras/handbook/reference-implementations.md +126 -0
  42. package/consumer-extras/handbook/voice-and-tone.md +262 -0
  43. package/consumer-extras/patterns/command-menu-pattern.md +1 -1
  44. package/consumer-extras/patterns/consumer-upgrade-checklist.md +0 -20
  45. package/consumer-extras/patterns/data-views-pattern.md +17 -54
  46. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +3 -5
  47. package/dist/components/data-table/filter-date-calendar.d.ts +10 -0
  48. package/dist/components/data-table/filter-date-calendar.js +280 -0
  49. package/dist/components/data-table/filter-date-calendar.js.map +1 -0
  50. package/dist/components/data-table/filter-text-value-input.d.ts +15 -0
  51. package/dist/components/data-table/filter-text-value-input.js +561 -0
  52. package/dist/components/data-table/filter-text-value-input.js.map +1 -0
  53. package/dist/components/data-table/index.d.ts +45 -0
  54. package/dist/components/data-table/index.js +3085 -0
  55. package/dist/components/data-table/index.js.map +1 -0
  56. package/dist/components/data-table/pagination.d.ts +28 -0
  57. package/dist/components/data-table/pagination.js +3264 -0
  58. package/dist/components/data-table/pagination.js.map +1 -0
  59. package/dist/components/data-table/types.d.ts +84 -0
  60. package/dist/components/data-table/types.js +3 -0
  61. package/dist/components/data-table/types.js.map +1 -0
  62. package/dist/components/data-table/use-table-state.d.ts +116 -0
  63. package/dist/components/data-table/use-table-state.js +670 -0
  64. package/dist/components/data-table/use-table-state.js.map +1 -0
  65. package/dist/components/data-views/board-card-primitives.d.ts +22 -0
  66. package/dist/components/data-views/board-card-primitives.js +84 -0
  67. package/dist/components/data-views/board-card-primitives.js.map +1 -0
  68. package/dist/components/data-views/data-row-list.d.ts +33 -0
  69. package/dist/components/data-views/data-row-list.js +106 -0
  70. package/dist/components/data-views/data-row-list.js.map +1 -0
  71. package/dist/components/data-views/finder-panel-view.d.ts +54 -0
  72. package/dist/components/data-views/finder-panel-view.js +388 -0
  73. package/dist/components/data-views/finder-panel-view.js.map +1 -0
  74. package/dist/components/data-views/folder-grid-view.d.ts +22 -0
  75. package/dist/components/data-views/folder-grid-view.js +58 -0
  76. package/dist/components/data-views/folder-grid-view.js.map +1 -0
  77. package/dist/components/data-views/hub-table.d.ts +167 -0
  78. package/dist/components/data-views/hub-table.js +5561 -0
  79. package/dist/components/data-views/hub-table.js.map +1 -0
  80. package/dist/components/data-views/index.d.ts +27 -0
  81. package/dist/components/data-views/index.js +6575 -0
  82. package/dist/components/data-views/index.js.map +1 -0
  83. package/dist/components/data-views/list-page-board-card.d.ts +72 -0
  84. package/dist/components/data-views/list-page-board-card.js +264 -0
  85. package/dist/components/data-views/list-page-board-card.js.map +1 -0
  86. package/dist/components/data-views/list-page-board-template.d.ts +24 -0
  87. package/dist/components/data-views/list-page-board-template.js +137 -0
  88. package/dist/components/data-views/list-page-board-template.js.map +1 -0
  89. package/dist/components/data-views/list-page-connected-view-body.d.ts +19 -0
  90. package/dist/components/data-views/list-page-connected-view-body.js +116 -0
  91. package/dist/components/data-views/list-page-connected-view-body.js.map +1 -0
  92. package/dist/components/data-views/list-page-split-details-placeholder.d.ts +14 -0
  93. package/dist/components/data-views/list-page-split-details-placeholder.js +38 -0
  94. package/dist/components/data-views/list-page-split-details-placeholder.js.map +1 -0
  95. package/dist/components/data-views/list-page-split-hub-chrome.d.ts +17 -0
  96. package/dist/components/data-views/list-page-split-hub-chrome.js +54 -0
  97. package/dist/components/data-views/list-page-split-hub-chrome.js.map +1 -0
  98. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +12 -0
  99. package/dist/components/data-views/list-page-split-hub-tokens.js +8 -0
  100. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -0
  101. package/dist/components/data-views/list-page-tree-column-header.d.ts +15 -0
  102. package/dist/components/data-views/list-page-tree-column-header.js +22 -0
  103. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -0
  104. package/dist/components/data-views/list-page-tree-panel-shell.d.ts +25 -0
  105. package/dist/components/data-views/list-page-tree-panel-shell.js +146 -0
  106. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -0
  107. package/dist/components/data-views/os-folder-glyph.d.ts +35 -0
  108. package/dist/components/data-views/os-folder-glyph.js +104 -0
  109. package/dist/components/data-views/os-folder-glyph.js.map +1 -0
  110. package/dist/components/data-views/outline-tree-menu.d.ts +36 -0
  111. package/dist/components/data-views/outline-tree-menu.js +131 -0
  112. package/dist/components/data-views/outline-tree-menu.js.map +1 -0
  113. package/dist/components/table-properties/column-row.d.ts +22 -0
  114. package/dist/components/table-properties/column-row.js +153 -0
  115. package/dist/components/table-properties/column-row.js.map +1 -0
  116. package/dist/components/table-properties/draggable-list.d.ts +24 -0
  117. package/dist/components/table-properties/draggable-list.js +53 -0
  118. package/dist/components/table-properties/draggable-list.js.map +1 -0
  119. package/dist/components/table-properties/drawer-button.d.ts +110 -0
  120. package/dist/components/table-properties/drawer-button.js +2748 -0
  121. package/dist/components/table-properties/drawer-button.js.map +1 -0
  122. package/dist/components/table-properties/drawer.d.ts +100 -0
  123. package/dist/components/table-properties/drawer.js +2595 -0
  124. package/dist/components/table-properties/drawer.js.map +1 -0
  125. package/dist/components/table-properties/filter-card.d.ts +24 -0
  126. package/dist/components/table-properties/filter-card.js +854 -0
  127. package/dist/components/table-properties/filter-card.js.map +1 -0
  128. package/dist/components/table-properties/index.d.ts +14 -0
  129. package/dist/components/table-properties/index.js +2768 -0
  130. package/dist/components/table-properties/index.js.map +1 -0
  131. package/dist/components/table-properties/sort-card.d.ts +20 -0
  132. package/dist/components/table-properties/sort-card.js +102 -0
  133. package/dist/components/table-properties/sort-card.js.map +1 -0
  134. package/dist/components/templates/dedicated-search-landing-template.d.ts +21 -0
  135. package/dist/components/templates/dedicated-search-landing-template.js +254 -0
  136. package/dist/components/templates/dedicated-search-landing-template.js.map +1 -0
  137. package/dist/components/templates/dedicated-search-results-template.d.ts +15 -0
  138. package/dist/components/templates/dedicated-search-results-template.js +16 -0
  139. package/dist/components/templates/dedicated-search-results-template.js.map +1 -0
  140. package/dist/components/templates/index.d.ts +9 -0
  141. package/dist/components/templates/index.js +2720 -0
  142. package/dist/components/templates/index.js.map +1 -0
  143. package/dist/components/templates/list-page.d.ts +83 -0
  144. package/dist/components/templates/list-page.js +2433 -0
  145. package/dist/components/templates/list-page.js.map +1 -0
  146. package/dist/components/templates/nested-secondary-panel-shell.d.ts +20 -0
  147. package/dist/components/templates/nested-secondary-panel-shell.js +54 -0
  148. package/dist/components/templates/nested-secondary-panel-shell.js.map +1 -0
  149. package/dist/components/ui/accordion.d.ts +10 -0
  150. package/dist/components/ui/accordion.js +74 -0
  151. package/dist/components/ui/accordion.js.map +1 -0
  152. package/dist/components/ui/alert-dialog.d.ts +37 -0
  153. package/dist/components/ui/alert-dialog.js +201 -0
  154. package/dist/components/ui/alert-dialog.js.map +1 -0
  155. package/dist/components/ui/avatar.d.ts +84 -0
  156. package/dist/components/ui/avatar.js +328 -0
  157. package/dist/components/ui/avatar.js.map +1 -0
  158. package/dist/components/ui/badge.d.ts +13 -0
  159. package/dist/components/ui/badge.js +49 -0
  160. package/dist/components/ui/badge.js.map +1 -0
  161. package/dist/components/ui/banner.d.ts +62 -0
  162. package/dist/components/ui/banner.js +364 -0
  163. package/dist/components/ui/banner.js.map +1 -0
  164. package/dist/components/ui/breadcrumb.d.ts +14 -0
  165. package/dist/components/ui/breadcrumb.js +114 -0
  166. package/dist/components/ui/breadcrumb.js.map +1 -0
  167. package/dist/components/ui/button.d.ts +16 -0
  168. package/dist/components/ui/button.js +59 -0
  169. package/dist/components/ui/button.js.map +1 -0
  170. package/dist/components/ui/calendar.d.ts +13 -0
  171. package/dist/components/ui/calendar.js +238 -0
  172. package/dist/components/ui/calendar.js.map +1 -0
  173. package/dist/components/ui/card.d.ts +14 -0
  174. package/dist/components/ui/card.js +102 -0
  175. package/dist/components/ui/card.js.map +1 -0
  176. package/dist/components/ui/chart.d.ts +58 -0
  177. package/dist/components/ui/chart.js +292 -0
  178. package/dist/components/ui/chart.js.map +1 -0
  179. package/dist/components/ui/checkbox.d.ts +23 -0
  180. package/dist/components/ui/checkbox.js +155 -0
  181. package/dist/components/ui/checkbox.js.map +1 -0
  182. package/dist/components/ui/coach-mark.d.ts +27 -0
  183. package/dist/components/ui/coach-mark.js +306 -0
  184. package/dist/components/ui/coach-mark.js.map +1 -0
  185. package/dist/components/ui/collapsible.d.ts +8 -0
  186. package/dist/components/ui/collapsible.js +35 -0
  187. package/dist/components/ui/collapsible.js.map +1 -0
  188. package/dist/components/ui/command.d.ts +36 -0
  189. package/dist/components/ui/command.js +274 -0
  190. package/dist/components/ui/command.js.map +1 -0
  191. package/dist/components/ui/context-menu.d.ts +32 -0
  192. package/dist/components/ui/context-menu.js +245 -0
  193. package/dist/components/ui/context-menu.js.map +1 -0
  194. package/dist/components/ui/date-picker-field.d.ts +38 -0
  195. package/dist/components/ui/date-picker-field.js +550 -0
  196. package/dist/components/ui/date-picker-field.js.map +1 -0
  197. package/dist/components/ui/dialog.d.ts +22 -0
  198. package/dist/components/ui/dialog.js +200 -0
  199. package/dist/components/ui/dialog.js.map +1 -0
  200. package/dist/components/ui/dot-pattern.d.ts +21 -0
  201. package/dist/components/ui/dot-pattern.js +139 -0
  202. package/dist/components/ui/dot-pattern.js.map +1 -0
  203. package/dist/components/ui/drag-handle-grip.d.ts +10 -0
  204. package/dist/components/ui/drag-handle-grip.js +15 -0
  205. package/dist/components/ui/drag-handle-grip.js.map +1 -0
  206. package/dist/components/ui/drawer.d.ts +16 -0
  207. package/dist/components/ui/drawer.js +125 -0
  208. package/dist/components/ui/drawer.js.map +1 -0
  209. package/dist/components/ui/dropdown-menu.d.ts +45 -0
  210. package/dist/components/ui/dropdown-menu.js +353 -0
  211. package/dist/components/ui/dropdown-menu.js.map +1 -0
  212. package/dist/components/ui/export-drawer.d.ts +11 -0
  213. package/dist/components/ui/export-drawer.js +1658 -0
  214. package/dist/components/ui/export-drawer.js.map +1 -0
  215. package/dist/components/ui/field.d.ts +30 -0
  216. package/dist/components/ui/field.js +249 -0
  217. package/dist/components/ui/field.js.map +1 -0
  218. package/dist/components/ui/form.d.ts +28 -0
  219. package/dist/components/ui/form.js +110 -0
  220. package/dist/components/ui/form.js.map +1 -0
  221. package/dist/components/ui/hover-card.d.ts +9 -0
  222. package/dist/components/ui/hover-card.js +43 -0
  223. package/dist/components/ui/hover-card.js.map +1 -0
  224. package/dist/components/ui/input-group.d.ts +20 -0
  225. package/dist/components/ui/input-group.js +219 -0
  226. package/dist/components/ui/input-group.js.map +1 -0
  227. package/dist/components/ui/input-mask.d.ts +39 -0
  228. package/dist/components/ui/input-mask.js +118 -0
  229. package/dist/components/ui/input-mask.js.map +1 -0
  230. package/dist/components/ui/input.d.ts +5 -0
  231. package/dist/components/ui/input.js +30 -0
  232. package/dist/components/ui/input.js.map +1 -0
  233. package/dist/components/ui/kbd.d.ts +20 -0
  234. package/dist/components/ui/kbd.js +45 -0
  235. package/dist/components/ui/kbd.js.map +1 -0
  236. package/dist/components/ui/key-metrics-context.d.ts +19 -0
  237. package/dist/components/ui/key-metrics-context.js +26 -0
  238. package/dist/components/ui/key-metrics-context.js.map +1 -0
  239. package/dist/components/ui/key-metrics.d.ts +131 -0
  240. package/dist/components/ui/key-metrics.js +1015 -0
  241. package/dist/components/ui/key-metrics.js.map +1 -0
  242. package/dist/components/ui/label.d.ts +6 -0
  243. package/dist/components/ui/label.js +28 -0
  244. package/dist/components/ui/label.js.map +1 -0
  245. package/dist/components/ui/list-page-view-frame.d.ts +22 -0
  246. package/dist/components/ui/list-page-view-frame.js +24 -0
  247. package/dist/components/ui/list-page-view-frame.js.map +1 -0
  248. package/dist/components/ui/page-header.d.ts +51 -0
  249. package/dist/components/ui/page-header.js +372 -0
  250. package/dist/components/ui/page-header.js.map +1 -0
  251. package/dist/components/ui/payment-card-fields.d.ts +10 -0
  252. package/dist/components/ui/payment-card-fields.js +80 -0
  253. package/dist/components/ui/payment-card-fields.js.map +1 -0
  254. package/dist/components/ui/popover.d.ts +10 -0
  255. package/dist/components/ui/popover.js +47 -0
  256. package/dist/components/ui/popover.js.map +1 -0
  257. package/dist/components/ui/radio-group.d.ts +29 -0
  258. package/dist/components/ui/radio-group.js +190 -0
  259. package/dist/components/ui/radio-group.js.map +1 -0
  260. package/dist/components/ui/resizable.d.ts +16 -0
  261. package/dist/components/ui/resizable.js +51 -0
  262. package/dist/components/ui/resizable.js.map +1 -0
  263. package/dist/components/ui/scroll-area.d.ts +8 -0
  264. package/dist/components/ui/scroll-area.js +66 -0
  265. package/dist/components/ui/scroll-area.js.map +1 -0
  266. package/dist/components/ui/select.d.ts +18 -0
  267. package/dist/components/ui/select.js +186 -0
  268. package/dist/components/ui/select.js.map +1 -0
  269. package/dist/components/ui/selection-tile-grid.d.ts +52 -0
  270. package/dist/components/ui/selection-tile-grid.js +347 -0
  271. package/dist/components/ui/selection-tile-grid.js.map +1 -0
  272. package/dist/components/ui/separator.d.ts +7 -0
  273. package/dist/components/ui/separator.js +33 -0
  274. package/dist/components/ui/separator.js.map +1 -0
  275. package/dist/components/ui/sheet.d.ts +18 -0
  276. package/dist/components/ui/sheet.js +181 -0
  277. package/dist/components/ui/sheet.js.map +1 -0
  278. package/dist/components/ui/sidebar.d.ts +94 -0
  279. package/dist/components/ui/sidebar.js +805 -0
  280. package/dist/components/ui/sidebar.js.map +1 -0
  281. package/dist/components/ui/skeleton.d.ts +5 -0
  282. package/dist/components/ui/skeleton.js +22 -0
  283. package/dist/components/ui/skeleton.js.map +1 -0
  284. package/dist/components/ui/slider.d.ts +7 -0
  285. package/dist/components/ui/slider.js +66 -0
  286. package/dist/components/ui/slider.js.map +1 -0
  287. package/dist/components/ui/sonner.d.ts +6 -0
  288. package/dist/components/ui/sonner.js +38 -0
  289. package/dist/components/ui/sonner.js.map +1 -0
  290. package/dist/components/ui/status-badge.d.ts +38 -0
  291. package/dist/components/ui/status-badge.js +77 -0
  292. package/dist/components/ui/status-badge.js.map +1 -0
  293. package/dist/components/ui/table.d.ts +13 -0
  294. package/dist/components/ui/table.js +115 -0
  295. package/dist/components/ui/table.js.map +1 -0
  296. package/dist/components/ui/tabs.d.ts +15 -0
  297. package/dist/components/ui/tabs.js +93 -0
  298. package/dist/components/ui/tabs.js.map +1 -0
  299. package/dist/components/ui/textarea.d.ts +6 -0
  300. package/dist/components/ui/textarea.js +25 -0
  301. package/dist/components/ui/textarea.js.map +1 -0
  302. package/dist/components/ui/tip.d.ts +12 -0
  303. package/dist/components/ui/tip.js +61 -0
  304. package/dist/components/ui/tip.js.map +1 -0
  305. package/dist/components/ui/toggle-group.d.ts +14 -0
  306. package/dist/components/ui/toggle-group.js +104 -0
  307. package/dist/components/ui/toggle-group.js.map +1 -0
  308. package/dist/components/ui/toggle-switch.d.ts +10 -0
  309. package/dist/components/ui/toggle-switch.js +33 -0
  310. package/dist/components/ui/toggle-switch.js.map +1 -0
  311. package/dist/components/ui/toggle.d.ts +13 -0
  312. package/dist/components/ui/toggle.js +51 -0
  313. package/dist/components/ui/toggle.js.map +1 -0
  314. package/dist/components/ui/tooltip.d.ts +10 -0
  315. package/dist/components/ui/tooltip.js +68 -0
  316. package/dist/components/ui/tooltip.js.map +1 -0
  317. package/dist/components/ui/view-segmented-control.d.ts +31 -0
  318. package/dist/components/ui/view-segmented-control.js +167 -0
  319. package/dist/components/ui/view-segmented-control.js.map +1 -0
  320. package/dist/data-list-view-registry-CyBoBML4.d.ts +73 -0
  321. package/dist/hooks/use-app-theme.d.ts +24 -0
  322. package/dist/hooks/use-app-theme.js +286 -0
  323. package/dist/hooks/use-app-theme.js.map +1 -0
  324. package/dist/hooks/use-coach-mark.d.ts +86 -0
  325. package/dist/hooks/use-coach-mark.js +218 -0
  326. package/dist/hooks/use-coach-mark.js.map +1 -0
  327. package/dist/hooks/use-mobile.d.ts +3 -0
  328. package/dist/hooks/use-mobile.js +29 -0
  329. package/dist/hooks/use-mobile.js.map +1 -0
  330. package/dist/hooks/use-mod-key-label.d.ts +6 -0
  331. package/dist/hooks/use-mod-key-label.js +25 -0
  332. package/dist/hooks/use-mod-key-label.js.map +1 -0
  333. package/dist/index.d.ts +120 -0
  334. package/dist/index.js +13324 -0
  335. package/dist/index.js.map +1 -0
  336. package/dist/lib/compose-refs.d.ts +6 -0
  337. package/dist/lib/compose-refs.js +17 -0
  338. package/dist/lib/compose-refs.js.map +1 -0
  339. package/dist/lib/conditional-rule-match.d.ts +30 -0
  340. package/dist/lib/conditional-rule-match.js +66 -0
  341. package/dist/lib/conditional-rule-match.js.map +1 -0
  342. package/dist/lib/data-list-display-options.d.ts +26 -0
  343. package/dist/lib/data-list-display-options.js +14 -0
  344. package/dist/lib/data-list-display-options.js.map +1 -0
  345. package/dist/lib/data-list-view-registry.d.ts +2 -0
  346. package/dist/lib/data-list-view-registry.js +102 -0
  347. package/dist/lib/data-list-view-registry.js.map +1 -0
  348. package/dist/lib/data-list-view-surface.d.ts +2 -0
  349. package/dist/lib/data-list-view-surface.js +80 -0
  350. package/dist/lib/data-list-view-surface.js.map +1 -0
  351. package/dist/lib/data-list-view.d.ts +21 -0
  352. package/dist/lib/data-list-view.js +25 -0
  353. package/dist/lib/data-list-view.js.map +1 -0
  354. package/dist/lib/date-filter.d.ts +22 -0
  355. package/dist/lib/date-filter.js +61 -0
  356. package/dist/lib/date-filter.js.map +1 -0
  357. package/dist/lib/dev-log.d.ts +8 -0
  358. package/dist/lib/dev-log.js +10 -0
  359. package/dist/lib/dev-log.js.map +1 -0
  360. package/dist/lib/dropdown-menu-surface.d.ts +14 -0
  361. package/dist/lib/dropdown-menu-surface.js +6 -0
  362. package/dist/lib/dropdown-menu-surface.js.map +1 -0
  363. package/dist/lib/editable-target.d.ts +12 -0
  364. package/dist/lib/editable-target.js +12 -0
  365. package/dist/lib/editable-target.js.map +1 -0
  366. package/dist/lib/list-page-table-properties.d.ts +35 -0
  367. package/dist/lib/list-page-table-properties.js +81 -0
  368. package/dist/lib/list-page-table-properties.js.map +1 -0
  369. package/dist/lib/raf-throttle.d.ts +23 -0
  370. package/dist/lib/raf-throttle.js +27 -0
  371. package/dist/lib/raf-throttle.js.map +1 -0
  372. package/dist/lib/row-height.d.ts +16 -0
  373. package/dist/lib/row-height.js +10 -0
  374. package/dist/lib/row-height.js.map +1 -0
  375. package/dist/lib/table-properties-types.d.ts +83 -0
  376. package/dist/lib/table-properties-types.js +19 -0
  377. package/dist/lib/table-properties-types.js.map +1 -0
  378. package/dist/lib/utils.d.ts +5 -0
  379. package/dist/lib/utils.js +11 -0
  380. package/dist/lib/utils.js.map +1 -0
  381. package/package.json +83 -19
  382. package/src/components/data-table/filter-date-calendar.tsx +38 -0
  383. package/src/components/data-table/filter-text-value-input.tsx +77 -0
  384. package/src/components/data-table/index.tsx +1678 -0
  385. package/src/components/data-table/pagination.tsx +255 -0
  386. package/src/components/data-table/types.ts +96 -0
  387. package/src/components/data-table/use-table-state.ts +767 -0
  388. package/src/components/data-views/board-card-primitives.tsx +93 -0
  389. package/src/components/data-views/data-row-list.tsx +183 -0
  390. package/src/components/data-views/finder-panel-view.tsx +405 -0
  391. package/src/components/data-views/folder-grid-view.tsx +86 -0
  392. package/src/components/data-views/hub-table.tsx +498 -0
  393. package/src/components/data-views/index.ts +28 -0
  394. package/src/components/data-views/list-page-board-card.tsx +192 -0
  395. package/src/components/data-views/list-page-board-template.tsx +122 -0
  396. package/src/components/data-views/list-page-connected-view-body.tsx +66 -0
  397. package/src/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  398. package/src/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  399. package/src/components/data-views/list-page-split-hub-tokens.ts +16 -0
  400. package/src/components/data-views/list-page-tree-column-header.tsx +31 -0
  401. package/src/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  402. package/src/components/data-views/os-folder-glyph.tsx +141 -0
  403. package/src/components/data-views/outline-tree-menu.tsx +157 -0
  404. package/src/components/table-properties/column-row.tsx +90 -0
  405. package/src/components/table-properties/draggable-list.ts +54 -0
  406. package/src/components/table-properties/drawer-button.tsx +300 -0
  407. package/src/components/table-properties/drawer.tsx +1148 -0
  408. package/src/components/table-properties/filter-card.tsx +251 -0
  409. package/src/components/table-properties/index.ts +36 -0
  410. package/src/components/table-properties/sort-card.tsx +63 -0
  411. package/src/components/templates/dedicated-search-landing-template.tsx +124 -0
  412. package/src/components/templates/dedicated-search-results-template.tsx +19 -0
  413. package/src/components/templates/index.ts +33 -0
  414. package/src/components/templates/list-page.tsx +602 -0
  415. package/src/components/templates/nested-secondary-panel-shell.tsx +70 -0
  416. package/src/components/ui/accordion.tsx +92 -0
  417. package/src/components/ui/alert-dialog.tsx +221 -0
  418. package/src/components/ui/avatar.tsx +13 -2
  419. package/src/components/ui/banner.tsx +2 -2
  420. package/src/components/ui/button.tsx +4 -4
  421. package/src/components/ui/calendar.tsx +1 -1
  422. package/src/components/ui/coach-mark.tsx +1 -1
  423. package/src/components/ui/context-menu.tsx +291 -0
  424. package/src/components/ui/date-picker-field.tsx +2 -2
  425. package/src/components/ui/dot-pattern.tsx +183 -0
  426. package/src/components/ui/export-drawer.tsx +375 -0
  427. package/src/components/ui/hover-card.tsx +66 -0
  428. package/src/components/ui/key-metrics-context.tsx +78 -0
  429. package/src/components/ui/key-metrics.tsx +1133 -0
  430. package/src/components/ui/list-page-view-frame.tsx +64 -0
  431. package/src/components/ui/page-header.tsx +244 -0
  432. package/src/components/ui/payment-card-fields.tsx +2 -2
  433. package/src/components/ui/resizable.tsx +68 -0
  434. package/src/components/ui/scroll-area.tsx +72 -0
  435. package/src/components/ui/selection-tile-grid.tsx +9 -2
  436. package/src/components/ui/sidebar.tsx +84 -12
  437. package/src/components/ui/slider.tsx +83 -0
  438. package/src/globals.css +2201 -7
  439. package/src/globals.d.ts +20 -0
  440. package/src/index.ts +68 -1
  441. package/src/lib/conditional-rule-match.ts +119 -0
  442. package/src/lib/data-list-display-options.ts +35 -0
  443. package/src/lib/data-list-view-registry.ts +104 -0
  444. package/src/lib/data-list-view-surface.ts +83 -0
  445. package/src/lib/data-list-view.ts +47 -0
  446. package/src/lib/dev-log.ts +10 -0
  447. package/src/lib/editable-target.ts +20 -0
  448. package/src/lib/list-page-table-properties.ts +48 -0
  449. package/src/lib/raf-throttle.ts +45 -0
  450. package/src/lib/row-height.ts +19 -0
  451. package/src/lib/table-properties-types.ts +98 -0
  452. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  453. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +3 -3
  454. package/template/.cursor/rules/exxat-data-tables.mdc +1 -1
  455. package/template/.cursor/rules/exxat-ds-agents.mdc +2 -2
  456. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +2 -2
  457. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  458. package/template/AGENTS.md +104 -78
  459. package/template/app/(app)/dashboard/loading.tsx +15 -3
  460. package/template/app/(app)/dashboard/page.tsx +14 -2
  461. package/template/app/(app)/examples/page.tsx +0 -2
  462. package/template/app/(app)/layout.tsx +17 -4
  463. package/template/app/(app)/loading.tsx +18 -1
  464. package/template/app/(app)/question-bank/find/page.tsx +1 -2
  465. package/template/app/(app)/question-bank/layout.tsx +1 -1
  466. package/template/app/(app)/question-bank/library/page.tsx +1 -2
  467. package/template/app/(app)/question-bank/list/page.tsx +1 -2
  468. package/template/app/(app)/question-bank/new/page.tsx +15 -20
  469. package/template/app/(app)/question-bank/page.tsx +1 -2
  470. package/template/app/(app)/settings/page.tsx +5 -4
  471. package/template/app/globals.css +14 -16
  472. package/template/components/ask-leo-sidebar.tsx +5 -1
  473. package/template/components/brand-color-picker.tsx +2 -2
  474. package/template/components/charts-overview.tsx +1 -1
  475. package/template/components/compliance-board-view.tsx +142 -0
  476. package/template/components/compliance-client.tsx +92 -0
  477. package/template/components/compliance-page-header.tsx +89 -0
  478. package/template/components/compliance-table.tsx +468 -0
  479. package/template/components/dashboard-report-charts.tsx +1 -1
  480. package/template/components/dashboard-tabs.tsx +1 -1
  481. package/template/components/data-table/filter-date-calendar.tsx +1 -38
  482. package/template/components/data-table/filter-text-value-input.tsx +1 -77
  483. package/template/components/data-table/index.tsx +1 -1634
  484. package/template/components/data-table/pagination.tsx +1 -255
  485. package/template/components/data-table/types.ts +1 -94
  486. package/template/components/data-table/use-table-state.test.ts +420 -0
  487. package/template/components/data-table/use-table-state.ts +1 -758
  488. package/template/components/data-view-dashboard-charts-compliance.tsx +963 -0
  489. package/template/components/data-view-dashboard-charts-team.tsx +971 -0
  490. package/template/components/data-view-dashboard-charts.tsx +1503 -0
  491. package/template/components/data-views/board-card-primitives.tsx +1 -93
  492. package/template/components/data-views/data-row-list.tsx +1 -183
  493. package/template/components/data-views/finder-panel-view.tsx +1 -405
  494. package/template/components/data-views/folder-grid-view.tsx +1 -86
  495. package/template/components/data-views/hub-table.tsx +1 -0
  496. package/template/components/data-views/index.ts +50 -37
  497. package/template/components/data-views/list-page-board-card.tsx +1 -192
  498. package/template/components/data-views/list-page-board-template.tsx +1 -122
  499. package/template/components/data-views/list-page-connected-view-body.tsx +1 -66
  500. package/template/components/data-views/list-page-split-details-placeholder.tsx +1 -39
  501. package/template/components/data-views/list-page-split-hub-chrome.tsx +1 -68
  502. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -16
  503. package/template/components/data-views/list-page-tree-column-header.tsx +1 -31
  504. package/template/components/data-views/list-page-tree-panel-shell.tsx +1 -91
  505. package/template/components/data-views/list-page-view-frame.tsx +5 -53
  506. package/template/components/data-views/os-folder-glyph.tsx +1 -129
  507. package/template/components/data-views/outline-tree-menu.tsx +1 -157
  508. package/template/components/export-drawer.test.tsx +71 -0
  509. package/template/components/export-drawer.tsx +1 -375
  510. package/template/components/exxat-product-logo.tsx +5 -5
  511. package/template/components/hub-tree-panel-view.tsx +2 -2
  512. package/template/components/invite-collaborators-drawer.tsx +3 -3
  513. package/template/components/key-metrics-ask-leo-bridge.tsx +40 -0
  514. package/template/components/key-metrics.tsx +1 -1063
  515. package/template/components/leo-insight-indicator.tsx +2 -2
  516. package/template/components/new-placement-back-btn.tsx +28 -0
  517. package/template/components/new-placement-form.tsx +942 -0
  518. package/template/components/new-question-composer.tsx +456 -408
  519. package/template/components/onboarding/index.ts +9 -0
  520. package/template/components/onboarding/onboarding-01.tsx +1 -1
  521. package/template/components/onboarding/onboarding-02.tsx +1 -1
  522. package/template/components/onboarding/onboarding-03.tsx +1 -1
  523. package/template/components/onboarding/onboarding-04.tsx +1 -1
  524. package/template/components/page-header.tsx +8 -226
  525. package/template/components/placement-board-card.tsx +250 -0
  526. package/template/components/placement-detail.tsx +438 -0
  527. package/template/components/placements-board-view.tsx +397 -0
  528. package/template/components/placements-client.tsx +220 -0
  529. package/template/components/placements-list-view.tsx +124 -0
  530. package/template/components/placements-page-header.tsx +166 -0
  531. package/template/components/placements-table-cells.test.tsx +22 -0
  532. package/template/components/placements-table-cells.tsx +173 -0
  533. package/template/components/placements-table-columns.tsx +210 -0
  534. package/template/components/placements-table.tsx +934 -0
  535. package/template/components/product-switcher.tsx +3 -4
  536. package/template/components/product-wordmark.tsx +2 -1
  537. package/template/components/question-bank-client.tsx +5 -5
  538. package/template/components/question-bank-hub-client.tsx +1 -1
  539. package/template/components/question-bank-new-folder-sheet.tsx +2 -2
  540. package/template/components/question-bank-secondary-nav.tsx +3 -3
  541. package/template/components/question-bank-table.tsx +541 -431
  542. package/template/components/rotations-empty-state.tsx +50 -0
  543. package/template/components/rotations-panel-activator.tsx +8 -0
  544. package/template/components/settings-appearance-card.tsx +3 -4
  545. package/template/components/settings-client.tsx +15 -59
  546. package/template/components/settings-form-row.tsx +4 -9
  547. package/template/components/{app-sidebar-dynamic.tsx → sidebar/app-sidebar-dynamic.tsx} +1 -1
  548. package/template/components/{app-sidebar.tsx → sidebar/app-sidebar.tsx} +59 -74
  549. package/template/components/sidebar/index.ts +16 -0
  550. package/template/components/{secondary-nav.tsx → sidebar/secondary-nav.tsx} +2 -2
  551. package/template/components/{secondary-panel.tsx → sidebar/secondary-panel.tsx} +50 -7
  552. package/template/components/{sidebar-auto-collapse.tsx → sidebar/sidebar-auto-collapse.tsx} +6 -2
  553. package/template/components/{sidebar-shell.tsx → sidebar/sidebar-shell.tsx} +1 -1
  554. package/template/components/site-header.tsx +1 -1
  555. package/template/components/sites-board-view.tsx +67 -0
  556. package/template/components/sites-client.tsx +154 -0
  557. package/template/components/sites-table.tsx +249 -0
  558. package/template/components/table-properties/column-row.tsx +1 -90
  559. package/template/components/table-properties/draggable-list.ts +1 -49
  560. package/template/components/table-properties/drawer-button.tsx +1 -262
  561. package/template/components/table-properties/drawer.tsx +1 -1166
  562. package/template/components/table-properties/filter-card.tsx +1 -251
  563. package/template/components/table-properties/sort-card.tsx +1 -59
  564. package/template/components/table-properties/types.ts +28 -71
  565. package/template/components/team-board-view.tsx +122 -0
  566. package/template/components/team-client.tsx +100 -0
  567. package/template/components/team-page-header.tsx +92 -0
  568. package/template/components/team-table.tsx +553 -0
  569. package/template/components/templates/dedicated-search-landing-template.tsx +1 -124
  570. package/template/components/templates/dedicated-search-results-template.tsx +1 -19
  571. package/template/components/templates/list-page.tsx +1 -608
  572. package/template/components/templates/nested-secondary-panel-shell.tsx +1 -63
  573. package/template/components/templates/new-focus-template.tsx +659 -0
  574. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  575. package/template/components/ui/accordion.tsx +1 -0
  576. package/template/components/ui/alert-dialog.tsx +1 -0
  577. package/template/components/ui/context-menu.tsx +1 -0
  578. package/template/components/ui/dot-pattern.tsx +1 -183
  579. package/template/components/ui/hover-card.tsx +1 -0
  580. package/template/components/ui/resizable.tsx +1 -68
  581. package/template/components/ui/scroll-area.tsx +1 -0
  582. package/template/components/ui/slider.tsx +1 -0
  583. package/template/docs/blueprints/README.md +86 -0
  584. package/template/docs/blueprints/_template.md +91 -0
  585. package/template/docs/blueprints/board-card.md +123 -0
  586. package/template/docs/blueprints/data-table.md +139 -0
  587. package/template/docs/blueprints/key-metrics.md +128 -0
  588. package/template/docs/blueprints/list-page-template.md +123 -0
  589. package/template/docs/blueprints/page-header.md +130 -0
  590. package/template/docs/command-menu-pattern.md +1 -1
  591. package/template/docs/component-selection-guide.md +224 -0
  592. package/template/docs/components-audit-2026-05.md +158 -0
  593. package/template/docs/data-views-pattern.md +17 -54
  594. package/template/docs/drawer-vs-dialog-pattern.md +1 -3
  595. package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +95 -0
  596. package/template/docs/migrations/0002-exxat-token-namespace.md +154 -0
  597. package/template/docs/migrations/0003-globals-css-canonical.md +110 -0
  598. package/template/docs/migrations/README.md +100 -0
  599. package/template/docs/migrations/_template.md +64 -0
  600. package/template/docs/shell-surface-elevation-pattern.md +3 -5
  601. package/template/docs/token-taxonomy.md +416 -0
  602. package/template/eslint.config.mjs +27 -0
  603. package/template/hooks/use-secondary-panel-hub-nav.ts +1 -1
  604. package/template/lib/command-menu-config.ts +0 -1
  605. package/template/lib/command-menu-search-data.ts +27 -11
  606. package/template/lib/compliance-supported-views.ts +10 -0
  607. package/template/lib/conditional-rule-match.ts +6 -97
  608. package/template/lib/data-list-display-options.ts +1 -49
  609. package/template/lib/data-list-view-registry.ts +1 -104
  610. package/template/lib/data-list-view-surface.ts +1 -83
  611. package/template/lib/data-list-view.ts +1 -47
  612. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  613. package/template/lib/data-view-dashboard-storage.ts +35 -38
  614. package/template/lib/dev-log.ts +1 -8
  615. package/template/lib/editable-target.ts +1 -10
  616. package/template/lib/list-page-table-properties.ts +1 -48
  617. package/template/lib/list-status-badges.ts +97 -4
  618. package/template/lib/mock/compliance-kpi.ts +61 -0
  619. package/template/lib/mock/compliance.ts +146 -0
  620. package/template/lib/mock/navigation.tsx +0 -9
  621. package/template/lib/mock/placements-kpi.ts +134 -0
  622. package/template/lib/mock/placements.ts +176 -0
  623. package/template/lib/mock/sites-directory.ts +16 -0
  624. package/template/lib/mock/sites-kpi.ts +25 -0
  625. package/template/lib/mock/team-kpi.ts +60 -0
  626. package/template/lib/mock/team.ts +118 -0
  627. package/template/lib/placement-board-card-layout.ts +79 -0
  628. package/template/lib/placements-supported-views.ts +12 -0
  629. package/template/lib/question-bank-supported-views.ts +0 -1
  630. package/template/lib/raf-throttle.ts +1 -45
  631. package/template/lib/row-height.ts +4 -10
  632. package/template/lib/sidebar-state-cookie.ts +11 -2
  633. package/template/lib/sites-supported-views.ts +10 -0
  634. package/template/lib/table-state-lifecycle.ts +2 -2
  635. package/template/lib/team-supported-views.ts +10 -0
  636. package/template/package.json +1 -0
  637. package/template/tests/setup.ts +25 -0
  638. package/consumer-extras/AGENTS.md +0 -76
  639. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +0 -37
  640. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +0 -57
  641. package/consumer-extras/patterns/consumer-app-pattern.md +0 -39
  642. package/consumer-extras/patterns/focused-workflow-page-pattern.md +0 -84
  643. package/src/components/ui/button-group.tsx +0 -81
  644. package/src/theme.css +0 -16
  645. package/src/tokens/README.md +0 -15
  646. package/src/tokens/base.css +0 -337
  647. package/src/tokens/high-contrast.css +0 -1195
  648. package/src/tokens/layers.css +0 -224
  649. package/src/tokens/tailwind-bridge.css +0 -118
  650. package/src/tokens/themes.css +0 -201
  651. package/template/app/(app)/data-list/layout.tsx +0 -43
  652. package/template/app/(app)/data-list/page.tsx +0 -10
  653. package/template/app/(app)/examples/focused-workflow/page.tsx +0 -5
  654. package/template/components/app-route-loading.tsx +0 -14
  655. package/template/components/dashboard-onboarding-gallery.tsx +0 -13
  656. package/template/components/dashboard-onboarding.tsx +0 -21
  657. package/template/components/data-views/list-page-calendar-view.tsx +0 -593
  658. package/template/components/data-views/list-page-folder-columns-panel.tsx +0 -345
  659. package/template/components/examples/focused-workflow-showcase.tsx +0 -183
  660. package/template/components/list-hub-board-view.tsx +0 -68
  661. package/template/components/list-hub-client.tsx +0 -186
  662. package/template/components/list-hub-list-view.tsx +0 -36
  663. package/template/components/list-hub-panel-activator.tsx +0 -8
  664. package/template/components/list-hub-secondary-nav.tsx +0 -121
  665. package/template/components/list-hub-table.tsx +0 -336
  666. package/template/components/question-bank-folder-columns-panel.tsx +0 -104
  667. package/template/components/question-bank-list-view.tsx +0 -53
  668. package/template/components/secondary-panel/nav-link-rows.tsx +0 -83
  669. package/template/components/secondary-panels/list-hub-panel.tsx +0 -39
  670. package/template/components/secondary-panels/question-bank-panel.tsx +0 -39
  671. package/template/components/secondary-panels/registry.tsx +0 -15
  672. package/template/components/section-cards.tsx +0 -106
  673. package/template/components/templates/focused-workflow-layouts.tsx +0 -448
  674. package/template/components/templates/focused-workflow-page-template.tsx +0 -69
  675. package/template/components/templates/page-loading-shell.tsx +0 -262
  676. package/template/components/ui/button-group.tsx +0 -1
  677. package/template/docs/consumer-app-pattern.md +0 -39
  678. package/template/docs/focused-workflow-page-pattern.md +0 -84
  679. package/template/lib/list-hub-nav.ts +0 -121
  680. package/template/lib/mock/list-hub-directory.ts +0 -27
  681. package/template/lib/mock/list-hub-kpi.ts +0 -27
  682. package/template/lib/page-loading-variant.ts +0 -40
  683. /package/template/components/{getting-started.tsx → onboarding/getting-started.tsx} +0 -0
  684. /package/template/components/{nav-documents.tsx → sidebar/nav-documents.tsx} +0 -0
  685. /package/template/components/{nav-main.tsx → sidebar/nav-main.tsx} +0 -0
  686. /package/template/components/{nav-secondary.tsx → sidebar/nav-secondary.tsx} +0 -0
  687. /package/template/components/{nav-user.tsx → sidebar/nav-user.tsx} +0 -0
  688. /package/template/components/{sidebar-auto-open.tsx → sidebar/sidebar-auto-open.tsx} +0 -0
@@ -1,1063 +1 @@
1
- "use client"
2
-
3
- /**
4
- * KeyMetrics — WCAG 2.1 AA reusable KPI panel
5
- *
6
- * Variants:
7
- * "card" (default) — shadcn Card wrapper with brand gradient fill
8
- * "flat" — full-width soft tint band (brand-tint → background) + bottom glow, no card chrome
9
- *
10
- * AA checklist:
11
- * ✓ Trend text never relies on colour alone — icon + label (WCAG 1.4.1)
12
- * ✓ `trend` matches signed change; `trendPolarity` flips sentiment when “up” is bad (see `docs/kpi-trend-pattern.md`)
13
- * ✓ Trend icons have aria-hidden; chip `aria-label` uses `metricTrendAriaQualifier` (1.1.1)
14
- * ✓ Select has accessible label via aria-label (4.1.2)
15
- * ✓ Insight action button has descriptive text (4.1.2)
16
- * ✓ Decorative dividers are aria-hidden (1.1.1)
17
- * ✓ Contrast: value text foreground ≥ 17:1, trend colours ≥ 4.5:1 (1.4.3)
18
- */
19
-
20
- import * as React from "react"
21
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
22
- import {
23
- Select,
24
- SelectContent,
25
- SelectItem,
26
- SelectTrigger,
27
- SelectValue,
28
- } from "@/components/ui/select"
29
- import { Separator } from "@/components/ui/separator"
30
- import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-sidebar"
31
- import { Button } from "@/components/ui/button"
32
- import {
33
- Tooltip,
34
- TooltipContent,
35
- TooltipTrigger,
36
- } from "@/components/ui/tooltip"
37
- import { cn } from "@/lib/utils"
38
-
39
- /** Tooltip + optional ⌘⌥K when the insight CTA is the default Ask Leo action. */
40
- function InsightAskLeoTooltip({
41
- actionLabel,
42
- children,
43
- }: {
44
- actionLabel?: string
45
- children: React.ReactNode
46
- }) {
47
- const label = actionLabel ?? "Ask Leo"
48
- const showShortcut = !actionLabel || actionLabel === "Ask Leo"
49
- if (!showShortcut) {
50
- return (
51
- <Tooltip>
52
- <TooltipTrigger asChild>{children}</TooltipTrigger>
53
- <TooltipContent side="top">{label}</TooltipContent>
54
- </Tooltip>
55
- )
56
- }
57
- return (
58
- <Tooltip>
59
- <TooltipTrigger asChild>{children}</TooltipTrigger>
60
- <TooltipContent side="top" className="flex flex-wrap items-center gap-1.5">
61
- <span>{label}</span>
62
- <AskLeoShortcutKbds />
63
- </TooltipContent>
64
- </Tooltip>
65
- )
66
- }
67
-
68
- /* ── Types ────────────────────────────────────────────────────────────────── */
69
-
70
- /**
71
- * Whether an **up** arrow should read as “good news” for tinting and assistive text.
72
- * - **`higher_is_better`** (default) — revenue, pass rate, approved count: up = favorable.
73
- * - **`lower_is_better`** — defects, overdue, **low PBI / quality flags**: more flags + up arrow = unfavorable.
74
- * - **`informational`** — volume or mix only; keep arrows **muted** (direction without value judgment).
75
- */
76
- export type MetricTrendPolarity = "higher_is_better" | "lower_is_better" | "informational"
77
-
78
- export type MetricTrendTone = "positive" | "negative" | "muted"
79
-
80
- /** Maps `trend` + polarity to semantic tone for colours (arrow direction still follows `trend`). */
81
- export function metricTrendTone(
82
- trend: "up" | "down" | "neutral",
83
- polarity: MetricTrendPolarity = "higher_is_better",
84
- ): MetricTrendTone {
85
- if (trend === "neutral") return "muted"
86
- if (polarity === "informational") return "muted"
87
- if (polarity === "higher_is_better") {
88
- return trend === "up" ? "positive" : "negative"
89
- }
90
- return trend === "up" ? "negative" : "positive"
91
- }
92
-
93
- /** Short clause for `aria-label` on the trend chip (paired with the delta string). */
94
- export function metricTrendAriaQualifier(
95
- trend: "up" | "down" | "neutral",
96
- polarity: MetricTrendPolarity = "higher_is_better",
97
- ): string {
98
- if (trend === "neutral") return "no net change"
99
- if (polarity === "informational") {
100
- return trend === "up" ? "increased" : "decreased"
101
- }
102
- if (polarity === "higher_is_better") {
103
- return trend === "up" ? "increased, favorable" : "decreased, unfavorable"
104
- }
105
- return trend === "up" ? "increased, unfavorable" : "decreased, favorable"
106
- }
107
-
108
- export interface MetricItem {
109
- /** Unique identifier for React keying */
110
- id: string
111
- /** Short label shown above the value */
112
- label: string
113
- /** Displayed value — e.g. "23", "98%", "1,250" */
114
- value: string | number
115
- /** Change delta — e.g. "+5", "-3", "+12" */
116
- delta: string | number
117
- /** Visual trend direction (arrow follows the signed change in the underlying metric). */
118
- trend: "up" | "down" | "neutral"
119
- /**
120
- * How to **tint** the trend chip. Omit = **`higher_is_better`** (legacy behaviour).
121
- * Arrows always match `trend`; sentiment colours flip for **`lower_is_better`**.
122
- */
123
- trendPolarity?: MetricTrendPolarity
124
- /** Makes the cell a link */
125
- href?: string
126
- /** Makes the cell a button */
127
- onClick?: () => void
128
- /**
129
- * "hero" — primary KPI (e.g. total count): larger value, same structure as siblings.
130
- * "default" — standard KPI strip cell.
131
- */
132
- metricVariant?: "default" | "hero"
133
- }
134
-
135
- export interface MetricInsight {
136
- /** Optional single line for custom copy; rail prefers `title` + `description` when both are set */
137
- statement?: string
138
- /** Card headline */
139
- title: string
140
- /** Supporting body copy */
141
- description?: string
142
- /** Optional deep-link for the ↗ button */
143
- href?: string
144
- /** CTA label — defaults to "Ask Leo" */
145
- actionLabel?: string
146
- /** Font Awesome class for the CTA icon — defaults to fa-wand-magic-sparkles */
147
- actionIcon?: string
148
- /** Callback for the CTA button */
149
- onAction?: () => void
150
- /** Severity determines the badge colour (default: warning) */
151
- severity?: "warning" | "info" | "error"
152
- }
153
-
154
- export interface PeriodOption {
155
- value: string
156
- label: string
157
- }
158
-
159
- export interface KeyMetricsProps {
160
- /**
161
- * "card" — shadcn Card with brand gradient (default)
162
- * "flat" — full-width gradient band, no card chrome
163
- */
164
- variant?: "card" | "flat" | "compact"
165
- /** Panel title */
166
- title?: string
167
- /** Subtitle / description below title */
168
- description?: string
169
- /** Array of KPI items — by default split into rows of 3 */
170
- metrics: MetricItem[]
171
- /** When true, all metrics share one horizontal row (md+ and compact mobile grid) */
172
- metricsSingleRow?: boolean
173
- /**
174
- * When true with `metricsSingleRow`, use a 2-column KPI grid so half-width dashboard cards
175
- * fit 1–4 KPIs without horizontal overflow (pair rows on md+; 2-col grid on small screens).
176
- * The insight rail (if any) stacks below the KPI grid instead of sitting beside it on md+.
177
- */
178
- metricsHalfWidthLayout?: boolean
179
- /** Optional insight card — see `insightFullWidth` */
180
- insight?: MetricInsight
181
- /**
182
- * When true, the insight sits on its own full-width row under the metrics (not a narrow side rail).
183
- */
184
- insightFullWidth?: boolean
185
- /** Comparison-period options for the Select */
186
- periods?: PeriodOption[]
187
- /** Initially-selected period value */
188
- defaultPeriod?: string
189
- /** Called with the new period value when the Select changes */
190
- onPeriodChange?: (period: string) => void
191
- /** When false, hides the title/description/period-selector header row (default: true) */
192
- showHeader?: boolean
193
- /**
194
- * Tighter insight card: one short title + line of body, no vertical filler;
195
- * aligns visually with a single-row KPI band.
196
- */
197
- insightCompact?: boolean
198
- className?: string
199
- }
200
-
201
- /**
202
- * KPI grid column step patterns — Tailwind v4 container-query classes.
203
- *
204
- * We deliberately AVOID `repeat(auto-fit, minmax(...))` here because it
205
- * produces awkward "N + leftover" layouts at intermediate widths (e.g. 3
206
- * tiles in row 1 + 1 lonely tile in row 2 for a 4-KPI strip). Instead we
207
- * step the column count through values that evenly divide the row size:
208
- * 1 → 2 → 4 for a 4-KPI strip (3 is skipped on purpose).
209
- *
210
- * The breakpoints are container-query based (`@[Xrem]:…`) so they react to
211
- * the metrics strip's OWN width, not the viewport — that's what makes the
212
- * 2×2 fallback kick in when the primary sidebar + secondary panel are
213
- * both open and the strip column is ~360 px wide, even on a 1280 px display.
214
- *
215
- * `metricsHalfWidthLayout` = strip shares its row with the insight rail
216
- * (3fr / 2fr split). Tighter breakpoints because available width is ~60%
217
- * of the section.
218
- */
219
- /**
220
- * Flat KPI hairlines — cell borders only (no grid gap fill / no surface).
221
- * Four tiles: default 4-across verticals; 2×2 hairlines only when @container is narrow.
222
- */
223
- function flatMetricsHairlineClass(
224
- itemCount: number,
225
- metricsHalfWidthLayout: boolean,
226
- ): string {
227
- if (itemCount <= 1) return "gap-0"
228
-
229
- const childBorder = "[&>*]:border-[color:var(--key-metrics-flat-divider)]"
230
-
231
- if (itemCount === 2) {
232
- return cn("gap-0", childBorder, "[&>*:first-child]:border-r")
233
- }
234
-
235
- if (itemCount === 4) {
236
- const narrow2x2 = metricsHalfWidthLayout
237
- ? "@[max-width:25.99rem]"
238
- : "@[max-width:29.99rem]"
239
- return cn(
240
- "gap-0",
241
- childBorder,
242
- /* Wide strip (matches `@[30rem]:grid-cols-4`) — verticals between all tiles, no horizontal */
243
- "[&>*:not(:last-child)]:border-r",
244
- /* Narrow strip (`@[18rem]`–`@[30rem]` 2×2) */
245
- `${narrow2x2}:[&>*:not(:last-child)]:border-r-0`,
246
- `${narrow2x2}:[&>*:nth-child(odd)]:border-r`,
247
- `${narrow2x2}:[&>*:not(:nth-last-child(-n+2))]:border-b`,
248
- )
249
- }
250
-
251
- return cn("gap-0", childBorder, "[&>*:not(:last-child)]:border-r")
252
- }
253
-
254
- function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boolean): string {
255
- const half = metricsHalfWidthLayout
256
- switch (rowLength) {
257
- case 1:
258
- return "grid-cols-1"
259
- case 2:
260
- return half
261
- ? "grid-cols-1 @[14rem]:grid-cols-2"
262
- : "grid-cols-1 @[18rem]:grid-cols-2"
263
- case 3:
264
- // 3 tiles divide evenly already — step 1 → 3.
265
- return half
266
- ? "grid-cols-1 @[18rem]:grid-cols-3"
267
- : "grid-cols-1 @[24rem]:grid-cols-3"
268
- case 4:
269
- // Step 1 → 2 (2×2 grid) → 4. Skip 3 — that's the awkward 3+1 layout.
270
- // Aggressive 4-col thresholds so the strip fits all four tiles even
271
- // when the primary sidebar + secondary panel + insight rail are all
272
- // expanded (typical question-bank layout puts the KPI grid at ~27rem).
273
- return half
274
- ? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-4"
275
- : "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-4"
276
- default:
277
- // 5+ KPIs (`exxat-kpi-max-four` caps the strip at 4, but key-metrics
278
- // is a generic primitive — fall back to a sensible step). 1 → 2 → 3 → 6.
279
- return half
280
- ? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-3 @[40rem]:grid-cols-6"
281
- : "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-3 @[56rem]:grid-cols-6"
282
- }
283
- }
284
-
285
- /* ── Default data ─────────────────────────────────────────────────────────── */
286
-
287
- const DEFAULT_PERIODS: PeriodOption[] = [
288
- { value: "week", label: "vs last week" },
289
- { value: "month", label: "vs last month" },
290
- { value: "quarter", label: "vs last quarter" },
291
- { value: "year", label: "vs last year" },
292
- ]
293
-
294
- /* ── Sub-components ───────────────────────────────────────────────────────── */
295
-
296
- /** Single KPI cell inside the metrics grid */
297
- const MetricCell = React.memo(function MetricCell({
298
- label,
299
- value,
300
- delta,
301
- trend,
302
- trendPolarity = "higher_is_better",
303
- href,
304
- onClick,
305
- metricVariant = "default",
306
- dense = false,
307
- edgeGutter = true,
308
- }: Omit<MetricItem, "id"> & { dense?: boolean; edgeGutter?: boolean }) {
309
- const isUp = trend === "up"
310
- const isDown = trend === "down"
311
- const tone = metricTrendTone(trend, trendPolarity)
312
- const isInteractive = !!(href || onClick)
313
- const isHero = metricVariant === "hero"
314
-
315
- const inner = (
316
- <>
317
- {/* Label row — min-height = 2 lines so values align when some titles wrap */}
318
- <div
319
- className={cn(
320
- "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 gap-y-0.5",
321
- dense ? "min-h-[2.125rem]" : "min-h-[2.625rem]",
322
- )}
323
- >
324
- <p
325
- className={cn(
326
- "min-w-0 text-muted-foreground leading-snug wrap-break-word",
327
- dense ? "text-xs" : "text-sm",
328
- isHero && "font-medium",
329
- )}
330
- >
331
- {label}
332
- </p>
333
- {isInteractive ? (
334
- <span className="mt-0.5 inline-flex shrink-0" aria-hidden="true">
335
- <i className="fa-light fa-arrow-right text-xs text-foreground/70 transition-colors duration-150 group-hover:text-interactive-hover-foreground sm:group-hover:translate-x-0.5" />
336
- </span>
337
- ) : null}
338
- </div>
339
-
340
- {/* Value + trend badge */}
341
- <div className="flex items-baseline gap-2 flex-wrap">
342
- <span
343
- className={cn(
344
- "font-bold tabular-nums leading-none text-foreground",
345
- dense
346
- ? isHero
347
- ? "text-lg sm:text-xl"
348
- : "text-base sm:text-lg"
349
- : isHero
350
- ? "text-2xl sm:text-[1.625rem]"
351
- : "text-xl sm:text-2xl",
352
- )}
353
- >
354
- {value}
355
- </span>
356
-
357
- {/* Trend chip — icon + text, never colour-only (WCAG 1.4.1) */}
358
- <span
359
- className={cn(
360
- "inline-flex items-center gap-1 font-medium leading-none",
361
- dense ? "text-xs sm:text-xs" : "text-xs sm:text-sm",
362
- tone === "positive" && "text-chart-2",
363
- tone === "negative" && "text-destructive",
364
- tone === "muted" && "text-muted-foreground",
365
- )}
366
- aria-label={`${metricTrendAriaQualifier(trend, trendPolarity)} ${delta}`}
367
- >
368
- {isUp && <i className="fa-light fa-arrow-trend-up text-[0.8rem]" aria-hidden="true" />}
369
- {isDown && <i className="fa-light fa-arrow-trend-down text-[0.8rem]" aria-hidden="true" />}
370
- {!isUp && !isDown && <i className="fa-light fa-minus text-[0.8rem]" aria-hidden="true" />}
371
- <span>{delta}</span>
372
- </span>
373
- </div>
374
- </>
375
- )
376
-
377
- const sharedClass = cn(
378
- "group flex min-w-0 flex-col gap-2 text-left outline-none",
379
- edgeGutter && "first:pl-0 last:pr-0",
380
- dense ? "gap-1.5 px-2 py-2 sm:px-3 sm:py-3" : "gap-2 px-3 py-3 sm:px-5 sm:py-4",
381
- isHero && "gap-2.5",
382
- isInteractive && [
383
- "cursor-pointer transition-colors duration-150",
384
- "hover:bg-foreground/5",
385
- "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
386
- ]
387
- )
388
-
389
- if (href) {
390
- return (
391
- <a href={href} className={sharedClass} aria-label={`${label}: ${value}`}>
392
- {inner}
393
- </a>
394
- )
395
- }
396
-
397
- if (onClick) {
398
- return (
399
- <button type="button" onClick={onClick} className={sharedClass} aria-label={`${label}: ${value}`}>
400
- {inner}
401
- </button>
402
- )
403
- }
404
-
405
- return <div className={sharedClass}>{inner}</div>
406
- })
407
-
408
- /** Body line for rail: `description`, else optional `statement` */
409
- function insightRailBody(insight: MetricInsight): string {
410
- const d = insight.description?.trim()
411
- if (d) return d
412
- return insight.statement?.trim() ?? ""
413
- }
414
-
415
- /**
416
- * Rail insight: severity badge + title + description + optional ↗, Ask Leo (no rule between copy and action).
417
- */
418
- function InsightRailStatementAction({
419
- insight,
420
- compact,
421
- }: {
422
- insight: MetricInsight
423
- compact: boolean
424
- }) {
425
- const badgeSize = compact ? "sm" : "default"
426
- const surface = compact
427
- ? "border border-border/50 bg-gradient-to-b from-muted/35 to-card"
428
- : "bg-card"
429
- const body = insightRailBody(insight)
430
-
431
- return (
432
- <Card
433
- role="region"
434
- aria-label="Insight"
435
- className={cn(
436
- "flex h-full min-h-0 flex-col overflow-hidden rounded-lg border-0 p-0 shadow-none ring-1 ring-foreground/8",
437
- surface
438
- )}
439
- >
440
- {/* flex-1 + mt-auto on the CTA: copy stays top-aligned when the rail stretches to KPI height */}
441
- <div className="flex min-h-0 flex-1 flex-col px-3 py-3 sm:px-4 sm:py-4">
442
- <div className="flex items-start gap-2.5">
443
- <InsightBadge severity={insight.severity} size={badgeSize} />
444
- <div className="min-w-0 flex-1">
445
- <p className="text-sm font-semibold leading-snug text-foreground">{insight.title}</p>
446
- {body ? (
447
- <p className="mt-1 text-sm leading-snug text-muted-foreground">{body}</p>
448
- ) : null}
449
- </div>
450
- {insight.href && (
451
- <a
452
- href={insight.href}
453
- className="mt-0.5 shrink-0 text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring"
454
- aria-label={`Open ${insight.title} — details`}
455
- >
456
- <i className="fa-light fa-arrow-up-right text-xs" aria-hidden="true" />
457
- </a>
458
- )}
459
- </div>
460
-
461
- <div className="mt-auto flex shrink-0 justify-end pt-3">
462
- <InsightAskLeoTooltip actionLabel={insight.actionLabel}>
463
- <Button
464
- variant={compact ? "outline" : "ghost"}
465
- size="sm"
466
- className={cn(
467
- "h-8 w-full gap-1.5 text-xs sm:w-auto",
468
- compact
469
- ? "border-border/60 bg-background px-3 text-foreground hover:bg-background"
470
- : "px-3 text-muted-foreground hover:text-interactive-hover-foreground"
471
- )}
472
- onClick={insight.onAction}
473
- aria-label={insight.actionLabel ?? "Ask Leo"}
474
- >
475
- <i
476
- className={
477
- insight.actionIcon
478
- ? `fa-light ${insight.actionIcon} text-xs`
479
- : "fa-duotone fa-solid fa-star-christmas text-xs text-brand"
480
- }
481
- aria-hidden="true"
482
- />
483
- {insight.actionLabel ?? "Ask Leo"}
484
- </Button>
485
- </InsightAskLeoTooltip>
486
- </div>
487
- </div>
488
- </Card>
489
- )
490
- }
491
-
492
- /** Severity icon badge for the insight card */
493
-
494
- function InsightBadge({
495
- severity = "warning",
496
- size = "default",
497
- }: {
498
- severity?: MetricInsight["severity"]
499
- size?: "default" | "sm"
500
- }) {
501
- const styles = {
502
- warning: {
503
- bg: "bg-[var(--insight-severity-warning-bg)]",
504
- icon: "fa-circle-exclamation",
505
- color: "text-[var(--insight-severity-warning-fg)]",
506
- },
507
- info: {
508
- bg: "bg-[var(--insight-severity-info-bg)]",
509
- icon: "fa-circle-info",
510
- color: "text-[var(--insight-severity-info-fg)]",
511
- },
512
- error: { bg: "bg-destructive/15", icon: "fa-circle-xmark", color: "text-destructive" },
513
- }[severity]
514
-
515
- return (
516
- <span
517
- className={cn(
518
- "inline-flex shrink-0 items-center justify-center rounded-full",
519
- size === "sm" ? "h-6 w-6 text-xs" : "h-7 w-7 text-sm",
520
- styles.bg,
521
- styles.color
522
- )}
523
- aria-hidden="true"
524
- >
525
- <i className={`fa-light ${styles.icon}`} />
526
- </span>
527
- )
528
- }
529
-
530
- /* ── Shared inner content ─────────────────────────────────────────────────── */
531
-
532
- interface InnerProps {
533
- title: string
534
- description: string
535
- period: string
536
- periods: PeriodOption[]
537
- metrics: MetricItem[]
538
- rows: MetricItem[][]
539
- insight?: MetricInsight
540
- onPeriodChange: (v: string) => void
541
- /** Extra padding class injected by flat variant */
542
- innerPadding?: string
543
- /** When false, the header (title/description/period select) is hidden */
544
- showHeader?: boolean
545
- insightCompact?: boolean
546
- insightFullWidth?: boolean
547
- metricsSingleRow?: boolean
548
- /** Tighter KPI cells + 2-col mobile grid (half-width dashboard card). */
549
- metricsHalfWidthLayout?: boolean
550
- /** Opaque fill behind each KPI cell when using hairline grid gaps (below `lg`). */
551
- metricsCellSurfaceClassName?: string
552
- /** Flat list-page band: softer dividers + tinted cells on a lavender-tinted surface */
553
- surfaceVariant?: "default" | "flat"
554
- }
555
-
556
- function KeyMetricsInner({
557
- title,
558
- description,
559
- period,
560
- periods,
561
- metrics,
562
- rows,
563
- insight,
564
- onPeriodChange,
565
- innerPadding = "",
566
- showHeader = true,
567
- insightCompact = false,
568
- insightFullWidth = false,
569
- metricsSingleRow = false,
570
- metricsHalfWidthLayout = false,
571
- metricsCellSurfaceClassName = "bg-background",
572
- surfaceVariant = "default",
573
- }: InnerProps) {
574
- const isFlatBand = surfaceVariant === "flat"
575
- const metricsGridClassName = isFlatBand
576
- ? flatMetricsHairlineClass(metrics.length, metricsHalfWidthLayout)
577
- : "gap-px bg-border"
578
- /** Side-by-side KPI + insight rail (md+). Disabled for half-width dashboard cards — insight stacks below. */
579
- const insightSideBySide = insight && !insightFullWidth && !metricsHalfWidthLayout
580
- const stackedRailInsight = insight && !insightFullWidth && metricsHalfWidthLayout
581
-
582
- return (
583
- <div data-slot="key-metrics" className="contents">
584
- {/* ── Header ──────────────────────────────────────────────────── */}
585
- {showHeader && (
586
- <div className={cn(
587
- "flex flex-col gap-2 pb-3",
588
- "sm:flex-row sm:items-center sm:justify-between sm:gap-4",
589
- innerPadding
590
- )}>
591
- <div>
592
- <p className="text-base font-semibold text-foreground leading-tight">{title}</p>
593
- <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>
594
- </div>
595
-
596
- {/* Period selector — align="end" keeps dropdown flush-right */}
597
- <Select value={period} onValueChange={onPeriodChange}>
598
- <SelectTrigger
599
- className="h-8 w-full sm:w-auto sm:min-w-[9rem] shrink-0 text-sm"
600
- aria-label="Select comparison period"
601
- >
602
- <SelectValue />
603
- </SelectTrigger>
604
- <SelectContent align="end" sideOffset={4}>
605
- {periods.map((p) => (
606
- <SelectItem key={p.value} value={p.value}>
607
- {p.label}
608
- </SelectItem>
609
- ))}
610
- </SelectContent>
611
- </Select>
612
- </div>
613
- )}
614
-
615
- {/* ── Body: metrics grid + optional insight ───────────────────── */}
616
- <div
617
- className={cn(
618
- "flex flex-col gap-0",
619
- /* 60% KPIs / 40% insight (3fr:2fr); lg+ only so phones/tablets stack KPIs + insight */
620
- insightSideBySide &&
621
- "lg:grid lg:grid-cols-[minmax(0,3fr)_minmax(13rem,2fr)] lg:items-stretch lg:gap-x-6 lg:gap-y-0",
622
- innerPadding
623
- )}
624
- >
625
-
626
- {/* Metrics section — self-start so KPI cells don’t stretch when the insight column is taller */}
627
- <div
628
- className={cn(
629
- "min-w-0 lg:flex lg:min-h-0 lg:flex-col",
630
- !insightSideBySide && "w-full",
631
- insightSideBySide && "lg:self-start"
632
- )}
633
- >
634
- {/*
635
- Phone (<md): one column. Tablet (md–lg): 2-column grid (e.g. 2×2 for four KPIs).
636
- Hairline separators use gap-px + opaque cell surfaces (divide-* breaks for 2-col order).
637
- Half-width dashboard cards keep divide-x + optional template columns.
638
- */}
639
- {metricsHalfWidthLayout ? (
640
- <div
641
- className={cn(
642
- "@container/metrics-strip grid lg:hidden",
643
- metricsSingleRow
644
- ? metricsRowColumnsClass(metrics.length, /* half */ true)
645
- : "grid-cols-2",
646
- metricsGridClassName,
647
- )}
648
- >
649
- {metrics.map((m) => (
650
- <div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
651
- <MetricCell {...m} dense edgeGutter={false} />
652
- </div>
653
- ))}
654
- </div>
655
- ) : (
656
- <div
657
- className={cn(
658
- "@container/metrics-strip grid lg:hidden",
659
- metricsRowColumnsClass(metrics.length, /* half */ false),
660
- metricsGridClassName,
661
- )}
662
- >
663
- {metrics.map((m) => (
664
- <div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
665
- <MetricCell {...m} dense={false} edgeGutter={false} />
666
- </div>
667
- ))}
668
- </div>
669
- )}
670
-
671
- {/*
672
- lg+: row-by-row container-queried grid. Uses a `gap-px + bg` hairline
673
- instead of `divide-x` so dividers render correctly when the row wraps
674
- from 4-across to a 2×2 grid (the awkward 3+1 layout is skipped — see
675
- `metricsRowColumnsClass`).
676
- */}
677
- <div className="@container/metrics-strip hidden lg:block">
678
- {rows.map((row, rowIdx) => (
679
- <React.Fragment key={rowIdx}>
680
- {rowIdx > 0 && !isFlatBand && (
681
- <Separator aria-hidden="true" className="my-1" />
682
- )}
683
- <div
684
- className={cn(
685
- "grid",
686
- metricsRowColumnsClass(row.length, metricsHalfWidthLayout),
687
- isFlatBand
688
- ? flatMetricsHairlineClass(row.length, metricsHalfWidthLayout)
689
- : metricsGridClassName,
690
- )}
691
- >
692
- {row.map((m) => (
693
- <div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
694
- <MetricCell {...m} dense={metricsHalfWidthLayout} edgeGutter={false} />
695
- </div>
696
- ))}
697
- </div>
698
- </React.Fragment>
699
- ))}
700
- </div>
701
- </div>
702
-
703
- {/* Insight card — only rendered when data provided */}
704
- {insight && (
705
- <>
706
- {insightFullWidth ? (
707
- <Separator
708
- aria-hidden="true"
709
- className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
710
- />
711
- ) : stackedRailInsight ? (
712
- <Separator
713
- aria-hidden="true"
714
- className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
715
- />
716
- ) : (
717
- <Separator
718
- aria-hidden="true"
719
- className={cn("my-3 lg:hidden", isFlatBand && "bg-foreground/[0.055]")}
720
- />
721
- )}
722
-
723
- <div
724
- className={cn(
725
- "flex min-h-0 min-w-0 w-full flex-col",
726
- /* Divider + padding replace vertical Separator so grid stays 2 columns */
727
- insightSideBySide &&
728
- !insightFullWidth &&
729
- cn(
730
- "lg:h-full lg:pl-6",
731
- /* Flat band: insight card ring is the divider — skip `border-l` (double line). */
732
- !isFlatBand && "lg:border-l lg:border-border",
733
- )
734
- )}
735
- >
736
- {insight && !insightFullWidth ? (
737
- <InsightRailStatementAction insight={insight} compact={insightCompact} />
738
- ) : (
739
- <Card
740
- role="region"
741
- aria-label="Insight"
742
- className={cn(
743
- "overflow-hidden rounded-lg p-0 ring-1 ring-foreground/8 shadow-none",
744
- "flex min-h-0 flex-col bg-muted/25"
745
- )}
746
- >
747
- {insightCompact ? (
748
- <div className="flex min-h-0 flex-1 flex-col gap-4 p-4 md:flex-row md:items-center md:justify-between md:gap-8 md:p-5">
749
- <div className="flex min-w-0 flex-1 flex-col gap-2">
750
- <div className="flex items-start gap-2.5">
751
- <InsightBadge severity={insight.severity} size="sm" />
752
- <div className="flex min-w-0 flex-1 items-start justify-between gap-2">
753
- <p className="text-base font-semibold leading-tight text-foreground">
754
- {insight.title}
755
- </p>
756
- {insight.href && (
757
- <a
758
- href={insight.href}
759
- className="shrink-0 text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring"
760
- aria-label={`Open ${insight.title} — details`}
761
- >
762
- <i className="fa-light fa-arrow-up-right text-xs" aria-hidden="true" />
763
- </a>
764
- )}
765
- </div>
766
- </div>
767
- {insight.description ? (
768
- <p className="text-sm leading-relaxed text-muted-foreground">
769
- {insight.description}
770
- </p>
771
- ) : null}
772
- </div>
773
- <div className="flex w-full shrink-0 md:w-auto">
774
- <InsightAskLeoTooltip actionLabel={insight.actionLabel}>
775
- <Button
776
- variant="ghost"
777
- size="sm"
778
- className="h-9 w-full gap-1.5 px-4 text-xs text-muted-foreground hover:text-interactive-hover-foreground md:min-w-[8.5rem]"
779
- onClick={insight.onAction}
780
- aria-label={insight.actionLabel ?? "Ask Leo"}
781
- >
782
- <i
783
- className={insight.actionIcon ? `fa-light ${insight.actionIcon} text-xs` : "fa-duotone fa-solid fa-star-christmas text-xs text-brand"}
784
- aria-hidden="true"
785
- />
786
- {insight.actionLabel ?? "Ask Leo"}
787
- </Button>
788
- </InsightAskLeoTooltip>
789
- </div>
790
- </div>
791
- ) : (
792
- <div className="flex min-h-0 flex-1 flex-col gap-4 p-4 md:flex-row md:items-center md:justify-between md:gap-8 md:p-5">
793
- <div className="flex min-w-0 flex-1 flex-col gap-3">
794
- <div className="flex items-start gap-3">
795
- <InsightBadge severity={insight.severity} />
796
- <div className="flex min-w-0 flex-1 items-start justify-between gap-2">
797
- <p className="text-base font-semibold leading-snug text-foreground">
798
- {insight.title}
799
- </p>
800
- {insight.href && (
801
- <a
802
- href={insight.href}
803
- className="shrink-0 text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring"
804
- aria-label={`Open ${insight.title} — details`}
805
- >
806
- <i className="fa-light fa-arrow-up-right text-xs" aria-hidden="true" />
807
- </a>
808
- )}
809
- </div>
810
- </div>
811
- {insight.description ? (
812
- <p className="text-sm leading-relaxed text-muted-foreground">
813
- {insight.description}
814
- </p>
815
- ) : null}
816
- </div>
817
- <div className="flex w-full shrink-0 md:w-auto">
818
- <InsightAskLeoTooltip actionLabel={insight.actionLabel}>
819
- <Button
820
- variant="ghost"
821
- size="sm"
822
- className="h-9 w-full gap-1.5 px-4 text-xs text-muted-foreground hover:text-interactive-hover-foreground md:min-w-[8.5rem]"
823
- onClick={insight.onAction}
824
- aria-label={insight.actionLabel ?? "Ask Leo"}
825
- >
826
- <i
827
- className={insight.actionIcon ? `fa-light ${insight.actionIcon} text-xs` : "fa-duotone fa-solid fa-star-christmas text-xs text-brand"}
828
- aria-hidden="true"
829
- />
830
- {insight.actionLabel ?? "Ask Leo"}
831
- </Button>
832
- </InsightAskLeoTooltip>
833
- </div>
834
- </div>
835
- )}
836
- </Card>
837
- )}
838
- </div>
839
- </>
840
- )}
841
- </div>
842
- </div>
843
- )
844
- }
845
-
846
- function chunkMetricPairs(metrics: MetricItem[]): MetricItem[][] {
847
- const out: MetricItem[][] = []
848
- for (let i = 0; i < metrics.length; i += 2) out.push(metrics.slice(i, i + 2))
849
- return out
850
- }
851
-
852
- /* ── Main component ───────────────────────────────────────────────────────── */
853
-
854
- export function KeyMetrics({
855
- variant = "card",
856
- title = "Key Metrics",
857
- description = "Overview of performance indicators",
858
- metrics = [],
859
- insight,
860
- periods = DEFAULT_PERIODS,
861
- defaultPeriod = "week",
862
- onPeriodChange,
863
- showHeader = true,
864
- insightCompact = false,
865
- insightFullWidth = false,
866
- metricsSingleRow = false,
867
- metricsHalfWidthLayout = false,
868
- className,
869
- }: KeyMetricsProps) {
870
- const [period, setPeriod] = React.useState(defaultPeriod)
871
- const { toggle: toggleAskLeo } = useAskLeo()
872
-
873
- function handlePeriodChange(v: string) {
874
- setPeriod(v)
875
- onPeriodChange?.(v)
876
- }
877
-
878
- /* Split metrics into rows of 3, or paired rows when half-width + single row, else one row */
879
- const rows: MetricItem[][] = metricsSingleRow
880
- ? metrics.length
881
- ? metricsHalfWidthLayout
882
- ? chunkMetricPairs(metrics)
883
- : [metrics]
884
- : []
885
- : (() => {
886
- const out: MetricItem[][] = []
887
- for (let i = 0; i < metrics.length; i += 3) {
888
- out.push(metrics.slice(i, i + 3))
889
- }
890
- return out
891
- })()
892
-
893
- const metricsCellSurfaceClassName =
894
- variant === "flat"
895
- ? "bg-transparent"
896
- : "bg-card dark:bg-transparent"
897
-
898
- const innerProps: InnerProps = {
899
- title,
900
- description,
901
- period,
902
- periods,
903
- metrics,
904
- rows,
905
- insight,
906
- onPeriodChange: handlePeriodChange,
907
- insightCompact,
908
- insightFullWidth,
909
- metricsSingleRow,
910
- metricsHalfWidthLayout,
911
- metricsCellSurfaceClassName,
912
- surfaceVariant: variant === "flat" ? "flat" : "default",
913
- }
914
-
915
- /*
916
- * ── GLOW GUIDELINE ────────────────────────────────────────────────────────
917
- * The bottom-glow treatment is a deliberate design signal. Use it only for:
918
- *
919
- * 1. AI / intelligence surfaces — e.g. AI Insights, Ask Leo responses,
920
- * any card that surfaces machine-generated content.
921
- * Opacity: 0.12–0.16 (subtle; the glow should not dominate)
922
- *
923
- * 2. Designer-designated hero sections — e.g. Key Metrics (the primary
924
- * KPI band), onboarding completion, or any section the product team
925
- * explicitly wants to "elevate" visually.
926
- * Opacity: 0.18–0.24 (more pronounced; intentional focal point)
927
- *
928
- * Do NOT add glow to:
929
- * • Standard data/content cards (Tasks, Activity, Learn, Charts…)
930
- * • Navigation or shell elements
931
- * • Cards that already use a coloured border or badge for status
932
- *
933
- * Implementation:
934
- * style={{ background: "radial-gradient(ellipse 110% 90% at 50% 100%,
935
- * oklch(from var(--brand-color) l c h / <opacity>) 0%, transparent 68%)" }}
936
- * + className="overflow-hidden" ← required to clip the gradient
937
- * ─────────────────────────────────────────────────────────────────────────
938
- */
939
- const glowStyle: React.CSSProperties = {
940
- background: "var(--key-metrics-card-glow-radial)",
941
- }
942
-
943
- /** List-page KPI band — transparent; only `--key-metrics-flat-band-radial` glow. */
944
- const flatBandStyle: React.CSSProperties = {
945
- background: "var(--key-metrics-flat-band-radial)",
946
- boxShadow: "var(--key-metrics-flat-band-shadow)",
947
- }
948
-
949
- /* ── Card variant — ChartCard-style chrome ───────────────────────────── */
950
- if (variant === "card") {
951
- return (
952
- <Card className={cn("shadow-xs overflow-hidden flex flex-col", className)} style={glowStyle}>
953
- <CardHeader className={cn("shrink-0 pb-2", metricsHalfWidthLayout && "space-y-2")}>
954
- <div
955
- className={cn(
956
- "flex gap-2",
957
- metricsHalfWidthLayout
958
- ? "flex-col min-[400px]:flex-row min-[400px]:items-start min-[400px]:justify-between"
959
- : "items-start",
960
- )}
961
- >
962
- <div className="flex-1 min-w-0">
963
- <CardTitle className="text-sm font-semibold leading-tight">{title}</CardTitle>
964
- <CardDescription className="text-xs mt-0.5">{description}</CardDescription>
965
- </div>
966
- <div className="flex flex-wrap items-center gap-1.5 shrink-0">
967
- <InsightAskLeoTooltip actionLabel="Ask Leo">
968
- <Button
969
- size="sm"
970
- variant="outline"
971
- className="h-7 shrink-0 text-xs gap-1.5 px-2"
972
- aria-label="Ask Leo about these metrics"
973
- onClick={toggleAskLeo}
974
- type="button"
975
- >
976
- <i className="fa-duotone fa-solid fa-star-christmas text-xs text-brand" aria-hidden="true" />
977
- <span>Ask Leo</span>
978
- </Button>
979
- </InsightAskLeoTooltip>
980
- <Select value={period} onValueChange={handlePeriodChange}>
981
- <SelectTrigger
982
- size="sm"
983
- className="w-auto min-w-[9rem] shrink-0 text-sm"
984
- aria-label="Select comparison period"
985
- >
986
- <SelectValue />
987
- </SelectTrigger>
988
- <SelectContent align="end" sideOffset={4}>
989
- {periods.map((p) => (
990
- <SelectItem key={p.value} value={p.value}>
991
- {p.label}
992
- </SelectItem>
993
- ))}
994
- </SelectContent>
995
- </Select>
996
- </div>
997
- </div>
998
- </CardHeader>
999
- <CardContent className="flex-1 pb-4">
1000
- <KeyMetricsInner {...innerProps} showHeader={false} />
1001
- </CardContent>
1002
- </Card>
1003
- )
1004
- }
1005
-
1006
- /* ── Compact variant — card chrome, no header, metrics only ──────────── */
1007
- if (variant === "compact") {
1008
- return (
1009
- <Card className={cn("shadow-xs overflow-hidden", className)} style={glowStyle}>
1010
- <CardContent className="py-3 px-4">
1011
- <KeyMetricsInner {...innerProps} showHeader={false} />
1012
- </CardContent>
1013
- </Card>
1014
- )
1015
- }
1016
-
1017
- /* ── Flat variant — no surface; bottom brand glow only ── */
1018
- return (
1019
- <section
1020
- aria-label={title}
1021
- className={cn("relative w-full overflow-hidden pt-5 pb-8", className)}
1022
- style={flatBandStyle}
1023
- >
1024
- <KeyMetricsInner
1025
- {...innerProps}
1026
- innerPadding="px-4 lg:px-6"
1027
- showHeader={showHeader}
1028
- />
1029
- </section>
1030
- )
1031
- }
1032
-
1033
- /**
1034
- * KeyMetricsContent — renders just the metrics grid + optional insight panel.
1035
- * No card wrapper, no header, no period selector.
1036
- * Designed for embedding inside a ChartCard with tabOptions period tabs.
1037
- */
1038
- export function KeyMetricsContent({
1039
- metrics = [],
1040
- insight,
1041
- insightCompact = false,
1042
- insightFullWidth = false,
1043
- }: Pick<KeyMetricsProps, "metrics" | "insight" | "insightCompact" | "insightFullWidth">) {
1044
- const rows: MetricItem[][] = []
1045
- for (let i = 0; i < metrics.length; i += 3) rows.push(metrics.slice(i, i + 3))
1046
-
1047
- return (
1048
- <KeyMetricsInner
1049
- title=""
1050
- description=""
1051
- period=""
1052
- periods={[]}
1053
- metrics={metrics}
1054
- rows={rows}
1055
- insight={insight}
1056
- onPeriodChange={() => {}}
1057
- showHeader={false}
1058
- insightCompact={insightCompact}
1059
- insightFullWidth={insightFullWidth}
1060
- metricsCellSurfaceClassName="bg-card dark:bg-transparent"
1061
- />
1062
- )
1063
- }
1
+ export * from "@exxatdesignux/ui/components/key-metrics"