basalt-ui 1.30.2 → 1.32.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 (357) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/MIGRATING.md +256 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +5 -5
  6. package/configs/oxlint-plugin.js +371 -4
  7. package/configs/oxlint-plugin.test.ts +327 -2
  8. package/configs/oxlint.json +2 -1
  9. package/dist/agent-chat/composer.d.ts.map +1 -1
  10. package/dist/agent-chat/composer.js +41 -43
  11. package/dist/agent-chat/thread-workspace.d.ts +2 -2
  12. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  13. package/dist/agent-chat/thread-workspace.js +3 -4
  14. package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
  15. package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
  16. package/dist/agent-chat/use-keyboard-inset.js +34 -0
  17. package/dist/charts/cursor/touch-pin.d.ts +45 -0
  18. package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
  19. package/dist/charts/cursor/touch-pin.js +79 -0
  20. package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
  21. package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
  22. package/dist/charts/hooks/use-discrete-cursor.js +198 -0
  23. package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
  24. package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
  25. package/dist/charts/hooks/use-hysteresis.js +12 -0
  26. package/dist/charts/hooks/useBandPlot.d.ts +7 -0
  27. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  28. package/dist/charts/hooks/useBandPlot.js +62 -18
  29. package/dist/charts/hooks/useChartCursor.d.ts +40 -11
  30. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  31. package/dist/charts/hooks/useChartCursor.js +102 -11
  32. package/dist/charts/index.d.ts +0 -1
  33. package/dist/charts/index.d.ts.map +1 -1
  34. package/dist/charts/index.js +0 -10
  35. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  36. package/dist/charts/kinds/BandStrip.js +47 -82
  37. package/dist/charts/kinds/Donut.d.ts +9 -3
  38. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  39. package/dist/charts/kinds/Donut.js +110 -48
  40. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  41. package/dist/charts/kinds/DualPanel.js +138 -145
  42. package/dist/charts/kinds/Heatmap.d.ts +7 -0
  43. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  44. package/dist/charts/kinds/Heatmap.js +37 -16
  45. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  46. package/dist/charts/kinds/MirroredBars.js +68 -111
  47. package/dist/charts/kinds/band-chrome.d.ts +31 -0
  48. package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
  49. package/dist/charts/kinds/band-chrome.js +71 -0
  50. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  51. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  52. package/dist/charts/kinds/donut-layout.js +52 -0
  53. package/dist/charts/layout/auto-margin.d.ts +7 -2
  54. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  55. package/dist/charts/layout/auto-margin.js +12 -3
  56. package/dist/charts/layout/end-labels.d.ts +64 -0
  57. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  58. package/dist/charts/layout/end-labels.js +76 -0
  59. package/dist/charts/primitives/Axes.d.ts +32 -4
  60. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  61. package/dist/charts/primitives/Axes.js +62 -16
  62. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  63. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  64. package/dist/charts/primitives/CartesianChart.js +264 -145
  65. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  66. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartCard.js +56 -21
  68. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  69. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartFrame.js +116 -34
  71. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  72. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  73. package/dist/charts/primitives/ChartLegend.js +86 -73
  74. package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
  75. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  76. package/dist/charts/primitives/ChartTooltip.js +20 -4
  77. package/dist/charts/primitives/Crosshair.js +2 -2
  78. package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
  79. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  80. package/dist/charts/primitives/HoverOverlay.js +54 -27
  81. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  82. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  83. package/dist/charts/primitives/chart-card-context.d.ts +33 -0
  84. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  85. package/dist/charts/primitives/chart-card-context.js +18 -0
  86. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  87. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  88. package/dist/charts/primitives/chart-frame-layout.js +75 -34
  89. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  90. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  91. package/dist/charts/primitives/chart-layout.js +175 -0
  92. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  93. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  94. package/dist/charts/primitives/chart-tier.js +42 -13
  95. package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
  96. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
  97. package/dist/charts/primitives/legend-disclosure.js +153 -0
  98. package/dist/charts/series.d.ts +4 -1
  99. package/dist/charts/series.d.ts.map +1 -1
  100. package/dist/charts/utils/ticks.d.ts +27 -8
  101. package/dist/charts/utils/ticks.d.ts.map +1 -1
  102. package/dist/charts/utils/ticks.js +35 -11
  103. package/dist/common/errors.d.ts +11 -1
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +16 -2
  106. package/dist/common/hit-floor.d.ts +10 -0
  107. package/dist/common/hit-floor.d.ts.map +1 -0
  108. package/dist/common/hit-floor.js +6 -0
  109. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  110. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  111. package/dist/common/use-measured-widths.d.ts +37 -0
  112. package/dist/common/use-measured-widths.d.ts.map +1 -0
  113. package/dist/common/use-measured-widths.js +51 -0
  114. package/dist/common/use-media-query.d.ts +6 -0
  115. package/dist/common/use-media-query.d.ts.map +1 -0
  116. package/dist/common/use-media-query.js +40 -0
  117. package/dist/common/validate.d.ts +9 -0
  118. package/dist/common/validate.d.ts.map +1 -1
  119. package/dist/common/validate.js +9 -4
  120. package/dist/content/article-layout.d.ts +4 -2
  121. package/dist/content/article-layout.d.ts.map +1 -1
  122. package/dist/content/article-layout.js +43 -3
  123. package/dist/content/article-layout.module.css +45 -4
  124. package/dist/content/article-layout.module.css.d.ts +3 -0
  125. package/dist/content/code-block.d.ts.map +1 -1
  126. package/dist/content/code-block.js +40 -2
  127. package/dist/content/code-block.module.css +32 -2
  128. package/dist/content/prose.module.css +13 -8
  129. package/dist/controls/actions.d.ts +29 -6
  130. package/dist/controls/actions.d.ts.map +1 -1
  131. package/dist/controls/actions.js +158 -27
  132. package/dist/controls/actions.module.css +14 -1
  133. package/dist/controls/actions.module.css.d.ts +1 -0
  134. package/dist/controls/controls.module.css +13 -31
  135. package/dist/controls/enum-filter.js +2 -2
  136. package/dist/controls/filter-context.d.ts +10 -0
  137. package/dist/controls/filter-context.d.ts.map +1 -1
  138. package/dist/controls/filter-context.js +2 -0
  139. package/dist/controls/filter-pill.d.ts.map +1 -1
  140. package/dist/controls/filter-pill.js +1 -0
  141. package/dist/controls/index.d.ts +1 -1
  142. package/dist/controls/index.d.ts.map +1 -1
  143. package/dist/controls/index.js +0 -2
  144. package/dist/controls/multi-select-filter.js +2 -2
  145. package/dist/controls/sync-button.module.css +2 -1
  146. package/dist/dashboard/section.d.ts.map +1 -1
  147. package/dist/dashboard/section.js +1 -0
  148. package/dist/dashboard/section.module.css +1 -26
  149. package/dist/dashboard/stat-card.d.ts +1 -1
  150. package/dist/dashboard/stat-card.d.ts.map +1 -1
  151. package/dist/dashboard/stat-card.js +4 -1
  152. package/dist/dashboard/stat-card.module.css +6 -9
  153. package/dist/dashboard/stat-group.module.css +33 -66
  154. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  155. package/dist/dashboard/widget-grid.module.css +11 -38
  156. package/dist/dashboard/widget-header.d.ts +14 -4
  157. package/dist/dashboard/widget-header.d.ts.map +1 -1
  158. package/dist/dashboard/widget-header.js +23 -6
  159. package/dist/dashboard/widget-header.module.css +145 -133
  160. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  161. package/dist/data/data-table-state.d.ts +83 -0
  162. package/dist/data/data-table-state.d.ts.map +1 -0
  163. package/dist/data/data-table-state.js +139 -0
  164. package/dist/data/data-table.d.ts +56 -2
  165. package/dist/data/data-table.d.ts.map +1 -1
  166. package/dist/data/data-table.js +436 -336
  167. package/dist/data/data-table.module.css +94 -0
  168. package/dist/data/data-table.module.css.d.ts +11 -0
  169. package/dist/data/virtual-list.d.ts.map +1 -1
  170. package/dist/data/virtual-list.js +10 -0
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +3 -3
  173. package/dist/forms/form-layout.module.css +20 -5
  174. package/dist/forms/form-layout.module.css.d.ts +1 -0
  175. package/dist/guard/index.d.ts +8 -2
  176. package/dist/guard/index.d.ts.map +1 -1
  177. package/dist/guard/index.js +68 -2
  178. package/dist/guard/types.d.ts +3 -3
  179. package/dist/guard/types.d.ts.map +1 -1
  180. package/dist/index.d.ts +1 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +3 -1
  183. package/dist/notifications/center.d.ts.map +1 -1
  184. package/dist/provider/index.d.ts +20 -1
  185. package/dist/provider/index.d.ts.map +1 -1
  186. package/dist/provider/index.js +30 -8
  187. package/dist/shell/app-brand.d.ts.map +1 -1
  188. package/dist/shell/app-brand.js +1 -0
  189. package/dist/shell/app-brand.module.css +2 -2
  190. package/dist/shell/app-header.module.css +18 -8
  191. package/dist/shell/app-main.module.css +54 -3
  192. package/dist/shell/app-main.module.css.d.ts +1 -0
  193. package/dist/shell/app-mobile-nav.module.css +16 -4
  194. package/dist/shell/app-sidebar.module.css +4 -6
  195. package/dist/shell/aside-docking.d.ts +31 -0
  196. package/dist/shell/aside-docking.d.ts.map +1 -0
  197. package/dist/shell/aside-docking.js +19 -0
  198. package/dist/shell/index.d.ts +1 -0
  199. package/dist/shell/index.d.ts.map +1 -1
  200. package/dist/shell/index.js +56 -16
  201. package/dist/shell/mobile-nav-model.d.ts +1 -1
  202. package/dist/shell/page-aside.d.ts +22 -3
  203. package/dist/shell/page-aside.d.ts.map +1 -1
  204. package/dist/shell/page-aside.js +27 -13
  205. package/dist/shell/page-bar.d.ts.map +1 -1
  206. package/dist/shell/page-bar.js +10 -5
  207. package/dist/shell/page-bar.module.css +10 -34
  208. package/dist/shell/use-breakpoint.d.ts +3 -4
  209. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  210. package/dist/shell/use-breakpoint.js +3 -25
  211. package/dist/shell/use-size-class.d.ts +6 -0
  212. package/dist/shell/use-size-class.d.ts.map +1 -0
  213. package/dist/shell/use-size-class.js +19 -0
  214. package/dist/state/persisted-or-local.d.ts +4 -1
  215. package/dist/state/persisted-or-local.d.ts.map +1 -1
  216. package/dist/state/persisted-or-local.js +4 -3
  217. package/dist/state/persisted.d.ts +10 -0
  218. package/dist/state/persisted.d.ts.map +1 -1
  219. package/dist/state/persisted.js +44 -14
  220. package/dist/styles.css +41 -0
  221. package/dist/surfaces.d.ts +4 -4
  222. package/dist/surfaces.d.ts.map +1 -1
  223. package/dist/surfaces.js +11 -3
  224. package/dist/theme/controls.module.css +7 -0
  225. package/dist/theme/ctl-theme.d.ts +1 -0
  226. package/dist/theme/ctl-theme.d.ts.map +1 -1
  227. package/dist/theme/ctl-theme.js +12 -4
  228. package/dist/theme/floating.module.css +37 -0
  229. package/dist/theme/floating.module.css.d.ts +3 -0
  230. package/dist/theme/index.d.ts +2 -2
  231. package/dist/theme/index.d.ts.map +1 -1
  232. package/dist/theme/index.js +34 -5
  233. package/dist/theme/nav-link.module.css +7 -0
  234. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  235. package/dist/tokens/index.d.ts +7 -16
  236. package/dist/tokens/index.d.ts.map +1 -1
  237. package/dist/tokens/index.js +23 -28
  238. package/dist/tokens/palette.d.ts +5 -27
  239. package/dist/tokens/palette.d.ts.map +1 -1
  240. package/dist/tokens/palette.js +8 -30
  241. package/dist/tokens/size-classes.d.ts +40 -0
  242. package/dist/tokens/size-classes.d.ts.map +1 -0
  243. package/dist/tokens/size-classes.js +29 -0
  244. package/dist/tokens.css +8 -4
  245. package/llms.txt +3 -3
  246. package/package.json +18 -17
  247. package/src/agent-chat/composer.tsx +74 -47
  248. package/src/agent-chat/thread-workspace.tsx +8 -11
  249. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  250. package/src/charts/cursor/touch-pin.ts +138 -0
  251. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  252. package/src/charts/hooks/use-hysteresis.ts +26 -0
  253. package/src/charts/hooks/useBandPlot.ts +90 -27
  254. package/src/charts/hooks/useChartCursor.ts +211 -26
  255. package/src/charts/index.ts +0 -8
  256. package/src/charts/kinds/BandStrip.tsx +25 -61
  257. package/src/charts/kinds/Donut.tsx +153 -62
  258. package/src/charts/kinds/DualPanel.tsx +21 -26
  259. package/src/charts/kinds/Heatmap.tsx +61 -22
  260. package/src/charts/kinds/MirroredBars.tsx +23 -65
  261. package/src/charts/kinds/band-chrome.tsx +91 -0
  262. package/src/charts/kinds/donut-layout.ts +82 -0
  263. package/src/charts/layout/auto-margin.ts +24 -5
  264. package/src/charts/layout/end-labels.ts +135 -0
  265. package/src/charts/primitives/Axes.tsx +112 -21
  266. package/src/charts/primitives/CartesianChart.tsx +282 -111
  267. package/src/charts/primitives/ChartCard.tsx +119 -48
  268. package/src/charts/primitives/ChartFrame.tsx +204 -60
  269. package/src/charts/primitives/ChartLegend.tsx +115 -78
  270. package/src/charts/primitives/ChartTooltip.tsx +24 -3
  271. package/src/charts/primitives/Crosshair.tsx +2 -2
  272. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  273. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  274. package/src/charts/primitives/HoverOverlay.tsx +62 -20
  275. package/src/charts/primitives/chart-card-context.ts +46 -0
  276. package/src/charts/primitives/chart-frame-layout.ts +207 -182
  277. package/src/charts/primitives/chart-layout.ts +318 -0
  278. package/src/charts/primitives/chart-tier.tsx +66 -21
  279. package/src/charts/primitives/legend-disclosure.tsx +177 -0
  280. package/src/charts/series.ts +4 -1
  281. package/src/charts/utils/ticks.ts +95 -19
  282. package/src/common/errors.ts +25 -1
  283. package/src/common/hit-floor.ts +9 -0
  284. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  285. package/src/common/use-measured-widths.ts +88 -0
  286. package/src/common/use-media-query.ts +56 -0
  287. package/src/common/validate.ts +17 -4
  288. package/src/content/article-layout.module.css +45 -4
  289. package/src/content/article-layout.module.css.d.ts +3 -0
  290. package/src/content/article-layout.tsx +60 -5
  291. package/src/content/code-block.module.css +32 -2
  292. package/src/content/code-block.tsx +47 -3
  293. package/src/content/prose.module.css +13 -8
  294. package/src/controls/actions.module.css +14 -1
  295. package/src/controls/actions.module.css.d.ts +1 -0
  296. package/src/controls/actions.tsx +223 -75
  297. package/src/controls/controls.module.css +13 -31
  298. package/src/controls/enum-filter.tsx +2 -2
  299. package/src/controls/filter-context.tsx +11 -0
  300. package/src/controls/filter-pill.tsx +1 -0
  301. package/src/controls/index.ts +0 -1
  302. package/src/controls/multi-select-filter.tsx +2 -2
  303. package/src/controls/sync-button.module.css +2 -1
  304. package/src/dashboard/section.module.css +1 -26
  305. package/src/dashboard/section.tsx +1 -0
  306. package/src/dashboard/stat-card.module.css +6 -9
  307. package/src/dashboard/stat-card.tsx +6 -3
  308. package/src/dashboard/stat-group.module.css +33 -66
  309. package/src/dashboard/widget-grid.module.css +11 -38
  310. package/src/dashboard/widget-grid.tsx +7 -6
  311. package/src/dashboard/widget-header.module.css +145 -133
  312. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  313. package/src/dashboard/widget-header.tsx +45 -11
  314. package/src/data/data-table-state.ts +218 -0
  315. package/src/data/data-table.module.css +94 -0
  316. package/src/data/data-table.module.css.d.ts +11 -0
  317. package/src/data/data-table.tsx +547 -299
  318. package/src/data/virtual-list.tsx +31 -0
  319. package/src/forms/form-layout.module.css +20 -5
  320. package/src/forms/form-layout.module.css.d.ts +1 -0
  321. package/src/forms/form-layout.tsx +12 -10
  322. package/src/guard/index.ts +188 -3
  323. package/src/guard/types.ts +3 -2
  324. package/src/index.ts +2 -0
  325. package/src/notifications/center.tsx +3 -0
  326. package/src/provider/index.tsx +60 -7
  327. package/src/shell/app-brand.module.css +2 -2
  328. package/src/shell/app-brand.tsx +1 -0
  329. package/src/shell/app-header.module.css +18 -8
  330. package/src/shell/app-main.module.css +54 -3
  331. package/src/shell/app-main.module.css.d.ts +1 -0
  332. package/src/shell/app-mobile-nav.module.css +16 -4
  333. package/src/shell/app-sidebar.module.css +4 -6
  334. package/src/shell/aside-docking.ts +54 -0
  335. package/src/shell/index.tsx +178 -129
  336. package/src/shell/mobile-nav-model.ts +1 -1
  337. package/src/shell/page-aside.tsx +70 -21
  338. package/src/shell/page-bar.module.css +10 -34
  339. package/src/shell/page-bar.tsx +10 -5
  340. package/src/shell/use-breakpoint.ts +9 -45
  341. package/src/shell/use-size-class.ts +31 -0
  342. package/src/state/persisted-or-local.ts +10 -4
  343. package/src/state/persisted.ts +76 -17
  344. package/src/styles.css +41 -0
  345. package/src/surfaces.ts +9 -1
  346. package/src/theme/controls.module.css +7 -0
  347. package/src/theme/ctl-theme.tsx +15 -3
  348. package/src/theme/floating.module.css +37 -0
  349. package/src/theme/floating.module.css.d.ts +3 -0
  350. package/src/theme/index.ts +46 -8
  351. package/src/theme/nav-link.module.css +7 -0
  352. package/src/theme-lab/derive-controls.tsx +2 -0
  353. package/src/tokens/index.ts +20 -30
  354. package/src/tokens/palette.ts +6 -28
  355. package/src/tokens/size-classes.ts +46 -0
  356. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  357. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `BasaltDataTable`'s state hooks — the table's own interactive state, the fold row disclosure, the
