@exxatdesignux/ui 0.2.19 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (688) hide show
  1. package/CHANGELOG.md +60 -7
  2. package/bin/sync-extras.mjs +116 -29
  3. package/consumer-extras/README.md +42 -7
  4. package/consumer-extras/cursor-rules/exxat-accessibility.mdc +39 -0
  5. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +26 -0
  6. package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +21 -0
  7. package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +21 -0
  8. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +44 -0
  9. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +32 -0
  10. package/consumer-extras/cursor-rules/exxat-command-menu.mdc +22 -0
  11. package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +53 -0
  12. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +41 -0
  13. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +25 -0
  14. package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +22 -0
  15. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +56 -0
  16. package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +31 -0
  17. package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +100 -0
  18. package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +28 -0
  19. package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +21 -0
  20. package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +31 -0
  21. package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +24 -0
  22. package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +31 -0
  23. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +30 -0
  24. package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +78 -0
  25. package/consumer-extras/cursor-rules/exxat-no-toast.mdc +25 -0
  26. package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +23 -0
  27. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +47 -0
  28. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +52 -0
  29. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +28 -0
  30. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +34 -0
  31. package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +77 -0
  32. package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +103 -0
  33. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +1 -1
  34. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  35. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +4 -15
  36. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +13 -28
  37. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +1 -1
  38. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +2 -4
  39. package/consumer-extras/handbook/HANDBOOK.md +185 -0
  40. package/consumer-extras/handbook/glossary.md +57 -0
  41. package/consumer-extras/handbook/reference-implementations.md +126 -0
  42. package/consumer-extras/handbook/voice-and-tone.md +262 -0
  43. package/consumer-extras/patterns/command-menu-pattern.md +1 -1
  44. package/consumer-extras/patterns/consumer-upgrade-checklist.md +0 -20
  45. package/consumer-extras/patterns/data-views-pattern.md +17 -54
  46. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +3 -5
  47. package/dist/components/data-table/filter-date-calendar.d.ts +10 -0
  48. package/dist/components/data-table/filter-date-calendar.js +280 -0
  49. package/dist/components/data-table/filter-date-calendar.js.map +1 -0
  50. package/dist/components/data-table/filter-text-value-input.d.ts +15 -0
  51. package/dist/components/data-table/filter-text-value-input.js +561 -0
  52. package/dist/components/data-table/filter-text-value-input.js.map +1 -0
  53. package/dist/components/data-table/index.d.ts +45 -0
  54. package/dist/components/data-table/index.js +3085 -0
  55. package/dist/components/data-table/index.js.map +1 -0
  56. package/dist/components/data-table/pagination.d.ts +28 -0
  57. package/dist/components/data-table/pagination.js +3264 -0
  58. package/dist/components/data-table/pagination.js.map +1 -0
  59. package/dist/components/data-table/types.d.ts +84 -0
  60. package/dist/components/data-table/types.js +3 -0
  61. package/dist/components/data-table/types.js.map +1 -0
  62. package/dist/components/data-table/use-table-state.d.ts +116 -0
  63. package/dist/components/data-table/use-table-state.js +670 -0
  64. package/dist/components/data-table/use-table-state.js.map +1 -0
  65. package/dist/components/data-views/board-card-primitives.d.ts +22 -0
  66. package/dist/components/data-views/board-card-primitives.js +84 -0
  67. package/dist/components/data-views/board-card-primitives.js.map +1 -0
  68. package/dist/components/data-views/data-row-list.d.ts +33 -0
  69. package/dist/components/data-views/data-row-list.js +106 -0
  70. package/dist/components/data-views/data-row-list.js.map +1 -0
  71. package/dist/components/data-views/finder-panel-view.d.ts +54 -0
  72. package/dist/components/data-views/finder-panel-view.js +388 -0
  73. package/dist/components/data-views/finder-panel-view.js.map +1 -0
  74. package/dist/components/data-views/folder-grid-view.d.ts +22 -0
  75. package/dist/components/data-views/folder-grid-view.js +58 -0
  76. package/dist/components/data-views/folder-grid-view.js.map +1 -0
  77. package/dist/components/data-views/hub-table.d.ts +167 -0
  78. package/dist/components/data-views/hub-table.js +5561 -0
  79. package/dist/components/data-views/hub-table.js.map +1 -0
  80. package/dist/components/data-views/index.d.ts +27 -0
  81. package/dist/components/data-views/index.js +6575 -0
  82. package/dist/components/data-views/index.js.map +1 -0
  83. package/dist/components/data-views/list-page-board-card.d.ts +72 -0
  84. package/dist/components/data-views/list-page-board-card.js +264 -0
  85. package/dist/components/data-views/list-page-board-card.js.map +1 -0
  86. package/dist/components/data-views/list-page-board-template.d.ts +24 -0
  87. package/dist/components/data-views/list-page-board-template.js +137 -0
  88. package/dist/components/data-views/list-page-board-template.js.map +1 -0
  89. package/dist/components/data-views/list-page-connected-view-body.d.ts +19 -0
  90. package/dist/components/data-views/list-page-connected-view-body.js +116 -0
  91. package/dist/components/data-views/list-page-connected-view-body.js.map +1 -0
  92. package/dist/components/data-views/list-page-split-details-placeholder.d.ts +14 -0
  93. package/dist/components/data-views/list-page-split-details-placeholder.js +38 -0
  94. package/dist/components/data-views/list-page-split-details-placeholder.js.map +1 -0
  95. package/dist/components/data-views/list-page-split-hub-chrome.d.ts +17 -0
  96. package/dist/components/data-views/list-page-split-hub-chrome.js +54 -0
  97. package/dist/components/data-views/list-page-split-hub-chrome.js.map +1 -0
  98. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +12 -0
  99. package/dist/components/data-views/list-page-split-hub-tokens.js +8 -0
  100. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -0
  101. package/dist/components/data-views/list-page-tree-column-header.d.ts +15 -0
  102. package/dist/components/data-views/list-page-tree-column-header.js +22 -0
  103. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -0
  104. package/dist/components/data-views/list-page-tree-panel-shell.d.ts +25 -0
  105. package/dist/components/data-views/list-page-tree-panel-shell.js +146 -0
  106. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -0
  107. package/dist/components/data-views/os-folder-glyph.d.ts +35 -0
  108. package/dist/components/data-views/os-folder-glyph.js +104 -0
  109. package/dist/components/data-views/os-folder-glyph.js.map +1 -0
  110. package/dist/components/data-views/outline-tree-menu.d.ts +36 -0
  111. package/dist/components/data-views/outline-tree-menu.js +131 -0
  112. package/dist/components/data-views/outline-tree-menu.js.map +1 -0
  113. package/dist/components/table-properties/column-row.d.ts +22 -0
  114. package/dist/components/table-properties/column-row.js +153 -0
  115. package/dist/components/table-properties/column-row.js.map +1 -0
  116. package/dist/components/table-properties/draggable-list.d.ts +24 -0
  117. package/dist/components/table-properties/draggable-list.js +53 -0
  118. package/dist/components/table-properties/draggable-list.js.map +1 -0
  119. package/dist/components/table-properties/drawer-button.d.ts +110 -0
  120. package/dist/components/table-properties/drawer-button.js +2748 -0
  121. package/dist/components/table-properties/drawer-button.js.map +1 -0
  122. package/dist/components/table-properties/drawer.d.ts +100 -0
  123. package/dist/components/table-properties/drawer.js +2595 -0
  124. package/dist/components/table-properties/drawer.js.map +1 -0
  125. package/dist/components/table-properties/filter-card.d.ts +24 -0
  126. package/dist/components/table-properties/filter-card.js +854 -0
  127. package/dist/components/table-properties/filter-card.js.map +1 -0
  128. package/dist/components/table-properties/index.d.ts +14 -0
  129. package/dist/components/table-properties/index.js +2768 -0
  130. package/dist/components/table-properties/index.js.map +1 -0
  131. package/dist/components/table-properties/sort-card.d.ts +20 -0
  132. package/dist/components/table-properties/sort-card.js +102 -0
  133. package/dist/components/table-properties/sort-card.js.map +1 -0
  134. package/dist/components/templates/dedicated-search-landing-template.d.ts +21 -0
  135. package/dist/components/templates/dedicated-search-landing-template.js +254 -0
  136. package/dist/components/templates/dedicated-search-landing-template.js.map +1 -0
  137. package/dist/components/templates/dedicated-search-results-template.d.ts +15 -0
  138. package/dist/components/templates/dedicated-search-results-template.js +16 -0
  139. package/dist/components/templates/dedicated-search-results-template.js.map +1 -0
  140. package/dist/components/templates/index.d.ts +9 -0
  141. package/dist/components/templates/index.js +2720 -0
  142. package/dist/components/templates/index.js.map +1 -0
  143. package/dist/components/templates/list-page.d.ts +83 -0
  144. package/dist/components/templates/list-page.js +2433 -0
  145. package/dist/components/templates/list-page.js.map +1 -0
  146. package/dist/components/templates/nested-secondary-panel-shell.d.ts +20 -0
  147. package/dist/components/templates/nested-secondary-panel-shell.js +54 -0
  148. package/dist/components/templates/nested-secondary-panel-shell.js.map +1 -0
  149. package/dist/components/ui/accordion.d.ts +10 -0
  150. package/dist/components/ui/accordion.js +74 -0
  151. package/dist/components/ui/accordion.js.map +1 -0
  152. package/dist/components/ui/alert-dialog.d.ts +37 -0
  153. package/dist/components/ui/alert-dialog.js +201 -0
  154. package/dist/components/ui/alert-dialog.js.map +1 -0
  155. package/dist/components/ui/avatar.d.ts +84 -0
  156. package/dist/components/ui/avatar.js +328 -0
  157. package/dist/components/ui/avatar.js.map +1 -0
  158. package/dist/components/ui/badge.d.ts +13 -0
  159. package/dist/components/ui/badge.js +49 -0
  160. package/dist/components/ui/badge.js.map +1 -0
  161. package/dist/components/ui/banner.d.ts +62 -0
  162. package/dist/components/ui/banner.js +364 -0
  163. package/dist/components/ui/banner.js.map +1 -0
  164. package/dist/components/ui/breadcrumb.d.ts +14 -0
  165. package/dist/components/ui/breadcrumb.js +114 -0
  166. package/dist/components/ui/breadcrumb.js.map +1 -0
  167. package/dist/components/ui/button.d.ts +16 -0
  168. package/dist/components/ui/button.js +59 -0
  169. package/dist/components/ui/button.js.map +1 -0
  170. package/dist/components/ui/calendar.d.ts +13 -0
  171. package/dist/components/ui/calendar.js +238 -0
  172. package/dist/components/ui/calendar.js.map +1 -0
  173. package/dist/components/ui/card.d.ts +14 -0
  174. package/dist/components/ui/card.js +102 -0
  175. package/dist/components/ui/card.js.map +1 -0
  176. package/dist/components/ui/chart.d.ts +58 -0
  177. package/dist/components/ui/chart.js +292 -0
  178. package/dist/components/ui/chart.js.map +1 -0
  179. package/dist/components/ui/checkbox.d.ts +23 -0
  180. package/dist/components/ui/checkbox.js +155 -0
  181. package/dist/components/ui/checkbox.js.map +1 -0
  182. package/dist/components/ui/coach-mark.d.ts +27 -0
  183. package/dist/components/ui/coach-mark.js +306 -0
  184. package/dist/components/ui/coach-mark.js.map +1 -0
  185. package/dist/components/ui/collapsible.d.ts +8 -0
  186. package/dist/components/ui/collapsible.js +35 -0
  187. package/dist/components/ui/collapsible.js.map +1 -0
  188. package/dist/components/ui/command.d.ts +36 -0
  189. package/dist/components/ui/command.js +274 -0
  190. package/dist/components/ui/command.js.map +1 -0
  191. package/dist/components/ui/context-menu.d.ts +32 -0
  192. package/dist/components/ui/context-menu.js +245 -0
  193. package/dist/components/ui/context-menu.js.map +1 -0
  194. package/dist/components/ui/date-picker-field.d.ts +38 -0
  195. package/dist/components/ui/date-picker-field.js +550 -0
  196. package/dist/components/ui/date-picker-field.js.map +1 -0
  197. package/dist/components/ui/dialog.d.ts +22 -0
  198. package/dist/components/ui/dialog.js +200 -0
  199. package/dist/components/ui/dialog.js.map +1 -0
  200. package/dist/components/ui/dot-pattern.d.ts +21 -0
  201. package/dist/components/ui/dot-pattern.js +139 -0
  202. package/dist/components/ui/dot-pattern.js.map +1 -0
  203. package/dist/components/ui/drag-handle-grip.d.ts +10 -0
  204. package/dist/components/ui/drag-handle-grip.js +15 -0
  205. package/dist/components/ui/drag-handle-grip.js.map +1 -0
  206. package/dist/components/ui/drawer.d.ts +16 -0
  207. package/dist/components/ui/drawer.js +125 -0
  208. package/dist/components/ui/drawer.js.map +1 -0
  209. package/dist/components/ui/dropdown-menu.d.ts +45 -0
  210. package/dist/components/ui/dropdown-menu.js +353 -0
  211. package/dist/components/ui/dropdown-menu.js.map +1 -0
  212. package/dist/components/ui/export-drawer.d.ts +11 -0
  213. package/dist/components/ui/export-drawer.js +1658 -0
  214. package/dist/components/ui/export-drawer.js.map +1 -0
  215. package/dist/components/ui/field.d.ts +30 -0
  216. package/dist/components/ui/field.js +249 -0
  217. package/dist/components/ui/field.js.map +1 -0
  218. package/dist/components/ui/form.d.ts +28 -0
  219. package/dist/components/ui/form.js +110 -0
  220. package/dist/components/ui/form.js.map +1 -0
  221. package/dist/components/ui/hover-card.d.ts +9 -0
  222. package/dist/components/ui/hover-card.js +43 -0
  223. package/dist/components/ui/hover-card.js.map +1 -0
  224. package/dist/components/ui/input-group.d.ts +20 -0
  225. package/dist/components/ui/input-group.js +219 -0
  226. package/dist/components/ui/input-group.js.map +1 -0
  227. package/dist/components/ui/input-mask.d.ts +39 -0
  228. package/dist/components/ui/input-mask.js +118 -0
  229. package/dist/components/ui/input-mask.js.map +1 -0
  230. package/dist/components/ui/input.d.ts +5 -0
  231. package/dist/components/ui/input.js +30 -0
  232. package/dist/components/ui/input.js.map +1 -0
  233. package/dist/components/ui/kbd.d.ts +20 -0
  234. package/dist/components/ui/kbd.js +45 -0
  235. package/dist/components/ui/kbd.js.map +1 -0
  236. package/dist/components/ui/key-metrics-context.d.ts +19 -0
  237. package/dist/components/ui/key-metrics-context.js +26 -0
  238. package/dist/components/ui/key-metrics-context.js.map +1 -0
  239. package/dist/components/ui/key-metrics.d.ts +131 -0
  240. package/dist/components/ui/key-metrics.js +1015 -0
  241. package/dist/components/ui/key-metrics.js.map +1 -0
  242. package/dist/components/ui/label.d.ts +6 -0
  243. package/dist/components/ui/label.js +28 -0
  244. package/dist/components/ui/label.js.map +1 -0
  245. package/dist/components/ui/list-page-view-frame.d.ts +22 -0
  246. package/dist/components/ui/list-page-view-frame.js +24 -0
  247. package/dist/components/ui/list-page-view-frame.js.map +1 -0
  248. package/dist/components/ui/page-header.d.ts +51 -0
  249. package/dist/components/ui/page-header.js +372 -0
  250. package/dist/components/ui/page-header.js.map +1 -0
  251. package/dist/components/ui/payment-card-fields.d.ts +10 -0
  252. package/dist/components/ui/payment-card-fields.js +80 -0
  253. package/dist/components/ui/payment-card-fields.js.map +1 -0
  254. package/dist/components/ui/popover.d.ts +10 -0
  255. package/dist/components/ui/popover.js +47 -0
  256. package/dist/components/ui/popover.js.map +1 -0
  257. package/dist/components/ui/radio-group.d.ts +29 -0
  258. package/dist/components/ui/radio-group.js +190 -0
  259. package/dist/components/ui/radio-group.js.map +1 -0
  260. package/dist/components/ui/resizable.d.ts +16 -0
  261. package/dist/components/ui/resizable.js +51 -0
  262. package/dist/components/ui/resizable.js.map +1 -0
  263. package/dist/components/ui/scroll-area.d.ts +8 -0
  264. package/dist/components/ui/scroll-area.js +66 -0
  265. package/dist/components/ui/scroll-area.js.map +1 -0
  266. package/dist/components/ui/select.d.ts +18 -0
  267. package/dist/components/ui/select.js +186 -0
  268. package/dist/components/ui/select.js.map +1 -0
  269. package/dist/components/ui/selection-tile-grid.d.ts +52 -0
  270. package/dist/components/ui/selection-tile-grid.js +347 -0
  271. package/dist/components/ui/selection-tile-grid.js.map +1 -0
  272. package/dist/components/ui/separator.d.ts +7 -0
  273. package/dist/components/ui/separator.js +33 -0
  274. package/dist/components/ui/separator.js.map +1 -0
  275. package/dist/components/ui/sheet.d.ts +18 -0
  276. package/dist/components/ui/sheet.js +181 -0
  277. package/dist/components/ui/sheet.js.map +1 -0
  278. package/dist/components/ui/sidebar.d.ts +94 -0
  279. package/dist/components/ui/sidebar.js +805 -0
  280. package/dist/components/ui/sidebar.js.map +1 -0
  281. package/dist/components/ui/skeleton.d.ts +5 -0
  282. package/dist/components/ui/skeleton.js +22 -0
  283. package/dist/components/ui/skeleton.js.map +1 -0
  284. package/dist/components/ui/slider.d.ts +7 -0
  285. package/dist/components/ui/slider.js +66 -0
  286. package/dist/components/ui/slider.js.map +1 -0
  287. package/dist/components/ui/sonner.d.ts +6 -0
  288. package/dist/components/ui/sonner.js +38 -0
  289. package/dist/components/ui/sonner.js.map +1 -0
  290. package/dist/components/ui/status-badge.d.ts +38 -0
  291. package/dist/components/ui/status-badge.js +77 -0
  292. package/dist/components/ui/status-badge.js.map +1 -0
  293. package/dist/components/ui/table.d.ts +13 -0
  294. package/dist/components/ui/table.js +115 -0
  295. package/dist/components/ui/table.js.map +1 -0
  296. package/dist/components/ui/tabs.d.ts +15 -0
  297. package/dist/components/ui/tabs.js +93 -0
  298. package/dist/components/ui/tabs.js.map +1 -0
  299. package/dist/components/ui/textarea.d.ts +6 -0
  300. package/dist/components/ui/textarea.js +25 -0
  301. package/dist/components/ui/textarea.js.map +1 -0
  302. package/dist/components/ui/tip.d.ts +12 -0
  303. package/dist/components/ui/tip.js +61 -0
  304. package/dist/components/ui/tip.js.map +1 -0
  305. package/dist/components/ui/toggle-group.d.ts +14 -0
  306. package/dist/components/ui/toggle-group.js +104 -0
  307. package/dist/components/ui/toggle-group.js.map +1 -0
  308. package/dist/components/ui/toggle-switch.d.ts +10 -0
  309. package/dist/components/ui/toggle-switch.js +33 -0
  310. package/dist/components/ui/toggle-switch.js.map +1 -0
  311. package/dist/components/ui/toggle.d.ts +13 -0
  312. package/dist/components/ui/toggle.js +51 -0
  313. package/dist/components/ui/toggle.js.map +1 -0
  314. package/dist/components/ui/tooltip.d.ts +10 -0
  315. package/dist/components/ui/tooltip.js +68 -0
  316. package/dist/components/ui/tooltip.js.map +1 -0
  317. package/dist/components/ui/view-segmented-control.d.ts +31 -0
  318. package/dist/components/ui/view-segmented-control.js +167 -0
  319. package/dist/components/ui/view-segmented-control.js.map +1 -0
  320. package/dist/data-list-view-registry-CyBoBML4.d.ts +73 -0
  321. package/dist/hooks/use-app-theme.d.ts +24 -0
  322. package/dist/hooks/use-app-theme.js +286 -0
  323. package/dist/hooks/use-app-theme.js.map +1 -0
  324. package/dist/hooks/use-coach-mark.d.ts +86 -0
  325. package/dist/hooks/use-coach-mark.js +218 -0
  326. package/dist/hooks/use-coach-mark.js.map +1 -0
  327. package/dist/hooks/use-mobile.d.ts +3 -0
  328. package/dist/hooks/use-mobile.js +29 -0
  329. package/dist/hooks/use-mobile.js.map +1 -0
  330. package/dist/hooks/use-mod-key-label.d.ts +6 -0
  331. package/dist/hooks/use-mod-key-label.js +25 -0
  332. package/dist/hooks/use-mod-key-label.js.map +1 -0
  333. package/dist/index.d.ts +120 -0
  334. package/dist/index.js +13324 -0
  335. package/dist/index.js.map +1 -0
  336. package/dist/lib/compose-refs.d.ts +6 -0
  337. package/dist/lib/compose-refs.js +17 -0
  338. package/dist/lib/compose-refs.js.map +1 -0
  339. package/dist/lib/conditional-rule-match.d.ts +30 -0
  340. package/dist/lib/conditional-rule-match.js +66 -0
  341. package/dist/lib/conditional-rule-match.js.map +1 -0
  342. package/dist/lib/data-list-display-options.d.ts +26 -0
  343. package/dist/lib/data-list-display-options.js +14 -0
  344. package/dist/lib/data-list-display-options.js.map +1 -0
  345. package/dist/lib/data-list-view-registry.d.ts +2 -0
  346. package/dist/lib/data-list-view-registry.js +102 -0
  347. package/dist/lib/data-list-view-registry.js.map +1 -0
  348. package/dist/lib/data-list-view-surface.d.ts +2 -0
  349. package/dist/lib/data-list-view-surface.js +80 -0
  350. package/dist/lib/data-list-view-surface.js.map +1 -0
  351. package/dist/lib/data-list-view.d.ts +21 -0
  352. package/dist/lib/data-list-view.js +25 -0
  353. package/dist/lib/data-list-view.js.map +1 -0
  354. package/dist/lib/date-filter.d.ts +22 -0
  355. package/dist/lib/date-filter.js +61 -0
  356. package/dist/lib/date-filter.js.map +1 -0
  357. package/dist/lib/dev-log.d.ts +8 -0
  358. package/dist/lib/dev-log.js +10 -0
  359. package/dist/lib/dev-log.js.map +1 -0
  360. package/dist/lib/dropdown-menu-surface.d.ts +14 -0
  361. package/dist/lib/dropdown-menu-surface.js +6 -0
  362. package/dist/lib/dropdown-menu-surface.js.map +1 -0
  363. package/dist/lib/editable-target.d.ts +12 -0
  364. package/dist/lib/editable-target.js +12 -0
  365. package/dist/lib/editable-target.js.map +1 -0
  366. package/dist/lib/list-page-table-properties.d.ts +35 -0
  367. package/dist/lib/list-page-table-properties.js +81 -0
  368. package/dist/lib/list-page-table-properties.js.map +1 -0
  369. package/dist/lib/raf-throttle.d.ts +23 -0
  370. package/dist/lib/raf-throttle.js +27 -0
  371. package/dist/lib/raf-throttle.js.map +1 -0
  372. package/dist/lib/row-height.d.ts +16 -0
  373. package/dist/lib/row-height.js +10 -0
  374. package/dist/lib/row-height.js.map +1 -0
  375. package/dist/lib/table-properties-types.d.ts +83 -0
  376. package/dist/lib/table-properties-types.js +19 -0
  377. package/dist/lib/table-properties-types.js.map +1 -0
  378. package/dist/lib/utils.d.ts +5 -0
  379. package/dist/lib/utils.js +11 -0
  380. package/dist/lib/utils.js.map +1 -0
  381. package/package.json +83 -19
  382. package/src/components/data-table/filter-date-calendar.tsx +38 -0
  383. package/src/components/data-table/filter-text-value-input.tsx +77 -0
  384. package/src/components/data-table/index.tsx +1678 -0
  385. package/src/components/data-table/pagination.tsx +255 -0
  386. package/src/components/data-table/types.ts +96 -0
  387. package/src/components/data-table/use-table-state.ts +767 -0
  388. package/src/components/data-views/board-card-primitives.tsx +93 -0
  389. package/src/components/data-views/data-row-list.tsx +183 -0
  390. package/src/components/data-views/finder-panel-view.tsx +405 -0
  391. package/src/components/data-views/folder-grid-view.tsx +86 -0
  392. package/src/components/data-views/hub-table.tsx +498 -0
  393. package/src/components/data-views/index.ts +28 -0
  394. package/src/components/data-views/list-page-board-card.tsx +192 -0
  395. package/src/components/data-views/list-page-board-template.tsx +122 -0
  396. package/src/components/data-views/list-page-connected-view-body.tsx +66 -0
  397. package/src/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  398. package/src/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  399. package/src/components/data-views/list-page-split-hub-tokens.ts +16 -0
  400. package/src/components/data-views/list-page-tree-column-header.tsx +31 -0
  401. package/src/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  402. package/src/components/data-views/os-folder-glyph.tsx +141 -0
  403. package/src/components/data-views/outline-tree-menu.tsx +157 -0
  404. package/src/components/table-properties/column-row.tsx +90 -0
  405. package/src/components/table-properties/draggable-list.ts +54 -0
  406. package/src/components/table-properties/drawer-button.tsx +300 -0
  407. package/src/components/table-properties/drawer.tsx +1148 -0
  408. package/src/components/table-properties/filter-card.tsx +251 -0
  409. package/src/components/table-properties/index.ts +36 -0
  410. package/src/components/table-properties/sort-card.tsx +63 -0
  411. package/src/components/templates/dedicated-search-landing-template.tsx +124 -0
  412. package/src/components/templates/dedicated-search-results-template.tsx +19 -0
  413. package/src/components/templates/index.ts +33 -0
  414. package/src/components/templates/list-page.tsx +602 -0
  415. package/src/components/templates/nested-secondary-panel-shell.tsx +70 -0
  416. package/src/components/ui/accordion.tsx +92 -0
  417. package/src/components/ui/alert-dialog.tsx +221 -0
  418. package/src/components/ui/avatar.tsx +13 -2
  419. package/src/components/ui/banner.tsx +2 -2
  420. package/src/components/ui/button.tsx +4 -4
  421. package/src/components/ui/calendar.tsx +1 -1
  422. package/src/components/ui/coach-mark.tsx +1 -1
  423. package/src/components/ui/context-menu.tsx +291 -0
  424. package/src/components/ui/date-picker-field.tsx +2 -2
  425. package/src/components/ui/dot-pattern.tsx +183 -0
  426. package/src/components/ui/export-drawer.tsx +375 -0
  427. package/src/components/ui/hover-card.tsx +66 -0
  428. package/src/components/ui/key-metrics-context.tsx +78 -0
  429. package/src/components/ui/key-metrics.tsx +1133 -0
  430. package/src/components/ui/list-page-view-frame.tsx +64 -0
  431. package/src/components/ui/page-header.tsx +244 -0
  432. package/src/components/ui/payment-card-fields.tsx +2 -2
  433. package/src/components/ui/resizable.tsx +68 -0
  434. package/src/components/ui/scroll-area.tsx +72 -0
  435. package/src/components/ui/selection-tile-grid.tsx +9 -2
  436. package/src/components/ui/sidebar.tsx +84 -12
  437. package/src/components/ui/slider.tsx +83 -0
  438. package/src/globals.css +2201 -7
  439. package/src/globals.d.ts +20 -0
  440. package/src/index.ts +68 -1
  441. package/src/lib/conditional-rule-match.ts +119 -0
  442. package/src/lib/data-list-display-options.ts +35 -0
  443. package/src/lib/data-list-view-registry.ts +104 -0
  444. package/src/lib/data-list-view-surface.ts +83 -0
  445. package/src/lib/data-list-view.ts +47 -0
  446. package/src/lib/dev-log.ts +10 -0
  447. package/src/lib/editable-target.ts +20 -0
  448. package/src/lib/list-page-table-properties.ts +48 -0
  449. package/src/lib/raf-throttle.ts +45 -0
  450. package/src/lib/row-height.ts +19 -0
  451. package/src/lib/table-properties-types.ts +98 -0
  452. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  453. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +3 -3
  454. package/template/.cursor/rules/exxat-data-tables.mdc +1 -1
  455. package/template/.cursor/rules/exxat-ds-agents.mdc +2 -2
  456. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +2 -2
  457. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  458. package/template/AGENTS.md +104 -78
  459. package/template/app/(app)/dashboard/loading.tsx +15 -3
  460. package/template/app/(app)/dashboard/page.tsx +14 -2
  461. package/template/app/(app)/examples/page.tsx +0 -2
  462. package/template/app/(app)/layout.tsx +17 -4
  463. package/template/app/(app)/loading.tsx +18 -1
  464. package/template/app/(app)/question-bank/find/page.tsx +1 -2
  465. package/template/app/(app)/question-bank/layout.tsx +1 -1
  466. package/template/app/(app)/question-bank/library/page.tsx +1 -2
  467. package/template/app/(app)/question-bank/list/page.tsx +1 -2
  468. package/template/app/(app)/question-bank/new/page.tsx +15 -20
  469. package/template/app/(app)/question-bank/page.tsx +1 -2
  470. package/template/app/(app)/settings/page.tsx +5 -4
  471. package/template/app/globals.css +14 -16
  472. package/template/components/ask-leo-sidebar.tsx +5 -1
  473. package/template/components/brand-color-picker.tsx +2 -2
  474. package/template/components/charts-overview.tsx +1 -1
  475. package/template/components/compliance-board-view.tsx +142 -0
  476. package/template/components/compliance-client.tsx +92 -0
  477. package/template/components/compliance-page-header.tsx +89 -0
  478. package/template/components/compliance-table.tsx +468 -0
  479. package/template/components/dashboard-report-charts.tsx +1 -1
  480. package/template/components/dashboard-tabs.tsx +1 -1
  481. package/template/components/data-table/filter-date-calendar.tsx +1 -38
  482. package/template/components/data-table/filter-text-value-input.tsx +1 -77
  483. package/template/components/data-table/index.tsx +1 -1634
  484. package/template/components/data-table/pagination.tsx +1 -255
  485. package/template/components/data-table/types.ts +1 -94
  486. package/template/components/data-table/use-table-state.test.ts +420 -0
  487. package/template/components/data-table/use-table-state.ts +1 -758
  488. package/template/components/data-view-dashboard-charts-compliance.tsx +963 -0
  489. package/template/components/data-view-dashboard-charts-team.tsx +971 -0
  490. package/template/components/data-view-dashboard-charts.tsx +1503 -0
  491. package/template/components/data-views/board-card-primitives.tsx +1 -93
  492. package/template/components/data-views/data-row-list.tsx +1 -183
  493. package/template/components/data-views/finder-panel-view.tsx +1 -405
  494. package/template/components/data-views/folder-grid-view.tsx +1 -86
  495. package/template/components/data-views/hub-table.tsx +1 -0
  496. package/template/components/data-views/index.ts +50 -37
  497. package/template/components/data-views/list-page-board-card.tsx +1 -192
  498. package/template/components/data-views/list-page-board-template.tsx +1 -122
  499. package/template/components/data-views/list-page-connected-view-body.tsx +1 -66
  500. package/template/components/data-views/list-page-split-details-placeholder.tsx +1 -39
  501. package/template/components/data-views/list-page-split-hub-chrome.tsx +1 -68
  502. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -16
  503. package/template/components/data-views/list-page-tree-column-header.tsx +1 -31
  504. package/template/components/data-views/list-page-tree-panel-shell.tsx +1 -91
  505. package/template/components/data-views/list-page-view-frame.tsx +5 -53
  506. package/template/components/data-views/os-folder-glyph.tsx +1 -129
  507. package/template/components/data-views/outline-tree-menu.tsx +1 -157
  508. package/template/components/export-drawer.test.tsx +71 -0
  509. package/template/components/export-drawer.tsx +1 -375
  510. package/template/components/exxat-product-logo.tsx +5 -5
  511. package/template/components/hub-tree-panel-view.tsx +2 -2
  512. package/template/components/invite-collaborators-drawer.tsx +3 -3
  513. package/template/components/key-metrics-ask-leo-bridge.tsx +40 -0
  514. package/template/components/key-metrics.tsx +1 -1063
  515. package/template/components/leo-insight-indicator.tsx +2 -2
  516. package/template/components/new-placement-back-btn.tsx +28 -0
  517. package/template/components/new-placement-form.tsx +942 -0
  518. package/template/components/new-question-composer.tsx +456 -408
  519. package/template/components/onboarding/index.ts +9 -0
  520. package/template/components/onboarding/onboarding-01.tsx +1 -1
  521. package/template/components/onboarding/onboarding-02.tsx +1 -1
  522. package/template/components/onboarding/onboarding-03.tsx +1 -1
  523. package/template/components/onboarding/onboarding-04.tsx +1 -1
  524. package/template/components/page-header.tsx +8 -226
  525. package/template/components/placement-board-card.tsx +250 -0
  526. package/template/components/placement-detail.tsx +438 -0
  527. package/template/components/placements-board-view.tsx +397 -0
  528. package/template/components/placements-client.tsx +220 -0
  529. package/template/components/placements-list-view.tsx +124 -0
  530. package/template/components/placements-page-header.tsx +166 -0
  531. package/template/components/placements-table-cells.test.tsx +22 -0
  532. package/template/components/placements-table-cells.tsx +173 -0
  533. package/template/components/placements-table-columns.tsx +210 -0
  534. package/template/components/placements-table.tsx +934 -0
  535. package/template/components/product-switcher.tsx +3 -4
  536. package/template/components/product-wordmark.tsx +2 -1
  537. package/template/components/question-bank-client.tsx +5 -5
  538. package/template/components/question-bank-hub-client.tsx +1 -1
  539. package/template/components/question-bank-new-folder-sheet.tsx +2 -2
  540. package/template/components/question-bank-secondary-nav.tsx +3 -3
  541. package/template/components/question-bank-table.tsx +541 -431
  542. package/template/components/rotations-empty-state.tsx +50 -0
  543. package/template/components/rotations-panel-activator.tsx +8 -0
  544. package/template/components/settings-appearance-card.tsx +3 -4
  545. package/template/components/settings-client.tsx +15 -59
  546. package/template/components/settings-form-row.tsx +4 -9
  547. package/template/components/{app-sidebar-dynamic.tsx → sidebar/app-sidebar-dynamic.tsx} +1 -1
  548. package/template/components/{app-sidebar.tsx → sidebar/app-sidebar.tsx} +59 -74
  549. package/template/components/sidebar/index.ts +16 -0
  550. package/template/components/{secondary-nav.tsx → sidebar/secondary-nav.tsx} +2 -2
  551. package/template/components/{secondary-panel.tsx → sidebar/secondary-panel.tsx} +50 -7
  552. package/template/components/{sidebar-auto-collapse.tsx → sidebar/sidebar-auto-collapse.tsx} +6 -2
  553. package/template/components/{sidebar-shell.tsx → sidebar/sidebar-shell.tsx} +1 -1
  554. package/template/components/site-header.tsx +1 -1
  555. package/template/components/sites-board-view.tsx +67 -0
  556. package/template/components/sites-client.tsx +154 -0
  557. package/template/components/sites-table.tsx +249 -0
  558. package/template/components/table-properties/column-row.tsx +1 -90
  559. package/template/components/table-properties/draggable-list.ts +1 -49
  560. package/template/components/table-properties/drawer-button.tsx +1 -262
  561. package/template/components/table-properties/drawer.tsx +1 -1166
  562. package/template/components/table-properties/filter-card.tsx +1 -251
  563. package/template/components/table-properties/sort-card.tsx +1 -59
  564. package/template/components/table-properties/types.ts +28 -71
  565. package/template/components/team-board-view.tsx +122 -0
  566. package/template/components/team-client.tsx +100 -0
  567. package/template/components/team-page-header.tsx +92 -0
  568. package/template/components/team-table.tsx +553 -0
  569. package/template/components/templates/dedicated-search-landing-template.tsx +1 -124
  570. package/template/components/templates/dedicated-search-results-template.tsx +1 -19
  571. package/template/components/templates/list-page.tsx +1 -608
  572. package/template/components/templates/nested-secondary-panel-shell.tsx +1 -63
  573. package/template/components/templates/new-focus-template.tsx +659 -0
  574. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  575. package/template/components/ui/accordion.tsx +1 -0
  576. package/template/components/ui/alert-dialog.tsx +1 -0
  577. package/template/components/ui/context-menu.tsx +1 -0
  578. package/template/components/ui/dot-pattern.tsx +1 -183
  579. package/template/components/ui/hover-card.tsx +1 -0
  580. package/template/components/ui/resizable.tsx +1 -68
  581. package/template/components/ui/scroll-area.tsx +1 -0
  582. package/template/components/ui/slider.tsx +1 -0
  583. package/template/docs/blueprints/README.md +86 -0
  584. package/template/docs/blueprints/_template.md +91 -0
  585. package/template/docs/blueprints/board-card.md +123 -0
  586. package/template/docs/blueprints/data-table.md +139 -0
  587. package/template/docs/blueprints/key-metrics.md +128 -0
  588. package/template/docs/blueprints/list-page-template.md +123 -0
  589. package/template/docs/blueprints/page-header.md +130 -0
  590. package/template/docs/command-menu-pattern.md +1 -1
  591. package/template/docs/component-selection-guide.md +224 -0
  592. package/template/docs/components-audit-2026-05.md +158 -0
  593. package/template/docs/data-views-pattern.md +17 -54
  594. package/template/docs/drawer-vs-dialog-pattern.md +1 -3
  595. package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +95 -0
  596. package/template/docs/migrations/0002-exxat-token-namespace.md +154 -0
  597. package/template/docs/migrations/0003-globals-css-canonical.md +110 -0
  598. package/template/docs/migrations/README.md +100 -0
  599. package/template/docs/migrations/_template.md +64 -0
  600. package/template/docs/shell-surface-elevation-pattern.md +3 -5
  601. package/template/docs/token-taxonomy.md +416 -0
  602. package/template/eslint.config.mjs +27 -0
  603. package/template/hooks/use-secondary-panel-hub-nav.ts +1 -1
  604. package/template/lib/command-menu-config.ts +0 -1
  605. package/template/lib/command-menu-search-data.ts +27 -11
  606. package/template/lib/compliance-supported-views.ts +10 -0
  607. package/template/lib/conditional-rule-match.ts +6 -97
  608. package/template/lib/data-list-display-options.ts +1 -49
  609. package/template/lib/data-list-view-registry.ts +1 -104
  610. package/template/lib/data-list-view-surface.ts +1 -83
  611. package/template/lib/data-list-view.ts +1 -47
  612. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  613. package/template/lib/data-view-dashboard-storage.ts +35 -38
  614. package/template/lib/dev-log.ts +1 -8
  615. package/template/lib/editable-target.ts +1 -10
  616. package/template/lib/list-page-table-properties.ts +1 -48
  617. package/template/lib/list-status-badges.ts +97 -4
  618. package/template/lib/mock/compliance-kpi.ts +61 -0
  619. package/template/lib/mock/compliance.ts +146 -0
  620. package/template/lib/mock/navigation.tsx +0 -9
  621. package/template/lib/mock/placements-kpi.ts +134 -0
  622. package/template/lib/mock/placements.ts +176 -0
  623. package/template/lib/mock/sites-directory.ts +16 -0
  624. package/template/lib/mock/sites-kpi.ts +25 -0
  625. package/template/lib/mock/team-kpi.ts +60 -0
  626. package/template/lib/mock/team.ts +118 -0
  627. package/template/lib/placement-board-card-layout.ts +79 -0
  628. package/template/lib/placements-supported-views.ts +12 -0
  629. package/template/lib/question-bank-supported-views.ts +0 -1
  630. package/template/lib/raf-throttle.ts +1 -45
  631. package/template/lib/row-height.ts +4 -10
  632. package/template/lib/sidebar-state-cookie.ts +11 -2
  633. package/template/lib/sites-supported-views.ts +10 -0
  634. package/template/lib/table-state-lifecycle.ts +2 -2
  635. package/template/lib/team-supported-views.ts +10 -0
  636. package/template/package.json +1 -0
  637. package/template/tests/setup.ts +25 -0
  638. package/consumer-extras/AGENTS.md +0 -76
  639. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +0 -37
  640. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +0 -57
  641. package/consumer-extras/patterns/consumer-app-pattern.md +0 -39
  642. package/consumer-extras/patterns/focused-workflow-page-pattern.md +0 -84
  643. package/src/components/ui/button-group.tsx +0 -81
  644. package/src/theme.css +0 -16
  645. package/src/tokens/README.md +0 -15
  646. package/src/tokens/base.css +0 -337
  647. package/src/tokens/high-contrast.css +0 -1195
  648. package/src/tokens/layers.css +0 -224
  649. package/src/tokens/tailwind-bridge.css +0 -118
  650. package/src/tokens/themes.css +0 -201
  651. package/template/app/(app)/data-list/layout.tsx +0 -43
  652. package/template/app/(app)/data-list/page.tsx +0 -10
  653. package/template/app/(app)/examples/focused-workflow/page.tsx +0 -5
  654. package/template/components/app-route-loading.tsx +0 -14
  655. package/template/components/dashboard-onboarding-gallery.tsx +0 -13
  656. package/template/components/dashboard-onboarding.tsx +0 -21
  657. package/template/components/data-views/list-page-calendar-view.tsx +0 -593
  658. package/template/components/data-views/list-page-folder-columns-panel.tsx +0 -345
  659. package/template/components/examples/focused-workflow-showcase.tsx +0 -183
  660. package/template/components/list-hub-board-view.tsx +0 -68
  661. package/template/components/list-hub-client.tsx +0 -186
  662. package/template/components/list-hub-list-view.tsx +0 -36
  663. package/template/components/list-hub-panel-activator.tsx +0 -8
  664. package/template/components/list-hub-secondary-nav.tsx +0 -121
  665. package/template/components/list-hub-table.tsx +0 -336
  666. package/template/components/question-bank-folder-columns-panel.tsx +0 -104
  667. package/template/components/question-bank-list-view.tsx +0 -53
  668. package/template/components/secondary-panel/nav-link-rows.tsx +0 -83
  669. package/template/components/secondary-panels/list-hub-panel.tsx +0 -39
  670. package/template/components/secondary-panels/question-bank-panel.tsx +0 -39
  671. package/template/components/secondary-panels/registry.tsx +0 -15
  672. package/template/components/section-cards.tsx +0 -106
  673. package/template/components/templates/focused-workflow-layouts.tsx +0 -448
  674. package/template/components/templates/focused-workflow-page-template.tsx +0 -69
  675. package/template/components/templates/page-loading-shell.tsx +0 -262
  676. package/template/components/ui/button-group.tsx +0 -1
  677. package/template/docs/consumer-app-pattern.md +0 -39
  678. package/template/docs/focused-workflow-page-pattern.md +0 -84
  679. package/template/lib/list-hub-nav.ts +0 -121
  680. package/template/lib/mock/list-hub-directory.ts +0 -27
  681. package/template/lib/mock/list-hub-kpi.ts +0 -27
  682. package/template/lib/page-loading-variant.ts +0 -40
  683. /package/template/components/{getting-started.tsx → onboarding/getting-started.tsx} +0 -0
  684. /package/template/components/{nav-documents.tsx → sidebar/nav-documents.tsx} +0 -0
  685. /package/template/components/{nav-main.tsx → sidebar/nav-main.tsx} +0 -0
  686. /package/template/components/{nav-secondary.tsx → sidebar/nav-secondary.tsx} +0 -0
  687. /package/template/components/{nav-user.tsx → sidebar/nav-user.tsx} +0 -0
  688. /package/template/components/{sidebar-auto-open.tsx → sidebar/sidebar-auto-open.tsx} +0 -0
