@exxatdesignux/ui 0.2.18 → 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 (618) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/bin/sync-extras.mjs +116 -29
  3. package/consumer-extras/README.md +43 -4
  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 +1 -1
  36. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +9 -9
  37. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +1 -1
  38. package/consumer-extras/handbook/HANDBOOK.md +185 -0
  39. package/consumer-extras/handbook/glossary.md +57 -0
  40. package/consumer-extras/handbook/reference-implementations.md +126 -0
  41. package/consumer-extras/handbook/voice-and-tone.md +262 -0
  42. package/consumer-extras/patterns/command-menu-pattern.md +1 -1
  43. package/consumer-extras/patterns/data-views-pattern.md +14 -14
  44. package/dist/components/data-table/filter-date-calendar.d.ts +10 -0
  45. package/dist/components/data-table/filter-date-calendar.js +280 -0
  46. package/dist/components/data-table/filter-date-calendar.js.map +1 -0
  47. package/dist/components/data-table/filter-text-value-input.d.ts +15 -0
  48. package/dist/components/data-table/filter-text-value-input.js +561 -0
  49. package/dist/components/data-table/filter-text-value-input.js.map +1 -0
  50. package/dist/components/data-table/index.d.ts +45 -0
  51. package/dist/components/data-table/index.js +3085 -0
  52. package/dist/components/data-table/index.js.map +1 -0
  53. package/dist/components/data-table/pagination.d.ts +28 -0
  54. package/dist/components/data-table/pagination.js +3264 -0
  55. package/dist/components/data-table/pagination.js.map +1 -0
  56. package/dist/components/data-table/types.d.ts +84 -0
  57. package/dist/components/data-table/types.js +3 -0
  58. package/dist/components/data-table/types.js.map +1 -0
  59. package/dist/components/data-table/use-table-state.d.ts +116 -0
  60. package/dist/components/data-table/use-table-state.js +670 -0
  61. package/dist/components/data-table/use-table-state.js.map +1 -0
  62. package/dist/components/data-views/board-card-primitives.d.ts +22 -0
  63. package/dist/components/data-views/board-card-primitives.js +84 -0
  64. package/dist/components/data-views/board-card-primitives.js.map +1 -0
  65. package/dist/components/data-views/data-row-list.d.ts +33 -0
  66. package/dist/components/data-views/data-row-list.js +106 -0
  67. package/dist/components/data-views/data-row-list.js.map +1 -0
  68. package/dist/components/data-views/finder-panel-view.d.ts +54 -0
  69. package/dist/components/data-views/finder-panel-view.js +388 -0
  70. package/dist/components/data-views/finder-panel-view.js.map +1 -0
  71. package/dist/components/data-views/folder-grid-view.d.ts +22 -0
  72. package/dist/components/data-views/folder-grid-view.js +58 -0
  73. package/dist/components/data-views/folder-grid-view.js.map +1 -0
  74. package/dist/components/data-views/hub-table.d.ts +167 -0
  75. package/dist/components/data-views/hub-table.js +5561 -0
  76. package/dist/components/data-views/hub-table.js.map +1 -0
  77. package/dist/components/data-views/index.d.ts +27 -0
  78. package/dist/components/data-views/index.js +6575 -0
  79. package/dist/components/data-views/index.js.map +1 -0
  80. package/dist/components/data-views/list-page-board-card.d.ts +72 -0
  81. package/dist/components/data-views/list-page-board-card.js +264 -0
  82. package/dist/components/data-views/list-page-board-card.js.map +1 -0
  83. package/dist/components/data-views/list-page-board-template.d.ts +24 -0
  84. package/dist/components/data-views/list-page-board-template.js +137 -0
  85. package/dist/components/data-views/list-page-board-template.js.map +1 -0
  86. package/dist/components/data-views/list-page-connected-view-body.d.ts +19 -0
  87. package/dist/components/data-views/list-page-connected-view-body.js +116 -0
  88. package/dist/components/data-views/list-page-connected-view-body.js.map +1 -0
  89. package/dist/components/data-views/list-page-split-details-placeholder.d.ts +14 -0
  90. package/dist/components/data-views/list-page-split-details-placeholder.js +38 -0
  91. package/dist/components/data-views/list-page-split-details-placeholder.js.map +1 -0
  92. package/dist/components/data-views/list-page-split-hub-chrome.d.ts +17 -0
  93. package/dist/components/data-views/list-page-split-hub-chrome.js +54 -0
  94. package/dist/components/data-views/list-page-split-hub-chrome.js.map +1 -0
  95. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +12 -0
  96. package/dist/components/data-views/list-page-split-hub-tokens.js +8 -0
  97. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -0
  98. package/dist/components/data-views/list-page-tree-column-header.d.ts +15 -0
  99. package/dist/components/data-views/list-page-tree-column-header.js +22 -0
  100. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -0
  101. package/dist/components/data-views/list-page-tree-panel-shell.d.ts +25 -0
  102. package/dist/components/data-views/list-page-tree-panel-shell.js +146 -0
  103. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -0
  104. package/dist/components/data-views/os-folder-glyph.d.ts +35 -0
  105. package/dist/components/data-views/os-folder-glyph.js +104 -0
  106. package/dist/components/data-views/os-folder-glyph.js.map +1 -0
  107. package/dist/components/data-views/outline-tree-menu.d.ts +36 -0
  108. package/dist/components/data-views/outline-tree-menu.js +131 -0
  109. package/dist/components/data-views/outline-tree-menu.js.map +1 -0
  110. package/dist/components/table-properties/column-row.d.ts +22 -0
  111. package/dist/components/table-properties/column-row.js +153 -0
  112. package/dist/components/table-properties/column-row.js.map +1 -0
  113. package/dist/components/table-properties/draggable-list.d.ts +24 -0
  114. package/dist/components/table-properties/draggable-list.js +53 -0
  115. package/dist/components/table-properties/draggable-list.js.map +1 -0
  116. package/dist/components/table-properties/drawer-button.d.ts +110 -0
  117. package/dist/components/table-properties/drawer-button.js +2748 -0
  118. package/dist/components/table-properties/drawer-button.js.map +1 -0
  119. package/dist/components/table-properties/drawer.d.ts +100 -0
  120. package/dist/components/table-properties/drawer.js +2595 -0
  121. package/dist/components/table-properties/drawer.js.map +1 -0
  122. package/dist/components/table-properties/filter-card.d.ts +24 -0
  123. package/dist/components/table-properties/filter-card.js +854 -0
  124. package/dist/components/table-properties/filter-card.js.map +1 -0
  125. package/dist/components/table-properties/index.d.ts +14 -0
  126. package/dist/components/table-properties/index.js +2768 -0
  127. package/dist/components/table-properties/index.js.map +1 -0
  128. package/dist/components/table-properties/sort-card.d.ts +20 -0
  129. package/dist/components/table-properties/sort-card.js +102 -0
  130. package/dist/components/table-properties/sort-card.js.map +1 -0
  131. package/dist/components/templates/dedicated-search-landing-template.d.ts +21 -0
  132. package/dist/components/templates/dedicated-search-landing-template.js +254 -0
  133. package/dist/components/templates/dedicated-search-landing-template.js.map +1 -0
  134. package/dist/components/templates/dedicated-search-results-template.d.ts +15 -0
  135. package/dist/components/templates/dedicated-search-results-template.js +16 -0
  136. package/dist/components/templates/dedicated-search-results-template.js.map +1 -0
  137. package/dist/components/templates/index.d.ts +9 -0
  138. package/dist/components/templates/index.js +2720 -0
  139. package/dist/components/templates/index.js.map +1 -0
  140. package/dist/components/templates/list-page.d.ts +83 -0
  141. package/dist/components/templates/list-page.js +2433 -0
  142. package/dist/components/templates/list-page.js.map +1 -0
  143. package/dist/components/templates/nested-secondary-panel-shell.d.ts +20 -0
  144. package/dist/components/templates/nested-secondary-panel-shell.js +54 -0
  145. package/dist/components/templates/nested-secondary-panel-shell.js.map +1 -0
  146. package/dist/components/ui/accordion.d.ts +10 -0
  147. package/dist/components/ui/accordion.js +74 -0
  148. package/dist/components/ui/accordion.js.map +1 -0
  149. package/dist/components/ui/alert-dialog.d.ts +37 -0
  150. package/dist/components/ui/alert-dialog.js +201 -0
  151. package/dist/components/ui/alert-dialog.js.map +1 -0
  152. package/dist/components/ui/avatar.d.ts +84 -0
  153. package/dist/components/ui/avatar.js +328 -0
  154. package/dist/components/ui/avatar.js.map +1 -0
  155. package/dist/components/ui/badge.d.ts +13 -0
  156. package/dist/components/ui/badge.js +49 -0
  157. package/dist/components/ui/badge.js.map +1 -0
  158. package/dist/components/ui/banner.d.ts +62 -0
  159. package/dist/components/ui/banner.js +364 -0
  160. package/dist/components/ui/banner.js.map +1 -0
  161. package/dist/components/ui/breadcrumb.d.ts +14 -0
  162. package/dist/components/ui/breadcrumb.js +114 -0
  163. package/dist/components/ui/breadcrumb.js.map +1 -0
  164. package/dist/components/ui/button.d.ts +16 -0
  165. package/dist/components/ui/button.js +59 -0
  166. package/dist/components/ui/button.js.map +1 -0
  167. package/dist/components/ui/calendar.d.ts +13 -0
  168. package/dist/components/ui/calendar.js +238 -0
  169. package/dist/components/ui/calendar.js.map +1 -0
  170. package/dist/components/ui/card.d.ts +14 -0
  171. package/dist/components/ui/card.js +102 -0
  172. package/dist/components/ui/card.js.map +1 -0
  173. package/dist/components/ui/chart.d.ts +58 -0
  174. package/dist/components/ui/chart.js +292 -0
  175. package/dist/components/ui/chart.js.map +1 -0
  176. package/dist/components/ui/checkbox.d.ts +23 -0
  177. package/dist/components/ui/checkbox.js +155 -0
  178. package/dist/components/ui/checkbox.js.map +1 -0
  179. package/dist/components/ui/coach-mark.d.ts +27 -0
  180. package/dist/components/ui/coach-mark.js +306 -0
  181. package/dist/components/ui/coach-mark.js.map +1 -0
  182. package/dist/components/ui/collapsible.d.ts +8 -0
  183. package/dist/components/ui/collapsible.js +35 -0
  184. package/dist/components/ui/collapsible.js.map +1 -0
  185. package/dist/components/ui/command.d.ts +36 -0
  186. package/dist/components/ui/command.js +274 -0
  187. package/dist/components/ui/command.js.map +1 -0
  188. package/dist/components/ui/context-menu.d.ts +32 -0
  189. package/dist/components/ui/context-menu.js +245 -0
  190. package/dist/components/ui/context-menu.js.map +1 -0
  191. package/dist/components/ui/date-picker-field.d.ts +38 -0
  192. package/dist/components/ui/date-picker-field.js +550 -0
  193. package/dist/components/ui/date-picker-field.js.map +1 -0
  194. package/dist/components/ui/dialog.d.ts +22 -0
  195. package/dist/components/ui/dialog.js +200 -0
  196. package/dist/components/ui/dialog.js.map +1 -0
  197. package/dist/components/ui/dot-pattern.d.ts +21 -0
  198. package/dist/components/ui/dot-pattern.js +139 -0
  199. package/dist/components/ui/dot-pattern.js.map +1 -0
  200. package/dist/components/ui/drag-handle-grip.d.ts +10 -0
  201. package/dist/components/ui/drag-handle-grip.js +15 -0
  202. package/dist/components/ui/drag-handle-grip.js.map +1 -0
  203. package/dist/components/ui/drawer.d.ts +16 -0
  204. package/dist/components/ui/drawer.js +125 -0
  205. package/dist/components/ui/drawer.js.map +1 -0
  206. package/dist/components/ui/dropdown-menu.d.ts +45 -0
  207. package/dist/components/ui/dropdown-menu.js +353 -0
  208. package/dist/components/ui/dropdown-menu.js.map +1 -0
  209. package/dist/components/ui/export-drawer.d.ts +11 -0
  210. package/dist/components/ui/export-drawer.js +1658 -0
  211. package/dist/components/ui/export-drawer.js.map +1 -0
  212. package/dist/components/ui/field.d.ts +30 -0
  213. package/dist/components/ui/field.js +249 -0
  214. package/dist/components/ui/field.js.map +1 -0
  215. package/dist/components/ui/form.d.ts +28 -0
  216. package/dist/components/ui/form.js +110 -0
  217. package/dist/components/ui/form.js.map +1 -0
  218. package/dist/components/ui/hover-card.d.ts +9 -0
  219. package/dist/components/ui/hover-card.js +43 -0
  220. package/dist/components/ui/hover-card.js.map +1 -0
  221. package/dist/components/ui/input-group.d.ts +20 -0
  222. package/dist/components/ui/input-group.js +219 -0
  223. package/dist/components/ui/input-group.js.map +1 -0
  224. package/dist/components/ui/input-mask.d.ts +39 -0
  225. package/dist/components/ui/input-mask.js +118 -0
  226. package/dist/components/ui/input-mask.js.map +1 -0
  227. package/dist/components/ui/input.d.ts +5 -0
  228. package/dist/components/ui/input.js +30 -0
  229. package/dist/components/ui/input.js.map +1 -0
  230. package/dist/components/ui/kbd.d.ts +20 -0
  231. package/dist/components/ui/kbd.js +45 -0
  232. package/dist/components/ui/kbd.js.map +1 -0
  233. package/dist/components/ui/key-metrics-context.d.ts +19 -0
  234. package/dist/components/ui/key-metrics-context.js +26 -0
  235. package/dist/components/ui/key-metrics-context.js.map +1 -0
  236. package/dist/components/ui/key-metrics.d.ts +131 -0
  237. package/dist/components/ui/key-metrics.js +1015 -0
  238. package/dist/components/ui/key-metrics.js.map +1 -0
  239. package/dist/components/ui/label.d.ts +6 -0
  240. package/dist/components/ui/label.js +28 -0
  241. package/dist/components/ui/label.js.map +1 -0
  242. package/dist/components/ui/list-page-view-frame.d.ts +22 -0
  243. package/dist/components/ui/list-page-view-frame.js +24 -0
  244. package/dist/components/ui/list-page-view-frame.js.map +1 -0
  245. package/dist/components/ui/page-header.d.ts +51 -0
  246. package/dist/components/ui/page-header.js +372 -0
  247. package/dist/components/ui/page-header.js.map +1 -0
  248. package/dist/components/ui/payment-card-fields.d.ts +10 -0
  249. package/dist/components/ui/payment-card-fields.js +80 -0
  250. package/dist/components/ui/payment-card-fields.js.map +1 -0
  251. package/dist/components/ui/popover.d.ts +10 -0
  252. package/dist/components/ui/popover.js +47 -0
  253. package/dist/components/ui/popover.js.map +1 -0
  254. package/dist/components/ui/radio-group.d.ts +29 -0
  255. package/dist/components/ui/radio-group.js +190 -0
  256. package/dist/components/ui/radio-group.js.map +1 -0
  257. package/dist/components/ui/resizable.d.ts +16 -0
  258. package/dist/components/ui/resizable.js +51 -0
  259. package/dist/components/ui/resizable.js.map +1 -0
  260. package/dist/components/ui/scroll-area.d.ts +8 -0
  261. package/dist/components/ui/scroll-area.js +66 -0
  262. package/dist/components/ui/scroll-area.js.map +1 -0
  263. package/dist/components/ui/select.d.ts +18 -0
  264. package/dist/components/ui/select.js +186 -0
  265. package/dist/components/ui/select.js.map +1 -0
  266. package/dist/components/ui/selection-tile-grid.d.ts +52 -0
  267. package/dist/components/ui/selection-tile-grid.js +347 -0
  268. package/dist/components/ui/selection-tile-grid.js.map +1 -0
  269. package/dist/components/ui/separator.d.ts +7 -0
  270. package/dist/components/ui/separator.js +33 -0
  271. package/dist/components/ui/separator.js.map +1 -0
  272. package/dist/components/ui/sheet.d.ts +18 -0
  273. package/dist/components/ui/sheet.js +181 -0
  274. package/dist/components/ui/sheet.js.map +1 -0
  275. package/dist/components/ui/sidebar.d.ts +94 -0
  276. package/dist/components/ui/sidebar.js +805 -0
  277. package/dist/components/ui/sidebar.js.map +1 -0
  278. package/dist/components/ui/skeleton.d.ts +5 -0
  279. package/dist/components/ui/skeleton.js +22 -0
  280. package/dist/components/ui/skeleton.js.map +1 -0
  281. package/dist/components/ui/slider.d.ts +7 -0
  282. package/dist/components/ui/slider.js +66 -0
  283. package/dist/components/ui/slider.js.map +1 -0
  284. package/dist/components/ui/sonner.d.ts +6 -0
  285. package/dist/components/ui/sonner.js +38 -0
  286. package/dist/components/ui/sonner.js.map +1 -0
  287. package/dist/components/ui/status-badge.d.ts +38 -0
  288. package/dist/components/ui/status-badge.js +77 -0
  289. package/dist/components/ui/status-badge.js.map +1 -0
  290. package/dist/components/ui/table.d.ts +13 -0
  291. package/dist/components/ui/table.js +115 -0
  292. package/dist/components/ui/table.js.map +1 -0
  293. package/dist/components/ui/tabs.d.ts +15 -0
  294. package/dist/components/ui/tabs.js +93 -0
  295. package/dist/components/ui/tabs.js.map +1 -0
  296. package/dist/components/ui/textarea.d.ts +6 -0
  297. package/dist/components/ui/textarea.js +25 -0
  298. package/dist/components/ui/textarea.js.map +1 -0
  299. package/dist/components/ui/tip.d.ts +12 -0
  300. package/dist/components/ui/tip.js +61 -0
  301. package/dist/components/ui/tip.js.map +1 -0
  302. package/dist/components/ui/toggle-group.d.ts +14 -0
  303. package/dist/components/ui/toggle-group.js +104 -0
  304. package/dist/components/ui/toggle-group.js.map +1 -0
  305. package/dist/components/ui/toggle-switch.d.ts +10 -0
  306. package/dist/components/ui/toggle-switch.js +33 -0
  307. package/dist/components/ui/toggle-switch.js.map +1 -0
  308. package/dist/components/ui/toggle.d.ts +13 -0
  309. package/dist/components/ui/toggle.js +51 -0
  310. package/dist/components/ui/toggle.js.map +1 -0
  311. package/dist/components/ui/tooltip.d.ts +10 -0
  312. package/dist/components/ui/tooltip.js +68 -0
  313. package/dist/components/ui/tooltip.js.map +1 -0
  314. package/dist/components/ui/view-segmented-control.d.ts +31 -0
  315. package/dist/components/ui/view-segmented-control.js +167 -0
  316. package/dist/components/ui/view-segmented-control.js.map +1 -0
  317. package/dist/data-list-view-registry-CyBoBML4.d.ts +73 -0
  318. package/dist/hooks/use-app-theme.d.ts +24 -0
  319. package/dist/hooks/use-app-theme.js +286 -0
  320. package/dist/hooks/use-app-theme.js.map +1 -0
  321. package/dist/hooks/use-coach-mark.d.ts +86 -0
  322. package/dist/hooks/use-coach-mark.js +218 -0
  323. package/dist/hooks/use-coach-mark.js.map +1 -0
  324. package/dist/hooks/use-mobile.d.ts +3 -0
  325. package/dist/hooks/use-mobile.js +29 -0
  326. package/dist/hooks/use-mobile.js.map +1 -0
  327. package/dist/hooks/use-mod-key-label.d.ts +6 -0
  328. package/dist/hooks/use-mod-key-label.js +25 -0
  329. package/dist/hooks/use-mod-key-label.js.map +1 -0
  330. package/dist/index.d.ts +120 -0
  331. package/dist/index.js +13324 -0
  332. package/dist/index.js.map +1 -0
  333. package/dist/lib/compose-refs.d.ts +6 -0
  334. package/dist/lib/compose-refs.js +17 -0
  335. package/dist/lib/compose-refs.js.map +1 -0
  336. package/dist/lib/conditional-rule-match.d.ts +30 -0
  337. package/dist/lib/conditional-rule-match.js +66 -0
  338. package/dist/lib/conditional-rule-match.js.map +1 -0
  339. package/dist/lib/data-list-display-options.d.ts +26 -0
  340. package/dist/lib/data-list-display-options.js +14 -0
  341. package/dist/lib/data-list-display-options.js.map +1 -0
  342. package/dist/lib/data-list-view-registry.d.ts +2 -0
  343. package/dist/lib/data-list-view-registry.js +102 -0
  344. package/dist/lib/data-list-view-registry.js.map +1 -0
  345. package/dist/lib/data-list-view-surface.d.ts +2 -0
  346. package/dist/lib/data-list-view-surface.js +80 -0
  347. package/dist/lib/data-list-view-surface.js.map +1 -0
  348. package/dist/lib/data-list-view.d.ts +21 -0
  349. package/dist/lib/data-list-view.js +25 -0
  350. package/dist/lib/data-list-view.js.map +1 -0
  351. package/dist/lib/date-filter.d.ts +22 -0
  352. package/dist/lib/date-filter.js +61 -0
  353. package/dist/lib/date-filter.js.map +1 -0
  354. package/dist/lib/dev-log.d.ts +8 -0
  355. package/dist/lib/dev-log.js +10 -0
  356. package/dist/lib/dev-log.js.map +1 -0
  357. package/dist/lib/dropdown-menu-surface.d.ts +14 -0
  358. package/dist/lib/dropdown-menu-surface.js +6 -0
  359. package/dist/lib/dropdown-menu-surface.js.map +1 -0
  360. package/dist/lib/editable-target.d.ts +12 -0
  361. package/dist/lib/editable-target.js +12 -0
  362. package/dist/lib/editable-target.js.map +1 -0
  363. package/dist/lib/list-page-table-properties.d.ts +35 -0
  364. package/dist/lib/list-page-table-properties.js +81 -0
  365. package/dist/lib/list-page-table-properties.js.map +1 -0
  366. package/dist/lib/raf-throttle.d.ts +23 -0
  367. package/dist/lib/raf-throttle.js +27 -0
  368. package/dist/lib/raf-throttle.js.map +1 -0
  369. package/dist/lib/row-height.d.ts +16 -0
  370. package/dist/lib/row-height.js +10 -0
  371. package/dist/lib/row-height.js.map +1 -0
  372. package/dist/lib/table-properties-types.d.ts +83 -0
  373. package/dist/lib/table-properties-types.js +19 -0
  374. package/dist/lib/table-properties-types.js.map +1 -0
  375. package/dist/lib/utils.d.ts +5 -0
  376. package/dist/lib/utils.js +11 -0
  377. package/dist/lib/utils.js.map +1 -0
  378. package/package.json +83 -18
  379. package/src/components/data-table/filter-date-calendar.tsx +38 -0
  380. package/src/components/data-table/filter-text-value-input.tsx +77 -0
  381. package/src/components/data-table/index.tsx +1678 -0
  382. package/src/components/data-table/pagination.tsx +255 -0
  383. package/src/components/data-table/types.ts +96 -0
  384. package/src/components/data-table/use-table-state.ts +767 -0
  385. package/src/components/data-views/board-card-primitives.tsx +93 -0
  386. package/src/components/data-views/data-row-list.tsx +183 -0
  387. package/src/components/data-views/finder-panel-view.tsx +405 -0
  388. package/src/components/data-views/folder-grid-view.tsx +86 -0
  389. package/src/components/data-views/hub-table.tsx +498 -0
  390. package/src/components/data-views/index.ts +28 -0
  391. package/src/components/data-views/list-page-board-card.tsx +192 -0
  392. package/src/components/data-views/list-page-board-template.tsx +122 -0
  393. package/src/components/data-views/list-page-connected-view-body.tsx +66 -0
  394. package/src/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  395. package/src/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  396. package/src/components/data-views/list-page-split-hub-tokens.ts +16 -0
  397. package/src/components/data-views/list-page-tree-column-header.tsx +31 -0
  398. package/src/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  399. package/src/components/data-views/os-folder-glyph.tsx +141 -0
  400. package/src/components/data-views/outline-tree-menu.tsx +157 -0
  401. package/src/components/table-properties/column-row.tsx +90 -0
  402. package/src/components/table-properties/draggable-list.ts +54 -0
  403. package/src/components/table-properties/drawer-button.tsx +300 -0
  404. package/src/components/table-properties/drawer.tsx +1148 -0
  405. package/src/components/table-properties/filter-card.tsx +251 -0
  406. package/src/components/table-properties/index.ts +36 -0
  407. package/src/components/table-properties/sort-card.tsx +63 -0
  408. package/src/components/templates/dedicated-search-landing-template.tsx +124 -0
  409. package/src/components/templates/dedicated-search-results-template.tsx +19 -0
  410. package/src/components/templates/index.ts +33 -0
  411. package/src/components/templates/list-page.tsx +602 -0
  412. package/src/components/templates/nested-secondary-panel-shell.tsx +70 -0
  413. package/src/components/ui/accordion.tsx +92 -0
  414. package/src/components/ui/alert-dialog.tsx +221 -0
  415. package/src/components/ui/avatar.tsx +13 -2
  416. package/src/components/ui/banner.tsx +2 -2
  417. package/src/components/ui/calendar.tsx +1 -1
  418. package/src/components/ui/coach-mark.tsx +1 -1
  419. package/src/components/ui/context-menu.tsx +291 -0
  420. package/src/components/ui/date-picker-field.tsx +2 -2
  421. package/src/components/ui/dot-pattern.tsx +183 -0
  422. package/src/components/ui/export-drawer.tsx +375 -0
  423. package/src/components/ui/hover-card.tsx +66 -0
  424. package/src/components/ui/key-metrics-context.tsx +78 -0
  425. package/src/components/ui/key-metrics.tsx +1133 -0
  426. package/src/components/ui/list-page-view-frame.tsx +64 -0
  427. package/src/components/ui/page-header.tsx +244 -0
  428. package/src/components/ui/payment-card-fields.tsx +2 -2
  429. package/src/components/ui/resizable.tsx +68 -0
  430. package/src/components/ui/scroll-area.tsx +72 -0
  431. package/src/components/ui/selection-tile-grid.tsx +9 -2
  432. package/src/components/ui/sidebar.tsx +84 -12
  433. package/src/components/ui/slider.tsx +83 -0
  434. package/src/globals.css +494 -151
  435. package/src/globals.d.ts +20 -0
  436. package/src/index.ts +68 -1
  437. package/src/lib/conditional-rule-match.ts +119 -0
  438. package/src/lib/data-list-display-options.ts +35 -0
  439. package/src/lib/data-list-view-registry.ts +104 -0
  440. package/src/lib/data-list-view-surface.ts +83 -0
  441. package/src/lib/data-list-view.ts +47 -0
  442. package/src/lib/dev-log.ts +10 -0
  443. package/src/lib/editable-target.ts +20 -0
  444. package/src/lib/list-page-table-properties.ts +48 -0
  445. package/src/lib/raf-throttle.ts +45 -0
  446. package/src/lib/row-height.ts +19 -0
  447. package/src/lib/table-properties-types.ts +98 -0
  448. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  449. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +3 -3
  450. package/template/.cursor/rules/exxat-data-tables.mdc +1 -1
  451. package/template/.cursor/rules/exxat-ds-agents.mdc +2 -2
  452. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +2 -2
  453. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  454. package/template/AGENTS.md +84 -20
  455. package/template/app/(app)/examples/page.tsx +0 -1
  456. package/template/app/(app)/layout.tsx +17 -4
  457. package/template/app/(app)/question-bank/layout.tsx +1 -1
  458. package/template/app/(app)/question-bank/new/page.tsx +11 -24
  459. package/template/app/globals.css +13 -1972
  460. package/template/components/ask-leo-sidebar.tsx +5 -1
  461. package/template/components/brand-color-picker.tsx +2 -2
  462. package/template/components/charts-overview.tsx +1 -1
  463. package/template/components/compliance-table.tsx +240 -384
  464. package/template/components/dashboard-report-charts.tsx +1 -1
  465. package/template/components/dashboard-tabs.tsx +1 -1
  466. package/template/components/data-table/filter-date-calendar.tsx +1 -38
  467. package/template/components/data-table/filter-text-value-input.tsx +1 -77
  468. package/template/components/data-table/index.tsx +1 -1634
  469. package/template/components/data-table/pagination.tsx +1 -255
  470. package/template/components/data-table/types.ts +1 -94
  471. package/template/components/data-table/use-table-state.test.ts +420 -0
  472. package/template/components/data-table/use-table-state.ts +1 -758
  473. package/template/components/data-view-dashboard-charts-compliance.tsx +2 -2
  474. package/template/components/data-view-dashboard-charts-team.tsx +2 -2
  475. package/template/components/data-view-dashboard-charts.tsx +2 -2
  476. package/template/components/data-views/board-card-primitives.tsx +1 -93
  477. package/template/components/data-views/data-row-list.tsx +1 -183
  478. package/template/components/data-views/finder-panel-view.tsx +1 -405
  479. package/template/components/data-views/folder-grid-view.tsx +1 -86
  480. package/template/components/data-views/hub-table.tsx +1 -0
  481. package/template/components/data-views/index.ts +42 -1
  482. package/template/components/data-views/list-page-board-card.tsx +1 -192
  483. package/template/components/data-views/list-page-board-template.tsx +1 -122
  484. package/template/components/data-views/list-page-connected-view-body.tsx +1 -0
  485. package/template/components/data-views/list-page-split-details-placeholder.tsx +1 -39
  486. package/template/components/data-views/list-page-split-hub-chrome.tsx +1 -60
  487. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -16
  488. package/template/components/data-views/list-page-tree-column-header.tsx +1 -31
  489. package/template/components/data-views/list-page-tree-panel-shell.tsx +1 -91
  490. package/template/components/data-views/list-page-view-frame.tsx +5 -53
  491. package/template/components/data-views/os-folder-glyph.tsx +1 -129
  492. package/template/components/data-views/outline-tree-menu.tsx +1 -157
  493. package/template/components/export-drawer.test.tsx +71 -0
  494. package/template/components/export-drawer.tsx +1 -375
  495. package/template/components/exxat-product-logo.tsx +5 -5
  496. package/template/components/hub-tree-panel-view.tsx +2 -2
  497. package/template/components/invite-collaborators-drawer.tsx +3 -3
  498. package/template/components/key-metrics-ask-leo-bridge.tsx +40 -0
  499. package/template/components/key-metrics.tsx +1 -1063
  500. package/template/components/leo-insight-indicator.tsx +2 -2
  501. package/template/components/new-placement-back-btn.tsx +1 -1
  502. package/template/components/new-placement-form.tsx +63 -189
  503. package/template/components/new-question-composer.tsx +432 -402
  504. package/template/components/onboarding/index.ts +9 -0
  505. package/template/components/onboarding/onboarding-01.tsx +1 -1
  506. package/template/components/onboarding/onboarding-02.tsx +1 -1
  507. package/template/components/onboarding/onboarding-03.tsx +1 -1
  508. package/template/components/onboarding/onboarding-04.tsx +1 -1
  509. package/template/components/page-header.tsx +8 -226
  510. package/template/components/placement-board-card.tsx +71 -83
  511. package/template/components/placements-board-view.tsx +3 -10
  512. package/template/components/placements-client.tsx +10 -42
  513. package/template/components/placements-list-view.tsx +22 -69
  514. package/template/components/placements-table-columns.tsx +8 -438
  515. package/template/components/placements-table.tsx +588 -1296
  516. package/template/components/product-switcher.tsx +1 -1
  517. package/template/components/product-wordmark.tsx +2 -1
  518. package/template/components/question-bank-client.tsx +4 -1
  519. package/template/components/question-bank-hub-client.tsx +1 -1
  520. package/template/components/question-bank-new-folder-sheet.tsx +2 -2
  521. package/template/components/question-bank-secondary-nav.tsx +3 -3
  522. package/template/components/question-bank-table.tsx +294 -526
  523. package/template/components/rotations-empty-state.tsx +1 -1
  524. package/template/components/rotations-panel-activator.tsx +1 -1
  525. package/template/components/settings-appearance-card.tsx +1 -1
  526. package/template/components/{app-sidebar-dynamic.tsx → sidebar/app-sidebar-dynamic.tsx} +1 -1
  527. package/template/components/{app-sidebar.tsx → sidebar/app-sidebar.tsx} +4 -4
  528. package/template/components/sidebar/index.ts +16 -0
  529. package/template/components/{secondary-nav.tsx → sidebar/secondary-nav.tsx} +2 -2
  530. package/template/components/{secondary-panel.tsx → sidebar/secondary-panel.tsx} +6 -3
  531. package/template/components/{sidebar-auto-collapse.tsx → sidebar/sidebar-auto-collapse.tsx} +6 -2
  532. package/template/components/{sidebar-shell.tsx → sidebar/sidebar-shell.tsx} +1 -1
  533. package/template/components/site-header.tsx +1 -1
  534. package/template/components/{sites-all-client.tsx → sites-client.tsx} +1 -1
  535. package/template/components/sites-table.tsx +124 -257
  536. package/template/components/table-properties/column-row.tsx +1 -90
  537. package/template/components/table-properties/draggable-list.ts +1 -49
  538. package/template/components/table-properties/drawer-button.tsx +1 -249
  539. package/template/components/table-properties/drawer.tsx +1 -1105
  540. package/template/components/table-properties/filter-card.tsx +1 -251
  541. package/template/components/table-properties/sort-card.tsx +1 -59
  542. package/template/components/table-properties/types.ts +28 -71
  543. package/template/components/team-table.tsx +242 -382
  544. package/template/components/templates/dedicated-search-landing-template.tsx +1 -124
  545. package/template/components/templates/dedicated-search-results-template.tsx +1 -19
  546. package/template/components/templates/list-page.tsx +1 -584
  547. package/template/components/templates/nested-secondary-panel-shell.tsx +1 -62
  548. package/template/components/templates/new-focus-template.tsx +659 -0
  549. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  550. package/template/components/ui/accordion.tsx +1 -0
  551. package/template/components/ui/alert-dialog.tsx +1 -0
  552. package/template/components/ui/context-menu.tsx +1 -0
  553. package/template/components/ui/dot-pattern.tsx +1 -183
  554. package/template/components/ui/hover-card.tsx +1 -0
  555. package/template/components/ui/resizable.tsx +1 -68
  556. package/template/components/ui/scroll-area.tsx +1 -0
  557. package/template/components/ui/slider.tsx +1 -0
  558. package/template/docs/blueprints/README.md +86 -0
  559. package/template/docs/blueprints/_template.md +91 -0
  560. package/template/docs/blueprints/board-card.md +123 -0
  561. package/template/docs/blueprints/data-table.md +139 -0
  562. package/template/docs/blueprints/key-metrics.md +128 -0
  563. package/template/docs/blueprints/list-page-template.md +123 -0
  564. package/template/docs/blueprints/page-header.md +130 -0
  565. package/template/docs/command-menu-pattern.md +1 -1
  566. package/template/docs/component-selection-guide.md +224 -0
  567. package/template/docs/components-audit-2026-05.md +158 -0
  568. package/template/docs/data-views-pattern.md +14 -14
  569. package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +95 -0
  570. package/template/docs/migrations/0002-exxat-token-namespace.md +154 -0
  571. package/template/docs/migrations/0003-globals-css-canonical.md +110 -0
  572. package/template/docs/migrations/README.md +100 -0
  573. package/template/docs/migrations/_template.md +64 -0
  574. package/template/docs/token-taxonomy.md +416 -0
  575. package/template/eslint.config.mjs +27 -0
  576. package/template/hooks/use-secondary-panel-hub-nav.ts +1 -1
  577. package/template/lib/command-menu-config.ts +0 -1
  578. package/template/lib/compliance-supported-views.ts +10 -0
  579. package/template/lib/conditional-rule-match.ts +6 -97
  580. package/template/lib/data-list-display-options.ts +1 -35
  581. package/template/lib/data-list-view-registry.ts +1 -0
  582. package/template/lib/data-list-view-surface.ts +1 -69
  583. package/template/lib/data-list-view.ts +1 -38
  584. package/template/lib/dev-log.ts +1 -8
  585. package/template/lib/editable-target.ts +1 -10
  586. package/template/lib/hub-connected-view-renderers.ts +58 -0
  587. package/template/lib/list-hub-supported-views.ts +10 -0
  588. package/template/lib/list-page-table-properties.ts +1 -52
  589. package/template/lib/mock/navigation.tsx +0 -8
  590. package/template/lib/mock/placements.ts +0 -7
  591. package/template/lib/placement-board-card-layout.ts +41 -41
  592. package/template/lib/placements-supported-views.ts +12 -0
  593. package/template/lib/question-bank-supported-views.ts +12 -0
  594. package/template/lib/raf-throttle.ts +1 -45
  595. package/template/lib/row-height.ts +4 -10
  596. package/template/lib/sidebar-state-cookie.ts +11 -2
  597. package/template/lib/sites-supported-views.ts +10 -0
  598. package/template/lib/team-supported-views.ts +10 -0
  599. package/template/package.json +1 -0
  600. package/template/tests/setup.ts +25 -0
  601. package/src/theme.css +0 -1132
  602. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  603. package/template/app/(app)/data-list/new/page.tsx +0 -34
  604. package/template/app/(app)/data-list/page.tsx +0 -10
  605. package/template/components/compliance-list-view.tsx +0 -54
  606. package/template/components/dashboard-onboarding-gallery.tsx +0 -13
  607. package/template/components/dashboard-onboarding.tsx +0 -21
  608. package/template/components/question-bank-list-view.tsx +0 -53
  609. package/template/components/section-cards.tsx +0 -106
  610. package/template/components/sites-list-view.tsx +0 -42
  611. package/template/components/team-list-view.tsx +0 -59
  612. package/template/lib/placement-lifecycle.ts +0 -5
  613. /package/template/components/{getting-started.tsx → onboarding/getting-started.tsx} +0 -0
  614. /package/template/components/{nav-documents.tsx → sidebar/nav-documents.tsx} +0 -0
  615. /package/template/components/{nav-main.tsx → sidebar/nav-main.tsx} +0 -0
  616. /package/template/components/{nav-secondary.tsx → sidebar/nav-secondary.tsx} +0 -0
  617. /package/template/components/{nav-user.tsx → sidebar/nav-user.tsx} +0 -0
  618. /package/template/components/{sidebar-auto-open.tsx → sidebar/sidebar-auto-open.tsx} +0 -0
