@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
@@ -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
+ }