@@ -0,0 +1,767 @@
1
+ "use client"
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // useTableState — all non-display state shared by DataTable and DataTablePaginated
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ import * as React from "react"
8
+ import type { RowHeight } from "../../lib/row-height"
9
+ import type { ColumnDef, SortDir } from "./types"
10
+ import type { ActiveFilter, FilterOperator, SortRule } from "../../lib/table-properties-types"
11
+ import { parseRowDateToYmd } from "../../lib/date-filter"
12
+
13
+ let _filterId = 0
14
+ function nextFilterId() { return `f-${++_filterId}` }
15
+
16
+ /**
17
+ * “Reflow” / high-zoom short viewport. At 200% zoom a 1080p monitor’s CSS
18
+ * height is ≈ 540px — `500px` was too low and never disabled pins. 640px
19
+ * catches typical 200% cases and small laptop tops without breaking `500px` flows.
20
+ * Column stickies + edge shadows harm reflow (WCAG 1.4.10).
21
+ */
22
+ const REFLOW_VIEWPORT_MQ = "(max-height: 640px)"
23
+
24
+ function subscribeReflowViewport(callback: () => void) {
25
+ if (typeof window === "undefined") return () => {}
26
+ const mql = window.matchMedia(REFLOW_VIEWPORT_MQ)
27
+ mql.addEventListener("change", callback)
28
+ return () => mql.removeEventListener("change", callback)
29
+ }
30
+ function getReflowViewportSnapshot() {
31
+ if (typeof window === "undefined") return false
32
+ return window.matchMedia(REFLOW_VIEWPORT_MQ).matches
33
+ }
34
+ function getServerReflowViewportSnapshot() {
35
+ return false
36
+ }
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Helpers
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ function digitsOnly(s: string): string {
43
+ return s.replace(/\D/g, "")
44
+ }
45
+
46
+ /** Build the default widths map from column defs */
47
+ function buildDefaultWidths<TData>(columns: ColumnDef<TData>[]): Record<string, number> {
48
+ const map: Record<string, number> = {}
49
+ for (const col of columns) {
50
+ if (col.width !== undefined) map[col.key] = col.width
51
+ }
52
+ return map
53
+ }
54
+
55
+ /** Build the initial pin state from column defs */
56
+ function buildDefaultPins<TData>(columns: ColumnDef<TData>[]): Record<string, "left" | "right"> {
57
+ const map: Record<string, "left" | "right"> = {}
58
+ for (const col of columns) {
59
+ if (col.defaultPin) map[col.key] = col.defaultPin
60
+ }
61
+ return map
62
+ }
63
+
64
+ function compareUnknownSort(a: unknown, b: unknown): number {
65
+ if (a === b) return 0
66
+ if (a == null && b == null) return 0
67
+ if (a == null) return 1
68
+ if (b == null) return -1
69
+ if (typeof a === "number" && typeof b === "number") return a < b ? -1 : a > b ? 1 : 0
70
+ if (typeof a === "string" && typeof b === "string") return a < b ? -1 : a > b ? 1 : 0
71
+ const as = String(a)
72
+ const bs = String(b)
73
+ return as < bs ? -1 : as > bs ? 1 : 0
74
+ }
75
+
76
+ /** Build the locked-pin set (columns that can never be unpinned) */
77
+ function buildLockedPins<TData>(columns: ColumnDef<TData>[]): Record<string, "left" | "right"> {
78
+ const map: Record<string, "left" | "right"> = {}
79
+ for (const col of columns) {
80
+ if (col.lockPin && col.defaultPin) map[col.key] = col.defaultPin
81
+ }
82
+ return map
83
+ }
84
+
85
+ // ─────────────────────────────────────────────────────────────────────────────
86
+ // Hook
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+
89
+ export function useTableState<TData extends Record<string, unknown>>(
90
+ data: TData[],
91
+ columns: ColumnDef<TData>[],
92
+ defaultSort?: { key: string; dir: SortDir },
93
+ paginationOverride?: { page: number; pageSize: number },
94
+ /**
95
+ * When defined (including `""`), toolbar search is synced from the URL (`?q=`).
96
+ * Use `searchParams.get("q") ?? ""` on question bank list routes; omit for other hubs.
97
+ */
98
+ syncedSearchFromUrl?: string,
99
+ ) {
100
+ // ── Sort ──────────────────────────────────────────────────────────────────
101
+ const [sortRules, setSortRules] = React.useState<SortRule[]>(() => {
102
+ if (defaultSort) {
103
+ return [{ id: "sort-default", fieldKey: defaultSort.key, direction: defaultSort.dir }]
104
+ }
105
+ return []
106
+ })
107
+
108
+ const primarySort = sortRules[0] ?? null
109
+ const sortKey: string = primarySort?.fieldKey ?? ""
110
+ const sortDir: SortDir = primarySort?.direction ?? "asc"
111
+
112
+ const addSortRule = React.useCallback((fieldKey: string) => {
113
+ setSortRules(prev => {
114
+ if (prev.some(r => r.fieldKey === fieldKey)) return prev
115
+ // New drawer sorts are primary (same as column-header sort), not trailing.
116
+ return [{ id: `sort-${Date.now()}`, fieldKey, direction: "asc" }, ...prev]
117
+ })
118
+ }, [setSortRules])
119
+
120
+ const removeSortRule = React.useCallback((id: string) => {
121
+ setSortRules(prev => prev.filter(r => r.id !== id))
122
+ }, [setSortRules])
123
+
124
+ const toggleSortDir = React.useCallback((id: string) => {
125
+ setSortRules(prev => prev.map(r =>
126
+ r.id === id ? { ...r, direction: r.direction === "asc" ? "desc" : "asc" } : r
127
+ ))
128
+ }, [setSortRules])
129
+
130
+ const handleSortByKey = React.useCallback((colKey: string) => {
131
+ setSortRules(prev => {
132
+ const idx = prev.findIndex(r => r.fieldKey === colKey)
133
+ if (idx === 0) {
134
+ return prev.map((r, i) => i === 0 ? { ...r, direction: r.direction === "asc" ? "desc" : "asc" } : r)
135
+ }
136
+ const filtered = prev.filter(r => r.fieldKey !== colKey)
137
+ return [{ id: `sort-${Date.now()}`, fieldKey: colKey, direction: "asc" }, ...filtered]
138
+ })
139
+ }, [setSortRules])
140
+
141
+ // ── Filters ───────────────────────────────────────────────────────────────
142
+ const [search, setSearch] = React.useState(() =>
143
+ syncedSearchFromUrl !== undefined ? syncedSearchFromUrl.trim() : "",
144
+ )
145
+ const [searchOpen, setSearchOpen] = React.useState(() =>
146
+ syncedSearchFromUrl !== undefined && Boolean(syncedSearchFromUrl.trim()),
147
+ )
148
+ const searchRef = React.useRef<HTMLInputElement>(null)
149
+ const [activeFilters, setActiveFilters] = React.useState<ActiveFilter[]>([])
150
+ const [filterConnectors, setFilterConnectors] = React.useState<Record<string, "and" | "or">>({})
151
+ const [openFilterId, setOpenFilterId] = React.useState<string | null>(null)
152
+ const [filterBarVisible, setFilterBarVisible] = React.useState(true)
153
+ const [drawerExpandedFilters, setDrawerExpandedFilters] = React.useState<Set<string>>(new Set())
154
+
155
+ React.useEffect(() => {
156
+ if (syncedSearchFromUrl === undefined) return
157
+ const next = syncedSearchFromUrl.trim()
158
+ setSearch(next)
159
+ setSearchOpen(next.length > 0)
160
+ }, [syncedSearchFromUrl])
161
+
162
+ const toggleConnector = React.useCallback((leftId: string) => {
163
+ setFilterConnectors(prev => ({ ...prev, [leftId]: prev[leftId] === "or" ? "and" : "or" }))
164
+ }, [setFilterConnectors])
165
+
166
+ function getConnector(leftId: string): "and" | "or" {
167
+ return filterConnectors[leftId] ?? "and"
168
+ }
169
+
170
+ const addFilter = React.useCallback((fieldKey: string, fromDrawer = false) => {
171
+ const col = columns.find(c => c.key === fieldKey)
172
+ if (!col?.filter) return
173
+ const id = nextFilterId()
174
+ const f = col.filter
175
+ const firstOperator: FilterOperator = (() => {
176
+ if (f.type === "select" || f.type === "date") {
177
+ const pick = f.operators?.find(o => o === "is" || o === "is_not")
178
+ return pick ?? "is"
179
+ }
180
+ return f.operators?.[0] ?? "contains"
181
+ })()
182
+ const newFilter: ActiveFilter = { id, fieldKey, operator: firstOperator, values: [] }
183
+ setActiveFilters(prev => [...prev, newFilter])
184
+ if (fromDrawer) {
185
+ setDrawerExpandedFilters(() => new Set([id]))
186
+ // Keep toolbar pills hidden until a value is chosen — avoids mounting every
187
+ // FilterPill (heavy) on each drawer "Add filter" click.
188
+ } else {
189
+ setOpenFilterId(id)
190
+ setFilterBarVisible(true)
191
+ }
192
+ }, [columns, setActiveFilters, setDrawerExpandedFilters, setOpenFilterId, setFilterBarVisible])
193
+
194
+ const updateFilter = React.useCallback((id: string, patch: Partial<ActiveFilter>) => {
195
+ let shouldShowFilterBar = false
196
+ setActiveFilters(prev => {
197
+ const next = prev.map(f => {
198
+ if (f.id !== id) return f
199
+ const merged = { ...f, ...patch }
200
+ const col = columns.find(c => c.key === merged.fieldKey)
201
+ if (merged.values.length > 0) {
202
+ shouldShowFilterBar =
203
+ col?.filter?.type === "text"
204
+ ? (merged.values[0] ?? "").trim().length > 0
205
+ : true
206
+ }
207
+ return merged
208
+ })
209
+ return next
210
+ })
211
+ if (shouldShowFilterBar) setFilterBarVisible(true)
212
+ }, [columns, setActiveFilters, setFilterBarVisible])
213
+
214
+ const removeFilter = React.useCallback((id: string) => {
215
+ // Use functional updates only — no stale-closure risk on activeFilters.
216
+ setActiveFilters(prev => {
217
+ const idx = prev.findIndex(f => f.id === id)
218
+ const next = prev.filter(f => f.id !== id)
219
+ setFilterConnectors(prevC => {
220
+ const c = { ...prevC }
221
+ if (idx > 0 && next.length > 0) {
222
+ const leftId = prev[idx - 1].id
223
+ c[leftId] = prevC[id] ?? prevC[leftId] ?? "and"
224
+ }
225
+ delete c[id]
226
+ return c
227
+ })
228
+ return next
229
+ })
230
+ setOpenFilterId(prev => prev === id ? null : prev)
231
+ }, [setActiveFilters, setFilterConnectors, setOpenFilterId])
232
+
233
+ // ── Group by ──────────────────────────────────────────────────────────────
234
+ const [groupBy, setGroupBy] = React.useState<string | null>(null)
235
+
236
+ // ── Per-column quick-search ───────────────────────────────────────────────
237
+ const [colMenuSearch, setColMenuSearch] = React.useState<Record<string, string>>({})
238
+
239
+ // ── Selection ─────────────────────────────────────────────────────────────
240
+ const [selected, setSelected] = React.useState<Set<string | number>>(new Set())
241
+
242
+ // ── Column widths ─────────────────────────────────────────────────────────
243
+ const [colWidths, setColWidths] = React.useState<Record<string, number>>(() => buildDefaultWidths(columns))
244
+ const resizeRef = React.useRef<{ key: string; startX: number; startW: number } | null>(null)
245
+
246
+ // ── Column order ──────────────────────────────────────────────────────────
247
+ const [colOrder, setColOrder] = React.useState<string[]>(() => columns.map(c => c.key))
248
+
249
+ // ── Column pins ───────────────────────────────────────────────────────────
250
+ const [colPins, setColPins] = React.useState<Record<string, "left" | "right">>(() => buildDefaultPins(columns))
251
+ const lockedPins = React.useMemo(() => buildLockedPins(columns), [columns])
252
+
253
+ // ── Column wrap ───────────────────────────────────────────────────────────
254
+ const [colWrap, setColWrap] = React.useState<Record<string, boolean>>({})
255
+
256
+ // ── Drawer / display settings ─────────────────────────────────────────────
257
+ const [sheetOpen, setSheetOpen] = React.useState(false)
258
+ /**
259
+ * Deep-link target for the Properties drawer. When a callsite wants to open
260
+ * the drawer focused on a specific panel (e.g. "conditional-rules" from the
261
+ * column header menu), it sets this before calling `setSheetOpen(true)`. The
262
+ * drawer's `initialPanel` prop reads it and syncs its internal `sheetPanel`
263
+ * accordingly. The toolbar Properties button clears it so it opens to "main".
264
+ */
265
+ const [sheetInitialPanel, setSheetInitialPanel] = React.useState<string | null>(null)
266
+ const [showGridlines, setShowGridlines] = React.useState(true)
267
+ const [rowHeight, setRowHeight] = React.useState<RowHeight>("default")
268
+ const [hiddenCols, setHiddenCols] = React.useState<Set<string>>(new Set())
269
+
270
+ const toggleColVisibility = React.useCallback((key: string) => {
271
+ setHiddenCols(prev => {
272
+ const next = new Set(prev)
273
+ if (next.has(key)) next.delete(key)
274
+ else next.add(key)
275
+ return next
276
+ })
277
+ }, [setHiddenCols])
278
+
279
+ const moveCol = React.useCallback((key: string, dir: "up" | "down") => {
280
+ setColOrder(prev => {
281
+ const lockedLeft = columns.filter(c => c.lockPin && c.defaultPin === "left").map(c => c.key)
282
+ const lockedRight = columns.filter(c => c.lockPin && c.defaultPin === "right").map(c => c.key)
283
+ const orderable = prev.filter(k => !lockedLeft.includes(k) && !lockedRight.includes(k))
284
+ const idx = orderable.indexOf(key)
285
+ if (dir === "up" && idx <= 0) return prev
286
+ if (dir === "down" && idx >= orderable.length - 1) return prev
287
+ const next = [...orderable]
288
+ const swap = dir === "up" ? idx - 1 : idx + 1
289
+ ;[next[idx], next[swap]] = [next[swap], next[idx]]
290
+ return [...lockedLeft, ...next, ...lockedRight]
291
+ })
292
+ }, [columns, setColOrder])
293
+
294
+ // ── Drag-to-reorder ───────────────────────────────────────────────────────
295
+ const draggedKey = React.useRef<string | null>(null)
296
+ const [dragOverKey, setDragOverKey] = React.useState<string | null>(null)
297
+
298
+ // ── Scroll / overflow ─────────────────────────────────────────────────────
299
+ const scrollRef = React.useRef<HTMLDivElement>(null)
300
+ const [scrolled, setScrolled] = React.useState(false)
301
+ const [scrollEnd, setScrollEnd] = React.useState(false)
302
+ const [isOverflowing, setIsOverflowing] = React.useState(false)
303
+
304
+ const isReflowViewport = React.useSyncExternalStore(
305
+ subscribeReflowViewport,
306
+ getReflowViewportSnapshot,
307
+ getServerReflowViewportSnapshot,
308
+ )
309
+
310
+ // ── Hovered row ───────────────────────────────────────────────────────────
311
+ const [hoveredRow, setHoveredRow] = React.useState<string | number | null>(null)
312
+
313
+ // ── Column lookup index (stable per `columns` reference) ─────────────────
314
+ // The previous implementation called `columns.find(c => c.key === ...)` inside
315
+ // every filter/sort comparator and every sticky-offset getter — for large
316
+ // datasets that's O(rows × cols) per render. Map lookups make those O(1).
317
+ const columnsByKey = React.useMemo(() => {
318
+ const map = new Map<string, ColumnDef<TData>>()
319
+ for (const col of columns) map.set(col.key, col)
320
+ return map
321
+ }, [columns])
322
+
323
+ // Searchable text cache. Per row, concatenate every value into one
324
+ // lower-cased blob ONCE and reuse it across keystrokes. Keyed by row
325
+ // identity via WeakMap so it never holds onto rows the consumer dropped.
326
+ const searchableTextCache = React.useRef<WeakMap<object, string>>(new WeakMap())
327
+ const getSearchableText = React.useCallback((row: TData): string => {
328
+ const cache = searchableTextCache.current
329
+ const cached = cache.get(row)
330
+ if (cached !== undefined) return cached
331
+ let blob = ""
332
+ for (const v of Object.values(row)) {
333
+ if (v == null) continue
334
+ blob += String(v).toLowerCase() + "\n"
335
+ }
336
+ cache.set(row, blob)
337
+ return blob
338
+ }, [])
339
+
340
+ // Per-row per-column lower-cased value cache (column quick-search +
341
+ // text-mask filters). One `Map` per row, lazily filled on first lookup.
342
+ const lowerValueCache = React.useRef<WeakMap<object, Map<string, string>>>(new WeakMap())
343
+ const getLowerValue = React.useCallback((row: TData, key: string): string => {
344
+ const wm = lowerValueCache.current
345
+ let perRow = wm.get(row)
346
+ if (!perRow) {
347
+ perRow = new Map()
348
+ wm.set(row, perRow)
349
+ }
350
+ const cached = perRow.get(key)
351
+ if (cached !== undefined) return cached
352
+ const computed = String(row[key] ?? "").toLowerCase()
353
+ perRow.set(key, computed)
354
+ return computed
355
+ }, [])
356
+
357
+ // Reset the row-keyed caches whenever the dataset reference changes so we
358
+ // don't pin stale strings for rows the consumer just replaced.
359
+ React.useEffect(() => {
360
+ searchableTextCache.current = new WeakMap()
361
+ lowerValueCache.current = new WeakMap()
362
+ }, [data])
363
+
364
+ // ── Derived: filtered + sorted rows ──────────────────────────────────────
365
+ const rows = React.useMemo(() => {
366
+ let result = data.slice()
367
+
368
+ const q = search.trim().toLowerCase()
369
+ if (q) {
370
+ result = result.filter(r => getSearchableText(r).includes(q))
371
+ }
372
+
373
+ const activeWithValues = activeFilters.filter(f => {
374
+ if (f.values.length === 0) return false
375
+ const col = columnsByKey.get(f.fieldKey)
376
+ if (col?.filter?.type === "text") {
377
+ return (f.values[0] ?? "").trim().length > 0
378
+ }
379
+ return true
380
+ })
381
+ if (activeWithValues.length > 0) {
382
+ // Pre-resolve column, operator, normalised needle, and select-value Set
383
+ // for each active filter ONCE (instead of per row).
384
+ type CompiledFilter = {
385
+ col: ColumnDef<TData>
386
+ id: string
387
+ type: "select" | "date" | "text"
388
+ operator: ActiveFilter["operator"]
389
+ selectValues?: Set<string>
390
+ dateTarget?: string
391
+ textNeedle?: string
392
+ digitsNeedle?: string
393
+ isDigitsMask?: boolean
394
+ }
395
+ const compiled: CompiledFilter[] = []
396
+ for (const f of activeWithValues) {
397
+ const col = columnsByKey.get(f.fieldKey)
398
+ if (!col?.filter) continue
399
+ if (col.filter.type === "select") {
400
+ compiled.push({
401
+ col,
402
+ id: f.id,
403
+ type: "select",
404
+ operator: f.operator,
405
+ selectValues: new Set(f.values),
406
+ })
407
+ } else if (col.filter.type === "date") {
408
+ compiled.push({
409
+ col,
410
+ id: f.id,
411
+ type: "date",
412
+ operator: f.operator,
413
+ dateTarget: f.values[0],
414
+ })
415
+ } else {
416
+ const raw = f.values[0] ?? ""
417
+ const isDigitsMask = col.filter.textMask === "phone" || col.filter.textMask === "zip"
418
+ compiled.push({
419
+ col,
420
+ id: f.id,
421
+ type: "text",
422
+ operator: f.operator,
423
+ isDigitsMask,
424
+ digitsNeedle: isDigitsMask ? digitsOnly(raw) : undefined,
425
+ textNeedle: !isDigitsMask ? raw.toLowerCase() : undefined,
426
+ })
427
+ }
428
+ }
429
+
430
+ const matchesCompiled = (r: TData, f: CompiledFilter): boolean => {
431
+ const rowVal = String(r[f.col.key] ?? "")
432
+ if (f.type === "select") {
433
+ const hit = f.selectValues!.has(rowVal)
434
+ return f.operator === "is" ? hit : !hit
435
+ }
436
+ if (f.type === "date") {
437
+ if (!f.dateTarget) return true
438
+ const rowYmd = parseRowDateToYmd(rowVal)
439
+ const op = f.operator === "is_not" ? "is_not" : "is"
440
+ if (rowYmd === null) return op === "is_not"
441
+ return op === "is" ? rowYmd === f.dateTarget : rowYmd !== f.dateTarget
442
+ }
443
+ if (f.isDigitsMask) {
444
+ if (!f.digitsNeedle) return true
445
+ const hay = digitsOnly(rowVal)
446
+ return f.operator === "contains" ? hay.includes(f.digitsNeedle) : !hay.includes(f.digitsNeedle)
447
+ }
448
+ if (!f.textNeedle) return true
449
+ const hay = getLowerValue(r, f.col.key)
450
+ return f.operator === "contains" ? hay.includes(f.textNeedle) : !hay.includes(f.textNeedle)
451
+ }
452
+
453
+ if (compiled.length > 0) {
454
+ result = result.filter(r => {
455
+ let res = matchesCompiled(r, compiled[0])
456
+ for (let i = 1; i < compiled.length; i++) {
457
+ const connector = filterConnectors[compiled[i - 1].id] ?? "and"
458
+ const match = matchesCompiled(r, compiled[i])
459
+ res = connector === "and" ? res && match : res || match
460
+ }
461
+ return res
462
+ })
463
+ }
464
+ }
465
+
466
+ // Column menu quick-search — pre-normalise needles outside the row loop.
467
+ const colMenuEntries: { key: string; lower: string }[] = []
468
+ for (const [key, raw] of Object.entries(colMenuSearch)) {
469
+ const trimmed = raw.trim()
470
+ if (trimmed) colMenuEntries.push({ key, lower: trimmed.toLowerCase() })
471
+ }
472
+ if (colMenuEntries.length > 0) {
473
+ result = result.filter(r => {
474
+ for (const { key, lower } of colMenuEntries) {
475
+ if (!getLowerValue(r, key).includes(lower)) return false
476
+ }
477
+ return true
478
+ })
479
+ }
480
+
481
+ // Sort — resolve each rule's sort key ONCE, then run the comparator over
482
+ // an indexed list so the inner loop is a tight array walk, not a chain of
483
+ // `columns.find` lookups per comparison.
484
+ if (sortRules.length > 0) {
485
+ const resolved: { sk: string; dir: SortDir }[] = []
486
+ for (const rule of sortRules) {
487
+ const col = columnsByKey.get(rule.fieldKey)
488
+ const sk = col?.sortKey ?? col?.key
489
+ if (sk) resolved.push({ sk: sk as string, dir: rule.direction })
490
+ }
491
+ if (resolved.length > 0) {
492
+ result.sort((a, b) => {
493
+ for (let i = 0; i < resolved.length; i++) {
494
+ const { sk, dir } = resolved[i]
495
+ const cmp = compareUnknownSort(a[sk], b[sk])
496
+ if (cmp !== 0) return dir === "asc" ? cmp : -cmp
497
+ }
498
+ return 0
499
+ })
500
+ }
501
+ }
502
+
503
+ return result
504
+ }, [
505
+ data,
506
+ search,
507
+ activeFilters,
508
+ filterConnectors,
509
+ colMenuSearch,
510
+ sortRules,
511
+ columnsByKey,
512
+ getSearchableText,
513
+ getLowerValue,
514
+ ])
515
+
516
+ // ── Paged rows (slice of rows when pagination is active) ─────────────────
517
+ const pagedRows = React.useMemo(() => {
518
+ if (!paginationOverride || paginationOverride.pageSize <= 0) return rows
519
+ const { page, pageSize } = paginationOverride
520
+ const safePage = Math.max(1, page)
521
+ return rows.slice((safePage - 1) * pageSize, safePage * pageSize)
522
+ // eslint-disable-next-line react-hooks/exhaustive-deps
523
+ }, [rows, paginationOverride?.page, paginationOverride?.pageSize])
524
+
525
+ // ── Grouped rows ──────────────────────────────────────────────────────────
526
+ const groupedRows = React.useMemo(() => {
527
+ if (!groupBy) return [{ groupKey: null as string | null, groupLabel: null as string | null, rows }]
528
+ const groups = new Map<string, TData[]>()
529
+ rows.forEach(row => {
530
+ const val = String(row[groupBy] ?? "—")
531
+ if (!groups.has(val)) groups.set(val, [])
532
+ groups.get(val)!.push(row)
533
+ })
534
+ return [...groups.entries()]
535
+ .sort(([a], [b]) => a.localeCompare(b))
536
+ .map(([key, groupRows]) => ({ groupKey: key, groupLabel: key, rows: groupRows }))
537
+ }, [rows, groupBy])
538
+
539
+ // ── Effective pins (respect overflow) ─────────────────────────────────────
540
+ const LOCKED_KEYS = React.useMemo(() => new Set(Object.keys(lockedPins)), [lockedPins])
541
+
542
+ // When the table fits within its container (not overflowing) there is no need
543
+ // to sticky-pin any column — even locked ones. Pins only activate once the
544
+ // user has to scroll horizontally so the selection / action edges stay visible.
545
+ // In reflow viewports (high zoom), disable all column stickies — shadow + sticky
546
+ // fight the short viewport and overlap content.
547
+ const effectivePins = React.useMemo(() => {
548
+ if (isReflowViewport || !isOverflowing) return {}
549
+ const result: Record<string, "left" | "right"> = {}
550
+ for (const [key, pin] of Object.entries(colPins)) {
551
+ result[key] = pin
552
+ }
553
+ return result
554
+ }, [colPins, isOverflowing, isReflowViewport])
555
+
556
+ // ── Display columns ───────────────────────────────────────────────────────
557
+ const displayCols = React.useMemo(() => {
558
+ const leftPinned: string[] = []
559
+ const free: string[] = []
560
+ const rightPinned: string[] = []
561
+ for (const k of colOrder) {
562
+ const pin = colPins[k]
563
+ if (pin === "left") leftPinned.push(k)
564
+ else if (pin === "right") rightPinned.push(k)
565
+ else free.push(k)
566
+ }
567
+ const ordered = [...leftPinned, ...free, ...rightPinned]
568
+ const out: ColumnDef<TData>[] = []
569
+ for (const k of ordered) {
570
+ if (hiddenCols.has(k)) continue
571
+ const col = columnsByKey.get(k)
572
+ if (col) out.push(col)
573
+ }
574
+ return out
575
+ }, [colOrder, colPins, hiddenCols, columnsByKey])
576
+
577
+ // ── Column actions ────────────────────────────────────────────────────────
578
+ function startResize(key: string, e: React.MouseEvent) {
579
+ e.preventDefault()
580
+ e.stopPropagation()
581
+ const minW = columns.find(c => c.key === key)?.minWidth ?? 60
582
+ const startW = colWidths[key] ?? (columns.find(c => c.key === key)?.width ?? 100)
583
+ resizeRef.current = { key, startX: e.clientX, startW }
584
+ const onMove = (ev: MouseEvent) => {
585
+ if (!resizeRef.current) return
586
+ const { key: k, startX, startW: sw } = resizeRef.current
587
+ setColWidths(p => ({ ...p, [k]: Math.max(minW, sw + ev.clientX - startX) }))
588
+ }
589
+ const onUp = () => {
590
+ resizeRef.current = null
591
+ document.removeEventListener("mousemove", onMove)
592
+ document.removeEventListener("mouseup", onUp)
593
+ }
594
+ document.addEventListener("mousemove", onMove)
595
+ document.addEventListener("mouseup", onUp)
596
+ }
597
+
598
+ function handleDragStart(key: string, e: React.DragEvent<HTMLTableCellElement>) {
599
+ draggedKey.current = key
600
+ e.dataTransfer.effectAllowed = "move"
601
+ }
602
+ function handleDragOver(key: string, e: React.DragEvent<HTMLTableCellElement>) {
603
+ e.preventDefault()
604
+ e.dataTransfer.dropEffect = "move"
605
+ if (draggedKey.current && draggedKey.current !== key) setDragOverKey(key)
606
+ }
607
+ function handleDrop(key: string) {
608
+ if (!draggedKey.current || draggedKey.current === key) { setDragOverKey(null); return }
609
+ const order = [...colOrder]
610
+ const from = order.indexOf(draggedKey.current)
611
+ const to = order.indexOf(key)
612
+ order.splice(from, 1)
613
+ order.splice(to, 0, draggedKey.current!)
614
+ setColOrder(order)
615
+ draggedKey.current = null
616
+ setDragOverKey(null)
617
+ }
618
+ function handleDragEnd() { draggedKey.current = null; setDragOverKey(null) }
619
+
620
+ function pinColumn(key: string, pin: "left" | "right") {
621
+ setColPins(p => ({ ...p, [key]: pin }))
622
+ }
623
+ function unpinColumn(key: string) {
624
+ if (lockedPins[key]) return
625
+ setColPins(p => { const n = { ...p }; delete n[key]; return n })
626
+ }
627
+ function toggleWrap(key: string) {
628
+ setColWrap(p => ({ ...p, [key]: !p[key] }))
629
+ }
630
+
631
+ // ── Scroll handlers ───────────────────────────────────────────────────────
632
+ function checkOverflow() {
633
+ const el = scrollRef.current
634
+ if (!el) return
635
+ setIsOverflowing(el.scrollWidth > el.clientWidth + 1)
636
+ }
637
+ function handleScroll() {
638
+ const el = scrollRef.current
639
+ if (!el) return
640
+ setScrolled(el.scrollLeft > 1)
641
+ setScrollEnd(el.scrollLeft >= el.scrollWidth - el.clientWidth - 1)
642
+ setIsOverflowing(el.scrollWidth > el.clientWidth + 1)
643
+ }
644
+
645
+ // ── Selection helpers ─────────────────────────────────────────────────────
646
+ function getRowId(row: TData, index: number, getIdFn?: (r: TData, i: number) => string | number): string | number {
647
+ return getIdFn ? getIdFn(row, index) : (row.id as string | number ?? index)
648
+ }
649
+
650
+ const toggleRow = React.useCallback((id: string | number) => {
651
+ setSelected(prev => {
652
+ const next = new Set(prev)
653
+ if (next.has(id)) next.delete(id)
654
+ else next.add(id)
655
+ return next
656
+ })
657
+ }, [setSelected])
658
+
659
+ const toggleAll = React.useCallback((allRowIds: (string | number)[]) => {
660
+ setSelected(prev => prev.size === allRowIds.length ? new Set() : new Set(allRowIds))
661
+ }, [setSelected])
662
+
663
+ // ── Sticky offset calculations ────────────────────────────────────────────
664
+ // Precompute every pinned column's offset ONCE per render so the per-cell
665
+ // `stickyStyle()` call is an O(1) map lookup instead of an O(cols) walk.
666
+ // With `cells = rows × cols`, the previous O(rows × cols²) became the
667
+ // dominant cost on wide tables.
668
+ const stickyOffsets = React.useMemo(() => {
669
+ const left = new Map<string, number>()
670
+ const right = new Map<string, number>()
671
+ let leftOffset = 0
672
+ for (const col of displayCols) {
673
+ if (effectivePins[col.key] !== "left") break
674
+ left.set(col.key, leftOffset)
675
+ leftOffset += colWidths[col.key] ?? col.width ?? 100
676
+ }
677
+ let rightOffset = 0
678
+ for (let i = displayCols.length - 1; i >= 0; i--) {
679
+ const col = displayCols[i]
680
+ if (effectivePins[col.key] !== "right") break
681
+ right.set(col.key, rightOffset)
682
+ rightOffset += colWidths[col.key] ?? col.width ?? 100
683
+ }
684
+ return { left, right }
685
+ }, [displayCols, effectivePins, colWidths])
686
+
687
+ const getStickyLeft = React.useCallback((key: string): number => {
688
+ return stickyOffsets.left.get(key) ?? 0
689
+ }, [stickyOffsets])
690
+
691
+ const getStickyRight = React.useCallback((key: string): number => {
692
+ return stickyOffsets.right.get(key) ?? 0
693
+ }, [stickyOffsets])
694
+
695
+ const stickyStyle = React.useCallback(
696
+ (key: string, isHeader = false): React.CSSProperties => {
697
+ if (isReflowViewport) return {}
698
+ const pin = effectivePins[key]
699
+ if (pin === "left") {
700
+ return isHeader
701
+ ? { position: "sticky", left: stickyOffsets.left.get(key) ?? 0, top: 0 }
702
+ : { position: "sticky", left: stickyOffsets.left.get(key) ?? 0 }
703
+ }
704
+ if (pin === "right") {
705
+ return isHeader
706
+ ? { position: "sticky", right: stickyOffsets.right.get(key) ?? 0, top: 0 }
707
+ : { position: "sticky", right: stickyOffsets.right.get(key) ?? 0 }
708
+ }
709
+ return isHeader ? { position: "sticky", top: 0 } : {}
710
+ },
711
+ [effectivePins, isReflowViewport, stickyOffsets],
712
+ )
713
+
714
+ const totalWidth = React.useMemo(
715
+ () => displayCols.reduce((s, c) => s + (colWidths[c.key] ?? c.width ?? 100), 0),
716
+ [displayCols, colWidths],
717
+ )
718
+
719
+ return {
720
+ // Sort
721
+ sortRules, setSortRules,
722
+ sortKey, sortDir,
723
+ addSortRule, removeSortRule, toggleSortDir, handleSortByKey,
724
+ // Filters
725
+ search, setSearch,
726
+ searchOpen, setSearchOpen,
727
+ searchRef,
728
+ activeFilters, setActiveFilters,
729
+ filterConnectors, setFilterConnectors, toggleConnector, getConnector,
730
+ openFilterId, setOpenFilterId,
731
+ filterBarVisible, setFilterBarVisible,
732
+ drawerExpandedFilters, setDrawerExpandedFilters,
733
+ addFilter, updateFilter, removeFilter,
734
+ // Group
735
+ groupBy, setGroupBy,
736
+ // Column quick-search
737
+ colMenuSearch, setColMenuSearch,
738
+ // Selection
739
+ selected, setSelected, toggleRow, toggleAll, getRowId,
740
+ // Column widths / order / pins / wrap
741
+ colWidths, setColWidths, resizeRef, startResize,
742
+ colOrder, setColOrder, moveCol,
743
+ colPins, setColPins, lockedPins, LOCKED_KEYS,
744
+ pinColumn, unpinColumn,
745
+ colWrap, setColWrap, toggleWrap,
746
+ // Drag-to-reorder
747
+ draggedKey, dragOverKey,
748
+ handleDragStart, handleDragOver, handleDrop, handleDragEnd,
749
+ // Scroll
750
+ scrollRef, scrolled, scrollEnd, isOverflowing,
751
+ checkOverflow, handleScroll,
752
+ // Hover
753
+ hoveredRow, setHoveredRow,
754
+ // Derived
755
+ rows, pagedRows, groupedRows,
756
+ effectivePins, displayCols,
757
+ isReflowViewport,
758
+ getStickyLeft, getStickyRight, stickyStyle,
759
+ totalWidth,
760
+ // Display settings
761
+ sheetOpen, setSheetOpen,
762
+ sheetInitialPanel, setSheetInitialPanel,
763
+ showGridlines, setShowGridlines,
764
+ rowHeight, setRowHeight,
765
+ hiddenCols, setHiddenCols, toggleColVisibility,
766
+ }
767
+ }