@@ -1,1634 +1 @@
1
- "use client"
2
-
3
- /**
4
- * DataTable<TData> — generic reusable table (no pagination)
5
- *
6
- * Column features:
7
- * • Resizable — drag right-edge handle on any non-locked column
8
- * • Drag-to-reorder — drag header cell for free (unpinned) columns
9
- * • Pin Left / Pin Right / Unpin — per-column context menu
10
- * • Sort Asc / Desc — per-column context menu (sortable columns)
11
- * • Wrap Text / Unwrap — per-column context menu
12
- * • Per-column quick search
13
- * • Row selection (checkboxes + floating bulk action bar)
14
- * • Group by (collapsible group rows)
15
- * • Hidden columns
16
- *
17
- * WCAG 2.1 AA:
18
- * ✓ aria-sort on sortable <th>
19
- * ✓ aria-label on every icon-only button
20
- * ✓ Select / Actions columns: sr-only header text + resolved labels for controls
21
- * ✓ Row checkboxes: visible on row focus-within, stop row click propagation (default control size; extended hit slop on Checkbox)
22
- * ✓ Bulk-action bar: role="status" aria-live="polite"
23
- * ✓ Resize handles: role="separator" aria-label
24
- */
25
-
26
- import * as React from "react"
27
- import { useTheme } from "next-themes"
28
- import { createPortal } from "react-dom"
29
- import { cn } from "@/lib/utils"
30
- import { rafThrottle } from "@/lib/raf-throttle"
31
- import { Button } from "@/components/ui/button"
32
- import { Input } from "@/components/ui/input"
33
- import { Kbd, KbdGroup } from "@/components/ui/kbd"
34
- import { Tip } from "@/components/ui/tip"
35
- import { useModKeyLabel } from "@/hooks/use-mod-key-label"
36
- import { isEditableTarget } from "@/lib/editable-target"
37
- import { Checkbox } from "@/components/ui/checkbox"
38
- import {
39
- DropdownMenu,
40
- DropdownMenuContent,
41
- DropdownMenuItem,
42
- DropdownMenuLabel,
43
- DropdownMenuSeparator,
44
- DropdownMenuTrigger,
45
- } from "@/components/ui/dropdown-menu"
46
- import {
47
- Popover,
48
- PopoverAnchor,
49
- PopoverContent,
50
- PopoverTrigger,
51
- } from "@/components/ui/popover"
52
- import {
53
- Tooltip,
54
- TooltipContent,
55
- TooltipProvider,
56
- TooltipTrigger,
57
- } from "@/components/ui/tooltip"
58
- import { OPERATOR_LABELS } from "@/components/table-properties/types"
59
- import type { ActiveFilter } from "@/components/table-properties/types"
60
- import { getConditionalCellBackground } from "@/lib/conditional-rule-match"
61
- import { formatYmdForDisplay } from "@/lib/date-filter"
62
- import { FilterDateCalendar } from "@/components/data-table/filter-date-calendar"
63
- import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
64
- import type { DataTableProps, ColumnDef, SortDir } from "./types"
65
- import { useTableState } from "./use-table-state"
66
-
67
- /** When `ColumnDef.label` is empty, use a standard name for select/actions columns. */
68
- function defaultColumnHeaderLabel(key: string): string | undefined {
69
- switch (key) {
70
- case "select":
71
- return "Select"
72
- case "actions":
73
- return "Actions"
74
- default:
75
- return undefined
76
- }
77
- }
78
-
79
- function resolvedColumnLabel<TData>(col: ColumnDef<TData>): string {
80
- const t = col.label?.trim()
81
- if (t) return t
82
- return defaultColumnHeaderLabel(col.key) ?? col.key
83
- }
84
-
85
- // ─────────────────────────────────────────────────────────────────────────────
86
- // Internal sub-components
87
- // ─────────────────────────────────────────────────────────────────────────────
88
-
89
- const SortChevron = React.memo(function SortChevron({ dir }: { dir: SortDir }) {
90
- return (
91
- <i className={`fa-solid fa-arrow-${dir === "asc" ? "up" : "down"} ml-1 text-xs`} aria-hidden="true" />
92
- )
93
- })
94
-
95
- // ─────────────────────────────────────────────────────────────────────────────
96
- // FilterPill — active filter pill with inline editor popover
97
- // (driven by ColumnDef.filter config rather than FILTER_FIELDS)
98
- // ─────────────────────────────────────────────────────────────────────────────
99
-
100
- interface FilterPillProps<TData> {
101
- filter: ActiveFilter
102
- columns: ColumnDef<TData>[]
103
- defaultOpen?: boolean
104
- onUpdate: (id: string, patch: Partial<ActiveFilter>) => void
105
- onRemove: (id: string) => void
106
- /** Optional custom cell renderer for filter option values */
107
- renderOptionValue?: (fieldKey: string, value: string) => React.ReactNode
108
- }
109
-
110
- function FilterPillBase<TData>({
111
- filter,
112
- columns,
113
- defaultOpen = false,
114
- onUpdate,
115
- onRemove,
116
- renderOptionValue,
117
- }: FilterPillProps<TData>) {
118
- const [open, setOpen] = React.useState(false)
119
- const [optSearch, setOptSearch] = React.useState("")
120
- const justAutoOpenedRef = React.useRef(false)
121
-
122
- React.useEffect(() => {
123
- if (defaultOpen) {
124
- justAutoOpenedRef.current = true
125
- const t = setTimeout(() => {
126
- setOpen(true)
127
- setTimeout(() => { justAutoOpenedRef.current = false }, 400)
128
- }, 0)
129
- return () => clearTimeout(t)
130
- }
131
- // eslint-disable-next-line react-hooks/exhaustive-deps
132
- }, [])
133
-
134
- const col = columns.find(c => c.key === filter.fieldKey)
135
- const filterDef = col?.filter
136
-
137
- React.useEffect(() => {
138
- if (!filterDef) return
139
- if (filterDef.type !== "select" && filterDef.type !== "date") return
140
- if (filter.operator !== "is" && filter.operator !== "is_not") {
141
- onUpdate(filter.id, { operator: "is" })
142
- }
143
- }, [filter.id, filterDef, filter.operator, onUpdate])
144
-
145
- if (!filterDef) return null
146
-
147
- const options = filterDef.options ?? []
148
- const showSearch = options.length > 8
149
- const filteredOpts = optSearch
150
- ? options.filter(o => o.label.toLowerCase().includes(optSearch.toLowerCase()))
151
- : options
152
-
153
- const operators = filterDef.operators ?? (
154
- filterDef.type === "select" || filterDef.type === "date"
155
- ? (["is", "is_not"] as const)
156
- : (["contains", "not_contains"] as const)
157
- )
158
-
159
- const valueLabel = (() => {
160
- if (filterDef.type === "select") {
161
- if (filter.values.length === 0) return "…"
162
- if (filter.values.length === 1) {
163
- return options.find(o => o.value === filter.values[0])?.label ?? filter.values[0]
164
- }
165
- return `${filter.values.length} selected`
166
- }
167
- if (filterDef.type === "date") {
168
- const ymd = filter.values[0]
169
- return ymd ? formatYmdForDisplay(ymd) : "…"
170
- }
171
- return filter.values[0] || "…"
172
- })()
173
-
174
- function toggleValue(val: string) {
175
- const next = filter.values.includes(val)
176
- ? filter.values.filter(v => v !== val)
177
- : [...filter.values, val]
178
- onUpdate(filter.id, { values: next })
179
- }
180
-
181
- function cycleOperator() {
182
- const idx = operators.indexOf(filter.operator as typeof operators[number])
183
- const i = idx === -1 ? 0 : idx
184
- onUpdate(filter.id, { operator: operators[(i + 1) % operators.length] })
185
- }
186
-
187
- const isActive =
188
- filterDef.type === "date"
189
- ? Boolean(filter.values[0])
190
- : filter.values.length > 0
191
- const hasSelection = filter.values.length > 0
192
- const iconClass = filterDef.icon ? `fa-light ${filterDef.icon}` : "fa-light fa-filter"
193
-
194
- return (
195
- <Popover open={open} onOpenChange={setOpen}>
196
- <PopoverAnchor asChild>
197
- <div
198
- className={cn(
199
- "inline-flex cursor-pointer items-center rounded border text-xs transition-colors",
200
- isActive ? "border-brand/45 bg-brand/10" : "border-input bg-background"
201
- )}
202
- >
203
- <PopoverTrigger asChild>
204
- <button
205
- type="button"
206
- className={cn(
207
- "inline-flex cursor-pointer items-center gap-1 h-6 pl-2 pr-1.5 rounded-l transition-colors",
208
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
209
- isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
210
- )}
211
- >
212
- <i
213
- className={cn(iconClass, "text-xs", isActive ? "text-brand" : "text-muted-foreground")}
214
- aria-hidden="true"
215
- />
216
- <span className="text-foreground">{col.label}</span>
217
- {isActive && <span className="text-foreground font-medium">{valueLabel}</span>}
218
- </button>
219
- </PopoverTrigger>
220
- <button
221
- type="button"
222
- aria-label={`Remove ${col.label} filter`}
223
- onClick={() => onRemove(filter.id)}
224
- className={cn(
225
- "inline-flex cursor-pointer items-center justify-center h-6 w-5 rounded-r transition-colors",
226
- "text-muted-foreground hover:text-destructive",
227
- isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
228
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
229
- )}
230
- >
231
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
232
- </button>
233
- </div>
234
- </PopoverAnchor>
235
-
236
- <PopoverContent
237
- className={cn(
238
- "p-0",
239
- filterDef.type === "date"
240
- ? "w-auto max-w-[min(calc(100vw-2rem),22rem)]"
241
- : "w-64",
242
- )}
243
- align="start"
244
- onFocusOutside={e => e.preventDefault()}
245
- onInteractOutside={e => {
246
- if (justAutoOpenedRef.current) {
247
- e.preventDefault()
248
- justAutoOpenedRef.current = false
249
- }
250
- }}
251
- >
252
- <div className="flex items-center justify-between px-3 py-2 border-b border-border">
253
- <div className="flex items-center gap-1 text-sm text-foreground">
254
- <span className="font-medium">{col.label}</span>
255
- <button
256
- type="button"
257
- onClick={cycleOperator}
258
- className="inline-flex items-center gap-0.5 text-muted-foreground hover:text-interactive-hover-foreground transition-colors rounded px-1 py-0.5 hover:bg-interactive-hover"
259
- >
260
- {OPERATOR_LABELS[filter.operator]}
261
- <i className="fa-light fa-chevron-down text-xs" aria-hidden="true" />
262
- </button>
263
- </div>
264
- <button
265
- type="button"
266
- aria-label="Remove filter"
267
- onClick={() => onRemove(filter.id)}
268
- className="text-muted-foreground hover:text-destructive transition-colors p-1 rounded hover:bg-interactive-hover"
269
- >
270
- <i className="fa-light fa-trash text-xs" aria-hidden="true" />
271
- </button>
272
- </div>
273
-
274
- {filterDef.type === "date" && (
275
- <div className="p-2">
276
- <FilterDateCalendar
277
- label={`${col.label} — choose date`}
278
- valueYmd={filter.values[0]}
279
- onChangeYmd={(ymd) =>
280
- onUpdate(filter.id, { values: ymd ? [ymd] : [] })
281
- }
282
- />
283
- </div>
284
- )}
285
-
286
- {filterDef.type === "select" && (
287
- <div className="py-1 max-h-64 overflow-y-auto">
288
- {showSearch && (
289
- <div className="px-2 pt-1 pb-1">
290
- <div className="relative">
291
- <Input
292
- type="text"
293
- placeholder="Search options…"
294
- value={optSearch}
295
- onChange={e => setOptSearch(e.target.value)}
296
- className={cn("h-7 text-xs", optSearch ? "pr-8" : "pr-2")}
297
- autoFocus
298
- />
299
- {optSearch ? (
300
- <button
301
- type="button"
302
- aria-label="Clear option search"
303
- onClick={() => setOptSearch("")}
304
- className="absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
305
- >
306
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
307
- </button>
308
- ) : null}
309
- </div>
310
- </div>
311
- )}
312
- {filteredOpts.map(opt => {
313
- const checked = filter.values.includes(opt.value)
314
- return (
315
- <div
316
- key={opt.value}
317
- role="option"
318
- aria-selected={checked}
319
- tabIndex={0}
320
- onClick={() => toggleValue(opt.value)}
321
- onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleValue(opt.value) } }}
322
- className="flex w-full items-center gap-2.5 px-3 py-1.5 text-sm hover:bg-interactive-hover transition-colors cursor-pointer select-none focus-visible:outline-none focus-visible:bg-interactive-hover"
323
- >
324
- <span
325
- aria-hidden="true"
326
- data-slot="checkbox"
327
- data-state={checked ? "checked" : "unchecked"}
328
- className={cn(
329
- "inline-flex items-center justify-center size-3.5 shrink-0 rounded-[4px] border transition-colors",
330
- checked ? "bg-primary border-primary text-primary-foreground" : "border-input bg-background"
331
- )}
332
- >
333
- {checked && <i className="fa-solid fa-check text-current" style={{ fontSize: "8px" }} />}
334
- </span>
335
- {renderOptionValue
336
- ? renderOptionValue(filter.fieldKey, opt.value)
337
- : <span className="text-foreground">{opt.label}</span>
338
- }
339
- </div>
340
- )
341
- })}
342
- {filteredOpts.length === 0 && (
343
- <p className="px-3 py-2 text-xs text-muted-foreground">No options found</p>
344
- )}
345
- </div>
346
- )}
347
-
348
- {filterDef.type === "text" && (
349
- <div className="p-2">
350
- <FilterTextValueInput
351
- mask={filterDef.textMask}
352
- placeholder={`Enter ${col.label.toLowerCase()}…`}
353
- value={filter.values[0] ?? ""}
354
- onValueChange={next => onUpdate(filter.id, { values: [next] })}
355
- aria-label={`${col.label} filter value`}
356
- className="h-8 text-xs focus-visible:border-ring focus-visible:ring-ring/50"
357
- autoFocus
358
- />
359
- </div>
360
- )}
361
- {hasSelection ? (
362
- <div className="sticky bottom-0 border-t border-border bg-popover p-2">
363
- <Button
364
- type="button"
365
- variant="outline"
366
- size="sm"
367
- onClick={() => onUpdate(filter.id, { values: [] })}
368
- className="w-full justify-center gap-1.5 text-xs text-muted-foreground"
369
- >
370
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
371
- Clear selection
372
- </Button>
373
- </div>
374
- ) : null}
375
- </PopoverContent>
376
- </Popover>
377
- )
378
- }
379
-
380
- // React.memo wrapper — preserves generic signature via cast.
381
- // FilterPillBase is a pure function of its props; memoizing it prevents
382
- // re-renders when unrelated table state (hover, scroll) changes.
383
- const FilterPill = React.memo(FilterPillBase) as typeof FilterPillBase
384
-
385
- // ─────────────────────────────────────────────────────────────────────────────
386
- // Sticky shadow utility
387
- // ─────────────────────────────────────────────────────────────────────────────
388
-
389
- function stickyShadow(pin: "left" | "right" | undefined): string {
390
- if (!pin) return ""
391
- const base = "after:content-[''] after:absolute after:top-0 after:bottom-0 after:w-3 after:pointer-events-none"
392
- if (pin === "left") {
393
- return cn(
394
- base,
395
- "after:left-full",
396
- "after:bg-[linear-gradient(to_right,var(--sticky-edge-fade),transparent)]",
397
- )
398
- }
399
- return cn(
400
- base,
401
- "after:right-full",
402
- "after:bg-[linear-gradient(to_left,var(--sticky-edge-fade),transparent)]",
403
- )
404
- }
405
-
406
- // ─────────────────────────────────────────────────────────────────────────────
407
- // DataTableToolbar — search, filter bar, properties slot (shared by table + board)
408
- // ─────────────────────────────────────────────────────────────────────────────
409
-
410
- export function DataTableToolbar<TData extends Record<string, unknown>>({
411
- state,
412
- columns,
413
- searchable = true,
414
- /** When false, hides filter pills, search, and filter controls (e.g. dashboard canvas edit mode). */
415
- showQueryControls = true,
416
- renderFilterOptionValue,
417
- toolbarSlot,
418
- searchAriaLabel = "Search table",
419
- }: {
420
- state: ReturnType<typeof useTableState<TData>>
421
- columns: ColumnDef<TData>[]
422
- searchable?: boolean
423
- showQueryControls?: boolean
424
- renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
425
- toolbarSlot?: (state: ReturnType<typeof useTableState<TData>>) => React.ReactNode
426
- /** Passed to the search input `aria-label` (e.g. "Search placements") */
427
- searchAriaLabel?: string
428
- }) {
429
- const {
430
- search, setSearch, searchOpen, setSearchOpen, searchRef,
431
- activeFilters, setActiveFilters, openFilterId,
432
- filterBarVisible, setFilterBarVisible,
433
- addFilter, updateFilter, removeFilter,
434
- } = state
435
-
436
- const filterableCols = columns.filter(c => c.filter)
437
- const searchModLabel = useModKeyLabel()
438
- const effectiveSearchable = showQueryControls && searchable
439
-
440
- React.useEffect(() => {
441
- if (!effectiveSearchable) return
442
- function onGlobalKeyDown(e: KeyboardEvent) {
443
- if (!e.metaKey && !e.ctrlKey) return
444
- if (e.altKey) return
445
- if (e.key.toLowerCase() !== "k") return
446
- if (isEditableTarget(e.target)) return
447
- e.preventDefault()
448
- setSearchOpen(true)
449
- queueMicrotask(() => searchRef.current?.focus())
450
- }
451
- document.addEventListener("keydown", onGlobalKeyDown)
452
- return () => document.removeEventListener("keydown", onGlobalKeyDown)
453
- }, [effectiveSearchable, setSearchOpen, searchRef])
454
-
455
- return (
456
- <div
457
- className={cn(
458
- "flex items-center gap-1.5 px-4 lg:px-6",
459
- showQueryControls ? "min-h-10 pt-2 pb-2" : "min-h-0 justify-end py-1.5",
460
- )}
461
- >
462
-
463
- {showQueryControls && filterBarVisible && filterableCols.length > 0 && (
464
- <div className="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
465
- {activeFilters.map(filter => (
466
- <React.Fragment key={filter.id}>
467
- <FilterPill
468
- filter={filter}
469
- columns={columns}
470
- defaultOpen={filter.id === openFilterId}
471
- onUpdate={updateFilter}
472
- onRemove={removeFilter}
473
- renderOptionValue={renderFilterOptionValue}
474
- />
475
- </React.Fragment>
476
- ))}
477
-
478
- <DropdownMenu>
479
- <DropdownMenuTrigger asChild>
480
- <button type="button"
481
- className="inline-flex cursor-pointer items-center gap-1 h-6 px-2 rounded text-xs text-muted-foreground hover:text-interactive-hover-foreground border border-dashed border-input/70 hover:border-input hover:bg-interactive-hover-subtle transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
482
- >
483
- <i className="fa-light fa-plus text-xs" aria-hidden="true" />
484
- Add filter
485
- </button>
486
- </DropdownMenuTrigger>
487
- <DropdownMenuContent align="start">
488
- <DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
489
- <DropdownMenuSeparator />
490
- {filterableCols.map(c => (
491
- <DropdownMenuItem key={c.key} onClick={() => addFilter(c.key)}>
492
- {c.filter?.icon && <i className={`fa-light ${c.filter.icon}`} aria-hidden="true" />}
493
- {c.label}
494
- </DropdownMenuItem>
495
- ))}
496
- </DropdownMenuContent>
497
- </DropdownMenu>
498
-
499
- {activeFilters.length > 0 && (
500
- <button
501
- type="button"
502
- onClick={() => setActiveFilters([])}
503
- className="cursor-pointer text-xs text-muted-foreground hover:text-interactive-hover-foreground transition-colors px-1"
504
- >
505
- Clear all
506
- </button>
507
- )}
508
- </div>
509
- )}
510
-
511
- <div
512
- className={cn(
513
- "flex items-center gap-1 shrink-0",
514
- showQueryControls && "ml-auto",
515
- )}
516
- >
517
-
518
- {effectiveSearchable && (
519
- searchOpen ? (
520
- <div className="relative flex items-center">
521
- <i className="fa-light fa-magnifying-glass absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground text-xs pointer-events-none" aria-hidden="true" />
522
- <Input
523
- ref={searchRef}
524
- type="text"
525
- role="searchbox"
526
- inputMode="search"
527
- autoComplete="off"
528
- placeholder="Search…"
529
- value={search}
530
- onChange={e => setSearch(e.target.value)}
531
- onBlur={() => { if (!search) setSearchOpen(false) }}
532
- onKeyDown={e => { if (e.key === "Escape") { setSearch(""); setSearchOpen(false) } }}
533
- className={cn("h-8 w-48 pl-7 text-xs", search ? "pr-8" : "pr-2")}
534
- aria-label={searchAriaLabel}
535
- />
536
- {search ? (
537
- <button
538
- type="button"
539
- aria-label="Clear search"
540
- onClick={() => setSearch("")}
541
- className="absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex cursor-pointer size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
542
- >
543
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
544
- </button>
545
- ) : null}
546
- </div>
547
- ) : (
548
- <TooltipProvider>
549
- <Tooltip>
550
- <TooltipTrigger asChild>
551
- <button type="button" aria-label="Search"
552
- onClick={() => { setSearchOpen(true); setTimeout(() => searchRef.current?.focus(), 10) }}
553
- className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
554
- >
555
- <i className="fa-light fa-magnifying-glass text-[13px]" aria-hidden="true" />
556
- </button>
557
- </TooltipTrigger>
558
- <TooltipContent side="bottom">
559
- <span>{searchAriaLabel}</span>
560
- <KbdGroup>
561
- <Kbd>{searchModLabel}</Kbd>
562
- <Kbd>K</Kbd>
563
- </KbdGroup>
564
- </TooltipContent>
565
- </Tooltip>
566
- </TooltipProvider>
567
- )
568
- )}
569
-
570
- {showQueryControls && filterableCols.length > 0 && (
571
- <>
572
- <div className="h-4 w-px bg-border/70" aria-hidden="true" />
573
- <TooltipProvider>
574
- <Tooltip>
575
- <TooltipTrigger asChild>
576
- {activeFilters.length > 0 ? (
577
- <button type="button"
578
- aria-label={filterBarVisible ? "Hide filters" : "Show filters"}
579
- onClick={() => setFilterBarVisible(v => !v)}
580
- className={cn(
581
- "inline-flex shrink-0 cursor-pointer items-center gap-1 size-8 justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
582
- filterBarVisible
583
- ? "bg-accent text-accent-foreground hover:bg-accent/90"
584
- : "text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover",
585
- )}
586
- >
587
- <i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
588
- <span className="text-xs font-semibold tabular-nums">{activeFilters.length}</span>
589
- </button>
590
- ) : (
591
- <DropdownMenu>
592
- <DropdownMenuTrigger asChild>
593
- <button type="button" aria-label="Add filter"
594
- onClick={() => setFilterBarVisible(true)}
595
- className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
596
- >
597
- <i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
598
- </button>
599
- </DropdownMenuTrigger>
600
- <DropdownMenuContent align="end">
601
- <DropdownMenuLabel className="text-xs">Filter by field</DropdownMenuLabel>
602
- <DropdownMenuSeparator />
603
- {filterableCols.map(c => (
604
- <DropdownMenuItem key={c.key} onClick={() => addFilter(c.key)}>
605
- {c.filter?.icon && <i className={`fa-light ${c.filter.icon}`} aria-hidden="true" />}
606
- {c.label}
607
- </DropdownMenuItem>
608
- ))}
609
- </DropdownMenuContent>
610
- </DropdownMenu>
611
- )}
612
- </TooltipTrigger>
613
- <TooltipContent side="bottom">
614
- {activeFilters.length > 0
615
- ? (filterBarVisible ? "Hide filters" : "Show filters")
616
- : "Filter"}
617
- </TooltipContent>
618
- </Tooltip>
619
- </TooltipProvider>
620
- </>
621
- )}
622
-
623
- {toolbarSlot && toolbarSlot(state)}
624
- </div>
625
- </div>
626
- )
627
- }
628
-
629
- // ─────────────────────────────────────────────────────────────────────────────
630
- // DataTable<TData>
631
- // ─────────────────────────────────────────────────────────────────────────────
632
-
633
- export interface DataTableExtendedProps<TData extends Record<string, unknown>>
634
- extends DataTableProps<TData> {
635
- /** Slot for a toolbar drawer button + drawer itself (e.g. TablePropertiesDrawer) */
636
- toolbarSlot?: (state: ReturnType<typeof useTableState<TData>>) => React.ReactNode
637
- /** Slot rendered inside the floating bulk-action bar (after the "N selected" label) */
638
- bulkActionsSlot?: (selected: Set<string | number>, rows: TData[]) => React.ReactNode
639
- /** Optional "add new row" row text — pass false to hide */
640
- addRowLabel?: string | false
641
- /** Custom option-value renderer for filter pills */
642
- renderFilterOptionValue?: (fieldKey: string, value: string) => React.ReactNode
643
- /** When set by DataTablePaginated — drives row slicing inside useTableState */
644
- paginationOverride?: { page: number; pageSize: number }
645
- /** When true, removes rounded bottom corners so a pagination bar can attach flush */
646
- hasFooter?: boolean
647
- /** Conditional formatting rules — apply bg color to cells based on value */
648
- conditionalRules?: import("./types").ConditionalRule[]
649
- /** When false, the column header row is hidden (Display options). */
650
- showColumnHeaders?: boolean
651
- /** When set, table uses this state (e.g. shared with board view) instead of internal useTableState. */
652
- state?: ReturnType<typeof useTableState<TData>>
653
- }
654
-
655
- type DataTableInnerProps<TData extends Record<string, unknown>> = DataTableExtendedProps<TData> & {
656
- state: ReturnType<typeof useTableState<TData>>
657
- }
658
-
659
- /** Max width for bulk bar in normal (non-reflow) zoom — ~28rem, centered in table. */
660
- const BULK_BAR_MAX_PX = 448
661
-
662
- /**
663
- * When the app theme is `dark`, the bulk strip is a **light** surface; shadcn
664
- * “dark:” button tokens are wrong — reapply light-look solid/outline/destructive/ghost.
665
- */
666
- const BULK_BAR_ON_LIGHT_STRIP = cn(
667
- "[&_button[data-variant=default]]:bg-zinc-900 [&_button[data-variant=default]]:text-zinc-50",
668
- "hover:[&_button[data-variant=default]]:bg-zinc-800",
669
- "[&_button[data-variant=outline]]:border-zinc-300/80 [&_button[data-variant=outline]]:bg-white [&_button[data-variant=outline]]:text-zinc-900",
670
- "hover:[&_button[data-variant=outline]]:bg-zinc-100",
671
- "[&_button[data-variant=destructive]]:border-rose-200/80 [&_button[data-variant=destructive]]:bg-rose-100 [&_button[data-variant=destructive]]:text-rose-800",
672
- "hover:[&_button[data-variant=destructive]]:bg-rose-200/40",
673
- "[&_button[data-variant=ghost]]:text-zinc-600 hover:[&_button[data-variant=ghost]]:bg-zinc-200/70 hover:[&_button[data-variant=ghost]]:text-zinc-900",
674
- )
675
-
676
- /**
677
- * Pins the bulk bar to the viewport bottom, aligned to the table scroll
678
- * wrapper. When `fullWidth` is false (normal zoom), width is
679
- * `min(tableWidth, 28rem)` and centered; when true (reflow), matches table
680
- * width.
681
- */
682
- function useBulkBarFixedToTableScrollEl(
683
- scrollRef: React.RefObject<HTMLDivElement | null>,
684
- active: boolean,
685
- fullWidth: boolean,
686
- ): React.CSSProperties | undefined {
687
- const [style, setStyle] = React.useState<React.CSSProperties | undefined>(undefined)
688
- React.useLayoutEffect(() => {
689
- if (!active) {
690
- setStyle(undefined)
691
- return
692
- }
693
- const el = scrollRef.current
694
- if (!el) {
695
- setStyle(undefined)
696
- return
697
- }
698
- const apply = () => {
699
- const r = el.getBoundingClientRect()
700
- let left = r.left
701
- let width = r.width
702
- if (!fullWidth) {
703
- const w = Math.min(r.width, BULK_BAR_MAX_PX)
704
- left = r.left + (r.width - w) / 2
705
- width = w
706
- }
707
- setStyle({
708
- position: "fixed",
709
- left,
710
- width,
711
- bottom: "max(0.5rem, env(safe-area-inset-bottom, 0px))",
712
- zIndex: 50,
713
- boxSizing: "border-box",
714
- margin: 0,
715
- right: "auto",
716
- })
717
- }
718
- apply()
719
- // rAF-coalesce so a single frame handles bursts of capture-phase scroll
720
- // events plus the ResizeObserver firing — instead of N getBoundingClientRect
721
- // + setState per second.
722
- const scheduled = rafThrottle(apply)
723
- const ro = new ResizeObserver(scheduled)
724
- ro.observe(el)
725
- window.addEventListener("resize", scheduled, { passive: true })
726
- window.addEventListener("scroll", scheduled, { passive: true, capture: true })
727
- return () => {
728
- scheduled.cancel()
729
- ro.disconnect()
730
- window.removeEventListener("resize", scheduled)
731
- window.removeEventListener("scroll", scheduled, { capture: true })
732
- }
733
- }, [active, fullWidth, scrollRef])
734
- return style
735
- }
736
-
737
- function DataTableInner<TData extends Record<string, unknown>>({
738
- // `data` / `defaultSort` flow into `useTableState` upstream; the inner table
739
- // reads them via `state` and never directly here. Keep the prop slots so
740
- // the public `DataTable<TData>` API stays unchanged.
741
- data: _data,
742
- columns,
743
- getRowId: getRowIdProp,
744
- getRowSelectionLabel,
745
- selectable = true,
746
- searchable = true,
747
- emptyState,
748
- onRowClick,
749
- defaultSort: _defaultSort,
750
- toolbarSlot,
751
- bulkActionsSlot,
752
- addRowLabel = false,
753
- renderFilterOptionValue,
754
- hasFooter = false,
755
- conditionalRules,
756
- showColumnHeaders = true,
757
- state,
758
- }: DataTableInnerProps<TData>) {
759
- const {
760
- setSortRules,
761
- sortKey, sortDir,
762
- handleSortByKey,
763
- addFilter,
764
- groupBy, setGroupBy,
765
- colMenuSearch, setColMenuSearch,
766
- selected, setSelected, toggleRow, toggleAll, getRowId,
767
- colWidths, startResize,
768
- colPins, lockedPins,
769
- pinColumn, unpinColumn,
770
- colWrap, toggleWrap,
771
- draggedKey, dragOverKey,
772
- handleDragStart, handleDragOver, handleDrop, handleDragEnd,
773
- scrollRef, handleScroll, checkOverflow,
774
- isOverflowing,
775
- setHoveredRow,
776
- rows, pagedRows, groupedRows,
777
- effectivePins, displayCols,
778
- isReflowViewport,
779
- stickyStyle,
780
- totalWidth,
781
- rowHeight,
782
- showGridlines,
783
- setSheetOpen,
784
- } = state
785
-
786
- // Mount overflow check + scrollport width for sticky group headers on horizontal scroll.
787
- React.useEffect(() => {
788
- const syncScrollport = () => {
789
- const el = scrollRef.current
790
- if (el) {
791
- el.style.setProperty("--dt-scrollport-width", `${el.clientWidth}px`)
792
- }
793
- checkOverflow()
794
- }
795
- syncScrollport()
796
- const el = scrollRef.current
797
- if (!el) return
798
- const ro = new ResizeObserver(syncScrollport)
799
- ro.observe(el)
800
- return () => ro.disconnect()
801
- // eslint-disable-next-line react-hooks/exhaustive-deps
802
- }, [])
803
-
804
- /** One-time horizontal nudge when the grid overflows and pins are active — hints that more columns scroll (overlay scrollbars, esp. Windows, are often invisible until interaction). */
805
- const pinnedScrollHintDoneRef = React.useRef(false)
806
- React.useEffect(() => {
807
- if (!isOverflowing || isReflowViewport || Object.keys(colPins).length === 0) return
808
- if (pinnedScrollHintDoneRef.current) return
809
- const el = scrollRef.current
810
- if (!el) return
811
- if (el.scrollLeft > 2) return
812
- const maxScroll = el.scrollWidth - el.clientWidth
813
- if (maxScroll < 16) return
814
- if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
815
- pinnedScrollHintDoneRef.current = true
816
- return
817
- }
818
-
819
- pinnedScrollHintDoneRef.current = true
820
- const delta = Math.min(96, Math.max(28, Math.round(maxScroll * 0.14)))
821
- const startDelayMs = 320
822
- const dwellMs = 520
823
-
824
- const t1 = window.setTimeout(() => {
825
- el.scrollTo({ left: delta, behavior: "smooth" })
826
- }, startDelayMs)
827
- const t2 = window.setTimeout(() => {
828
- el.scrollTo({ left: 0, behavior: "smooth" })
829
- }, startDelayMs + dwellMs)
830
-
831
- return () => {
832
- window.clearTimeout(t1)
833
- window.clearTimeout(t2)
834
- }
835
- }, [isOverflowing, isReflowViewport, colPins, scrollRef])
836
-
837
- const lastLeftPinKey = [...displayCols].reverse().find(c => effectivePins[c.key] === "left")?.key
838
- const firstRightPinKey = displayCols.find(c => effectivePins[c.key] === "right")?.key
839
-
840
- function floatingHeaderPinnedStyle(key: string): React.CSSProperties | undefined {
841
- const pin = effectivePins[key]
842
- if (!pin) return undefined
843
-
844
- const visibleWidth =
845
- typeof floatingHeaderStyle?.width === "number"
846
- ? floatingHeaderStyle.width
847
- : tableWrapRef.current?.clientWidth ?? floatingHeaderTableWidth
848
- const maxScroll = Math.max(0, floatingHeaderTableWidth - visibleWidth)
849
- const translateX = pin === "left"
850
- ? headerScrollLeft
851
- : headerScrollLeft - maxScroll
852
-
853
- // The floating sticky header is horizontally translated as one table.
854
- // Counter-translate pinned header cells so they remain locked to the viewport edge.
855
- return { position: "relative", transform: `translateX(${translateX}px)` }
856
- }
857
-
858
- // Row IDs for the current visible rows
859
- const allRowIds = rows.map((r, i) => getRowId(r, i, getRowIdProp))
860
- const allSelected = rows.length > 0 && selected.size === rows.length
861
- const someSelected = selected.size > 0 && !allSelected
862
- const anySelected = selected.size > 0
863
-
864
- const { resolvedTheme } = useTheme()
865
- const isAppDark = resolvedTheme === "dark"
866
-
867
- const bulkBarUseFixedLayout = anySelected
868
- /** Reflow: bar spans table width. Normal zoom: bar centered, max 28rem. */
869
- const bulkBarFixedStyle = useBulkBarFixedToTableScrollEl(
870
- scrollRef,
871
- bulkBarUseFixedLayout,
872
- isReflowViewport,
873
- )
874
- const tableWrapRef = React.useRef<HTMLDivElement | null>(null)
875
- const tableHeadRef = React.useRef<HTMLTableSectionElement | null>(null)
876
- const [headerIsStuck, setHeaderIsStuck] = React.useState(false)
877
- const [headerScrollLeft, setHeaderScrollLeft] = React.useState(0)
878
- const [floatingHeaderStyle, setFloatingHeaderStyle] = React.useState<React.CSSProperties | undefined>(undefined)
879
- const [floatingHeaderTableWidth, setFloatingHeaderTableWidth] = React.useState(totalWidth)
880
- const [isClient, setIsClient] = React.useState(false)
881
-
882
- React.useEffect(() => {
883
- setIsClient(true)
884
- }, [])
885
-
886
- React.useEffect(() => {
887
- const wrapEl = tableWrapRef.current
888
- const headEl = tableHeadRef.current
889
- if (!wrapEl || !headEl || !showColumnHeaders) {
890
- setHeaderIsStuck(false)
891
- return
892
- }
893
-
894
- const update = () => {
895
- const wrapRect = wrapEl.getBoundingClientRect()
896
- const headHeight = headEl.getBoundingClientRect().height || 0
897
- const rootStyle = getComputedStyle(document.documentElement)
898
- const headerOffset = Number.parseFloat(rootStyle.getPropertyValue("--header-height")) || 0
899
- const stuck = wrapRect.top <= headerOffset && wrapRect.bottom > (headHeight + headerOffset + 1)
900
- setHeaderIsStuck(prev => (prev === stuck ? prev : stuck))
901
- }
902
-
903
- update()
904
- // rAF-coalesce: capture-phase scroll fires for every ancestor (sidebar,
905
- // dashboard panels, anchored sheets), so a single getBoundingClientRect
906
- // per frame is more than enough to keep the sticky header aligned.
907
- const scheduled = rafThrottle(update)
908
- window.addEventListener("scroll", scheduled, { passive: true, capture: true })
909
- window.addEventListener("resize", scheduled, { passive: true })
910
- return () => {
911
- scheduled.cancel()
912
- window.removeEventListener("scroll", scheduled, { capture: true })
913
- window.removeEventListener("resize", scheduled)
914
- }
915
- }, [showColumnHeaders, rows.length, displayCols.length])
916
-
917
- React.useLayoutEffect(() => {
918
- if (!headerIsStuck || !showColumnHeaders) {
919
- setFloatingHeaderStyle(undefined)
920
- return
921
- }
922
- const wrapEl = tableWrapRef.current
923
- if (!wrapEl) {
924
- setFloatingHeaderStyle(undefined)
925
- return
926
- }
927
-
928
- const apply = () => {
929
- const rect = wrapEl.getBoundingClientRect()
930
- const rootStyle = getComputedStyle(document.documentElement)
931
- const headerOffset = Number.parseFloat(rootStyle.getPropertyValue("--header-height")) || 0
932
- const cs = getComputedStyle(wrapEl)
933
- const borderLeft = parseFloat(cs.borderLeftWidth) || 0
934
- const borderRight = parseFloat(cs.borderRightWidth) || 0
935
- const visibleWidth = Math.max(0, wrapEl.clientWidth - borderLeft - borderRight)
936
- const renderedTableWidth = Math.max(
937
- totalWidth,
938
- visibleWidth,
939
- wrapEl.querySelector("table")?.getBoundingClientRect().width ?? 0,
940
- )
941
- setFloatingHeaderStyle({
942
- position: "fixed",
943
- top: headerOffset,
944
- left: rect.left + borderLeft,
945
- width: visibleWidth,
946
- zIndex: 50,
947
- })
948
- setFloatingHeaderTableWidth(renderedTableWidth)
949
- setHeaderScrollLeft(wrapEl.scrollLeft)
950
- }
951
-
952
- apply()
953
- const scheduled = rafThrottle(apply)
954
- const ro = new ResizeObserver(scheduled)
955
- ro.observe(wrapEl)
956
- window.addEventListener("scroll", scheduled, { passive: true, capture: true })
957
- window.addEventListener("resize", scheduled, { passive: true })
958
- return () => {
959
- scheduled.cancel()
960
- ro.disconnect()
961
- window.removeEventListener("scroll", scheduled, { capture: true })
962
- window.removeEventListener("resize", scheduled)
963
- }
964
- }, [headerIsStuck, showColumnHeaders, totalWidth, displayCols.length])
965
-
966
- function ariaSortAttr(colKey: string): React.AriaAttributes["aria-sort"] {
967
- return sortKey !== colKey ? "none" : sortDir === "asc" ? "ascending" : "descending"
968
- }
969
-
970
- function cellStyle(key: string): React.CSSProperties {
971
- return stickyStyle(key)
972
- }
973
-
974
- // ─── Render ───────────────────────────────────────────────────────────────
975
- return (
976
- <div className="flex min-w-0 w-full flex-col gap-0">
977
-
978
- <DataTableToolbar
979
- state={state}
980
- columns={columns}
981
- searchable={searchable}
982
- renderFilterOptionValue={renderFilterOptionValue}
983
- toolbarSlot={toolbarSlot}
984
- searchAriaLabel="Search table"
985
- />
986
-
987
- {isClient && showColumnHeaders && headerIsStuck && floatingHeaderStyle
988
- ? createPortal(
989
- <div
990
- style={floatingHeaderStyle}
991
- className="pointer-events-auto"
992
- >
993
- <div className="overflow-hidden border border-border bg-dt-header-bg shadow-[0_10px_18px_-14px_rgba(15,23,42,0.5)] dark:shadow-[0_12px_20px_-14px_rgba(0,0,0,0.75)]">
994
- <div style={{ transform: `translateX(${-headerScrollLeft}px)` }}>
995
- <table
996
- className="w-full text-sm border-separate border-spacing-0"
997
- style={{ tableLayout: "fixed", width: floatingHeaderTableWidth }}
998
- >
999
- <colgroup>
1000
- {displayCols.map(col => (
1001
- <col key={col.key} style={{ width: colWidths[col.key] ?? col.width ?? 100 }} />
1002
- ))}
1003
- </colgroup>
1004
- <thead className="bg-dt-header-bg">
1005
- <tr>
1006
- {displayCols.map(col => {
1007
- const isPinned = !!effectivePins[col.key]
1008
- const isEdgePinCol = col.key === lastLeftPinKey || col.key === firstRightPinKey
1009
- return (
1010
- <th
1011
- key={col.key}
1012
- scope="col"
1013
- style={floatingHeaderPinnedStyle(col.key)}
1014
- className={cn(
1015
- "h-9 px-3 text-left align-middle select-none",
1016
- "text-xs font-medium text-muted-foreground tracking-wide",
1017
- "bg-dt-header-bg border-b border-border",
1018
- showGridlines && (!isEdgePinCol
1019
- ? "border-r border-border last:border-r-0"
1020
- : "last:border-r-0"),
1021
- isPinned ? "z-40" : "z-30",
1022
- isPinned && "relative",
1023
- isEdgePinCol && stickyShadow(effectivePins[col.key]),
1024
- )}
1025
- >
1026
- <div className="flex items-center justify-between gap-1 min-w-0">
1027
- <div className="flex items-center min-w-0 flex-1">
1028
- {col.key === "select" ? (
1029
- selectable && (
1030
- <span className="inline-flex items-center justify-center self-center">
1031
- <span className="sr-only">{resolvedColumnLabel(col)}</span>
1032
- <Checkbox
1033
- checked={allSelected ? true : someSelected ? "indeterminate" : false}
1034
- onCheckedChange={() => toggleAll(allRowIds)}
1035
- aria-label="Select all rows"
1036
- />
1037
- </span>
1038
- )
1039
- ) : col.sortable && col.sortKey ? (
1040
- <button
1041
- type="button"
1042
- onClick={() => handleSortByKey(col.key)}
1043
- className={cn(
1044
- "inline-flex items-center hover:text-interactive-hover-foreground transition-colors whitespace-nowrap",
1045
- sortKey === col.key && "text-foreground",
1046
- )}
1047
- >
1048
- {col.label?.trim() ? col.label : resolvedColumnLabel(col)}
1049
- {sortKey === col.key ? <SortChevron dir={sortDir} /> : null}
1050
- </button>
1051
- ) : (
1052
- <span className="truncate whitespace-nowrap">
1053
- {col.label?.trim()
1054
- ? col.label
1055
- : defaultColumnHeaderLabel(col.key) ?? col.key}
1056
- </span>
1057
- )}
1058
- </div>
1059
- </div>
1060
- </th>
1061
- )
1062
- })}
1063
- </tr>
1064
- </thead>
1065
- </table>
1066
- </div>
1067
- </div>
1068
- </div>,
1069
- document.body,
1070
- )
1071
- : null}
1072
-
1073
- {/* ── Table ────────────────────────────────────────────────────────── */}
1074
- <div
1075
- ref={el => {
1076
- tableWrapRef.current = el
1077
- scrollRef.current = el
1078
- }}
1079
- onScroll={e => {
1080
- handleScroll()
1081
- setHeaderScrollLeft((e.currentTarget as HTMLDivElement).scrollLeft)
1082
- }}
1083
- className={cn(
1084
- "mx-4 lg:mx-6 overflow-x-auto border border-border",
1085
- hasFooter ? "rounded-t-lg" : "rounded-lg",
1086
- )}
1087
- >
1088
- <table
1089
- className="w-full text-sm border-separate border-spacing-0"
1090
- style={{
1091
- tableLayout: "fixed",
1092
- minWidth: totalWidth,
1093
- width: headerIsStuck ? floatingHeaderTableWidth : undefined,
1094
- }}
1095
- >
1096
- <colgroup>
1097
- {displayCols.map(col => (
1098
- <col key={col.key} style={{ width: colWidths[col.key] ?? col.width ?? 100 }} />
1099
- ))}
1100
- </colgroup>
1101
-
1102
- {/* ── Table head ──────────────────────────────────────────────── */}
1103
- <thead
1104
- ref={tableHeadRef}
1105
- className={cn(
1106
- "bg-dt-header-bg",
1107
- headerIsStuck && "invisible",
1108
- !showColumnHeaders && "hidden"
1109
- )}
1110
- >
1111
- <tr>
1112
- {displayCols.map(col => {
1113
- const isPinned = !!effectivePins[col.key]
1114
- const isLocked = !!lockedPins[col.key]
1115
- const isFree = !colPins[col.key]
1116
- const isResizable = !isLocked || (col.key !== "select")
1117
-
1118
- const isEdgePinCol = col.key === lastLeftPinKey || col.key === firstRightPinKey
1119
-
1120
- return (
1121
- <th
1122
- key={col.key}
1123
- scope="col"
1124
- aria-sort={col.sortable && col.sortKey ? ariaSortAttr(col.sortKey as string) : undefined}
1125
- draggable={isFree}
1126
- onDragStart={isFree ? e => handleDragStart(col.key, e) : undefined}
1127
- onDragOver={isFree ? e => handleDragOver(col.key, e) : undefined}
1128
- onDrop={isFree ? () => handleDrop(col.key) : undefined}
1129
- onDragEnd={isFree ? handleDragEnd : undefined}
1130
- style={stickyStyle(col.key, false)}
1131
- className={cn(
1132
- "group/th relative h-9 px-3 text-left align-middle select-none",
1133
- "text-xs font-medium text-muted-foreground tracking-wide",
1134
- "bg-dt-header-bg border-b border-border",
1135
- showGridlines && (!isEdgePinCol
1136
- ? "border-r border-border last:border-r-0"
1137
- : "last:border-r-0"),
1138
- isPinned ? "z-40" : "z-30",
1139
- isFree && "cursor-grab active:cursor-grabbing",
1140
- dragOverKey === col.key && draggedKey.current !== col.key && "bg-accent/40",
1141
- isEdgePinCol && stickyShadow(effectivePins[col.key])
1142
- )}
1143
- >
1144
- <div className="flex items-center justify-between gap-1 min-w-0">
1145
- <div className="flex items-center min-w-0 flex-1">
1146
- {col.header ? (
1147
- col.header()
1148
- ) : col.key === "select" ? (
1149
- selectable && (
1150
- <span className="inline-flex items-center justify-center self-center">
1151
- <span className="sr-only">{resolvedColumnLabel(col)}</span>
1152
- <Checkbox
1153
- checked={allSelected ? true : someSelected ? "indeterminate" : false}
1154
- onCheckedChange={() => toggleAll(allRowIds)}
1155
- aria-label="Select all rows"
1156
- />
1157
- </span>
1158
- )
1159
- ) : col.sortable && col.sortKey ? (
1160
- <Tip label={`Sort by ${resolvedColumnLabel(col)}`} side="top">
1161
- <button
1162
- type="button"
1163
- onClick={() => handleSortByKey(col.key)}
1164
- className={cn(
1165
- "inline-flex items-center hover:text-interactive-hover-foreground transition-colors whitespace-nowrap",
1166
- sortKey === col.key && "text-foreground"
1167
- )}
1168
- >
1169
- {col.label?.trim() ? col.label : resolvedColumnLabel(col)}
1170
- {sortKey === col.key && <SortChevron dir={sortDir} />}
1171
- </button>
1172
- </Tip>
1173
- ) : (
1174
- <Tip label={resolvedColumnLabel(col)} side="top">
1175
- <span className="whitespace-nowrap">
1176
- {col.label?.trim() ? (
1177
- col.label
1178
- ) : defaultColumnHeaderLabel(col.key) ? (
1179
- <span className="sr-only">{defaultColumnHeaderLabel(col.key)}</span>
1180
- ) : (
1181
- <span className="sr-only">{col.key}</span>
1182
- )}
1183
- </span>
1184
- </Tip>
1185
- )}
1186
- </div>
1187
-
1188
- {/* Column context menu — not on checkbox or locked-right columns */}
1189
- {col.key !== "select" && !lockedPins[col.key]?.includes("right") && col.key !== (columns.find(c => c.lockPin && c.defaultPin === "right")?.key) && (
1190
- <DropdownMenu>
1191
- <Tip label="Column options" side="top">
1192
- <DropdownMenuTrigger asChild>
1193
- <button
1194
- type="button"
1195
- aria-label={`${resolvedColumnLabel(col)} column options`}
1196
- onClick={e => e.stopPropagation()}
1197
- className={cn(
1198
- "opacity-0 group-hover/th:opacity-100 group-focus-within/th:opacity-100",
1199
- "inline-flex shrink-0 items-center justify-center size-7 rounded-md",
1200
- "text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover-row",
1201
- "transition-opacity focus-visible:opacity-100",
1202
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
1203
- )}
1204
- >
1205
- <i className="fa-light fa-ellipsis-vertical text-xs" aria-hidden="true" />
1206
- </button>
1207
- </DropdownMenuTrigger>
1208
- </Tip>
1209
- <DropdownMenuContent align="start">
1210
-
1211
- {/* Column quick-search */}
1212
- <div className="px-2 pt-2 pb-1">
1213
- <div className="relative">
1214
- <i className="fa-light fa-magnifying-glass absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs pointer-events-none" aria-hidden="true" />
1215
- <Input
1216
- placeholder={`Search ${resolvedColumnLabel(col)}…`}
1217
- value={colMenuSearch[col.key] ?? ""}
1218
- onChange={e => setColMenuSearch(prev => ({ ...prev, [col.key]: e.target.value }))}
1219
- onKeyDown={e => e.stopPropagation()}
1220
- className="h-7 pl-6 text-xs"
1221
- />
1222
- {colMenuSearch[col.key] && (
1223
- <button
1224
- type="button"
1225
- aria-label="Clear search"
1226
- onClick={() => setColMenuSearch(prev => ({ ...prev, [col.key]: "" }))}
1227
- className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-interactive-hover-foreground transition-colors"
1228
- >
1229
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
1230
- </button>
1231
- )}
1232
- </div>
1233
- </div>
1234
- <DropdownMenuSeparator />
1235
-
1236
- {/* Pin options */}
1237
- {!isLocked && (
1238
- <>
1239
- <DropdownMenuItem
1240
- onClick={() => pinColumn(col.key, "left")}
1241
- disabled={colPins[col.key] === "left"}
1242
- >
1243
- <i className="fa-light fa-arrow-left-to-line" aria-hidden="true" />
1244
- Pin Left
1245
- </DropdownMenuItem>
1246
- <DropdownMenuItem
1247
- onClick={() => pinColumn(col.key, "right")}
1248
- disabled={colPins[col.key] === "right"}
1249
- >
1250
- <i className="fa-light fa-arrow-right-to-line" aria-hidden="true" />
1251
- Pin Right
1252
- </DropdownMenuItem>
1253
- {colPins[col.key] && (
1254
- <DropdownMenuItem onClick={() => unpinColumn(col.key)}>
1255
- <i className="fa-light fa-thumbtack-slash" aria-hidden="true" />
1256
- Unpin
1257
- </DropdownMenuItem>
1258
- )}
1259
- <DropdownMenuSeparator />
1260
- </>
1261
- )}
1262
-
1263
- {/* Sort options */}
1264
- {col.sortable && col.sortKey && (
1265
- <>
1266
- <DropdownMenuItem onClick={() => setSortRules(prev => {
1267
- const filtered = prev.filter(r => r.fieldKey !== col.key)
1268
- return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "asc" as const }, ...filtered]
1269
- })}>
1270
- <i className="fa-light fa-arrow-up-a-z text-xs shrink-0" aria-hidden="true" />
1271
- Sort Ascending
1272
- </DropdownMenuItem>
1273
- <DropdownMenuItem onClick={() => setSortRules(prev => {
1274
- const filtered = prev.filter(r => r.fieldKey !== col.key)
1275
- return [{ id: `sort-${Date.now()}`, fieldKey: col.key, direction: "desc" as const }, ...filtered]
1276
- })}>
1277
- <i className="fa-light fa-arrow-down-a-z text-xs shrink-0" aria-hidden="true" />
1278
- Sort Descending
1279
- </DropdownMenuItem>
1280
- <DropdownMenuSeparator />
1281
- </>
1282
- )}
1283
-
1284
- {/* Text wrap toggle */}
1285
- <DropdownMenuItem onClick={() => toggleWrap(col.key)}>
1286
- <i className="fa-light fa-text-width" aria-hidden="true" />
1287
- {colWrap[col.key] ? "Unwrap Text" : "Wrap Text"}
1288
- </DropdownMenuItem>
1289
-
1290
- {/* Filter / Group by */}
1291
- <DropdownMenuSeparator />
1292
- {col.filter && (
1293
- <DropdownMenuItem onClick={() => addFilter(col.key)}>
1294
- <i className="fa-light fa-filter" aria-hidden="true" />
1295
- Filter by this column
1296
- </DropdownMenuItem>
1297
- )}
1298
- <DropdownMenuItem
1299
- onClick={() => setGroupBy(groupBy === col.key ? null : col.key)}
1300
- >
1301
- <i className="fa-light fa-layer-group" aria-hidden="true" />
1302
- {groupBy === col.key ? "Remove Grouping" : "Group by this Column"}
1303
- </DropdownMenuItem>
1304
-
1305
- {/* Conditional rule shortcut */}
1306
- <DropdownMenuSeparator />
1307
- <DropdownMenuItem onClick={() => setSheetOpen(true)}>
1308
- <i className="fa-light fa-palette" aria-hidden="true" />
1309
- Add Conditional Rule
1310
- </DropdownMenuItem>
1311
-
1312
- </DropdownMenuContent>
1313
- </DropdownMenu>
1314
- )}
1315
- </div>
1316
-
1317
- {/* Resize handle */}
1318
- {isResizable && col.key !== "select" && (
1319
- <div
1320
- role="separator"
1321
- aria-label={`Resize ${resolvedColumnLabel(col)} column`}
1322
- aria-orientation="vertical"
1323
- onMouseDown={e => startResize(col.key, e)}
1324
- className="absolute right-0 top-1 bottom-1 w-1.5 cursor-col-resize rounded-full hover:bg-interactive-hover-foreground/50 active:bg-muted-foreground/70 transition-colors"
1325
- />
1326
- )}
1327
- </th>
1328
- )
1329
- })}
1330
- </tr>
1331
- </thead>
1332
-
1333
- {/* ── Table body ───────────────────────────────────────────────── */}
1334
- <tbody>
1335
- {(pagedRows !== rows
1336
- ? [{ groupKey: null as string | null, groupLabel: null as string | null, rows: pagedRows }]
1337
- : groupedRows
1338
- ).map(({ groupKey, groupLabel, rows: groupRows }) => (
1339
- <React.Fragment key={groupKey ?? "__all__"}>
1340
- {groupLabel && (
1341
- <tr>
1342
- <td colSpan={displayCols.length} className="p-0 border-b border-border bg-dt-group-bg">
1343
- <div
1344
- className={cn(
1345
- "sticky left-0 z-[25] px-4 py-1.5 text-xs font-semibold text-muted-foreground tracking-wide bg-dt-group-bg select-none",
1346
- !isReflowViewport && "shadow-[4px_0_8px_-4px_var(--sticky-edge-fade)]",
1347
- )}
1348
- style={{ width: "var(--dt-scrollport-width, 100%)" }}
1349
- >
1350
- {groupLabel}
1351
- <span className="ml-2 font-normal normal-case opacity-60 tracking-normal">
1352
- {groupRows.length} record{groupRows.length !== 1 ? "s" : ""}
1353
- </span>
1354
- </div>
1355
- </td>
1356
- </tr>
1357
- )}
1358
- {groupRows.map((row, rowIndex) => {
1359
- const rowId = getRowId(row, rowIndex, getRowIdProp)
1360
- const isSelected = selected.has(rowId)
1361
- const rowClickable = Boolean(onRowClick) || selectable
1362
- function handleRowClick(e: React.MouseEvent<HTMLTableRowElement>) {
1363
- if (!rowClickable) return
1364
- const el = e.target as HTMLElement | null
1365
- if (!el) return
1366
- if (el.closest("button, a, input, textarea, select, label, [role='checkbox']")) return
1367
- if (onRowClick) {
1368
- onRowClick(row)
1369
- return
1370
- }
1371
- if (selectable) {
1372
- toggleRow(rowId)
1373
- }
1374
- }
1375
- return (
1376
- <tr
1377
- key={String(rowId)}
1378
- data-state={isSelected ? "selected" : undefined}
1379
- onMouseEnter={() => setHoveredRow(rowId)}
1380
- onMouseLeave={() => setHoveredRow(null)}
1381
- onClick={rowClickable ? handleRowClick : undefined}
1382
- data-new={Boolean((row as Record<string, unknown>).isNew) || undefined}
1383
- className={cn(
1384
- "group/row transition-colors",
1385
- "hover:bg-dt-row-hover",
1386
- isSelected && "bg-dt-row-selected text-dt-row-selected-fg",
1387
- rowClickable && "cursor-pointer",
1388
- Boolean((row as Record<string, unknown>).isNew) && "bg-dt-new-row-bg border-l-2 border-l-dt-new-row-border"
1389
- )}
1390
- >
1391
- {displayCols.map(col => {
1392
- const isPinned = !!effectivePins[col.key]
1393
- const wrap = colWrap[col.key]
1394
- const isEdgePin = col.key === lastLeftPinKey || col.key === firstRightPinKey
1395
- const rowPy = rowHeight === "compact" ? "py-1" : rowHeight === "comfortable" ? "py-4" : "py-2.5"
1396
- const cs = cellStyle(col.key)
1397
-
1398
- const tdBase = cn(
1399
- `px-3 ${rowPy} align-middle`,
1400
- showGridlines && !isEdgePin && "border-r border-border last:border-r-0",
1401
- "border-b border-border group-last/row:border-b-0",
1402
- isPinned && [
1403
- "z-20 pinned-cell",
1404
- "bg-dt-row-bg",
1405
- "group-data-[state=selected]/row:bg-dt-row-selected",
1406
- "group-hover/row:bg-dt-row-hover",
1407
- isEdgePin && stickyShadow(effectivePins[col.key]),
1408
- ]
1409
- )
1410
-
1411
- const conditionalBg = getConditionalCellBackground(
1412
- row,
1413
- col.key,
1414
- conditionalRules,
1415
- columns,
1416
- )
1417
-
1418
- const tdStyle = conditionalBg
1419
- ? { ...cs, background: conditionalBg }
1420
- : cs
1421
-
1422
- // Special synthetic columns
1423
- if (col.key === "select") {
1424
- const selectionLabel = getRowSelectionLabel?.(row, rowIndex)
1425
- const ariaLabel = selectionLabel
1426
- ? `Select row, ${selectionLabel}`
1427
- : `Select row ${rowIndex + 1}`
1428
- return (
1429
- <td key="select" className={cn(tdBase, "text-center")} style={tdStyle}>
1430
- {selectable && (
1431
- // inline-flex: inline elements inside <td> are never
1432
- // stretched by table-cell height in Chrome/Safari/Firefox.
1433
- // Block-level flex/grid always inherits full cell height at zoom.
1434
- <span
1435
- className={cn(
1436
- "inline-flex items-center justify-center transition-opacity",
1437
- anySelected
1438
- ? "opacity-100"
1439
- : "opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100",
1440
- )}
1441
- onClick={e => e.stopPropagation()}
1442
- >
1443
- <Checkbox
1444
- checked={isSelected}
1445
- onCheckedChange={() => toggleRow(rowId)}
1446
- aria-label={ariaLabel}
1447
- onClick={e => e.stopPropagation()}
1448
- />
1449
- </span>
1450
- )}
1451
- </td>
1452
- )
1453
- }
1454
-
1455
- // Custom cell renderer
1456
- if (col.cell) {
1457
- return (
1458
- <td
1459
- key={col.key}
1460
- className={cn(
1461
- tdBase,
1462
- // When wrap is on, override truncate/overflow on any descendant
1463
- wrap && "[&_.truncate]:!whitespace-normal [&_.truncate]:!overflow-visible [&_.truncate]:!text-clip",
1464
- )}
1465
- style={tdStyle}
1466
- >
1467
- {col.cell(row, {
1468
- rowIndex,
1469
- selected: isSelected,
1470
- onSelect: checked => checked ? setSelected(prev => new Set([...prev, rowId])) : toggleRow(rowId),
1471
- })}
1472
- </td>
1473
- )
1474
- }
1475
-
1476
- // Default: render string value with optional truncation
1477
- const rawVal = String(row[col.key] ?? "")
1478
- return (
1479
- <td key={col.key} className={cn(tdBase, "text-sm text-foreground/80")} style={tdStyle}>
1480
- <span className={wrap ? "whitespace-normal" : "block truncate"} title={!wrap ? rawVal : undefined}>
1481
- {rawVal}
1482
- </span>
1483
- </td>
1484
- )
1485
- })}
1486
- </tr>
1487
- )
1488
- })}
1489
- </React.Fragment>
1490
- ))}
1491
-
1492
- {/* Empty state */}
1493
- {rows.length === 0 && (
1494
- <tr>
1495
- <td colSpan={displayCols.length} className="h-24 px-3 text-center text-sm text-muted-foreground">
1496
- {emptyState ?? "No results match your filters."}
1497
- </td>
1498
- </tr>
1499
- )}
1500
-
1501
- {/* Add new row stub */}
1502
- {addRowLabel !== false && (
1503
- <tr
1504
- role="button"
1505
- tabIndex={0}
1506
- onKeyDown={e => { if (e.key === "Enter" || e.key === " ") e.preventDefault() }}
1507
- className="cursor-pointer hover:bg-dt-row-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
1508
- aria-label={`Add new ${addRowLabel}`}
1509
- >
1510
- <td colSpan={displayCols.length} className="px-3 py-2.5 align-middle">
1511
- <span className="flex items-center gap-1.5 text-sm text-muted-foreground">
1512
- <i className="fa-light fa-plus text-xs" aria-hidden="true" />
1513
- {addRowLabel}
1514
- </span>
1515
- </td>
1516
- </tr>
1517
- )}
1518
- </tbody>
1519
- </table>
1520
- </div>
1521
-
1522
- {/* ── Bulk selection bar — dark strip in light app; light strip in dark app.
1523
- Normal zoom: max ~28rem, centered. Reflow: full table width. Inner
1524
- `dark` in light app → shadcn `dark:` buttons; in dark app → explicit
1525
- light-surface button overrides.
1526
- */}
1527
- {anySelected && (
1528
- <div
1529
- role="status"
1530
- aria-live="polite"
1531
- aria-label={`${selected.size} row${selected.size !== 1 ? "s" : ""} selected`}
1532
- data-exxat-bulk-bar=""
1533
- style={bulkBarFixedStyle}
1534
- className={cn(
1535
- "flex min-w-0 max-w-full items-stretch overflow-hidden",
1536
- isAppDark
1537
- ? "rounded-lg border border-zinc-300/80 bg-zinc-100 text-zinc-900 shadow-lg"
1538
- : "rounded-lg border border-zinc-800 bg-zinc-900 text-zinc-100 shadow-lg",
1539
- "animate-in fade-in-0 duration-150",
1540
- "w-auto max-w-none",
1541
- )}
1542
- >
1543
- <div
1544
- className={cn(
1545
- "flex shrink-0 items-center gap-2 border-r py-2.5 pl-3 pr-2",
1546
- isAppDark ? "border-zinc-300/50" : "border-zinc-600/50",
1547
- )}
1548
- aria-hidden="true"
1549
- >
1550
- <span
1551
- className={cn(
1552
- "inline-flex size-8 items-center justify-center rounded-md",
1553
- isAppDark ? "text-zinc-500" : "text-zinc-400",
1554
- )}
1555
- aria-hidden="true"
1556
- >
1557
- <i className="fa-light fa-clipboard-list text-[1.1rem] leading-none" />
1558
- </span>
1559
- <span
1560
- className={cn(
1561
- "min-w-6 rounded-md px-1.5 py-0.5 text-center text-xs font-semibold leading-none tabular-nums",
1562
- isAppDark ? "bg-zinc-200/90 text-zinc-900" : "bg-zinc-800 text-zinc-100",
1563
- )}
1564
- >
1565
- {selected.size}
1566
- </span>
1567
- </div>
1568
-
1569
- <div
1570
- className={cn(
1571
- "flex min-w-0 min-h-0 flex-1 items-stretch",
1572
- !isAppDark && "dark",
1573
- isAppDark && BULK_BAR_ON_LIGHT_STRIP,
1574
- )}
1575
- >
1576
- <div
1577
- className={cn(
1578
- "min-w-0 flex-1 self-center",
1579
- "overflow-x-auto overscroll-x-contain [scrollbar-width:thin] [touch-action:pan-x]",
1580
- )}
1581
- >
1582
- <div className="flex w-max min-w-0 max-w-full flex-nowrap items-center gap-2 py-2.5 pl-2 pr-2">
1583
- {bulkActionsSlot ? (
1584
- bulkActionsSlot(selected, rows)
1585
- ) : (
1586
- <>
1587
- <Button size="sm" variant="outline" className="shrink-0">
1588
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
1589
- </Button>
1590
- <Button size="sm" variant="destructive" className="shrink-0">
1591
- <i className="fa-light fa-trash" aria-hidden="true" /> Delete
1592
- </Button>
1593
- </>
1594
- )}
1595
- </div>
1596
- </div>
1597
-
1598
- <div
1599
- className={cn(
1600
- "flex shrink-0 items-center border-l py-2.5 pl-2 pr-2.5",
1601
- isAppDark ? "border-zinc-300/50" : "border-zinc-600/50",
1602
- )}
1603
- >
1604
- <Tip label="Clear selection" side="top">
1605
- <Button
1606
- type="button"
1607
- size="icon-sm"
1608
- variant="ghost"
1609
- aria-label="Clear selection"
1610
- onClick={() => setSelected(new Set())}
1611
- className="shrink-0"
1612
- >
1613
- <i className="fa-light fa-xmark" aria-hidden="true" />
1614
- </Button>
1615
- </Tip>
1616
- </div>
1617
- </div>
1618
- </div>
1619
- )}
1620
- </div>
1621
- )
1622
- }
1623
-
1624
- function DataTableWithInternalState<TData extends Record<string, unknown>>(props: DataTableExtendedProps<TData>) {
1625
- const state = useTableState(props.data, props.columns, props.defaultSort, props.paginationOverride)
1626
- return <DataTableInner {...props} state={state} />
1627
- }
1628
-
1629
- export function DataTable<TData extends Record<string, unknown>>(props: DataTableExtendedProps<TData>) {
1630
- if (props.state) {
1631
- return <DataTableInner {...props} state={props.state} />
1632
- }
1633
- return <DataTableWithInternalState {...props} />
1634
- }
1
+ export * from "@exxatdesignux/ui/components/data-table"