basalt-ui 1.26.0 → 1.28.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 (1069) hide show
  1. package/AGENTS.md +23 -23
  2. package/CHANGELOG.md +67 -0
  3. package/MIGRATING.md +896 -37
  4. package/README.md +33 -3
  5. package/agent/rules/basalt-batteries.md +25 -10
  6. package/agent/rules/basalt-controls.md +73 -43
  7. package/agent/rules/basalt-mantine.md +17 -6
  8. package/agent/rules/basalt-state.md +5 -0
  9. package/agent/rules/basalt-tokens.md +1 -1
  10. package/configs/oxlint-plugin.js +1286 -73
  11. package/configs/oxlint-plugin.test.ts +1338 -29
  12. package/configs/oxlint.json +12 -5
  13. package/dist/agent/coalesce.d.ts.map +1 -1
  14. package/dist/agent/coalesce.js +2 -1
  15. package/dist/agent/merge.d.ts.map +1 -1
  16. package/dist/agent/merge.js +2 -1
  17. package/dist/agent/part-list.d.ts.map +1 -1
  18. package/dist/agent/part-list.js +2 -0
  19. package/dist/agent/stick-to-bottom.d.ts +3 -6
  20. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  21. package/dist/agent/stick-to-bottom.js +6 -3
  22. package/dist/agent-chat/composer.d.ts +6 -2
  23. package/dist/agent-chat/composer.d.ts.map +1 -1
  24. package/dist/agent-chat/composer.js +49 -33
  25. package/dist/agent-chat/relative-time.d.ts +0 -9
  26. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  27. package/dist/agent-chat/relative-time.js +2 -1
  28. package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
  29. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  30. package/dist/agent-chat/thread-detail-panel.js +8 -5
  31. package/dist/agent-chat/thread-feed-row.d.ts +2 -1
  32. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  33. package/dist/agent-chat/thread-feed-row.js +92 -92
  34. package/dist/agent-chat/thread-feed.d.ts +3 -2
  35. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  36. package/dist/agent-chat/thread-feed.js +8 -3
  37. package/dist/agent-chat/thread-message.d.ts +4 -3
  38. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  39. package/dist/agent-chat/thread-message.js +70 -31
  40. package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
  41. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-outcome-card.js +9 -2
  43. package/dist/agent-chat/thread-workspace.d.ts +7 -2
  44. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  45. package/dist/agent-chat/thread-workspace.js +62 -29
  46. package/dist/agent-chat/tool-chip.d.ts +3 -2
  47. package/dist/agent-chat/tool-chip.d.ts.map +1 -1
  48. package/dist/agent-chat/tool-chip.js +7 -2
  49. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  50. package/dist/charts/hooks/useBandPlot.js +15 -8
  51. package/dist/charts/index.d.ts +8 -7
  52. package/dist/charts/index.d.ts.map +1 -1
  53. package/dist/charts/index.js +45 -5
  54. package/dist/charts/kinds/BandStrip.d.ts +10 -7
  55. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  56. package/dist/charts/kinds/BandStrip.js +12 -7
  57. package/dist/charts/kinds/Bars.d.ts +12 -6
  58. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  59. package/dist/charts/kinds/Bars.js +37 -11
  60. package/dist/charts/kinds/Donut.d.ts +6 -5
  61. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  62. package/dist/charts/kinds/Donut.js +19 -2
  63. package/dist/charts/kinds/DualPanel.d.ts +6 -5
  64. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  65. package/dist/charts/kinds/DualPanel.js +36 -8
  66. package/dist/charts/kinds/Heatmap.d.ts +10 -6
  67. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  68. package/dist/charts/kinds/Heatmap.js +58 -31
  69. package/dist/charts/kinds/MirroredBars.d.ts +6 -5
  70. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  71. package/dist/charts/kinds/MirroredBars.js +9 -2
  72. package/dist/charts/kinds/MultiLine.d.ts +11 -6
  73. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  74. package/dist/charts/kinds/MultiLine.js +24 -15
  75. package/dist/charts/kinds/StackedArea.d.ts +18 -8
  76. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  77. package/dist/charts/kinds/StackedArea.js +29 -4
  78. package/dist/charts/kinds/ZonedLine.d.ts +12 -8
  79. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  80. package/dist/charts/kinds/ZonedLine.js +33 -23
  81. package/dist/charts/layout/auto-margin.d.ts +65 -8
  82. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  83. package/dist/charts/layout/auto-margin.js +45 -10
  84. package/dist/charts/layout/log-ticks.d.ts +27 -0
  85. package/dist/charts/layout/log-ticks.d.ts.map +1 -0
  86. package/dist/charts/layout/log-ticks.js +42 -0
  87. package/dist/charts/primitives/Axes.d.ts +11 -2
  88. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  89. package/dist/charts/primitives/Axes.js +15 -5
  90. package/dist/charts/primitives/CartesianChart.d.ts +45 -7
  91. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  92. package/dist/charts/primitives/CartesianChart.js +86 -28
  93. package/dist/charts/primitives/ChartCard.d.ts +27 -2
  94. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  95. package/dist/charts/primitives/ChartCard.js +37 -17
  96. package/dist/charts/primitives/ChartFrame.d.ts +30 -4
  97. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  98. package/dist/charts/primitives/ChartFrame.js +45 -15
  99. package/dist/charts/primitives/ChartLegend.d.ts +13 -2
  100. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  101. package/dist/charts/primitives/ChartLegend.js +33 -13
  102. package/dist/charts/primitives/ChartPending.d.ts +77 -4
  103. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  104. package/dist/charts/primitives/ChartPending.js +100 -6
  105. package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
  106. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  107. package/dist/charts/primitives/ChartTooltip.js +18 -5
  108. package/dist/charts/primitives/Crosshair.d.ts +3 -0
  109. package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
  110. package/dist/charts/primitives/Crosshair.js +3 -1
  111. package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
  112. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
  113. package/dist/charts/primitives/chart-frame-layout.js +89 -0
  114. package/dist/charts/primitives/chart-tier.d.ts +16 -0
  115. package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
  116. package/dist/charts/primitives/chart-tier.js +20 -0
  117. package/dist/charts/series.d.ts +55 -1
  118. package/dist/charts/series.d.ts.map +1 -1
  119. package/dist/charts/series.js +27 -1
  120. package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
  121. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  122. package/dist/charts/sparklines/BarSparkline.js +9 -3
  123. package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
  124. package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
  125. package/dist/charts/sparklines/LineSparkline.js +15 -3
  126. package/dist/charts/sparklines/index.d.ts +2 -2
  127. package/dist/charts/sparklines/index.d.ts.map +1 -1
  128. package/dist/charts/utils/format.d.ts +92 -2
  129. package/dist/charts/utils/format.d.ts.map +1 -1
  130. package/dist/charts/utils/format.js +64 -1
  131. package/dist/charts/utils/ticks.d.ts +81 -2
  132. package/dist/charts/utils/ticks.d.ts.map +1 -1
  133. package/dist/charts/utils/ticks.js +54 -4
  134. package/dist/cli/index.d.ts.map +1 -1
  135. package/dist/cli/index.js +2 -2
  136. package/dist/commands/ShortcutsHelp.d.ts +3 -2
  137. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  138. package/dist/commands/ShortcutsHelp.js +8 -2
  139. package/dist/commands/define-commands.d.ts.map +1 -1
  140. package/dist/commands/define-commands.js +2 -2
  141. package/dist/commands/define-overlays.d.ts +102 -0
  142. package/dist/commands/define-overlays.d.ts.map +1 -1
  143. package/dist/commands/define-overlays.js +102 -2
  144. package/dist/commands/index.d.ts +10 -1
  145. package/dist/commands/index.d.ts.map +1 -1
  146. package/dist/commands/index.js +3 -1
  147. package/dist/commands/overlays-mount.d.ts +13 -6
  148. package/dist/commands/overlays-mount.d.ts.map +1 -1
  149. package/dist/commands/overlays-mount.js +23 -14
  150. package/dist/common/errors.d.ts +34 -0
  151. package/dist/common/errors.d.ts.map +1 -0
  152. package/dist/common/errors.js +36 -0
  153. package/dist/common/index.d.ts +15 -0
  154. package/dist/common/index.d.ts.map +1 -0
  155. package/dist/common/index.js +29 -0
  156. package/dist/common/props.d.ts +63 -0
  157. package/dist/common/props.d.ts.map +1 -0
  158. package/dist/common/props.js +6 -0
  159. package/dist/common/query-state-like.d.ts +43 -0
  160. package/dist/common/query-state-like.d.ts.map +1 -0
  161. package/dist/common/query-state-like.js +20 -0
  162. package/dist/common/refs.d.ts +29 -0
  163. package/dist/common/refs.d.ts.map +1 -0
  164. package/dist/common/refs.js +29 -0
  165. package/dist/common/scroll-parent.d.ts +23 -0
  166. package/dist/common/scroll-parent.d.ts.map +1 -0
  167. package/dist/common/scroll-parent.js +21 -0
  168. package/dist/common/validate.d.ts +38 -0
  169. package/dist/common/validate.d.ts.map +1 -0
  170. package/dist/common/validate.js +36 -0
  171. package/dist/connectivity/connectivity-indicator.d.ts +3 -1
  172. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  173. package/dist/connectivity/connectivity-indicator.js +3 -1
  174. package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
  175. package/dist/connectivity/connectivity-provider.js +4 -1
  176. package/dist/connectivity/index.d.ts +1 -0
  177. package/dist/connectivity/index.d.ts.map +1 -1
  178. package/dist/connectivity/types.d.ts +2 -2
  179. package/dist/connectivity/types.d.ts.map +1 -1
  180. package/dist/connectivity/use-connectivity.d.ts.map +1 -1
  181. package/dist/content/article-layout.module.css +11 -4
  182. package/dist/content/callout.d.ts +8 -4
  183. package/dist/content/callout.d.ts.map +1 -1
  184. package/dist/content/callout.js +15 -8
  185. package/dist/content/guide.d.ts +5 -4
  186. package/dist/content/guide.d.ts.map +1 -1
  187. package/dist/content/guide.js +13 -4
  188. package/dist/content/heading-anchor.d.ts +24 -2
  189. package/dist/content/heading-anchor.d.ts.map +1 -1
  190. package/dist/content/heading-anchor.js +23 -9
  191. package/dist/content/index.d.ts.map +1 -1
  192. package/dist/content/markdown.d.ts.map +1 -1
  193. package/dist/content/markdown.js +2 -0
  194. package/dist/content/prose.module.css +9 -7
  195. package/dist/content/reading-progress.d.ts +10 -4
  196. package/dist/content/reading-progress.d.ts.map +1 -1
  197. package/dist/content/reading-progress.js +26 -10
  198. package/dist/content/slug.d.ts +4 -0
  199. package/dist/content/slug.d.ts.map +1 -1
  200. package/dist/content/toc.d.ts +11 -0
  201. package/dist/content/toc.d.ts.map +1 -1
  202. package/dist/content/toc.js +23 -10
  203. package/dist/controls/actions.d.ts +49 -6
  204. package/dist/controls/actions.d.ts.map +1 -1
  205. package/dist/controls/actions.js +39 -8
  206. package/dist/controls/compare-filter.d.ts +3 -2
  207. package/dist/controls/compare-filter.d.ts.map +1 -1
  208. package/dist/controls/compare-filter.js +17 -2
  209. package/dist/controls/control-group.d.ts +3 -3
  210. package/dist/controls/control-group.d.ts.map +1 -1
  211. package/dist/controls/control-group.js +9 -2
  212. package/dist/controls/controls.module.css +169 -186
  213. package/dist/controls/controls.module.css.d.ts +9 -11
  214. package/dist/controls/enum-filter.d.ts +13 -3
  215. package/dist/controls/enum-filter.d.ts.map +1 -1
  216. package/dist/controls/enum-filter.js +32 -15
  217. package/dist/controls/filter-context.d.ts +15 -6
  218. package/dist/controls/filter-context.d.ts.map +1 -1
  219. package/dist/controls/filter-pill.d.ts +3 -3
  220. package/dist/controls/filter-pill.d.ts.map +1 -1
  221. package/dist/controls/filter-pill.js +4 -1
  222. package/dist/controls/filter-set.d.ts +3 -2
  223. package/dist/controls/filter-set.d.ts.map +1 -1
  224. package/dist/controls/filter-set.js +66 -41
  225. package/dist/controls/filter-sheet.d.ts +20 -80
  226. package/dist/controls/filter-sheet.d.ts.map +1 -1
  227. package/dist/controls/filter-sheet.js +9 -95
  228. package/dist/controls/index.d.ts +3 -0
  229. package/dist/controls/index.d.ts.map +1 -1
  230. package/dist/controls/index.js +6 -0
  231. package/dist/controls/multi-select-filter.d.ts +18 -2
  232. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  233. package/dist/controls/multi-select-filter.js +124 -22
  234. package/dist/controls/number-filter.d.ts +28 -0
  235. package/dist/controls/number-filter.d.ts.map +1 -0
  236. package/dist/controls/number-filter.js +172 -0
  237. package/dist/controls/panel-row.d.ts +132 -0
  238. package/dist/controls/panel-row.d.ts.map +1 -0
  239. package/dist/controls/panel-row.js +173 -0
  240. package/dist/controls/panel-row.module.css +86 -0
  241. package/dist/controls/panel-row.module.css.d.ts +13 -0
  242. package/dist/controls/range-filter.d.ts +20 -5
  243. package/dist/controls/range-filter.d.ts.map +1 -1
  244. package/dist/controls/range-filter.js +75 -38
  245. package/dist/controls/search-filter.d.ts +3 -2
  246. package/dist/controls/search-filter.d.ts.map +1 -1
  247. package/dist/controls/search-filter.js +26 -11
  248. package/dist/controls/select-filter.d.ts +2 -1
  249. package/dist/controls/select-filter.d.ts.map +1 -1
  250. package/dist/controls/select-filter.js +4 -0
  251. package/dist/controls/slider-control.d.ts +36 -0
  252. package/dist/controls/slider-control.d.ts.map +1 -0
  253. package/dist/controls/slider-control.js +64 -0
  254. package/dist/controls/sync-button.d.ts +3 -2
  255. package/dist/controls/sync-button.d.ts.map +1 -1
  256. package/dist/controls/sync-button.js +16 -3
  257. package/dist/controls/toggle-filter.d.ts +3 -2
  258. package/dist/controls/toggle-filter.d.ts.map +1 -1
  259. package/dist/controls/toggle-filter.js +29 -12
  260. package/dist/controls/view-tabs.d.ts +3 -2
  261. package/dist/controls/view-tabs.d.ts.map +1 -1
  262. package/dist/controls/view-tabs.js +43 -11
  263. package/dist/controls-dates/date-range-picker.d.ts +1 -1
  264. package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
  265. package/dist/controls-dates/date-range-picker.js +6 -1
  266. package/dist/dashboard/delta-badge.d.ts +5 -1
  267. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  268. package/dist/dashboard/empty-state.d.ts +10 -3
  269. package/dist/dashboard/empty-state.d.ts.map +1 -1
  270. package/dist/dashboard/empty-state.js +37 -6
  271. package/dist/dashboard/index.d.ts +6 -4
  272. package/dist/dashboard/index.d.ts.map +1 -1
  273. package/dist/dashboard/index.js +10 -2
  274. package/dist/dashboard/query-state.d.ts +48 -27
  275. package/dist/dashboard/query-state.d.ts.map +1 -1
  276. package/dist/dashboard/query-state.js +111 -57
  277. package/dist/dashboard/settings-section.d.ts +19 -9
  278. package/dist/dashboard/settings-section.d.ts.map +1 -1
  279. package/dist/dashboard/settings-section.js +74 -25
  280. package/dist/dashboard/stat-card.d.ts +76 -19
  281. package/dist/dashboard/stat-card.d.ts.map +1 -1
  282. package/dist/dashboard/stat-card.js +78 -38
  283. package/dist/dashboard/stat-card.module.css +112 -26
  284. package/dist/dashboard/stat-card.module.css.d.ts +5 -0
  285. package/dist/dashboard/stat-group.d.ts +41 -0
  286. package/dist/dashboard/stat-group.d.ts.map +1 -0
  287. package/dist/dashboard/stat-group.js +30 -0
  288. package/dist/dashboard/stat-group.module.css +66 -0
  289. package/dist/dashboard/stat-group.module.css.d.ts +8 -0
  290. package/dist/dashboard/widget-grid.d.ts +27 -0
  291. package/dist/dashboard/widget-grid.d.ts.map +1 -0
  292. package/dist/dashboard/widget-grid.js +41 -0
  293. package/dist/dashboard/widget-grid.module.css +53 -0
  294. package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
  295. package/dist/data/data-table.d.ts +151 -12
  296. package/dist/data/data-table.d.ts.map +1 -1
  297. package/dist/data/data-table.js +392 -138
  298. package/dist/data/data-table.module.css +64 -0
  299. package/dist/data/data-table.module.css.d.ts +11 -0
  300. package/dist/data/index.d.ts +2 -2
  301. package/dist/data/index.d.ts.map +1 -1
  302. package/dist/data/index.js +24 -2
  303. package/dist/data/query-branch.d.ts +28 -0
  304. package/dist/data/query-branch.d.ts.map +1 -0
  305. package/dist/data/query-branch.js +10 -0
  306. package/dist/data/table.d.ts +2 -2
  307. package/dist/data/table.d.ts.map +1 -1
  308. package/dist/data/virtual-list.d.ts +65 -6
  309. package/dist/data/virtual-list.d.ts.map +1 -1
  310. package/dist/data/virtual-list.js +89 -38
  311. package/dist/data/virtual.d.ts +1 -1
  312. package/dist/data/virtual.d.ts.map +1 -1
  313. package/dist/forms/FormErrorSummary.d.ts +4 -3
  314. package/dist/forms/FormErrorSummary.d.ts.map +1 -1
  315. package/dist/forms/FormErrorSummary.js +6 -4
  316. package/dist/forms/create-form.d.ts +2 -2
  317. package/dist/forms/field.d.ts +46 -6
  318. package/dist/forms/field.d.ts.map +1 -1
  319. package/dist/forms/field.js +10 -5
  320. package/dist/forms/form-layout.d.ts +142 -0
  321. package/dist/forms/form-layout.d.ts.map +1 -0
  322. package/dist/forms/form-layout.js +110 -0
  323. package/dist/forms/form-layout.module.css +145 -0
  324. package/dist/forms/form-layout.module.css.d.ts +20 -0
  325. package/dist/forms/form-state.d.ts +42 -0
  326. package/dist/forms/form-state.d.ts.map +1 -0
  327. package/dist/forms/form-state.js +26 -0
  328. package/dist/forms/index.d.ts +22 -6
  329. package/dist/forms/index.d.ts.map +1 -1
  330. package/dist/forms/index.js +19 -3
  331. package/dist/forms/use-field-array.d.ts +60 -0
  332. package/dist/forms/use-field-array.d.ts.map +1 -0
  333. package/dist/forms/use-field-array.js +20 -0
  334. package/dist/forms/use-form-draft.d.ts +25 -10
  335. package/dist/forms/use-form-draft.d.ts.map +1 -1
  336. package/dist/forms/use-form-draft.js +55 -18
  337. package/dist/forms/use-form-submit.d.ts +88 -0
  338. package/dist/forms/use-form-submit.d.ts.map +1 -0
  339. package/dist/forms/use-form-submit.js +63 -0
  340. package/dist/guard/index.d.ts +1 -1
  341. package/dist/guard/index.d.ts.map +1 -1
  342. package/dist/guard/index.js +27 -11
  343. package/dist/index.d.ts +10 -8
  344. package/dist/index.d.ts.map +1 -1
  345. package/dist/index.js +44 -4
  346. package/dist/nav/types.d.ts +15 -6
  347. package/dist/nav/types.d.ts.map +1 -1
  348. package/dist/notifications/bell.d.ts +3 -2
  349. package/dist/notifications/bell.d.ts.map +1 -1
  350. package/dist/notifications/bell.js +73 -56
  351. package/dist/notifications/center.d.ts +15 -4
  352. package/dist/notifications/center.d.ts.map +1 -1
  353. package/dist/notifications/center.js +5 -2
  354. package/dist/notifications/define-notifications.d.ts +56 -14
  355. package/dist/notifications/define-notifications.d.ts.map +1 -1
  356. package/dist/notifications/define-notifications.js +2 -1
  357. package/dist/notifications/index.d.ts +6 -2
  358. package/dist/notifications/index.d.ts.map +1 -1
  359. package/dist/notifications/index.js +5 -1
  360. package/dist/notifications/mount-guard.d.ts +8 -0
  361. package/dist/notifications/mount-guard.d.ts.map +1 -0
  362. package/dist/notifications/mount-guard.js +19 -0
  363. package/dist/notifications/notify.d.ts +58 -1
  364. package/dist/notifications/notify.d.ts.map +1 -1
  365. package/dist/notifications/notify.js +46 -2
  366. package/dist/notifications/overlay.d.ts.map +1 -1
  367. package/dist/notifications/overlay.js +2 -0
  368. package/dist/notifications/store.d.ts +7 -0
  369. package/dist/notifications/store.d.ts.map +1 -1
  370. package/dist/provider/index.d.ts +37 -5
  371. package/dist/provider/index.d.ts.map +1 -1
  372. package/dist/provider/index.js +66 -28
  373. package/dist/provider/lab-theme.d.ts.map +1 -1
  374. package/dist/provider/lab-theme.js +2 -1
  375. package/dist/query/devtools.d.ts.map +1 -1
  376. package/dist/query/devtools.js +1 -0
  377. package/dist/router-tanstack/field.d.ts +1 -1
  378. package/dist/router-tanstack/field.d.ts.map +1 -1
  379. package/dist/router-tanstack/index.d.ts +1 -1
  380. package/dist/router-tanstack/index.d.ts.map +1 -1
  381. package/dist/router-tanstack/search-store.d.ts +4 -1
  382. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  383. package/dist/router-tanstack/search-store.js +92 -8
  384. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  385. package/dist/router-tanstack/use-nav.js +1 -1
  386. package/dist/section/index.d.ts +1 -1
  387. package/dist/section/index.d.ts.map +1 -1
  388. package/dist/section/section.d.ts +36 -2
  389. package/dist/section/section.d.ts.map +1 -1
  390. package/dist/section/section.js +49 -57
  391. package/dist/section/section.module.css +11 -0
  392. package/dist/shell/account-types.d.ts +5 -2
  393. package/dist/shell/account-types.d.ts.map +1 -1
  394. package/dist/shell/app-brand.d.ts +13 -0
  395. package/dist/shell/app-brand.d.ts.map +1 -0
  396. package/dist/shell/app-brand.js +94 -0
  397. package/dist/shell/app-brand.module.css +109 -0
  398. package/dist/shell/app-brand.module.css.d.ts +14 -0
  399. package/dist/shell/app-breadcrumbs.d.ts +4 -2
  400. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  401. package/dist/shell/app-breadcrumbs.js +33 -22
  402. package/dist/shell/app-header.module.css +10 -5
  403. package/dist/shell/app-main.module.css +107 -0
  404. package/dist/shell/app-main.module.css.d.ts +11 -0
  405. package/dist/shell/app-mobile-nav.d.ts +3 -2
  406. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  407. package/dist/shell/app-mobile-nav.js +29 -15
  408. package/dist/shell/app-mobile-nav.module.css +48 -27
  409. package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
  410. package/dist/shell/app-sidebar-account.d.ts +2 -1
  411. package/dist/shell/app-sidebar-account.d.ts.map +1 -1
  412. package/dist/shell/app-sidebar-account.js +57 -22
  413. package/dist/shell/app-sidebar.d.ts +9 -3
  414. package/dist/shell/app-sidebar.d.ts.map +1 -1
  415. package/dist/shell/app-sidebar.js +55 -109
  416. package/dist/shell/app-sidebar.module.css +53 -96
  417. package/dist/shell/app-sidebar.module.css.d.ts +0 -5
  418. package/dist/shell/index.d.ts +14 -6
  419. package/dist/shell/index.d.ts.map +1 -1
  420. package/dist/shell/index.js +47 -12
  421. package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
  422. package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
  423. package/dist/shell/isomorphic-layout-effect.js +5 -0
  424. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  425. package/dist/shell/mobile-nav-model.js +27 -6
  426. package/dist/shell/nav-count-badge.d.ts +5 -3
  427. package/dist/shell/nav-count-badge.d.ts.map +1 -1
  428. package/dist/shell/nav-count-badge.js +3 -1
  429. package/dist/shell/page-aside.d.ts +43 -0
  430. package/dist/shell/page-aside.d.ts.map +1 -0
  431. package/dist/shell/page-aside.js +202 -0
  432. package/dist/shell/page-aside.module.css +174 -0
  433. package/dist/shell/page-aside.module.css.d.ts +14 -0
  434. package/dist/shell/page-bar.d.ts +55 -15
  435. package/dist/shell/page-bar.d.ts.map +1 -1
  436. package/dist/shell/page-bar.js +130 -41
  437. package/dist/shell/page-bar.module.css +107 -13
  438. package/dist/shell/page-bar.module.css.d.ts +4 -1
  439. package/dist/shell/sidebar-search.d.ts +4 -2
  440. package/dist/shell/sidebar-search.d.ts.map +1 -1
  441. package/dist/shell/sidebar-search.js +9 -4
  442. package/dist/shell/sidebar-search.module.css +1 -1
  443. package/dist/state/fields.d.ts +227 -34
  444. package/dist/state/fields.d.ts.map +1 -1
  445. package/dist/state/fields.js +77 -30
  446. package/dist/state/persisted-or-local.d.ts +9 -0
  447. package/dist/state/persisted-or-local.d.ts.map +1 -0
  448. package/dist/state/persisted-or-local.js +24 -0
  449. package/dist/state/persisted.d.ts +12 -0
  450. package/dist/state/persisted.d.ts.map +1 -1
  451. package/dist/state/persisted.js +36 -18
  452. package/dist/state.d.ts +1 -1
  453. package/dist/state.d.ts.map +1 -1
  454. package/dist/state.js +2 -6
  455. package/dist/surfaces.d.ts +12 -11
  456. package/dist/surfaces.d.ts.map +1 -1
  457. package/dist/surfaces.js +43 -12
  458. package/dist/theme/ctl-theme.d.ts.map +1 -1
  459. package/dist/theme/index.d.ts.map +1 -1
  460. package/dist/theme/index.js +102 -17
  461. package/dist/theme/nav-link.module.css +37 -26
  462. package/dist/theme/segmented-control.module.css +22 -0
  463. package/dist/theme/segmented-control.module.css.d.ts +1 -0
  464. package/dist/theme-lab/boot.d.ts +66 -0
  465. package/dist/theme-lab/boot.d.ts.map +1 -0
  466. package/dist/theme-lab/boot.js +82 -0
  467. package/dist/theme-lab/index.d.ts +4 -43
  468. package/dist/theme-lab/index.d.ts.map +1 -1
  469. package/dist/theme-lab/index.js +9 -69
  470. package/dist/theme-toggle/index.d.ts +3 -2
  471. package/dist/theme-toggle/index.d.ts.map +1 -1
  472. package/dist/theme-toggle/index.js +8 -2
  473. package/dist/tokens/index.d.ts +42 -0
  474. package/dist/tokens/index.d.ts.map +1 -1
  475. package/dist/tokens/index.js +35 -6
  476. package/dist/tokens/palette.d.ts +45 -25
  477. package/dist/tokens/palette.d.ts.map +1 -1
  478. package/dist/tokens/palette.js +31 -14
  479. package/dist/tokens.css +4 -5
  480. package/dist/utils/is-dev.d.ts +3 -0
  481. package/dist/utils/is-dev.d.ts.map +1 -0
  482. package/dist/utils/is-dev.js +6 -0
  483. package/dist/widget-header/delta-badge.d.ts +7 -1
  484. package/dist/widget-header/delta-badge.d.ts.map +1 -1
  485. package/dist/widget-header/delta-badge.js +8 -1
  486. package/dist/widget-header/index.d.ts +2 -2
  487. package/dist/widget-header/index.d.ts.map +1 -1
  488. package/dist/widget-header/index.js +3 -1
  489. package/dist/widget-header/widget-header.d.ts +99 -4
  490. package/dist/widget-header/widget-header.d.ts.map +1 -1
  491. package/dist/widget-header/widget-header.js +45 -18
  492. package/dist/widget-header/widget-header.module.css +87 -7
  493. package/dist/widget-header/widget-header.module.css.d.ts +1 -0
  494. package/llms.txt +6 -6
  495. package/package.json +49 -25
  496. package/src/agent/coalesce.ts +2 -1
  497. package/src/agent/merge.ts +2 -1
  498. package/src/agent/part-list.tsx +2 -0
  499. package/src/agent/stick-to-bottom.tsx +9 -11
  500. package/src/agent-chat/composer.tsx +124 -103
  501. package/src/agent-chat/relative-time.ts +2 -1
  502. package/src/agent-chat/thread-detail-panel.tsx +9 -5
  503. package/src/agent-chat/thread-feed-row.tsx +17 -9
  504. package/src/agent-chat/thread-feed.tsx +9 -3
  505. package/src/agent-chat/thread-message.tsx +48 -13
  506. package/src/agent-chat/thread-outcome-card.tsx +9 -1
  507. package/src/agent-chat/thread-workspace.tsx +72 -33
  508. package/src/agent-chat/tool-chip.tsx +8 -2
  509. package/src/charts/hooks/useBandPlot.ts +26 -8
  510. package/src/charts/index.ts +39 -4
  511. package/src/charts/kinds/BandStrip.tsx +36 -14
  512. package/src/charts/kinds/Bars.tsx +67 -16
  513. package/src/charts/kinds/Donut.tsx +30 -3
  514. package/src/charts/kinds/DualPanel.tsx +56 -10
  515. package/src/charts/kinds/Heatmap.tsx +90 -33
  516. package/src/charts/kinds/MirroredBars.tsx +21 -3
  517. package/src/charts/kinds/MultiLine.tsx +61 -18
  518. package/src/charts/kinds/StackedArea.tsx +77 -6
  519. package/src/charts/kinds/ZonedLine.tsx +71 -31
  520. package/src/charts/layout/auto-margin.ts +134 -21
  521. package/src/charts/layout/log-ticks.ts +90 -0
  522. package/src/charts/primitives/Axes.tsx +33 -5
  523. package/src/charts/primitives/CartesianChart.tsx +194 -41
  524. package/src/charts/primitives/ChartCard.tsx +66 -25
  525. package/src/charts/primitives/ChartFrame.tsx +107 -26
  526. package/src/charts/primitives/ChartLegend.tsx +43 -13
  527. package/src/charts/primitives/ChartPending.tsx +185 -7
  528. package/src/charts/primitives/ChartTooltip.tsx +37 -5
  529. package/src/charts/primitives/Crosshair.tsx +6 -3
  530. package/src/charts/primitives/chart-frame-layout.ts +212 -0
  531. package/src/charts/primitives/chart-tier.tsx +37 -0
  532. package/src/charts/series.ts +77 -1
  533. package/src/charts/sparklines/BarSparkline.tsx +11 -3
  534. package/src/charts/sparklines/LineSparkline.tsx +18 -4
  535. package/src/charts/sparklines/index.ts +2 -2
  536. package/src/charts/utils/format.ts +163 -2
  537. package/src/charts/utils/ticks.ts +149 -4
  538. package/src/cli/index.ts +8 -3
  539. package/src/commands/ShortcutsHelp.tsx +10 -3
  540. package/src/commands/define-commands.ts +2 -2
  541. package/src/commands/define-overlays.ts +216 -1
  542. package/src/commands/index.ts +12 -0
  543. package/src/commands/overlays-mount.tsx +60 -26
  544. package/src/common/errors.ts +73 -0
  545. package/src/common/index.ts +21 -0
  546. package/src/common/props.ts +71 -0
  547. package/src/common/query-state-like.ts +64 -0
  548. package/src/common/refs.ts +58 -0
  549. package/src/common/scroll-parent.ts +42 -0
  550. package/src/common/validate.ts +94 -0
  551. package/src/connectivity/connectivity-indicator.tsx +6 -1
  552. package/src/connectivity/connectivity-provider.tsx +4 -1
  553. package/src/connectivity/index.ts +1 -0
  554. package/src/connectivity/types.ts +5 -2
  555. package/src/connectivity/use-connectivity.ts +3 -2
  556. package/src/content/article-layout.module.css +11 -4
  557. package/src/content/callout.tsx +13 -7
  558. package/src/content/guide.tsx +14 -4
  559. package/src/content/heading-anchor.tsx +14 -3
  560. package/src/content/index.ts +2 -0
  561. package/src/content/markdown.tsx +2 -0
  562. package/src/content/prose.module.css +9 -7
  563. package/src/content/reading-progress.tsx +40 -15
  564. package/src/content/slug.ts +4 -0
  565. package/src/content/toc.tsx +47 -10
  566. package/src/controls/actions.tsx +89 -9
  567. package/src/controls/compare-filter.tsx +20 -3
  568. package/src/controls/control-group.tsx +11 -4
  569. package/src/controls/controls.module.css +169 -186
  570. package/src/controls/controls.module.css.d.ts +9 -11
  571. package/src/controls/enum-filter.tsx +46 -19
  572. package/src/controls/filter-context.tsx +21 -8
  573. package/src/controls/filter-pill.tsx +6 -3
  574. package/src/controls/filter-set.tsx +32 -13
  575. package/src/controls/filter-sheet.tsx +44 -194
  576. package/src/controls/index.ts +8 -0
  577. package/src/controls/multi-select-filter.tsx +179 -29
  578. package/src/controls/number-filter.tsx +329 -0
  579. package/src/controls/panel-row.module.css +86 -0
  580. package/src/controls/panel-row.module.css.d.ts +13 -0
  581. package/src/controls/panel-row.tsx +392 -0
  582. package/src/controls/range-filter.tsx +133 -43
  583. package/src/controls/search-filter.tsx +31 -13
  584. package/src/controls/select-filter.tsx +11 -1
  585. package/src/controls/slider-control.tsx +159 -0
  586. package/src/controls/sync-button.tsx +13 -3
  587. package/src/controls/toggle-filter.tsx +37 -17
  588. package/src/controls/view-tabs.tsx +63 -11
  589. package/src/controls-dates/date-range-picker.tsx +8 -1
  590. package/src/dashboard/delta-badge.tsx +5 -1
  591. package/src/dashboard/empty-state.tsx +72 -25
  592. package/src/dashboard/index.ts +19 -2
  593. package/src/dashboard/query-state.tsx +154 -84
  594. package/src/dashboard/settings-section.tsx +87 -31
  595. package/src/dashboard/stat-card.module.css +112 -26
  596. package/src/dashboard/stat-card.module.css.d.ts +5 -0
  597. package/src/dashboard/stat-card.tsx +207 -68
  598. package/src/dashboard/stat-group.module.css +66 -0
  599. package/src/dashboard/stat-group.module.css.d.ts +8 -0
  600. package/src/dashboard/stat-group.tsx +74 -0
  601. package/src/dashboard/widget-grid.module.css +53 -0
  602. package/src/dashboard/widget-grid.module.css.d.ts +9 -0
  603. package/src/dashboard/widget-grid.tsx +127 -0
  604. package/src/data/data-table.module.css +64 -0
  605. package/src/data/data-table.module.css.d.ts +11 -0
  606. package/src/data/data-table.tsx +927 -346
  607. package/src/data/index.ts +30 -2
  608. package/src/data/query-branch.ts +53 -0
  609. package/src/data/table.ts +5 -0
  610. package/src/data/virtual-list.tsx +189 -55
  611. package/src/data/virtual.ts +5 -1
  612. package/src/forms/FormErrorSummary.tsx +13 -6
  613. package/src/forms/create-form.ts +2 -2
  614. package/src/forms/field.ts +60 -11
  615. package/src/forms/form-layout.module.css +145 -0
  616. package/src/forms/form-layout.module.css.d.ts +20 -0
  617. package/src/forms/form-layout.tsx +305 -0
  618. package/src/forms/form-state.tsx +94 -0
  619. package/src/forms/index.ts +38 -7
  620. package/src/forms/use-field-array.ts +94 -0
  621. package/src/forms/use-form-draft.ts +145 -53
  622. package/src/forms/use-form-submit.ts +199 -0
  623. package/src/guard/index.ts +91 -23
  624. package/src/index.ts +79 -10
  625. package/src/nav/types.ts +15 -6
  626. package/src/notifications/bell.tsx +18 -4
  627. package/src/notifications/center.tsx +18 -5
  628. package/src/notifications/define-notifications.ts +64 -18
  629. package/src/notifications/index.ts +12 -1
  630. package/src/notifications/mount-guard.ts +41 -0
  631. package/src/notifications/notify.tsx +122 -3
  632. package/src/notifications/overlay.tsx +2 -0
  633. package/src/notifications/store.ts +7 -0
  634. package/src/provider/index.tsx +101 -13
  635. package/src/provider/lab-theme.ts +2 -1
  636. package/src/query/devtools.tsx +1 -0
  637. package/src/router-tanstack/field.ts +5 -0
  638. package/src/router-tanstack/index.ts +4 -0
  639. package/src/router-tanstack/search-store.ts +171 -12
  640. package/src/router-tanstack/use-nav.ts +18 -13
  641. package/src/section/index.ts +1 -1
  642. package/src/section/section.module.css +11 -0
  643. package/src/section/section.tsx +163 -96
  644. package/src/shell/account-types.ts +5 -2
  645. package/src/shell/app-brand.module.css +109 -0
  646. package/src/shell/app-brand.module.css.d.ts +14 -0
  647. package/src/shell/app-brand.tsx +140 -0
  648. package/src/shell/app-breadcrumbs.tsx +19 -9
  649. package/src/shell/app-header.module.css +10 -5
  650. package/src/shell/app-main.module.css +107 -0
  651. package/src/shell/app-main.module.css.d.ts +11 -0
  652. package/src/shell/app-mobile-nav.module.css +48 -27
  653. package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
  654. package/src/shell/app-mobile-nav.tsx +43 -11
  655. package/src/shell/app-sidebar-account.tsx +29 -6
  656. package/src/shell/app-sidebar.module.css +53 -96
  657. package/src/shell/app-sidebar.module.css.d.ts +0 -5
  658. package/src/shell/app-sidebar.tsx +40 -113
  659. package/src/shell/index.tsx +156 -65
  660. package/src/shell/isomorphic-layout-effect.ts +14 -0
  661. package/src/shell/mobile-nav-model.ts +66 -10
  662. package/src/shell/nav-count-badge.tsx +6 -1
  663. package/src/shell/page-aside.module.css +174 -0
  664. package/src/shell/page-aside.module.css.d.ts +14 -0
  665. package/src/shell/page-aside.tsx +377 -0
  666. package/src/shell/page-bar.module.css +107 -13
  667. package/src/shell/page-bar.module.css.d.ts +4 -1
  668. package/src/shell/page-bar.tsx +294 -94
  669. package/src/shell/sidebar-search.module.css +1 -1
  670. package/src/shell/sidebar-search.tsx +13 -4
  671. package/src/state/fields.ts +360 -69
  672. package/src/state/persisted-or-local.ts +50 -0
  673. package/src/state/persisted.ts +76 -26
  674. package/src/state.ts +4 -7
  675. package/src/surfaces.ts +43 -12
  676. package/src/theme/ctl-theme.tsx +10 -0
  677. package/src/theme/index.ts +129 -16
  678. package/src/theme/nav-link.module.css +37 -26
  679. package/src/theme/segmented-control.module.css +22 -0
  680. package/src/theme/segmented-control.module.css.d.ts +1 -0
  681. package/src/theme-lab/boot.ts +140 -0
  682. package/src/theme-lab/index.tsx +21 -112
  683. package/src/theme-toggle/index.tsx +10 -3
  684. package/src/tokens/index.ts +50 -6
  685. package/src/tokens/palette.ts +58 -30
  686. package/src/utils/is-dev.ts +4 -0
  687. package/src/widget-header/delta-badge.tsx +25 -5
  688. package/src/widget-header/index.ts +10 -2
  689. package/src/widget-header/widget-header.module.css +87 -7
  690. package/src/widget-header/widget-header.module.css.d.ts +1 -0
  691. package/src/widget-header/widget-header.tsx +166 -42
  692. package/dist/agent/adapter.test.d.ts +0 -2
  693. package/dist/agent/adapter.test.d.ts.map +0 -1
  694. package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
  695. package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
  696. package/dist/agent/coalesce.test.d.ts +0 -2
  697. package/dist/agent/coalesce.test.d.ts.map +0 -1
  698. package/dist/agent/foreign.test.d.ts +0 -2
  699. package/dist/agent/foreign.test.d.ts.map +0 -1
  700. package/dist/agent/id.test.d.ts +0 -2
  701. package/dist/agent/id.test.d.ts.map +0 -1
  702. package/dist/agent/merge.test.d.ts +0 -2
  703. package/dist/agent/merge.test.d.ts.map +0 -1
  704. package/dist/agent/part-list.test.d.ts +0 -2
  705. package/dist/agent/part-list.test.d.ts.map +0 -1
  706. package/dist/agent/parts.test.d.ts +0 -2
  707. package/dist/agent/parts.test.d.ts.map +0 -1
  708. package/dist/agent/thread.test.d.ts +0 -2
  709. package/dist/agent/thread.test.d.ts.map +0 -1
  710. package/dist/agent/transport.test.d.ts +0 -2
  711. package/dist/agent/transport.test.d.ts.map +0 -1
  712. package/dist/agent/use-agent-stream.test.d.ts +0 -2
  713. package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
  714. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
  715. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
  716. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
  717. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
  718. package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
  719. package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
  720. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
  721. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
  722. package/dist/agent-chat/composer.test.d.ts +0 -2
  723. package/dist/agent-chat/composer.test.d.ts.map +0 -1
  724. package/dist/agent-chat/relative-time.test.d.ts +0 -2
  725. package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
  726. package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
  727. package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
  728. package/dist/agent-chat/thread-feed.test.d.ts +0 -2
  729. package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
  730. package/dist/agent-chat/thread-message.test.d.ts +0 -2
  731. package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
  732. package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
  733. package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
  734. package/dist/agent-chat/tool-chip.test.d.ts +0 -2
  735. package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
  736. package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
  737. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
  738. package/dist/charts/cursor/resolve.test.d.ts +0 -2
  739. package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
  740. package/dist/charts/cursor/store.test.d.ts +0 -2
  741. package/dist/charts/cursor/store.test.d.ts.map +0 -1
  742. package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
  743. package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
  744. package/dist/charts/kinds/Bars.test.d.ts +0 -2
  745. package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
  746. package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
  747. package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
  748. package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
  749. package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
  750. package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
  751. package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
  752. package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
  753. package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
  754. package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
  755. package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
  756. package/dist/charts/layout/auto-margin.test.d.ts +0 -2
  757. package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
  758. package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
  759. package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
  760. package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
  761. package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
  762. package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
  763. package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
  764. package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
  765. package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
  766. package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
  767. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
  768. package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
  769. package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
  770. package/dist/charts/series.test.d.ts +0 -2
  771. package/dist/charts/series.test.d.ts.map +0 -1
  772. package/dist/charts/utils/domain.test.d.ts +0 -2
  773. package/dist/charts/utils/domain.test.d.ts.map +0 -1
  774. package/dist/charts/utils/measure-text.test.d.ts +0 -2
  775. package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
  776. package/dist/cli/check-coverage.test.d.ts +0 -2
  777. package/dist/cli/check-coverage.test.d.ts.map +0 -1
  778. package/dist/cli/check-theme.test.d.ts +0 -2
  779. package/dist/cli/check-theme.test.d.ts.map +0 -1
  780. package/dist/cli/help.test.d.ts +0 -2
  781. package/dist/cli/help.test.d.ts.map +0 -1
  782. package/dist/cli/init-sync.test.d.ts +0 -2
  783. package/dist/cli/init-sync.test.d.ts.map +0 -1
  784. package/dist/cli/placement-engine.test.d.ts +0 -2
  785. package/dist/cli/placement-engine.test.d.ts.map +0 -1
  786. package/dist/cli/shipped-versions.test.d.ts +0 -2
  787. package/dist/cli/shipped-versions.test.d.ts.map +0 -1
  788. package/dist/cli/tarball-parity.test.d.ts +0 -2
  789. package/dist/cli/tarball-parity.test.d.ts.map +0 -1
  790. package/dist/cli/template-format.test.d.ts +0 -2
  791. package/dist/cli/template-format.test.d.ts.map +0 -1
  792. package/dist/cli/template-ledger.test.d.ts +0 -2
  793. package/dist/cli/template-ledger.test.d.ts.map +0 -1
  794. package/dist/cli/tokens-css.test.d.ts +0 -2
  795. package/dist/cli/tokens-css.test.d.ts.map +0 -1
  796. package/dist/cli/toolchain-wiring.test.d.ts +0 -2
  797. package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
  798. package/dist/cli/waiver-audit.test.d.ts +0 -2
  799. package/dist/cli/waiver-audit.test.d.ts.map +0 -1
  800. package/dist/content/article-actions.test.d.ts +0 -2
  801. package/dist/content/article-actions.test.d.ts.map +0 -1
  802. package/dist/content/article-model.test.d.ts +0 -2
  803. package/dist/content/article-model.test.d.ts.map +0 -1
  804. package/dist/content/block-split.test.d.ts +0 -2
  805. package/dist/content/block-split.test.d.ts.map +0 -1
  806. package/dist/content/heading-components.test.d.ts +0 -2
  807. package/dist/content/heading-components.test.d.ts.map +0 -1
  808. package/dist/content/markdown.test.d.ts +0 -2
  809. package/dist/content/markdown.test.d.ts.map +0 -1
  810. package/dist/content/prose.module.css.test.d.ts +0 -2
  811. package/dist/content/prose.module.css.test.d.ts.map +0 -1
  812. package/dist/content/sanitize.test.d.ts +0 -2
  813. package/dist/content/sanitize.test.d.ts.map +0 -1
  814. package/dist/content/slug.test.d.ts +0 -2
  815. package/dist/content/slug.test.d.ts.map +0 -1
  816. package/dist/controls/actions.test.d.ts +0 -2
  817. package/dist/controls/actions.test.d.ts.map +0 -1
  818. package/dist/controls/actions.type-guard.test.d.ts +0 -2
  819. package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
  820. package/dist/controls/control-group.test.d.ts +0 -2
  821. package/dist/controls/control-group.test.d.ts.map +0 -1
  822. package/dist/controls/controls.router.test.d.ts +0 -2
  823. package/dist/controls/controls.router.test.d.ts.map +0 -1
  824. package/dist/controls/filter-set.test.d.ts +0 -2
  825. package/dist/controls/filter-set.test.d.ts.map +0 -1
  826. package/dist/controls/peer-boundary.test.d.ts +0 -2
  827. package/dist/controls/peer-boundary.test.d.ts.map +0 -1
  828. package/dist/controls/props-contract.test.d.ts +0 -2
  829. package/dist/controls/props-contract.test.d.ts.map +0 -1
  830. package/dist/controls/sync-button.test.d.ts +0 -2
  831. package/dist/controls/sync-button.test.d.ts.map +0 -1
  832. package/dist/dashboard/query-state.test.d.ts +0 -2
  833. package/dist/dashboard/query-state.test.d.ts.map +0 -1
  834. package/dist/dashboard/settings-section.test.d.ts +0 -2
  835. package/dist/dashboard/settings-section.test.d.ts.map +0 -1
  836. package/dist/dashboard/stat-card.test.d.ts +0 -2
  837. package/dist/dashboard/stat-card.test.d.ts.map +0 -1
  838. package/dist/data/data-table.test.d.ts +0 -2
  839. package/dist/data/data-table.test.d.ts.map +0 -1
  840. package/dist/guard/check-source.test.d.ts +0 -2
  841. package/dist/guard/check-source.test.d.ts.map +0 -1
  842. package/dist/guard/grace.test.d.ts +0 -20
  843. package/dist/guard/grace.test.d.ts.map +0 -1
  844. package/dist/guard/guard-hook.test.d.ts +0 -2
  845. package/dist/guard/guard-hook.test.d.ts.map +0 -1
  846. package/dist/provider/index.test.d.ts +0 -2
  847. package/dist/provider/index.test.d.ts.map +0 -1
  848. package/dist/provider/lab-theme.test.d.ts +0 -2
  849. package/dist/provider/lab-theme.test.d.ts.map +0 -1
  850. package/dist/query/error-message.test.d.ts +0 -2
  851. package/dist/query/error-message.test.d.ts.map +0 -1
  852. package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
  853. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
  854. package/dist/router-tanstack/nav.test.d.ts +0 -2
  855. package/dist/router-tanstack/nav.test.d.ts.map +0 -1
  856. package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
  857. package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
  858. package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
  859. package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
  860. package/dist/router-tanstack/search-store.test.d.ts +0 -2
  861. package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
  862. package/dist/router-tanstack/use-nav.test.d.ts +0 -2
  863. package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
  864. package/dist/section/section.test.d.ts +0 -2
  865. package/dist/section/section.test.d.ts.map +0 -1
  866. package/dist/shell/app-mobile-nav.test.d.ts +0 -2
  867. package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
  868. package/dist/shell/app-sidebar.test.d.ts +0 -2
  869. package/dist/shell/app-sidebar.test.d.ts.map +0 -1
  870. package/dist/shell/index.test.d.ts +0 -2
  871. package/dist/shell/index.test.d.ts.map +0 -1
  872. package/dist/shell/mobile-nav-model.test.d.ts +0 -2
  873. package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
  874. package/dist/shell/page-bar.test.d.ts +0 -2
  875. package/dist/shell/page-bar.test.d.ts.map +0 -1
  876. package/dist/state/fields.test.d.ts +0 -2
  877. package/dist/state/fields.test.d.ts.map +0 -1
  878. package/dist/styles.floor.test.d.ts +0 -2
  879. package/dist/styles.floor.test.d.ts.map +0 -1
  880. package/dist/theme/border-coverage.test.d.ts +0 -18
  881. package/dist/theme/border-coverage.test.d.ts.map +0 -1
  882. package/dist/theme/contrast.test.d.ts +0 -2
  883. package/dist/theme/contrast.test.d.ts.map +0 -1
  884. package/dist/theme/ctl-theme.test.d.ts +0 -2
  885. package/dist/theme/ctl-theme.test.d.ts.map +0 -1
  886. package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
  887. package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
  888. package/dist/theme/density-option.test.d.ts +0 -2
  889. package/dist/theme/density-option.test.d.ts.map +0 -1
  890. package/dist/theme/density-relations.test.d.ts +0 -2
  891. package/dist/theme/density-relations.test.d.ts.map +0 -1
  892. package/dist/theme/derive-theme.test.d.ts +0 -2
  893. package/dist/theme/derive-theme.test.d.ts.map +0 -1
  894. package/dist/theme/divider-law.test.d.ts +0 -2
  895. package/dist/theme/divider-law.test.d.ts.map +0 -1
  896. package/dist/theme/fonts-option.test.d.ts +0 -2
  897. package/dist/theme/fonts-option.test.d.ts.map +0 -1
  898. package/dist/theme/icon-slot.test.d.ts +0 -2
  899. package/dist/theme/icon-slot.test.d.ts.map +0 -1
  900. package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
  901. package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
  902. package/dist/theme/radius-option.test.d.ts +0 -2
  903. package/dist/theme/radius-option.test.d.ts.map +0 -1
  904. package/dist/theme/radius.test.d.ts +0 -2
  905. package/dist/theme/radius.test.d.ts.map +0 -1
  906. package/dist/theme/shadow-surfaces.test.d.ts +0 -26
  907. package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
  908. package/dist/theme/spacing.test.d.ts +0 -2
  909. package/dist/theme/spacing.test.d.ts.map +0 -1
  910. package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
  911. package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
  912. package/dist/theme-lab/derive-controls.test.d.ts +0 -2
  913. package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
  914. package/dist/theme-lab/derive-state.test.d.ts +0 -2
  915. package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
  916. package/dist/theme-lab/theme-lab.test.d.ts +0 -2
  917. package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
  918. package/dist/tokens/build-fonts-css.test.d.ts +0 -2
  919. package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
  920. package/dist/tokens/build-radius-css.test.d.ts +0 -2
  921. package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
  922. package/dist/tokens/chart-margin.test.d.ts +0 -2
  923. package/dist/tokens/chart-margin.test.d.ts.map +0 -1
  924. package/dist/tokens/density.test.d.ts +0 -2
  925. package/dist/tokens/density.test.d.ts.map +0 -1
  926. package/dist/tokens/derive-radius.test.d.ts +0 -2
  927. package/dist/tokens/derive-radius.test.d.ts.map +0 -1
  928. package/dist/tokens/derive.test.d.ts +0 -2
  929. package/dist/tokens/derive.test.d.ts.map +0 -1
  930. package/dist/tokens/palette.test.d.ts +0 -2
  931. package/dist/tokens/palette.test.d.ts.map +0 -1
  932. package/dist/tokens/text-scale.test.d.ts +0 -2
  933. package/dist/tokens/text-scale.test.d.ts.map +0 -1
  934. package/dist/vite-color-mix.test.d.ts +0 -2
  935. package/dist/vite-color-mix.test.d.ts.map +0 -1
  936. package/dist/vite.test.d.ts +0 -2
  937. package/dist/vite.test.d.ts.map +0 -1
  938. package/dist/widget-header/delta-badge.test.d.ts +0 -2
  939. package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
  940. package/dist/widget-header/no-mantine.test.d.ts +0 -2
  941. package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
  942. package/dist/widget-header/widget-header.test.d.ts +0 -2
  943. package/dist/widget-header/widget-header.test.d.ts.map +0 -1
  944. package/src/agent/adapter.test.ts +0 -1640
  945. package/src/agent/ai-sdk-transport.test.ts +0 -658
  946. package/src/agent/coalesce.test.ts +0 -387
  947. package/src/agent/foreign.test.ts +0 -51
  948. package/src/agent/id.test.ts +0 -169
  949. package/src/agent/merge.test.ts +0 -206
  950. package/src/agent/part-list.test.tsx +0 -112
  951. package/src/agent/parts.test.ts +0 -720
  952. package/src/agent/thread.test.ts +0 -220
  953. package/src/agent/transport.test.ts +0 -70
  954. package/src/agent/use-agent-stream.test.tsx +0 -287
  955. package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
  956. package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
  957. package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
  958. package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
  959. package/src/agent-chat/composer.test.tsx +0 -924
  960. package/src/agent-chat/relative-time.test.ts +0 -95
  961. package/src/agent-chat/thread-feed-row.test.tsx +0 -510
  962. package/src/agent-chat/thread-feed.test.tsx +0 -241
  963. package/src/agent-chat/thread-message.test.tsx +0 -1285
  964. package/src/agent-chat/thread-workspace.test.tsx +0 -169
  965. package/src/agent-chat/tool-chip.test.tsx +0 -139
  966. package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
  967. package/src/charts/cursor/resolve.test.ts +0 -151
  968. package/src/charts/cursor/store.test.ts +0 -64
  969. package/src/charts/kinds/BandStrip.test.tsx +0 -282
  970. package/src/charts/kinds/Bars.test.tsx +0 -183
  971. package/src/charts/kinds/DualPanel.test.tsx +0 -527
  972. package/src/charts/kinds/MirroredBars.test.tsx +0 -232
  973. package/src/charts/kinds/MultiLine.test.tsx +0 -218
  974. package/src/charts/kinds/StackedArea.test.tsx +0 -114
  975. package/src/charts/kinds/ZonedLine.test.tsx +0 -147
  976. package/src/charts/layout/auto-margin.test.ts +0 -42
  977. package/src/charts/primitives/CartesianChart.test.tsx +0 -647
  978. package/src/charts/primitives/ChartCard.test.tsx +0 -66
  979. package/src/charts/primitives/ChartFrame.test.tsx +0 -142
  980. package/src/charts/primitives/ChartLegend.test.tsx +0 -78
  981. package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
  982. package/src/charts/primitives/XZoneRects.test.tsx +0 -123
  983. package/src/charts/series.test.ts +0 -112
  984. package/src/charts/utils/domain.test.ts +0 -54
  985. package/src/charts/utils/measure-text.test.ts +0 -36
  986. package/src/cli/check-coverage.test.ts +0 -297
  987. package/src/cli/check-theme.test.ts +0 -837
  988. package/src/cli/help.test.ts +0 -170
  989. package/src/cli/init-sync.test.ts +0 -343
  990. package/src/cli/placement-engine.test.ts +0 -298
  991. package/src/cli/shipped-versions.test.ts +0 -83
  992. package/src/cli/tarball-parity.test.ts +0 -44
  993. package/src/cli/template-format.test.ts +0 -64
  994. package/src/cli/template-ledger.test.ts +0 -219
  995. package/src/cli/tokens-css.test.ts +0 -249
  996. package/src/cli/toolchain-wiring.test.ts +0 -1358
  997. package/src/cli/waiver-audit.test.ts +0 -215
  998. package/src/content/article-actions.test.ts +0 -78
  999. package/src/content/article-model.test.ts +0 -119
  1000. package/src/content/block-split.test.ts +0 -75
  1001. package/src/content/heading-components.test.tsx +0 -65
  1002. package/src/content/markdown.test.tsx +0 -603
  1003. package/src/content/prose.module.css.test.ts +0 -39
  1004. package/src/content/sanitize.test.ts +0 -659
  1005. package/src/content/slug.test.ts +0 -62
  1006. package/src/controls/actions.test.tsx +0 -252
  1007. package/src/controls/actions.type-guard.test.ts +0 -63
  1008. package/src/controls/control-group.test.tsx +0 -229
  1009. package/src/controls/controls.router.test.tsx +0 -637
  1010. package/src/controls/filter-set.test.tsx +0 -472
  1011. package/src/controls/peer-boundary.test.ts +0 -85
  1012. package/src/controls/props-contract.test.tsx +0 -106
  1013. package/src/controls/sync-button.test.tsx +0 -183
  1014. package/src/dashboard/query-state.test.tsx +0 -152
  1015. package/src/dashboard/settings-section.test.tsx +0 -56
  1016. package/src/dashboard/stat-card.test.tsx +0 -154
  1017. package/src/data/data-table.test.tsx +0 -434
  1018. package/src/guard/check-source.test.ts +0 -3355
  1019. package/src/guard/grace.test.ts +0 -149
  1020. package/src/guard/guard-hook.test.ts +0 -203
  1021. package/src/provider/index.test.tsx +0 -101
  1022. package/src/provider/lab-theme.test.ts +0 -147
  1023. package/src/query/error-message.test.ts +0 -67
  1024. package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
  1025. package/src/router-tanstack/nav.test.ts +0 -111
  1026. package/src/router-tanstack/search-param-store.test.ts +0 -135
  1027. package/src/router-tanstack/search-store.router.test.tsx +0 -661
  1028. package/src/router-tanstack/search-store.test.ts +0 -418
  1029. package/src/router-tanstack/use-nav.test.tsx +0 -187
  1030. package/src/section/section.test.tsx +0 -204
  1031. package/src/shell/app-mobile-nav.test.tsx +0 -403
  1032. package/src/shell/app-sidebar.test.tsx +0 -509
  1033. package/src/shell/index.test.tsx +0 -428
  1034. package/src/shell/mobile-nav-model.test.ts +0 -517
  1035. package/src/shell/page-bar.test.tsx +0 -316
  1036. package/src/state/fields.test.tsx +0 -258
  1037. package/src/styles.floor.test.ts +0 -36
  1038. package/src/theme/border-coverage.test.ts +0 -272
  1039. package/src/theme/contrast.test.ts +0 -354
  1040. package/src/theme/ctl-theme.test.tsx +0 -275
  1041. package/src/theme/ctl-tier-coverage.test.ts +0 -170
  1042. package/src/theme/density-option.test.ts +0 -96
  1043. package/src/theme/density-relations.test.ts +0 -506
  1044. package/src/theme/derive-theme.test.ts +0 -101
  1045. package/src/theme/divider-law.test.ts +0 -259
  1046. package/src/theme/fonts-option.test.ts +0 -75
  1047. package/src/theme/icon-slot.test.tsx +0 -275
  1048. package/src/theme/layout-rhythm-css.test.ts +0 -72
  1049. package/src/theme/radius-option.test.ts +0 -86
  1050. package/src/theme/radius.test.ts +0 -142
  1051. package/src/theme/shadow-surfaces.test.ts +0 -488
  1052. package/src/theme/spacing.test.ts +0 -495
  1053. package/src/theme/use-basalt-spacing.test.tsx +0 -55
  1054. package/src/theme-lab/derive-controls.test.ts +0 -94
  1055. package/src/theme-lab/derive-state.test.ts +0 -71
  1056. package/src/theme-lab/theme-lab.test.ts +0 -46
  1057. package/src/tokens/build-fonts-css.test.ts +0 -52
  1058. package/src/tokens/build-radius-css.test.ts +0 -25
  1059. package/src/tokens/chart-margin.test.ts +0 -39
  1060. package/src/tokens/density.test.ts +0 -259
  1061. package/src/tokens/derive-radius.test.ts +0 -62
  1062. package/src/tokens/derive.test.ts +0 -30
  1063. package/src/tokens/palette.test.ts +0 -114
  1064. package/src/tokens/text-scale.test.ts +0 -51
  1065. package/src/vite-color-mix.test.ts +0 -46
  1066. package/src/vite.test.ts +0 -423
  1067. package/src/widget-header/delta-badge.test.tsx +0 -60
  1068. package/src/widget-header/no-mantine.test.ts +0 -33
  1069. package/src/widget-header/widget-header.test.tsx +0 -187