3
+ * card projection and the row/card activation props. Internal to `./data/table`; nothing here is on a
4
+ * published subpath.
5
+ */
6
+ import type {
7
+ ColumnFiltersState,
8
+ ColumnPinningState,
9
+ PaginationState,
10
+ RowSelectionState,
11
+ SortingState,
12
+ Updater,
13
+ } from '@tanstack/react-table'
14
+ import { functionalUpdate } from '@tanstack/react-table'
15
+ import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from 'react'
16
+ import { useCallback, useEffect, useRef, useState } from 'react'
17
+ import { useMeasuredWidths } from '../common/use-measured-widths'
18
+ import { CONTAINER_CLASSES } from '../tokens/size-classes'
19
+ import classes from './data-table.module.css'
20
+
21
+ export const EMPTY_FOLD_SET: ReadonlySet<string> = new Set()
22
+
23
+ /**
24
+ * Which rows have their fold disclosure open. `row.id` defaults to the row's index in `data` (no
25
+ * `getRowId`) — stable across a client-side sort/filter/pagination, which reorder or subset the SAME
26
+ * core rows. PRUNED rather than cleared on every `data` change: a non-memoized `data` prop (an inline
27
+ * `.map()`, an unstable query result) hands a brand-new array of the SAME rows on every parent
28
+ * re-render, and clearing on that reference alone collapsed every open disclosure the instant
29
+ * anything upstream re-rendered.
30
+ *
31
+ * Pruning by ID ALONE is only safe with `getRowId` — a caller-declared identity that genuinely
32
+ * survives a page turn or a refetch, so an id no longer present (a manual-pagination page turn, a
33
+ * delete) still has to go, or a differently-shaped record seated at a reused id would silently read
34
+ * as pre-expanded. Without `getRowId` the id IS the array index, and an index survives a refetch
35
+ * trivially — it is still "0", "1", … even when every object behind it is a different record.
36
+ * Keeping an expanded index across THAT would silently attach the old disclosure to a stranger's
37
+ * row. So the index-id lane keeps a `prevData` snapshot and asks the narrower, correct question: is
38
+ * the object AT this index still the same reference as last time? A non-memoized array of the SAME
39
+ * rows answers yes at every index (the case above); a page turn or a refetch answers no.
40
+ */
41
+ export function useRowDisclosure<T>({
42
+ data,
43
+ getRowId,
44
+ }: {
45
+ data: readonly T[]
46
+ getRowId: ((row: T, index: number) => string) | undefined
47
+ }): { expanded: ReadonlySet<string>; toggle: (rowId: string) => void } {
48
+ const [expanded, setExpanded] = useState<ReadonlySet<string>>(EMPTY_FOLD_SET)
49
+ const toggle = useCallback((rowId: string) => {
50
+ setExpanded((current) => {
51
+ const next = new Set(current)
52
+ if (next.has(rowId)) next.delete(rowId)
53
+ else next.add(rowId)
54
+ return next
55
+ })
56
+ }, [])
57
+ const prevDataRef = useRef(data)
58
+ useEffect(() => {
59
+ const prevData = prevDataRef.current
60
+ prevDataRef.current = data
61
+ setExpanded((current) => {
62
+ if (current.size === 0) return current
63
+ const liveIds = getRowId && new Set(data.map((row, index) => getRowId(row, index)))
64
+ const isLive = (id: string) => {
65
+ if (liveIds) return liveIds.has(id)
66
+ const index = Number(id)
67
+ return data[index] !== undefined && data[index] === prevData[index]
68
+ }
69
+ const next = new Set([...current].filter(isLive))
70
+ return next.size === current.size ? current : next
71
+ })
72
+ }, [data, getRowId])
73
+ return { expanded, toggle }
74
+ }
75
+
76
+ /** `useState` whose every write also reports the settled value — the one shape sorting, the global
77
+ * filter, the column filters and pagination all share. The callback runs inside the updater, so it
78
+ * sees exactly the value TanStack applied. */
79
+ function useReportedState<S>(
80
+ initial: S,
81
+ onChange: ((next: S) => void) | undefined,
82
+ ): [S, (updater: Updater<S>) => void] {
83
+ const [value, setValue] = useState(initial)
84
+ const update = useCallback(
85
+ (updater: Updater<S>) => {
86
+ setValue((prev) => {
87
+ const next = functionalUpdate(updater, prev)
88
+ onChange?.(next)
89
+ return next
90
+ })
91
+ },
92
+ [onChange],
93
+ )
94
+ return [value, update]
95
+ }
96
+
97
+ /** The `BasaltDataTableProps` fields `useDataTableState` reads — declared here rather than picked
98
+ * off the props type, so the hook owns its contract and this module never imports the component. */
99
+ export type DataTableStateProps = {
100
+ initialSorting?: SortingState | undefined
101
+ onSortingChange?: ((sorting: SortingState) => void) | undefined
102
+ initialGlobalFilter?: string | undefined
103
+ onGlobalFilterChange?: ((value: string) => void) | undefined
104
+ onColumnFiltersChange?: ((filters: ColumnFiltersState) => void) | undefined
105
+ initialPagination?: PaginationState | undefined
106
+ onPaginationChange?: ((pagination: PaginationState) => void) | undefined
107
+ initialColumnPinning?: ColumnPinningState | undefined
108
+ rowSelection?: RowSelectionState | undefined
109
+ onRowSelectionChange?: ((selection: RowSelectionState) => void) | undefined
110
+ }
111
+
112
+ /** The table's own interactive state and the TanStack `on*Change` handlers that write it — every
113
+ * piece uncontrolled with an optional change callback, except row selection, which is controlled
114
+ * whenever `rowSelection` is passed. */
115
+ export function useDataTableState(props: DataTableStateProps, defaultPageSize: number) {
116
+ const { rowSelection, onRowSelectionChange } = props
117
+ const [sorting, onSortingChange] = useReportedState(
118
+ props.initialSorting ?? [],
119
+ props.onSortingChange,
120
+ )
121
+ const [globalFilter, onGlobalFilterChange] = useReportedState(
122
+ props.initialGlobalFilter ?? '',
123
+ props.onGlobalFilterChange,
124
+ )
125
+ const [columnFilters, onColumnFiltersChange] = useReportedState<ColumnFiltersState>(
126
+ [],
127
+ props.onColumnFiltersChange,
128
+ )
129
+ const [pagination, onPaginationChange] = useReportedState(
130
+ props.initialPagination ?? { pageIndex: 0, pageSize: defaultPageSize },
131
+ props.onPaginationChange,
132
+ )
133
+ const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(
134
+ props.initialColumnPinning ?? {},
135
+ )
136
+ // Uncontrolled by default; `rowSelection`, when passed, is the truth and this only mirrors it so
137
+ // the updater below has a base to apply against.
138
+ const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
139
+
140
+ const handleRowSelectionChange = useCallback(
141
+ (updater: Updater<RowSelectionState>) => {
142
+ // CONTROLLED: the caller's map is the base AND the only writer. Mirroring it into state too
143
+ // would leave a copy that the caller never moves, and that stale copy becomes the selection
144
+ // the moment `rowSelection` goes back to `undefined` — plus every tick would render twice for
145
+ // one change. Report the next map and let the caller own it.
146
+ if (rowSelection !== undefined) {
147
+ onRowSelectionChange?.(functionalUpdate(updater, rowSelection))
148
+ return
149
+ }
150
+ setInternalRowSelection((prev) => {
151
+ const next = functionalUpdate(updater, prev)
152
+ onRowSelectionChange?.(next)
153
+ return next
154
+ })
155
+ },
156
+ [rowSelection, onRowSelectionChange],
157
+ )
158
+
159
+ return {
160
+ sorting,
161
+ onSortingChange,
162
+ globalFilter,
163
+ onGlobalFilterChange,
164
+ columnFilters,
165
+ onColumnFiltersChange,
166
+ pagination,
167
+ onPaginationChange,
168
+ columnPinning,
169
+ onColumnPinningChange: setColumnPinning,
170
+ rowSelection: rowSelection ?? internalRowSelection,
171
+ onRowSelectionChange: handleRowSelectionChange,
172
+ }
173
+ }
174
+
175
+ /** Whether the table renders as `renderCard` projections: its own root measured below the `regular`
176
+ * container class. A zero width is an un-laid-out ancestor, not a narrow table, so it moves nothing
177
+ * — the default (and the SSR answer) is the table. The root's width never depends on which body it
178
+ * holds, so the swap cannot oscillate. */
179
+ export function useCardProjection(enabled: boolean): {
180
+ rootRef: RefObject<HTMLDivElement | null>
181
+ cards: boolean
182
+ } {
183
+ const rootRef = useRef<HTMLDivElement>(null)
184
+ const [narrow, setNarrow] = useState(false)
185
+ useMeasuredWidths({
186
+ resolveRoot: () => rootRef.current,
187
+ signature: '',
188
+ enabled,
189
+ onMeasure: (root) => {
190
+ if (root.clientWidth === 0) return
191
+ const next = root.clientWidth < CONTAINER_CLASSES.regular
192
+ setNarrow((current) => (current === next ? current : next))
193
+ },
194
+ })
195
+ return { rootRef, cards: enabled && narrow }
196
+ }
197
+
198
+ /** Click + Enter activation, shared by a row and its card. Enter only: Space is the browser's own
199
+ * page-scroll on a focused non-button, and stealing it from a keyboard reader moving down a long
200
+ * table costs more than the second activation key buys. */
201
+ export function activationProps<T>(onRowActivate: ((row: T) => void) | undefined, row: T) {
202
+ if (onRowActivate === undefined) return undefined
203
+ return {
204
+ className: classes.activatable,
205
+ 'data-activatable': true,
206
+ tabIndex: 0,
207
+ onClick: () => onRowActivate(row),
208
+ onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => {
209
+ if (event.key !== 'Enter') return
210
+ // Only the element's own Enter. A cell or card may hold a button, a link or the selection
211
+ // checkbox, and keydown bubbles — so without this an Enter on a nested control fired that
212
+ // control AND opened the row's detail behind it.
213
+ if (event.target !== event.currentTarget) return
214
+ event.preventDefault()
215
+ onRowActivate(row)
216
+ },
217
+ }
218
+ }
@@ -62,3 +62,97 @@
62
62
  .containment[data-contained='true'] {
63
63
  overflow-x: auto;
64
64
  }