@@ -32,7 +32,17 @@
32
32
  * enablePagination
33
33
  * />
34
34
  */
35
- import { Box, Group, Pagination, Select, Skeleton, Table, Text, TextInput } from '@mantine/core'
35
+ import {
36
+ Box,
37
+ Checkbox,
38
+ Group,
39
+ Pagination,
40
+ Select,
41
+ Skeleton,
42
+ Table,
43
+ Text,
44
+ TextInput,
45
+ } from '@mantine/core'
36
46
  import type { MantineSpacing } from '@mantine/core'
37
47
  import type {
38
48
  Column,
@@ -42,6 +52,7 @@ import type {
42
52
  FilterFn,
43
53
  PaginationState,
44
54
  RowData,
55
+ RowSelectionState,
45
56
  SortingState,
46
57
  Table as TanstackTable,
47
58
  Updater,
@@ -54,9 +65,20 @@ import {
54
65
  getSortedRowModel,
55
66
  useReactTable,
56
67
  } from '@tanstack/react-table'
57
- import type { CSSProperties, ReactNode } from 'react'
58
- import { useCallback, useMemo, useState } from 'react'
68
+ import type {
69
+ CSSProperties,
70
+ KeyboardEvent as ReactKeyboardEvent,
71
+ ReactNode,
72
+ RefObject,
73
+ } from 'react'
74
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
75
+ import { cx } from '../common/props'
76
+ import type { BasaltProps, SlotStylesProps } from '../common/props'
77
+ import { BASALT_PREFIX } from '../common/errors'
78
+ import { assertRequiredProps, useValidateProps } from '../common/validate'
59
79
  import { FilterSet } from '../controls'
80
+ import { ActionGroup, BarActionSlot } from '../controls/actions'
81
+ import type { BarAction, SlotActions } from '../controls/actions'
60
82
  // `EnumFilter` is deliberately NOT on the `./controls` barrel (`docs/CONTROLS-SPEC.md` §3 —
61
83
  // reaching for it directly is hand-rolling a filter, `basalt/hand-rolled-filter`); a facet column
62
84
  // is the one place inside the framework itself that legitimately builds a `FieldHandle` over
@@ -67,6 +89,11 @@ import { CtlSlot } from '../theme'
67
89
  import { alpha, VX } from '../tokens'
68
90
  import type { EnumField, FieldHandle, MultiField } from '../state'
69
91
  import { WidgetHeader } from '../widget-header'
92
+ import { ErrorState } from '../dashboard/query-state'
93
+ import type { QueryStateLike } from '../dashboard/query-state'
94
+ import { dataQueryBranch } from './query-branch'
95
+ import { isDev } from '../utils/is-dev'
96
+ import classes from './data-table.module.css'
70
97
 
71
98
  // ── Column alignment ──────────────────────────────────────────────────────────
72
99
 
@@ -122,20 +149,30 @@ export type DataTableFacetOption = {
122
149
  label: string
123
150
  }
124
151
 
152
+ /**
153
+ * A TanStack column id typed off the row shape `T` where `accessor`/manual `id` inference allows
154
+ * it — `Extract<keyof T, string>` for a real field name, widened by `(string & {})` so a
155
+ * synthetic/derived column id (no matching key on `T`) still type-checks. Defaults to `unknown` so
156
+ * a bare `DataTableFacet[]` / `DataTableColumnPinning` (no type argument) keeps working exactly as
157
+ * before. Shared by {@link DataTableFacet}'s `columnId` and {@link DataTableColumnPinning}'s
158
+ * `left`/`right`.
159
+ */
160
+ export type DataTableColumnId<T = unknown> = Extract<keyof T, string> | (string & {})
161
+
125
162
  /**
126
163
  * Declares one faceted column filter, rendered as an `EnumFilter` pill (or `MultiSelectFilter`
127
164
  * when `multiple` is set) inside a `FilterSet` in the toolbar (`docs/CONTROLS-SPEC.md` §3).
128
165
  *
129
166
  * @example
130
- * const facets: DataTableFacet[] = [
167
+ * const facets: DataTableFacet<Employee>[] = [
131
168
  * { columnId: 'department', label: 'Department', options: [{ value: 'Engineering', label: 'Engineering' }] },
132
169
  * { columnId: 'role', label: 'Role', multiple: true, options: roleOptions },
133
170
  * ]
134
171
  * <BasaltDataTable data={rows} columns={columns} facets={facets} />
135
172
  */
136
- export type DataTableFacet = {
173
+ export type DataTableFacet<T = unknown> = {
137
174
  /** The TanStack column id — the column's manual `id`, or its `accessorKey` string. */
138
- columnId: string
175
+ columnId: DataTableColumnId<T>
139
176
  /** The pill's label — read at rest (single-select) and as the `Filters`/count fallback (multi). */
140
177
  label: string
141
178
  /** Selectable options for this facet. */
@@ -144,6 +181,17 @@ export type DataTableFacet = {
144
181
  multiple?: boolean
145
182
  }
146
183
 
184
+ /**
185
+ * Column ids pinned to each edge — the same `T`-derived id union as {@link DataTableFacet}'s
186
+ * `columnId`, applied to TanStack's own `ColumnPinningState` shape (`{ left, right }`, each a bare
187
+ * `string[]` unrelated to any row type). Structurally assignable to `ColumnPinningState`, so no
188
+ * cast is needed where this flows into `useState<ColumnPinningState>`.
189
+ */
190
+ export type DataTableColumnPinning<T = unknown> = {
191
+ left?: DataTableColumnId<T>[]
192
+ right?: DataTableColumnId<T>[]
193
+ }
194
+
147
195
  /**
148
196
  * The "no filter" member of a single-select facet's `FieldHandle`. A closed enum field always has
149
197
  * a real member as its fallback (C4) — there is no "unset" — so a facet, whose underlying
@@ -161,7 +209,7 @@ export type DataTableFacet = {
161
209
  const FACET_ALL_VALUE = '__basalt_facet_all__'
162
210
 
163
211
  /** The synthetic row's label — see {@link FACET_ALL_VALUE}. */
164
- function facetAllLabel(facet: DataTableFacet): string {
212
+ function facetAllLabel<T>(facet: DataTableFacet<T>): string {
165
213
  return `Any ${facet.label.toLowerCase()}`
166
214
  }
167
215
 
@@ -174,7 +222,7 @@ function facetAllLabel(facet: DataTableFacet): string {
174
222
  */
175
223
  function facetEnumHandle<T>(
176
224
  column: Column<T, unknown>,
177
- facet: DataTableFacet,
225
+ facet: DataTableFacet<T>,
178
226
  ): FieldHandle<EnumField<string>> {
179
227
  return {
180
228
  kind: 'enum',
@@ -189,6 +237,10 @@ function facetEnumHandle<T>(
189
237
  },
190
238
  ] as const
191
239
  },
240
+ // A facet column has no lane to unset: clearing the column filter IS its fallback state.
241
+ clear: () => {
242
+ column.setFilterValue(undefined)
243
+ },
192
244
  isDefault: (value) => value === FACET_ALL_VALUE,
193
245
  }
194
246
  }
@@ -200,7 +252,7 @@ function facetEnumHandle<T>(
200
252
  */
201
253
  function facetMultiHandle<T>(
202
254
  column: Column<T, unknown>,
203
- facet: DataTableFacet,
255
+ facet: DataTableFacet<T>,
204
256
  ): FieldHandle<MultiField<string>> {
205
257
  return {
206
258
  kind: 'multi',
@@ -215,12 +267,114 @@ function facetMultiHandle<T>(
215
267
  },
216
268
  ] as const
217
269
  },
270
+ // Same as the single-select facet: an empty column filter is this handle's fallback.
271
+ clear: () => {
272
+ column.setFilterValue(undefined)
273
+ },
218
274
  isDefault: (value) => value.length === 0,
219
275
  }
220
276
  }
221
277
 
278
+ /**
279
+ * Resolves a facet's `columnId` against the table. A mistyped id used to fall through to
280
+ * `if (!column) return null` — the facet pill silently vanished from the toolbar, the same class
281
+ * of defect the sibling `meta.align` lane and `manualPagination` both treat as a dev-time throw.
282
+ * Mirrors `resolveAlign`'s dev-throw shape: throw naming the id in dev
283
+ * (`basaltViteConfig` defines `process.env.NODE_ENV`, so a production bundle constant-folds this
284
+ * away), degrade to "no pill for this facet" in production.
285
+ */
286
+ function resolveFacetColumn<T>(
287
+ table: TanstackTable<T>,
288
+ facet: DataTableFacet<T>,
289
+ ): Column<T, unknown> | undefined {
290
+ const column = table.getColumn(facet.columnId)
291
+ if (column) return column
292
+ if (isDev()) {
293
+ const knownIds = table
294
+ .getAllColumns()
295
+ .map((known) => known.id)
296
+ .join(', ')
297
+ throw new Error(
298
+ `BasaltDataTable: facet columnId "${facet.columnId}" matches no column — known column ids: ` +
299
+ `${knownIds}.`,
300
+ )
301
+ }
302
+ return undefined
303
+ }
304
+
305
+ // ── Row selection ─────────────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * The synthetic checkbox column `enableRowSelection` prepends. Its id is namespaced so it can never
309
+ * collide with a real accessor key, and it is excluded from sorting — a checkbox column sorts by
310
+ * selection state, which is a reordering nobody asked for.
311
+ */
312
+ const SELECT_COLUMN_ID = '__basalt_select__'
313
+
314
+ /**
315
+ * Built per render inside a `useMemo`, not hoisted as a constant: `ColumnDef` is generic in `T`, and
316
+ * a single shared instance would have to be cast at every call site.
317
+ *
318
+ * `onClick`'s `stopPropagation` is the load-bearing line. The checkbox lives inside the row, and a
319
+ * row carrying `onRowActivate` opens a detail from a click — so without it, ticking the box also
320
+ * navigated away from the table the tick was meant to act on.
321
+ */
322
+ function selectionColumn<T>(): ColumnDef<T, unknown> {
323
+ return {
324
+ id: SELECT_COLUMN_ID,
325
+ enableSorting: false,
326
+ header: ({ table }) => (
327
+ <Checkbox
328
+ size="xs"
329
+ aria-label="Select all rows"
330
+ checked={table.getIsAllPageRowsSelected()}
331
+ indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
332
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
333
+ />
334
+ ),
335
+ cell: ({ row }) => (
336
+ <Checkbox
337
+ size="xs"
338
+ aria-label={`Select row ${row.index + 1}`}
339
+ checked={row.getIsSelected()}
340
+ disabled={!row.getCanSelect()}
341
+ onChange={row.getToggleSelectedHandler()}
342
+ onClick={(event) => event.stopPropagation()}
343
+ />
344
+ ),
345
+ }
346
+ }
347
+
348
+ /**
349
+ * The bar a selection raises, above the table and below its header — the count, then the actions,
350
+ * through the SAME `BarAction[]` vocabulary `PageBar` uses, so the ≤3-inline fold and the mobile
351
+ * kebab are basalt's here too (law C7/C15) instead of a hand-rolled `Group` of buttons.
352
+ *
353
+ * It renders only while at least one row is selected. A bar reserved at zero is a row of page
354
+ * furniture that says nothing, and a table that changes height when the first box is ticked is the
355
+ * price of not reserving it — the cheaper of the two, because the selection is what the reader just
356
+ * caused.
357
+ */
358
+ function BulkActionBar({ count, actions }: { count: number; actions: BarAction[] }) {
359
+ return (
360
+ <Group className={classes.bulkBar} gap="xs" align="center" wrap="wrap" mb="xs">
361
+ <Text className={classes.bulkCount}>{count} selected</Text>
362
+ <CtlSlot>
363
+ <ActionGroup secondary={actions} />
364
+ </CtlSlot>
365
+ </Group>
366
+ )
367
+ }
368
+
222
369
  // ── Props ─────────────────────────────────────────────────────────────────────
223
370
 
371
+ /**
372
+ * The four boxes `BasaltDataTable` paints: `root` is the outer fragment's replacement wrapper,
373
+ * `toolbar` is the search/facets/actions row, `table` is the Mantine `Table` itself, `footer` is
374
+ * the pagination bar.
375
+ */
376
+ export type BasaltDataTableSlot = 'root' | 'toolbar' | 'table' | 'footer'
377
+
224
378
  /**
225
379
  * Props for {@link BasaltDataTable}.
226
380
  *
@@ -234,268 +388,383 @@ function facetMultiHandle<T>(
234
388
  * emptyState: <Text c="dimmed">No results found.</Text>,
235
389
  * }
236
390
  */
237
- export type BasaltDataTableProps<T> = {
238
- // ── Header (WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ──────────────────────────
239
-
240
- /**
241
- * Optional heading rendered above the toolbar via `WidgetHeader tier="widget"`. `count` always
242
- * reads `table.getRowCount()` (C11) — never a raw `data.length` — so it tracks the row model
243
- * (post-filter/-pagination) rather than the unfiltered input.
244
- */
245
- title?: string
246
- /** Optional leading icon, forwarded to `WidgetHeader`. */
247
- icon?: ReactNode
248
- /** Optional muted line rendered below the title row. */
249
- subtitle?: string
250
-
251
- /** Row data array. */
252
- data: T[]
253
- /**
254
- * Column definitions. Use `createColumnHelper<T>()` from `@tanstack/react-table`
255
- * for a typed accessor builder — `col.accessor('field', …)` columns sort by value.
256
- */
257
- // The cell-value type varies per column, so the array is heterogeneous in TValue. `any` here
258
- // is the TanStack-idiomatic escape (accessor columns infer their own TValue); row typing stays
259
- // exact via the `T` data generic.
260
- // oxlint-disable-next-line typescript/no-explicit-any -- TanStack-idiomatic heterogeneous column array
261
- columns: ColumnDef<T, any>[]
262
- /**
263
- * Enable column sorting via clickable headers. Client-side — the rows in `data` are what get
264
- * reordered.
265
- *
266
- * Under `manualPagination` that is one page, so leaving this on without also passing
267
- * `manualSorting` is a contract violation and throws in dev — see `manualPagination`.
268
- * @default true
269
- */
270
- enableSorting?: boolean
271
- /**
272
- * Hand sorting to the server: the table stops reordering `data` locally and renders it in the
273
- * order given, while the headers keep toggling `SortingState` and reporting it through
274
- * `onSortingChange` — which is where you re-request the sorted page.
275
- *
276
- * This is the required companion to `manualPagination` whenever sort headers are live; without
277
- * it a server-paginated table sorts one page and the "of N" bar presents that as a sort of all N.
278
- * @default false
279
- * @example
280
- * <BasaltDataTable
281
- * data={page.rows}
282
- * columns={columns}
283
- * enablePagination
284
- * manualPagination
285
- * manualSorting
286
- * rowCount={page.total}
287
- * onSortingChange={(s) => refetch({ sort: s[0] })}
288
- * />
289
- */
290
- manualSorting?: boolean
291
- /** Stripe alternate rows. Forwarded to Mantine `Table` — `'odd'`/`'even'` pick the phase. */
292
- striped?: boolean | 'odd' | 'even'
293
- /** Highlight hovered rows. Forwarded to Mantine `Table`. */
294
- highlightOnHover?: boolean
295
- /**
296
- * Rendered when no rows are VISIBLE — `data` is empty, or a filter/page matches none of it.
297
- * Falls back to a simple message when omitted. (Before 1.25.0 this tracked `data.length` alone,
298
- * so a search matching nothing rendered a blank `<tbody>` with no message at all.)
299
- */
300
- emptyState?: ReactNode
301
- /**
302
- * When true, renders skeleton placeholder rows instead of the empty-state branch.
303
- * The header remains visible. Use while async data is loading.
304
- */
305
- isLoading?: boolean
306
- /**
307
- * Number of skeleton rows to render when `isLoading` is true.
308
- * @default 5
309
- */
310
- skeletonRows?: number
311
- /**
312
- * Initial sorting state. Drives `useState` initial value — useful for restoring
313
- * sort order from URL search params (e.g. `initialSorting={Route.useSearch().sorting}`).
314
- * @example
315
- * // URL-sync pattern with TanStack Router
316
- * const { sorting } = Route.useSearch()
317
- * <BasaltDataTable initialSorting={sorting} onSortingChange={(s) => navigate({ search: { sorting: s } })} … />
318
- */
319
- initialSorting?: SortingState
320
- /**
321
- * Called whenever the internal sorting state changes. Receives the new `SortingState`.
322
- * The table continues to manage sorting internally (uncontrolled) when this is omitted.
323
- */
324
- onSortingChange?: (sorting: SortingState) => void
325
-
326
- // ── Toolbar (search + facets + actions) ──────────────────────────────────────
327
-
328
- /**
329
- * Shows a global search `TextInput` above the table, wired to TanStack's `globalFilter` state
330
- * (substring match against every column's stringified cell value).
331
- * @default false
332
- * @example
333
- * <BasaltDataTable data={rows} columns={columns} enableGlobalFilter searchIcon={<IconSearch size={14} />} />
334
- */
335
- enableGlobalFilter?: boolean
336
- /**
337
- * Placeholder for the global search input.
338
- * @default 'Search…'
339
- */
340
- globalFilterPlaceholder?: string
341
- /**
342
- * Leading icon rendered inside the global search input (Mantine `leftSection`). basalt-ui ships
343
- * no icon set — pass any `ReactNode` (e.g. `<IconSearch size={14} />`).
344
- */
345
- searchIcon?: ReactNode
346
- /**
347
- * Initial global filter value. Drives `useState` initial value — mirrors `initialSorting` for
348
- * restoring the search term from a URL search param.
349
- */
350
- initialGlobalFilter?: string
351
- /** Called whenever the internal global filter value changes. */
352
- onGlobalFilterChange?: (value: string) => void
353
- /**
354
- * Faceted column filters, rendered in the toolbar as `EnumFilter`/`MultiSelectFilter` pills
355
- * inside a `FilterSet`, wired to TanStack's per-column `columnFilters` state. The toolbar
356
- * renders whenever this array is non-empty, `enableGlobalFilter` is set, or `actions` is passed.
357
- * @example
358
- * facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
359
- */
360
- facets?: DataTableFacet[]
361
- /** Called whenever the faceted column filters change — the seam for server-side faceting. */
362
- onColumnFiltersChange?: (filters: ColumnFiltersState) => void
363
- /**
364
- * Hand filtering to the server: the table stops narrowing `data` locally and renders every row
365
- * given, while the search input and facets keep reporting through `onGlobalFilterChange` /
366
- * `onColumnFiltersChange` — which is where you re-request the filtered page.
367
- *
368
- * Required alongside `manualPagination` whenever `enableGlobalFilter` or `facets` is set;
369
- * without it the controls narrow one page while "of N" keeps counting the whole set.
370
- * @default false
371
- */
372
- manualFiltering?: boolean
373
- /**
374
- * Right-aligned toolbar slot (e.g. an "Export" button). Renders the toolbar row even when no
375
- * search input or facets are configured. Renamed from `toolbarActions` — the toolbar (search +
376
- * facets + this slot) is wrapped in `CtlSlot` (C1/C5), so its controls resolve to the `ctl` tier.
377
- */
378
- actions?: ReactNode
379
-
380
- // ── Pagination ────────────────────────────────────────────────────────────────
381
-
382
- /**
383
- * Enables the bottom pagination bar ("Showing X–Y of N", rows-per-page `Select`, Mantine
384
- * `Pagination`) wired to TanStack's `pagination` state. Client-side by default — see
385
- * `manualPagination` for server-driven pagination.
386
- * @default false
387
- */
388
- enablePagination?: boolean
389
- /**
390
- * Rows-per-page choices offered in the pagination bar's Select.
391
- * @default [10, 25, 50, 100]
392
- */
393
- pageSizeOptions?: number[]
394
- /**
395
- * Initial pagination state. Drives `useState` initial value — mirrors `initialSorting` for
396
- * restoring the page/size from a URL search param.
397
- */
398
- initialPagination?: PaginationState
399
- /** Called whenever the internal pagination state changes. */
400
- onPaginationChange?: (pagination: PaginationState) => void
401
- /**
402
- * Disables local pagination slicing — pass the already-paginated page of `data` plus `rowCount`
403
- * (and optionally `pageCount`) for server-driven pagination. `onPaginationChange` is where you
404
- * fetch the next page.
405
- *
406
- * **It imposes a contract, enforced at render.** `data` is now one page, but the pagination bar
407
- * says "Showing 1–25 of 412" — so every OTHER client-side control becomes a claim about 412 rows
408
- * that it can only make about 25. Each has to be resolved explicitly, because only the call site
409
- * knows whether the server does the work:
410
- *
411
- * - `rowCount` (or `pageCount`) must be given, or "of N" counts the page and the pager collapses
412
- * to a single page nobody can leave;
413
- * - sorting must be handed over with `manualSorting`, or switched off with `enableSorting={false}`;
414
- * - `enableGlobalFilter` / `facets`, if used, must be handed over with `manualFiltering`;
415
- * - `enablePagination` must be on, or `manualPagination` is inert and the page renders as if it
416
- * were the whole table.
417
- *
418
- * Unresolved, each throws in dev and degrades to the honest render in production (no sort
419
- * headers, no filter controls, no "of N") — never to the plausible wrong answer.
420
- * @default false
421
- * @example
422
- * <BasaltDataTable
423
- * data={page.rows}
424
- * columns={columns}
425
- * enablePagination
426
- * manualPagination
427
- * rowCount={page.total}
428
- * initialPagination={{ pageIndex: page.index, pageSize: page.size }}
429
- * onPaginationChange={(p) => fetchPage(p)}
430
- * />
431
- */
432
- manualPagination?: boolean
433
- /** Total row count across all pages. Required for `manualPagination` to render "of N" and compute page count. */
434
- rowCount?: number
435
- /** Total page count, when known. Only consulted under `manualPagination`; omit to derive it from `rowCount`/`pageSize`. */
436
- pageCount?: number
437
-
438
- // ── Column pinning ────────────────────────────────────────────────────────────
439
-
440
- /**
441
- * Enables sticky left/right column pinning — pinned columns stick to the edge with a panel
442
- * background and a hairline shadow separator while the table scrolls horizontally. Pin columns
443
- * via `initialColumnPinning` (`{ left: string[], right: string[] }`).
444
- * @default false
445
- * @example
446
- * <BasaltDataTable data={rows} columns={columns} enablePinning initialColumnPinning={{ left: ['name'] }} />
447
- */
448
- enablePinning?: boolean
449
- /** Initial column-pinning state — which column ids stick to the left/right edge. */
450
- initialColumnPinning?: ColumnPinningState
451
-
452
- // ── Body chrome ─────────────────────────────────────────────────────────────
453
-
454
- /**
455
- * Caps the scrolling body (px number or CSS length) so a long table cannot blow a card out
456
- * vertically — the header stays put and the rows scroll under it. Renders Mantine's
457
- * `Table.ScrollContainer type="native"`, which is the same node the docs sanction as the raw
458
- * escape for a bespoke table: the blessed lane and the escape hatch produce identical DOM, so
459
- * adopting one does not contradict the other.
460
- *
461
- * `type="native"` is required rather than preferred: `ScrollArea`'s custom viewport is the
462
- * positioning context a sticky `<thead>` resolves against, so the default `'scrollarea'` type
463
- * pins the header to the page viewport instead of the table's box.
464
- */
465
- maxHeight?: number | string
466
- /**
467
- * `min-width` below which the body scrolls horizontally. Setting either this or `maxHeight`
468
- * turns on the scroll container; `maxHeight` alone implies `minWidth: 0`.
469
- */
470
- minWidth?: number | string
471
- /** Sticky header row. Pair with `maxHeight` to stick within the table rather than the page. */
472
- stickyHeader?: boolean
473
- /** Offset for a sticky header sitting under a fixed app header (px number or CSS length). */
474
- stickyHeaderOffset?: number | string
475
- /** Vertical cell padding. Forwarded to Mantine `Table`. */
476
- verticalSpacing?: MantineSpacing
477
- /** Horizontal cell padding. Forwarded to Mantine `Table`. */
478
- horizontalSpacing?: MantineSpacing
479
- /** Row separators. Forwarded to Mantine `Table`. @default true (Mantine's own default) */
480
- withRowBorders?: boolean
481
- /**
482
- * Outer table border — a full box around the table.
483
- *
484
- * **`false` by default since this minor** (it was `true`). Three separate faults, all visible on
485
- * the playground's `Controls (mobile)` page at 390px: its TOP edge sat directly under the table's
486
- * own `WidgetHeader`, separating a header from its own content; its BOTTOM edge landed on the last
487
- * row's own hairline, two rules at zero distance; and its left/right edges carried no information
488
- * at all on a table that fills the page (`docs/DESIGN-SPEC.md` §8 — a border is a layout divider,
489
- * and there is nothing here to divide). The head-row rule and the between-row rules stay, so the
490
- * table still reads as a table.
491
- *
492
- * Pass `true` for a table that genuinely needs a frame — a small table floating in whitespace
493
- * beside other content, where the box IS the grouping.
494
- *
495
- * @default false
496
- */
497
- withTableBorder?: boolean
498
- }
391
+ export type BasaltDataTableProps<T> = BasaltProps &
392
+ SlotStylesProps<BasaltDataTableSlot> & {
393
+ // ── Header (WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ──────────────────────────
394
+
395
+ /**
396
+ * Optional heading rendered above the toolbar via `WidgetHeader tier="widget"`. `count` always
397
+ * reads `table.getRowCount()` (C11) — never a raw `data.length` — so it tracks the row model
398
+ * (post-filter/-pagination) rather than the unfiltered input.
399
+ */
400
+ title?: string
401
+ /** Optional leading icon, forwarded to `WidgetHeader`. */
402
+ icon?: ReactNode
403
+ /** Optional muted line rendered below the title row. */
404
+ subtitle?: string
405
+
406
+ /** Row data array. */
407
+ data: T[]
408
+ /**
409
+ * Column definitions. Use `createColumnHelper<T>()` from `@tanstack/react-table`
410
+ * for a typed accessor builder — `col.accessor('field', …)` columns sort by value.
411
+ */
412
+ // The cell-value type varies per column, so the array is heterogeneous in TValue. `any` here
413
+ // is the TanStack-idiomatic escape (accessor columns infer their own TValue); row typing stays
414
+ // exact via the `T` data generic.
415
+ // oxlint-disable-next-line typescript/no-explicit-any -- TanStack-idiomatic heterogeneous column array
416
+ columns: ColumnDef<T, any>[]
417
+ /**
418
+ * Enable column sorting via clickable headers. Client-side — the rows in `data` are what get
419
+ * reordered.
420
+ *
421
+ * Under `manualPagination` that is one page, so leaving this on without also passing
422
+ * `manualSorting` is a contract violation and throws in dev — see `manualPagination`.
423
+ * @default true
424
+ */
425
+ enableSorting?: boolean
426
+ /**
427
+ * Hand sorting to the server: the table stops reordering `data` locally and renders it in the
428
+ * order given, while the headers keep toggling `SortingState` and reporting it through
429
+ * `onSortingChange` — which is where you re-request the sorted page.
430
+ *
431
+ * This is the required companion to `manualPagination` whenever sort headers are live; without
432
+ * it a server-paginated table sorts one page and the "of N" bar presents that as a sort of all N.
433
+ * @default false
434
+ * @example
435
+ * <BasaltDataTable
436
+ * data={page.rows}
437
+ * columns={columns}
438
+ * enablePagination
439
+ * manualPagination
440
+ * manualSorting
441
+ * rowCount={page.total}
442
+ * onSortingChange={(s) => refetch({ sort: s[0] })}
443
+ * />
444
+ */
445
+ manualSorting?: boolean
446
+ /** Stripe alternate rows. Forwarded to Mantine `Table` — `'odd'`/`'even'` pick the phase. */
447
+ striped?: boolean | 'odd' | 'even'
448
+ /** Highlight hovered rows. Forwarded to Mantine `Table`. */
449
+ highlightOnHover?: boolean
450
+ /**
451
+ * Rendered when no rows are VISIBLE — `data` is empty, or a filter/page matches none of it.
452
+ * Falls back to a simple message when omitted. (Before 1.25.0 this tracked `data.length` alone,
453
+ * so a search matching nothing rendered a blank `<tbody>` with no message at all.)
454
+ */
455
+ emptyState?: ReactNode
456
+ /**
457
+ * When true, renders skeleton placeholder rows instead of the empty-state branch.
458
+ * The header remains visible. Use while async data is loading.
459
+ *
460
+ * Superseded by `query`, which resolves this branch AND the two this prop cannot express — pass
461
+ * both and `query` wins, with a dev warning.
462
+ */
463
+ isLoading?: boolean
464
+ /**
465
+ * The result behind `data`, resolved into a body: pending → the skeleton rows below, error with
466
+ * no data → an `ErrorState` row spanning the table (with the query's own `refetch` behind
467
+ * Retry), anything else → the rows, or `emptyState` when none are visible.
468
+ *
469
+ * This is law C3's uniform container contract (`docs/CONTROLS-SPEC.md` §1). The table had NO
470
+ * error branch at all (components audit #3), so a 500 rendered *No data to display.* — the
471
+ * exact false claim `QueryState` was built to delete, one container over.
472
+ *
473
+ * The header, toolbar, `<thead>` and pagination bar stay drawn through every branch: only the
474
+ * `<tbody>` swaps, which is why this is not `QueryState` wrapped around the table. A refetch
475
+ * that fails while rows are already on screen keeps the rows — compose `QueryState` AROUND the
476
+ * table when you want its cached-data banner too.
477
+ *
478
+ * @example
479
+ * const q = useQuery({ queryKey: ['rows'], queryFn: () => api.rows() })
480
+ * <BasaltDataTable data={q.data ?? []} columns={columns} query={q} />
481
+ */
482
+ query?: QueryStateLike<unknown>
483
+ /**
484
+ * Number of skeleton rows to render when `isLoading` is true.
485
+ * @default 5
486
+ */
487
+ skeletonRows?: number
488
+ /**
489
+ * Initial sorting state. Drives `useState` initial value — useful for restoring
490
+ * sort order from URL search params (e.g. `initialSorting={Route.useSearch().sorting}`).
491
+ * @example
492
+ * // URL-sync pattern with TanStack Router
493
+ * const { sorting } = Route.useSearch()
494
+ * <BasaltDataTable initialSorting={sorting} onSortingChange={(s) => navigate({ search: { sorting: s } })} … />
495
+ */
496
+ initialSorting?: SortingState
497
+ /**
498
+ * Called whenever the internal sorting state changes. Receives the new `SortingState`.
499
+ * The table continues to manage sorting internally (uncontrolled) when this is omitted.
500
+ */
501
+ onSortingChange?: (sorting: SortingState) => void
502
+
503
+ // ── Toolbar (search + facets + actions) ──────────────────────────────────────
504
+
505
+ /**
506
+ * Shows a global search `TextInput` above the table, wired to TanStack's `globalFilter` state
507
+ * (substring match against every column's stringified cell value).
508
+ * @default false
509
+ * @example
510
+ * <BasaltDataTable data={rows} columns={columns} enableGlobalFilter searchIcon={<IconSearch size={14} />} />
511
+ */
512
+ enableGlobalFilter?: boolean
513
+ /**
514
+ * Placeholder for the global search input.
515
+ * @default 'Search…'
516
+ */
517
+ globalFilterPlaceholder?: string
518
+ /**
519
+ * Leading icon rendered inside the global search input (Mantine `leftSection`). basalt-ui ships
520
+ * no icon set — pass any `ReactNode` (e.g. `<IconSearch size={14} />`).
521
+ */
522
+ searchIcon?: ReactNode
523
+ /**
524
+ * Initial global filter value. Drives `useState` initial value — mirrors `initialSorting` for
525
+ * restoring the search term from a URL search param.
526
+ */
527
+ initialGlobalFilter?: string
528
+ /** Called whenever the internal global filter value changes. */
529
+ onGlobalFilterChange?: (value: string) => void
530
+ /**
531
+ * Faceted column filters, rendered in the toolbar as `EnumFilter`/`MultiSelectFilter` pills
532
+ * inside a `FilterSet`, wired to TanStack's per-column `columnFilters` state. The toolbar
533
+ * renders whenever this array is non-empty, `enableGlobalFilter` is set, or `actions` is passed.
534
+ * @example
535
+ * facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
536
+ */
537
+ facets?: DataTableFacet<T>[]
538
+ /** Called whenever the faceted column filters change — the seam for server-side faceting. */
539
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void
540
+ /**
541
+ * Hand filtering to the server: the table stops narrowing `data` locally and renders every row
542
+ * given, while the search input and facets keep reporting through `onGlobalFilterChange` /
543
+ * `onColumnFiltersChange` — which is where you re-request the filtered page.
544
+ *
545
+ * Required alongside `manualPagination` whenever `enableGlobalFilter` or `facets` is set;
546
+ * without it the controls narrow one page while "of N" keeps counting the whole set.
547
+ * @default false
548
+ */
549
+ manualFiltering?: boolean
550
+ /**
551
+ * Right-aligned toolbar slot (e.g. an "Export" button). Renders the toolbar row even when no
552
+ * search input or facets are configured. Renamed from `toolbarActions` — the toolbar (search +
553
+ * facets + this slot) is wrapped in `CtlSlot` (C1/C5), so its controls resolve to the `ctl` tier.
554
+ *
555
+ * Two accepted forms (law C15): a typed `BarAction[]`, projected through the same row `PageBar`
556
+ * and `ActionGroup` use — ≤3 inline, the rest folded into `More`, one kebab below `sm` — or an
557
+ * opaque `ReactNode`, rendered verbatim exactly as before.
558
+ */
559
+ actions?: SlotActions
560
+
561
+ // ── Row activation + selection ────────────────────────────────────────────────
562
+
563
+ /**
564
+ * Makes each row openable — click, or Enter on a focused row — with the ROW's original datum.
565
+ * The hook that pairs a table with `PageAside`: row → detail, without the caller adding an
566
+ * `onClick` to every cell renderer.
567
+ *
568
+ * The row keeps its native `<tr>` semantics and takes `tabIndex=0` plus `data-activatable`,
569
+ * never `role="button"`: a role swap takes the row out of the table's row/cell semantics, so a
570
+ * screen reader stops announcing each cell's column header — which is the reason the data is in
571
+ * a table. Cursor and focus ring come from the module class.
572
+ *
573
+ * A click that originates inside the selection checkbox does NOT activate (the checkbox stops
574
+ * the event); a click on a control your own `cell` renders should do the same.
575
+ *
576
+ * @example
577
+ * <BasaltDataTable data={rows} columns={columns} onRowActivate={(row) => setSelected(row)} />
578
+ */
579
+ onRowActivate?: (row: T) => void
580
+ /**
581
+ * Prepends a checkbox column (header = select-all on the page) and arms TanStack's row-selection
582
+ * feature. Selection is uncontrolled unless `rowSelection` is passed.
583
+ * @default false
584
+ */
585
+ enableRowSelection?: boolean
586
+ /**
587
+ * Controlled selection — TanStack's own `RowSelectionState` (`Record<rowId, boolean>`), keyed by
588
+ * `getRowId` (row index by default). Omit for uncontrolled selection held by the table.
589
+ */
590
+ rowSelection?: RowSelectionState
591
+ /** Called with the next `RowSelectionState` on every selection change, controlled or not. */
592
+ onRowSelectionChange?: (selection: RowSelectionState) => void
593
+ /**
594
+ * Stable row id, handed to TanStack. Pass one whenever the selection must survive a re-sort, a
595
+ * page change or a refetch — the default id is the row's INDEX, so row 3 stays "selected" when a
596
+ * different record moves into position 3.
597
+ */
598
+ getRowId?: (row: T, index: number) => string
599
+ /**
600
+ * The actions a selection offers, rendered in a bar above the table while at least one row is
601
+ * selected — and nothing at all while none is.
602
+ *
603
+ * A FUNCTION of the selected rows, not a bare array, and that is the whole signature decision:
604
+ * `BarActionItem.onClick` is `() => void` (the one vocabulary every home shares, unforked), so
605
+ * the rows have to reach the handler through the closure the caller writes here. It also means
606
+ * an UNCONTROLLED table can offer bulk actions at all — the selection lives inside the table,
607
+ * and this is how it gets out.
608
+ *
609
+ * @example
610
+ * bulkActions={(rows) => [
611
+ * { key: 'export', label: `Export ${rows.length}`, onClick: () => exportCsv(rows) },
612
+ * { key: 'delete', label: 'Delete', danger: true, onClick: () => remove(rows) },
613
+ * ]}
614
+ */
615
+ bulkActions?: (rows: T[]) => BarAction[]
616
+
617
+ // ── Pagination ────────────────────────────────────────────────────────────────
618
+
619
+ /**
620
+ * Enables the bottom pagination bar ("Showing X–Y of N", rows-per-page `Select`, Mantine
621
+ * `Pagination`) wired to TanStack's `pagination` state. Client-side by default — see
622
+ * `manualPagination` for server-driven pagination.
623
+ * @default false
624
+ */
625
+ enablePagination?: boolean
626
+ /**
627
+ * Rows-per-page choices offered in the pagination bar's Select.
628
+ * @default [10, 25, 50, 100]
629
+ */
630
+ pageSizeOptions?: number[]
631
+ /**
632
+ * Initial pagination state. Drives `useState` initial value — mirrors `initialSorting` for
633
+ * restoring the page/size from a URL search param.
634
+ */
635
+ initialPagination?: PaginationState
636
+ /** Called whenever the internal pagination state changes. */
637
+ onPaginationChange?: (pagination: PaginationState) => void
638
+ /**
639
+ * Disables local pagination slicing — pass the already-paginated page of `data` plus `rowCount`
640
+ * (and optionally `pageCount`) for server-driven pagination. `onPaginationChange` is where you
641
+ * fetch the next page.
642
+ *
643
+ * **It imposes a contract, enforced at render.** `data` is now one page, but the pagination bar
644
+ * says "Showing 1–25 of 412" — so every OTHER client-side control becomes a claim about 412 rows
645
+ * that it can only make about 25. Each has to be resolved explicitly, because only the call site
646
+ * knows whether the server does the work:
647
+ *
648
+ * - `rowCount` (or `pageCount`) must be given, or "of N" counts the page and the pager collapses
649
+ * to a single page nobody can leave;
650
+ * - sorting must be handed over with `manualSorting`, or switched off with `enableSorting={false}`;
651
+ * - `enableGlobalFilter` / `facets`, if used, must be handed over with `manualFiltering`;
652
+ * - `enablePagination` must be on, or `manualPagination` is inert and the page renders as if it
653
+ * were the whole table.
654
+ *
655
+ * Unresolved, each throws in dev and degrades to the honest render in production (no sort
656
+ * headers, no filter controls, no "of N") — never to the plausible wrong answer.
657
+ * @default false
658
+ * @example
659
+ * <BasaltDataTable
660
+ * data={page.rows}
661
+ * columns={columns}
662
+ * enablePagination
663
+ * manualPagination
664
+ * rowCount={page.total}
665
+ * initialPagination={{ pageIndex: page.index, pageSize: page.size }}
666
+ * onPaginationChange={(p) => fetchPage(p)}
667
+ * />
668
+ */
669
+ manualPagination?: boolean
670
+ /** Total row count across all pages. Required for `manualPagination` to render "of N" and compute page count. */
671
+ rowCount?: number
672
+ /** Total page count, when known. Only consulted under `manualPagination`; omit to derive it from `rowCount`/`pageSize`. */
673
+ pageCount?: number
674
+
675
+ // ── Column pinning ────────────────────────────────────────────────────────────
676
+
677
+ /**
678
+ * Enables sticky left/right column pinning — pinned columns stick to the edge with a panel
679
+ * background and a hairline shadow separator while the table scrolls horizontally. Pin columns
680
+ * via `initialColumnPinning` (`{ left: string[], right: string[] }`).
681
+ * @default false
682
+ * @example
683
+ * <BasaltDataTable data={rows} columns={columns} enablePinning initialColumnPinning={{ left: ['name'] }} />
684
+ */
685
+ enablePinning?: boolean
686
+ /** Initial column-pinning state — which column ids stick to the left/right edge. */
687
+ initialColumnPinning?: DataTableColumnPinning<T>
688
+
689
+ // ── Body chrome ─────────────────────────────────────────────────────────────
690
+
691
+ /**
692
+ * Caps the scrolling body (px number or CSS length) so a long table cannot blow a card out
693
+ * vertically — the header stays put and the rows scroll under it. Renders Mantine's
694
+ * `Table.ScrollContainer type="native"`, which is the same node the docs sanction as the raw
695
+ * escape for a bespoke table: the blessed lane and the escape hatch produce identical DOM, so
696
+ * adopting one does not contradict the other.
697
+ *
698
+ * `type="native"` is required rather than preferred: `ScrollArea`'s custom viewport is the
699
+ * positioning context a sticky `<thead>` resolves against, so the default `'scrollarea'` type
700
+ * pins the header to the page viewport instead of the table's box.
701
+ */
702
+ maxHeight?: number | string
703
+ /**
704
+ * `min-width` below which the body scrolls horizontally. Setting either this or `maxHeight`
705
+ * turns on the CAPPED scroll container; `maxHeight` alone implies `minWidth: 0`.
706
+ *
707
+ * It is a FLOOR, not the containment switch — since the shell made `AppShell.Main` the
708
+ * scrollport, every table renders inside a `Table.ScrollContainer type="native"` whether or not
709
+ * this is set (`minWidth: 0` when it is not), so a table wider than its column scrolls inside
710
+ * its own card instead of widening the page. Reach for this only to hold a MINIMUM table width
711
+ * — e.g. so eight columns keep their readable widths and scroll rather than compress.
712
+ *
713
+ * The one table whose wrapper is MEASURED rather than declared is `stickyHeader` with neither
714
+ * this nor `maxHeight`; see that prop and `useMeasuredContainment` in the implementation for
715
+ * why, and pair it with `maxHeight` if the table must both scroll and stick.
716
+ */
717
+ minWidth?: number | string
718
+ /**
719
+ * Sticky header row. Pair with `maxHeight` to stick within the table rather than the page.
720
+ *
721
+ * On its own — no `maxHeight`, no `minWidth` — the header sticks against the page's scrollport
722
+ * (`AppShell.Main`) only while the table FITS its container, and basalt measures that: the
723
+ * wrapper declares no overflow while it fits and flips to `overflow-x: auto` once the table is
724
+ * wider, publishing `data-contained`. The columns stay reachable either way and the page never
725
+ * scrolls sideways, but a sticky header cannot stick inside an uncapped overflow box — it has
726
+ * no scroll range — so it is inert at that width. A dev warning states the trade; pair this
727
+ * with `maxHeight` for a table that must both scroll and stick at every width.
728
+ */
729
+ stickyHeader?: boolean
730
+ /**
731
+ * Offset for a sticky header sitting under whatever chrome scrolls above it (px number or CSS
732
+ * length).
733
+ *
734
+ * PAGE-scroll only, and IGNORED whenever `maxHeight`/`minWidth` turn on the scroll container:
735
+ * that box is then the header's own scrollport, so an offset would park the `<thead>` that many
736
+ * pixels down inside the body — painting it over the rows instead of above them. Inside the
737
+ * container the header sticks to the scroller's own top edge.
738
+ *
739
+ * Inside `BasaltShell` the page's scrollport is `AppShell.Main`, and the app header sits
740
+ * OUTSIDE it — so the only chrome left to clear is `PageBar` row 2's band:
741
+ * `var(--basalt-page-bar-h, 0px)`, not the header height plus it.
742
+ */
743
+ stickyHeaderOffset?: number | string
744
+ /** Vertical cell padding. Forwarded to Mantine `Table`. */
745
+ verticalSpacing?: MantineSpacing
746
+ /** Horizontal cell padding. Forwarded to Mantine `Table`. */
747
+ horizontalSpacing?: MantineSpacing
748
+ /** Row separators. Forwarded to Mantine `Table`. @default true (Mantine's own default) */
749
+ withRowBorders?: boolean
750
+ /**
751
+ * Outer table border — a full box around the table.
752
+ *
753
+ * **`false` by default since this minor** (it was `true`). Three separate faults, all visible on
754
+ * the playground's `Controls (mobile)` page at 390px: its TOP edge sat directly under the table's
755
+ * own `WidgetHeader`, separating a header from its own content; its BOTTOM edge landed on the last
756
+ * row's own hairline, two rules at zero distance; and its left/right edges carried no information
757
+ * at all on a table that fills the page (`docs/DESIGN-SPEC.md` §8 — a border is a layout divider,
758
+ * and there is nothing here to divide). The head-row rule and the between-row rules stay, so the
759
+ * table still reads as a table.
760
+ *
761
+ * Pass `true` for a table that genuinely needs a frame — a small table floating in whitespace
762
+ * beside other content, where the box IS the grouping.
763
+ *
764
+ * @default false
765
+ */
766
+ withTableBorder?: boolean
767
+ }
499
768
 
500
769
  // ── Sort indicator ────────────────────────────────────────────────────────────
501
770
 
@@ -530,19 +799,38 @@ const RANGE_LABEL_STYLE: CSSProperties = {
530
799
  const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]
531
800
 
532
801
  /**
533
- * The header row's two halves. The lead SHRINKS and the toolbar does not — a title can ellipsize, a
534
- * 220px search field with a facet pill beside it cannot, and when both were shrinkable flex split
535
- * the overflow between them and clipped the search's placeholder mid-word.
802
+ * The header row's two halves. The lead GROWS and the toolbar does not — a title can ellipsize into
803
+ * whatever is left, and a search field that grew with it was 600px on a wide table.
804
+ *
805
+ * Both SHRINK, and that half is not a preference. `CtlSlot` between the header row and the toolbar
806
+ * is `display: contents`, so the toolbar `Group` IS the row's flex item — and with the
807
+ * `flex: 0 0 auto` it used to carry (plus flex's default `min-width: auto`) it could not give up a
808
+ * pixel. MEASURED at 390x844 on `/data`: a 220px search beside a 230px facet pill row, gap 11, is a
809
+ * 461px item in a 302px column, and `AppShell.Main` read `scrollWidth` 505 against `clientWidth`
810
+ * 390 — the whole app draggable sideways over a table header. `0 1 auto` + `min-width: 0` lets the
811
+ * item shrink to the column, and `wrap="wrap"` then does the rest: the search takes one line and
812
+ * the pills the next. Nothing here is width-gated, so it holds in a narrow grid cell at any
813
+ * viewport, the same way the chart tier resolves.
536
814
  */
537
815
  const TABLE_HEADER_LEAD_STYLE: CSSProperties = { flex: '1 1 auto', minWidth: 0 }
538
- const TABLE_TOOLBAR_STYLE: CSSProperties = { flex: '0 0 auto' }
816
+ const TABLE_TOOLBAR_STYLE: CSSProperties = { flex: '0 1 auto', minWidth: 0 }
539
817
  /**
540
818
  * The toolbar search's width. Mantine's Input grows to its container, which in a `justify:
541
819
  * space-between` row means "whatever is left" — so the field was 600px on a wide table and 180px on a
542
820
  * narrow one, for the same one-word query. A stated width also makes the toolbar's own minimum width
543
821
  * knowable, which is what lets it wrap under the title below `sm` with no media query.
822
+ *
823
+ * It is a BASIS, not a `width`: `flex: 0 1 <SEARCH_WIDTH>px` holds the field at 220px wherever 220
824
+ * is available and lets it shrink where it is not, floored at `12ch` (a legible query plus the
825
+ * search glyph) and capped at `100%` so it can never be the box that outgrows its column. A literal
826
+ * `w={220}` in a 302px column was half of the overflow above.
544
827
  */
545
828
  const SEARCH_WIDTH = 220
829
+ const TABLE_SEARCH_STYLE: CSSProperties = {
830
+ flex: `0 1 ${SEARCH_WIDTH}px`,
831
+ minWidth: '12ch',
832
+ maxWidth: '100%',
833
+ }
546
834
  /** The rows-per-page Select's width — see `SEARCH_WIDTH` for why a toolbar field states one. */
547
835
  const PAGE_SIZE_WIDTH = 116
548
836
 
@@ -598,13 +886,6 @@ function getPinnedCellStyle<T>(
598
886
 
599
887
  // ── The manual-pagination contract ────────────────────────────────────────────
600
888
 
601
- /** The house dev gate (`src/provider`, `src/charts/kinds/BandStrip` use the same expression):
602
- * `basaltViteConfig` defines `process.env.NODE_ENV`, so a production bundle constant-folds this to
603
- * `false` and drops the throw. Read per call, never hoisted, so a test can flip it. */
604
- function isDev(): boolean {
605
- return process.env['NODE_ENV'] !== 'production'
606
- }
607
-
608
889
  /** One client-side control that `manualPagination` turns into a claim the table cannot support. */
609
890
  type ManualPaginationBreach = 'inert' | 'total' | 'sorting' | 'filtering'
610
891
 
@@ -687,6 +968,99 @@ function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): vo
687
968
  console.error(message)
688
969
  }
689
970
 
971
+ /**
972
+ * MEASURED CONTAINMENT — the wrapper for the ONE table shape whose overflow mode cannot be decided
973
+ * statically: `stickyHeader` with neither `maxHeight` nor `minWidth`.
974
+ *
975
+ * Every other table renders inside `Table.ScrollContainer type="native"`, because a bare `<table>`
976
+ * sizes to its own min-content and a five-column one measured 448px inside a 390px viewport,
977
+ * dragging the whole page sideways (`tests/layout/no-horizontal-overflow.layout.test.ts`). This
978
+ * shape cannot take that container unconditionally: an `overflow-x: auto` box computes `overflow-y`
979
+ * to `auto` as well, which makes it the sticky header's own scrollport — and with no height cap
980
+ * that box has no scroll range at all, so the `<thead>` scrolls clean away (MEASURED in Chrome:
981
+ * top 10.5 → −389.5 across a 400px scroll). Containing it unconditionally deletes the feature;
982
+ * leaving it bare unconditionally widens the page.
983
+ *
984
+ * So the wrapper's overflow is decided by MEASUREMENT, the same fit-check `useTrackFits`
985
+ * (`controls/panel-row.tsx`) runs for a segmented track. While the table FITS its wrapper the
986
+ * wrapper is `overflow: visible` — bare, and the page-sticky header sticks against `AppShell.Main`
987
+ * exactly as it did before. Once the table is WIDER the wrapper flips to `overflow-x: auto`, the
988
+ * columns stay reachable by horizontal scroll, and the header is necessarily inert at that width.
989
+ * That is the honest trade: a header that stops sticking is recoverable, a page that scrolls
990
+ * sideways takes every fixed and sticky element on it with it.
991
+ *
992
+ * Three properties that are easy to lose:
993
+ *
994
+ * - **The observer stays alive in BOTH states** — no one-way latch, unlike `useTrackFits`. The
995
+ * wrapper must revert to bare when the space comes back (the window widened, the sidebar
996
+ * collapsed, the aside closed), and a contained table's `offsetWidth` is still its min-content
997
+ * width, so the same comparison keeps answering correctly from inside the contained state.
998
+ * - **Equality counts as FITS**, which is what stops the two states oscillating: a contained
999
+ * wrapper that grows past the table reads `offsetWidth === clientWidth` (the `width: 100%` table
1000
+ * resolves against the container) and flips back exactly once.
1001
+ * - **A zero `clientWidth` is `unknown`, not overflow** — the ancestor chain has not been laid out
1002
+ * yet (the aside animates its width in from 0, `docs/ASIDE-SPEC.md` §0). Committing to contained
1003
+ * on that reading would wrap a table that was never too wide.
1004
+ *
1005
+ * `useLayoutEffect` runs before paint, so a table that is already measurable resolves without a
1006
+ * flash of the wrong mode. SSR reaches neither the effect nor a `window`, and the default state is
1007
+ * bare — the same node the server and the first client paint both render.
1008
+ */
1009
+ function useMeasuredContainment(active: boolean): {
1010
+ wrapperRef: RefObject<HTMLDivElement | null>
1011
+ contained: boolean
1012
+ } {
1013
+ const wrapperRef = useRef<HTMLDivElement>(null)
1014
+ const [contained, setContained] = useState(false)
1015
+
1016
+ useLayoutEffect(() => {
1017
+ if (!active) return
1018
+ const wrapper = wrapperRef.current
1019
+ if (wrapper === null) return
1020
+
1021
+ // Three answers, not two — see the docblock. `unknown` moves neither the state nor the
1022
+ // observer; it is simply not an answer yet.
1023
+ const checkFit = (): 'fits' | 'contained' | 'unknown' => {
1024
+ const table = wrapper.querySelector('table')
1025
+ if (table === null) return 'unknown'
1026
+ if (wrapper.clientWidth === 0 || table.offsetWidth === 0) return 'unknown'
1027
+ return table.offsetWidth <= wrapper.clientWidth ? 'fits' : 'contained'
1028
+ }
1029
+
1030
+ const apply = (): void => {
1031
+ const result = checkFit()
1032
+ if (result === 'unknown') return
1033
+ const next = result === 'contained'
1034
+ setContained((current) => (current === next ? current : next))
1035
+ }
1036
+
1037
+ if (typeof ResizeObserver === 'undefined') {
1038
+ // No recovery path without an observer, so the one same-tick read is this environment's
1039
+ // only chance. `unknown` stays bare, which is the pre-measurement default anyway.
1040
+ apply()
1041
+ return
1042
+ }
1043
+
1044
+ const observer = new ResizeObserver(apply)
1045
+ observer.observe(wrapper)
1046
+ const table = wrapper.querySelector('table')
1047
+ // The TABLE too, not only the wrapper: a column set that re-measures (a facet filter changing
1048
+ // the widest cell, a font settling) moves the table's min-content width while the wrapper's
1049
+ // box never changes at all.
1050
+ if (table !== null) observer.observe(table)
1051
+
1052
+ // Same check once, synchronously, so an already-settled table resolves before paint rather
1053
+ // than waiting for the observer's first async tick.
1054
+ apply()
1055
+
1056
+ return () => {
1057
+ observer.disconnect()
1058
+ }
1059
+ }, [active])
1060
+
1061
+ return { wrapperRef, contained: active && contained }
1062
+ }
1063
+
690
1064
  // ── Component ─────────────────────────────────────────────────────────────────
691
1065
 
692
1066
  /**
@@ -714,48 +1088,63 @@ function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): vo
714
1088
  * emptyState={<Text c="dimmed">No users found.</Text>}
715
1089
  * />
716
1090
  */
717
- export function BasaltDataTable<T>({
718
- title,
719
- icon,
720
- subtitle,
721
- data,
722
- columns,
723
- enableSorting = true,
724
- manualSorting = false,
725
- striped,
726
- highlightOnHover,
727
- emptyState,
728
- isLoading = false,
729
- skeletonRows = 5,
730
- initialSorting,
731
- onSortingChange,
732
- enableGlobalFilter = false,
733
- globalFilterPlaceholder = 'Search…',
734
- searchIcon,
735
- initialGlobalFilter,
736
- onGlobalFilterChange,
737
- facets,
738
- onColumnFiltersChange,
739
- manualFiltering = false,
740
- actions,
741
- enablePagination = false,
742
- pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
743
- initialPagination,
744
- onPaginationChange,
745
- manualPagination = false,
746
- rowCount,
747
- pageCount,
748
- enablePinning = false,
749
- initialColumnPinning,
750
- maxHeight,
751
- minWidth,
752
- stickyHeader,
753
- stickyHeaderOffset,
754
- verticalSpacing,
755
- horizontalSpacing,
756
- withRowBorders,
757
- withTableBorder = false,
758
- }: BasaltDataTableProps<T>) {
1091
+ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
1092
+ // F-ERR-1: without this, a table missing `data`/`columns` fails deep inside `useReactTable` as a
1093
+ // raw `TypeError` caught by `BasaltErrorBoundary` — a blank subtree with no message naming either
1094
+ // prop.
1095
+ assertRequiredProps('BasaltDataTable', props, ['data', 'columns'])
1096
+ const {
1097
+ title,
1098
+ icon,
1099
+ subtitle,
1100
+ data,
1101
+ columns,
1102
+ enableSorting = true,
1103
+ manualSorting = false,
1104
+ striped,
1105
+ highlightOnHover,
1106
+ emptyState,
1107
+ isLoading = false,
1108
+ query,
1109
+ skeletonRows = 5,
1110
+ initialSorting,
1111
+ onSortingChange,
1112
+ enableGlobalFilter = false,
1113
+ globalFilterPlaceholder = 'Search…',
1114
+ searchIcon,
1115
+ initialGlobalFilter,
1116
+ onGlobalFilterChange,
1117
+ facets,
1118
+ onColumnFiltersChange,
1119
+ manualFiltering = false,
1120
+ actions,
1121
+ onRowActivate,
1122
+ enableRowSelection = false,
1123
+ rowSelection,
1124
+ onRowSelectionChange,
1125
+ getRowId,
1126
+ bulkActions,
1127
+ enablePagination = false,
1128
+ pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
1129
+ initialPagination,
1130
+ onPaginationChange,
1131
+ manualPagination = false,
1132
+ rowCount,
1133
+ pageCount,
1134
+ enablePinning = false,
1135
+ initialColumnPinning,
1136
+ maxHeight,
1137
+ minWidth,
1138
+ stickyHeader,
1139
+ stickyHeaderOffset,
1140
+ verticalSpacing,
1141
+ horizontalSpacing,
1142
+ withRowBorders,
1143
+ withTableBorder = false,
1144
+ className,
1145
+ style,
1146
+ classNames,
1147
+ } = props
759
1148
  const [sorting, setSorting] = useState<SortingState>(initialSorting ?? [])
760
1149
  const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter ?? '')
761
1150
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
@@ -763,6 +1152,51 @@ export function BasaltDataTable<T>({
763
1152
  initialPagination ?? { pageIndex: 0, pageSize: pageSizeOptions[0] ?? 10 },
764
1153
  )
765
1154
  const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(initialColumnPinning ?? {})
1155
+ // Uncontrolled by default; `rowSelection`, when passed, is the truth and this only mirrors it so
1156
+ // the updater below has a base to apply against.
1157
+ const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
1158
+ const rowSelectionState = rowSelection ?? internalRowSelection
1159
+
1160
+ // `stickyHeaderOffset` is DROPPED whenever the table owns a scroll container — see
1161
+ // `resolvedStickyHeaderOffset` below for why it is always wrong there. Dropping it silently is
1162
+ // the part worth a message: the header still sticks, so the table looks right until it overlaps
1163
+ // the page chrome the offset was measured against, and nothing anywhere says the prop was ignored.
1164
+ //
1165
+ // The second check is `query` beside `isLoading`: `query` resolves the pending branch AND the two
1166
+ // `isLoading` cannot express, so it wins outright. Silently, that reads as the boolean having no
1167
+ // effect — which is indistinguishable from a bug in the caller's own loading flag.
1168
+ useValidateProps(
1169
+ 'BasaltDataTable',
1170
+ () => [
1171
+ stickyHeaderOffset === undefined || (maxHeight === undefined && minWidth === undefined)
1172
+ ? null
1173
+ : `${BASALT_PREFIX} BasaltDataTable: prop "stickyHeaderOffset" is ignored beside ` +
1174
+ '"maxHeight"/"minWidth" — those render a scroll container, and the sticky `<thead>` then ' +
1175
+ "anchors to that box's own top edge rather than to the window. Drop the offset, or drop " +
1176
+ 'the cap and let the page scroll.',
1177
+ query === undefined || props.isLoading === undefined
1178
+ ? null
1179
+ : `${BASALT_PREFIX} BasaltDataTable: props "query" and "isLoading" are both set — "query" ` +
1180
+ 'wins and "isLoading" is ignored. `query` already resolves the pending branch, plus the ' +
1181
+ 'error and empty ones the boolean cannot express. Drop "isLoading".',
1182
+ stickyHeader !== true || maxHeight !== undefined || minWidth !== undefined
1183
+ ? null
1184
+ : `${BASALT_PREFIX} BasaltDataTable: prop "stickyHeader" with neither "maxHeight" nor ` +
1185
+ '"minWidth" sticks only while the table FITS its container. basalt measures it: a table ' +
1186
+ 'wider than the space it has flips its wrapper to `overflow-x: auto` so the columns ' +
1187
+ 'stay reachable and the page never scrolls sideways, and a sticky header cannot stick ' +
1188
+ 'inside that box — it has no scroll range. Pair it with "maxHeight" for a table that ' +
1189
+ 'must both scroll and stick.',
1190
+ ],
1191
+ [
1192
+ stickyHeaderOffset,
1193
+ maxHeight,
1194
+ minWidth,
1195
+ stickyHeader,
1196
+ query === undefined,
1197
+ props.isLoading === undefined,
1198
+ ],
1199
+ )
766
1200
 
767
1201
  const handleSortingChange = useCallback(
768
1202
  (updater: Updater<SortingState>) => {
@@ -808,10 +1242,29 @@ export function BasaltDataTable<T>({
808
1242
  [onPaginationChange],
809
1243
  )
810
1244
 
1245
+ const handleRowSelectionChange = useCallback(
1246
+ (updater: Updater<RowSelectionState>) => {
1247
+ // CONTROLLED: the caller's map is the base AND the only writer. Mirroring it into state too
1248
+ // would leave a copy that the caller never moves, and that stale copy becomes the selection
1249
+ // the moment `rowSelection` goes back to `undefined` — plus every tick would render twice for
1250
+ // one change. Report the next map and let the caller own it.
1251
+ if (rowSelection !== undefined) {
1252
+ onRowSelectionChange?.(typeof updater === 'function' ? updater(rowSelection) : updater)
1253
+ return
1254
+ }
1255
+ setInternalRowSelection((prev) => {
1256
+ const next = typeof updater === 'function' ? updater(prev) : updater
1257
+ onRowSelectionChange?.(next)
1258
+ return next
1259
+ })
1260
+ },
1261
+ [rowSelection, onRowSelectionChange],
1262
+ )
1263
+
811
1264
  // Facets are TanStack `columnFilters` entries under the hood — inject an exact-match (or
812
1265
  // any-of, for `multiple`) filterFn onto the matching column so the built-in `includesString`
813
1266
  // auto-filter (substring match) never misfires against a Select's exact option value.
814
- const tableColumns = useMemo(() => {
1267
+ const facetColumns = useMemo(() => {
815
1268
  if (!facets || facets.length === 0) return columns
816
1269
  const facetById = new Map(facets.map((facet) => [facet.columnId, facet]))
817
1270
  return columns.map((column) => {
@@ -836,6 +1289,14 @@ export function BasaltDataTable<T>({
836
1289
  })
837
1290
  }, [columns, facets])
838
1291
 
1292
+ // The checkbox column is PREPENDED here rather than asked of the caller, so `columns` stays the
1293
+ // caller's own data columns and `meta.align` / `numeral` never have to reason about a synthetic
1294
+ // one. Everything counting columns downstream reads `columnCount`, below.
1295
+ const tableColumns = useMemo(
1296
+ () => (enableRowSelection ? [selectionColumn<T>(), ...facetColumns] : facetColumns),
1297
+ [facetColumns, enableRowSelection],
1298
+ )
1299
+
839
1300
  // Enforced before the table is built, so the degraded options below are the ones TanStack gets —
840
1301
  // a degraded table must not merely hide a control, it must not compute the answer either.
841
1302
  const hasFacets = Boolean(facets && facets.length > 0)
@@ -866,12 +1327,18 @@ export function BasaltDataTable<T>({
866
1327
  columnFilters,
867
1328
  columnPinning,
868
1329
  ...(enablePagination && { pagination }),
1330
+ ...(enableRowSelection && { rowSelection: rowSelectionState }),
869
1331
  },
870
1332
  onSortingChange: handleSortingChange,
871
1333
  onGlobalFilterChange: handleGlobalFilterChange,
872
1334
  onColumnFiltersChange: handleColumnFiltersChange,
873
1335
  onColumnPinningChange: setColumnPinning,
874
1336
  ...(enablePagination && { onPaginationChange: handlePaginationChange }),
1337
+ ...(enableRowSelection && {
1338
+ enableRowSelection: true,
1339
+ onRowSelectionChange: handleRowSelectionChange,
1340
+ }),
1341
+ ...(getRowId !== undefined && { getRowId }),
875
1342
  getCoreRowModel: getCoreRowModel(),
876
1343
  getSortedRowModel: getSortedRowModel(),
877
1344
  getFilteredRowModel: getFilteredRowModel(),
@@ -895,17 +1362,76 @@ export function BasaltDataTable<T>({
895
1362
  const showSearch = enableGlobalFilter && filteringEnabled
896
1363
  const showFacets = hasFacets && filteringEnabled
897
1364
  const showToolbar = showSearch || showFacets || Boolean(actions)
1365
+
1366
+ // ── The query branch (law C3) ───────────────────────────────────────────────
1367
+ // `query`, when given, owns the body: it resolves the pending branch `isLoading` used to own,
1368
+ // plus the error branch the table had none of. `isLoading` stays live for every caller who never
1369
+ // had a query result to hand — the dev warning above covers the pair.
1370
+ const branch = query === undefined ? undefined : dataQueryBranch('BasaltDataTable', query)
1371
+ const showSkeleton = branch === undefined ? isLoading : branch === 'pending'
1372
+ const queryError = branch === 'error' && query !== undefined ? query : undefined
1373
+
1374
+ // Every colSpan and every skeleton row counts the RENDERED columns, which is one more than
1375
+ // `columns` once the checkbox column is prepended — a stale count left the empty state and the
1376
+ // error row one cell short of the header.
1377
+ const columnCount = columns.length + (enableRowSelection ? 1 : 0)
1378
+
1379
+ // `getSelectedRowModel()` is TanStack's own memo, so its `rows` array keeps its identity until
1380
+ // the selection or the row model actually moves — which is what makes both memos below hold.
1381
+ const selectedModelRows = enableRowSelection ? table.getSelectedRowModel().rows : undefined
1382
+ const selectedRows = useMemo(
1383
+ () => selectedModelRows?.map((row) => row.original) ?? [],
1384
+ [selectedModelRows],
1385
+ )
1386
+ // `bulkActions` is the caller's function of the selection, and calling it inline rebuilt every
1387
+ // action object on every render of the table — new `onClick` identities into the whole bar, for a
1388
+ // selection that had not changed.
1389
+ const bulkBarActions = useMemo(
1390
+ () =>
1391
+ bulkActions === undefined || selectedRows.length === 0
1392
+ ? undefined
1393
+ : bulkActions(selectedRows),
1394
+ [bulkActions, selectedRows],
1395
+ )
898
1396
  const headerGroups = enablePinning ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
899
1397
  // The rows actually rendered, not `data` — a filter or a page index that matches nothing leaves
900
1398
  // `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
901
1399
  const rows = table.getRowModel().rows
902
1400
 
1401
+ // A capped body or a horizontal floor both need the same native scroll node. Everything else —
1402
+ // pinning included — either takes that same container (the uncapped, non-sticky default) or the
1403
+ // measured wrapper below; there is no third overflow box.
1404
+ const scrolls = maxHeight !== undefined || minWidth !== undefined
1405
+ // `stickyHeaderOffset` is a WINDOW-scroll concept — the height of whatever fixed chrome the page
1406
+ // scrolls under (the AppShell header plus `PageBar` row 2). Inside the scroll container it is
1407
+ // always wrong: that box is the sticky header's own scrollport, so the offset parks the `<thead>`
1408
+ // that many pixels DOWN INSIDE the body rather than above it. MEASURED on `/data-stress`
1409
+ // (`stickyHeader` + `minWidth`, no `maxHeight`): `calc(48px + 46px)` put the header at y=339 with
1410
+ // row 1 at y=284 and row 2 at y=327 — the header painted over row 2 at initial scroll, at 1440x900
1411
+ // and 390x844 alike. `overflow-x: auto` computes `overflow-y` to `auto` too, so the container is a
1412
+ // scrollport even with no `maxHeight` and the header never scrolls back into place. The scroller's
1413
+ // own top edge is the only correct anchor here, so the offset is dropped rather than honoured.
1414
+ //
1415
+ // THE ONE SHAPE WHOSE OVERFLOW IS MEASURED, NOT DECLARED. Every other table renders inside a
1416
+ // `Table.ScrollContainer` (see the render branch below), because a bare `<table>` sizes to its
1417
+ // own min-content and a 5-column table is ~450px wide — it widened the whole page at 390px, which
1418
+ // is the defect `tests/layout/no-horizontal-overflow.layout.test.ts` guards. A page-scrolled
1419
+ // sticky header cannot take that container unconditionally, so it takes the wrapper
1420
+ // `useMeasuredContainment` drives instead: bare while the table fits, `overflow-x: auto` once it
1421
+ // does not. Its docblock carries the full accounting.
1422
+ const pageStickyHeader = !scrolls && stickyHeader === true
1423
+ const containment = useMeasuredContainment(pageStickyHeader)
1424
+ const resolvedStickyHeaderOffset = scrolls ? undefined : stickyHeaderOffset
1425
+
903
1426
  const tableNode = (
904
1427
  <Table
1428
+ className={cx(classNames?.table)}
905
1429
  {...(striped !== undefined && { striped })}
906
1430
  {...(highlightOnHover !== undefined && { highlightOnHover })}
907
1431
  {...(stickyHeader !== undefined && { stickyHeader })}
908
- {...(stickyHeaderOffset !== undefined && { stickyHeaderOffset })}
1432
+ {...(resolvedStickyHeaderOffset !== undefined && {
1433
+ stickyHeaderOffset: resolvedStickyHeaderOffset,
1434
+ })}
909
1435
  {...(verticalSpacing !== undefined && { verticalSpacing })}
910
1436
  {...(horizontalSpacing !== undefined && { horizontalSpacing })}
911
1437
  {...(withRowBorders !== undefined && { withRowBorders })}
@@ -962,10 +1488,25 @@ export function BasaltDataTable<T>({
962
1488
  ))}
963
1489
  </Table.Thead>
964
1490
  <Table.Tbody>
965
- {isLoading ? (
1491
+ {queryError !== undefined ? (
1492
+ // The branch the table did not have (components audit #3). It spans every rendered
1493
+ // column and carries the query's OWN `refetch` behind Retry, so the failure is reported
1494
+ // where the rows would have been — not as an empty table saying nothing failed.
1495
+ <Table.Tr>
1496
+ <Table.Td colSpan={columnCount}>
1497
+ <ErrorState
1498
+ error={queryError.error}
1499
+ title="Could not load"
1500
+ tier="section"
1501
+ retrying={queryError.fetchStatus === 'fetching'}
1502
+ onRetry={() => void queryError.refetch()}
1503
+ />
1504
+ </Table.Td>
1505
+ </Table.Tr>
1506
+ ) : showSkeleton ? (
966
1507
  Array.from({ length: skeletonRows }, (_, rowIndex) => (
967
1508
  <Table.Tr key={`skeleton-${rowIndex}`}>
968
- {columns.map((_, colIndex) => (
1509
+ {Array.from({ length: columnCount }, (__, colIndex) => (
969
1510
  <Table.Td key={`skeleton-${rowIndex}-${colIndex}`}>
970
1511
  <Skeleton height={16} radius="sm" />
971
1512
  </Table.Td>
@@ -974,7 +1515,7 @@ export function BasaltDataTable<T>({
974
1515
  ))
975
1516
  ) : rows.length === 0 ? (
976
1517
  <Table.Tr>
977
- <Table.Td colSpan={columns.length}>
1518
+ <Table.Td colSpan={columnCount}>
978
1519
  {emptyState ?? (
979
1520
  <Text c="dimmed" ta="center" size="sm" py="sm">
980
1521
  No data to display.
@@ -992,7 +1533,28 @@ export function BasaltDataTable<T>({
992
1533
  ]
993
1534
  : row.getVisibleCells()
994
1535
  return (
995
- <Table.Tr key={row.id}>
1536
+ <Table.Tr
1537
+ key={row.id}
1538
+ {...(enableRowSelection && row.getIsSelected() && { 'data-selected': true })}
1539
+ {...(onRowActivate !== undefined && {
1540
+ className: classes.activatable,
1541
+ 'data-activatable': true,
1542
+ tabIndex: 0,
1543
+ onClick: () => onRowActivate(row.original),
1544
+ // Enter only. Space is the browser's own page-scroll on a focused non-button, and
1545
+ // stealing it from a keyboard reader moving down a long table costs more than the
1546
+ // second activation key buys.
1547
+ onKeyDown: (event: ReactKeyboardEvent<HTMLTableRowElement>) => {
1548
+ if (event.key !== 'Enter') return
1549
+ // Only the ROW's own Enter. A cell may hold a button, a link or the selection
1550
+ // checkbox, and keydown bubbles — so without this an Enter on a nested control
1551
+ // fired that control AND opened the row's detail behind it.
1552
+ if (event.target !== event.currentTarget) return
1553
+ event.preventDefault()
1554
+ onRowActivate(row.original)
1555
+ },
1556
+ })}
1557
+ >
996
1558
  {cells.map((cell) => {
997
1559
  const pinnedStyle = enablePinning
998
1560
  ? getPinnedCellStyle(table, cell.column)
@@ -1022,10 +1584,6 @@ export function BasaltDataTable<T>({
1022
1584
  </Table>
1023
1585
  )
1024
1586
 
1025
- // A capped body or a horizontal floor both need the same native scroll node; pinning keeps its
1026
- // simpler overflow-x Box when neither is set.
1027
- const scrolls = maxHeight !== undefined || minWidth !== undefined
1028
-
1029
1587
  const paginationState = table.getState().pagination
1030
1588
  // The CURRENT page size is always an option, even when it is not in `pageSizeOptions`. Mantine's
1031
1589
  // Select renders EMPTY when its `value` matches no row, so a table opened at
@@ -1039,7 +1597,7 @@ export function BasaltDataTable<T>({
1039
1597
  const rangeEnd = Math.min((paginationState.pageIndex + 1) * paginationState.pageSize, total)
1040
1598
 
1041
1599
  return (
1042
- <>
1600
+ <div className={cx(classNames?.root, className)} {...(style !== undefined && { style })}>
1043
1601
  {/*
1044
1602
  * ONE header row: the `WidgetHeader` (title · count) on the left, the toolbar right-aligned in
1045
1603
  * the SAME row. It was two stacked rows with `mb="xs"` on each, so a titled table with search
@@ -1066,11 +1624,17 @@ export function BasaltDataTable<T>({
1066
1624
  ) : null}
1067
1625
  {showToolbar && (
1068
1626
  <CtlSlot>
1069
- <Group gap="xs" wrap="wrap" align="center" style={TABLE_TOOLBAR_STYLE}>
1627
+ <Group
1628
+ className={cx(classNames?.toolbar)}
1629
+ gap="xs"
1630
+ wrap="wrap"
1631
+ align="center"
1632
+ style={TABLE_TOOLBAR_STYLE}
1633
+ >
1070
1634
  {showSearch && (
1071
1635
  <TextInput
1072
1636
  radius="md"
1073
- w={SEARCH_WIDTH}
1637
+ style={TABLE_SEARCH_STYLE}
1074
1638
  placeholder={globalFilterPlaceholder}
1075
1639
  leftSection={searchIcon}
1076
1640
  value={globalFilter}
@@ -1080,7 +1644,7 @@ export function BasaltDataTable<T>({
1080
1644
  {showFacets && (
1081
1645
  <FilterSet>
1082
1646
  {facets?.map((facet) => {
1083
- const column = table.getColumn(facet.columnId)
1647
+ const column = resolveFacetColumn(table, facet)
1084
1648
  if (!column) return null
1085
1649
  return facet.multiple ? (
1086
1650
  <MultiSelectFilter
@@ -1098,13 +1662,27 @@ export function BasaltDataTable<T>({
1098
1662
  })}
1099
1663
  </FilterSet>
1100
1664
  )}
1101
- {actions}
1665
+ {actions !== undefined && <BarActionSlot actions={actions} />}
1102
1666
  </Group>
1103
1667
  </CtlSlot>
1104
1668
  )}
1105
1669
  </Group>
1106
1670
  )}
1107
- {scrolls ? (
1671
+ {bulkBarActions !== undefined && (
1672
+ <BulkActionBar count={selectedRows.length} actions={bulkBarActions} />
1673
+ )}
1674
+ {pageStickyHeader ? (
1675
+ // Pinning takes this wrapper too — a pinned column's offsets are `position: sticky` on the
1676
+ // cells and need no overflow box of their own, so the `overflow-x: auto` Box that used to
1677
+ // sit here was the exact inert-sticky defect this shape exists to avoid.
1678
+ <div
1679
+ ref={containment.wrapperRef}
1680
+ className={classes.containment}
1681
+ data-contained={containment.contained ? 'true' : 'false'}
1682
+ >
1683
+ {tableNode}
1684
+ </div>
1685
+ ) : (
1108
1686
  <Table.ScrollContainer
1109
1687
  type="native"
1110
1688
  minWidth={minWidth ?? 0}
@@ -1112,13 +1690,16 @@ export function BasaltDataTable<T>({
1112
1690
  >
1113
1691
  {tableNode}
1114
1692
  </Table.ScrollContainer>
1115
- ) : enablePinning ? (
1116
- <Box style={{ overflowX: 'auto' }}>{tableNode}</Box>
1117
- ) : (
1118
- tableNode
1119
1693
  )}
1120
1694
  {enablePagination && (
1121
- <Group justify="space-between" mt="xs" wrap="wrap" gap="xs" align="center">
1695
+ <Group
1696
+ className={cx(classNames?.footer)}
1697
+ justify="space-between"
1698
+ mt="xs"
1699
+ wrap="wrap"
1700
+ gap="xs"
1701
+ align="center"
1702
+ >
1122
1703
  <Text style={RANGE_LABEL_STYLE}>
1123
1704
  Showing {rangeStart}–{rangeEnd}
1124
1705
  {totalIsAuthoritative ? ` of ${total}` : ''}
@@ -1143,6 +1724,6 @@ export function BasaltDataTable<T>({
1143
1724
  </Group>
1144
1725
  </Group>
1145
1726
  )}
1146
- </>
1727
+ </div>
1147
1728
  )
1148
1729
  }