65
+
66
+ /*
67
+ * The selection column's hit area is the WHOLE cell (`docs/CONTROLS-SPEC.md` §2): the
68
+ * cell is the positioning box and the wrapping <label>'s ::after stretches over it, so a tap
69
+ * anywhere in the cell toggles the checkbox. Not `data-basalt-hit` — that primitive centres a
70
+ * `--vx-hit` square on its element; this one must fill a cell of any width.
71
+ */
72
+ .selectCell {
73
+ position: relative;
74
+ }
75
+
76
+ .selectHit {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ cursor: pointer;
81
+ }
82
+
83
+ .selectHit::after {
84
+ content: '';
85
+ position: absolute;
86
+ inset: 0;
87
+ }
88
+
89
+ /*
90
+ * The column-fold measuring box (`useColumnFold` in `data-table.tsx`) — a plain block wrapper
91
+ * around whichever overflow shape the table already picks (the measured containment div, or
92
+ * `Table.ScrollContainer`). `min-width: 0` is the same flex/grid floor `.containment` needs: a
93
+ * wrapper sized by its own min-content would never read a shrunken `clientWidth` back.
94
+ */
95
+ .foldWrapper {
96
+ min-width: 0;
97
+ }
98
+
99
+ /*
100
+ * The fold-disclosure toggle column (`docs/CONTROLS-SPEC.md` §2) — a narrow, centred cell
101
+ * matching `HIT_COARSE` in `common/hit-floor.ts`, so the header's empty cell and every row's
102
+ * button cell line up under one another regardless of how many columns folded.
103
+ */
104
+ .foldToggleCell {
105
+ width: 2.75rem;
106
+ text-align: center;
107
+ }
108
+
109
+ .foldToggle {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ border: none;
114
+ background: none;
115
+ padding: 0;
116
+ color: var(--vx-muted);
117
+ cursor: pointer;
118
+ }
119
+
120
+ .foldToggle:hover {
121
+ color: var(--vx-ink);
122
+ }
123
+
124
+ /* The disclosure row itself carries no border of its own — it reads as part of the row above it,
125
+ not a new one. */
126
+ .foldDisclosureRowTr {
127
+ border-top: none;
128
+ }
129
+
130
+ .foldDisclosureCell {
131
+ padding-top: 0;
132
+ background: var(--vx-surface-subtle);
133
+ }
134
+
135
+ .foldDisclosure {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: var(--vx-space-row-inset-y, 0.375rem);
139
+ padding: var(--vx-space-row-inset-y, 0.375rem) 0;
140
+ }
141
+
142
+ .foldDisclosureRow {
143
+ display: flex;
144
+ justify-content: space-between;
145
+ align-items: baseline;
146
+ gap: var(--vx-space-row-inset-x, 0.5rem);
147
+ }
148
+
149
+ .foldDisclosureLabel {
150
+ font-size: var(--vx-text-xs);
151
+ color: var(--vx-muted);
152
+ }
153
+
154
+ .foldDisclosureValue {
155
+ font-size: var(--vx-text-sm);
156
+ color: var(--vx-ink);
157
+ text-align: right;
158
+ }
@@ -7,5 +7,16 @@ declare const classes: {
7
7
  readonly bulkBar: string
8
8
  readonly bulkCount: string
9
9
  readonly containment: string
10
+ readonly foldDisclosure: string
11
+ readonly foldDisclosureCell: string
12
+ readonly foldDisclosureLabel: string
13
+ readonly foldDisclosureRow: string
14
+ readonly foldDisclosureRowTr: string
15
+ readonly foldDisclosureValue: string
16
+ readonly foldToggle: string
17
+ readonly foldToggleCell: string
18
+ readonly foldWrapper: string
19
+ readonly selectCell: string
20
+ readonly selectHit: string
10
21
  }
11
22
  export default classes