basalt-ui 1.27.0 → 1.29.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 (1245) hide show
  1. package/AGENTS.md +28 -35
  2. package/CHANGELOG.md +72 -0
  3. package/MIGRATING.md +759 -22
  4. package/README.md +56 -69
  5. package/agent/rules/basalt-batteries.md +60 -168
  6. package/agent/rules/basalt-charts.md +56 -75
  7. package/agent/rules/basalt-controls.md +81 -87
  8. package/agent/rules/basalt-mantine.md +64 -98
  9. package/agent/rules/basalt-state.md +42 -60
  10. package/agent/rules/basalt-tokens.md +46 -67
  11. package/agent/skills/basalt-app/SKILL.md +2 -0
  12. package/agent/templates/CLAUDE-block.md.tpl +14 -14
  13. package/configs/oxlint-plugin.js +964 -33
  14. package/configs/oxlint-plugin.test.ts +902 -2
  15. package/configs/oxlint.json +6 -0
  16. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  17. package/dist/agent/ai-sdk-transport.js +7 -4
  18. package/dist/agent/coalesce.d.ts.map +1 -1
  19. package/dist/agent/coalesce.js +2 -1
  20. package/dist/agent/merge.d.ts.map +1 -1
  21. package/dist/agent/merge.js +2 -1
  22. package/dist/agent/part-list.d.ts.map +1 -1
  23. package/dist/agent/part-list.js +2 -0
  24. package/dist/agent/stick-to-bottom.d.ts +3 -6
  25. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  26. package/dist/agent/stick-to-bottom.js +6 -3
  27. package/dist/agent/transport.d.ts +13 -2
  28. package/dist/agent/transport.d.ts.map +1 -1
  29. package/dist/agent/use-agent-thread-runs.d.ts +12 -1
  30. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  31. package/dist/agent/use-agent-thread-runs.js +20 -6
  32. package/dist/agent-chat/composer.d.ts +6 -2
  33. package/dist/agent-chat/composer.d.ts.map +1 -1
  34. package/dist/agent-chat/composer.js +49 -33
  35. package/dist/agent-chat/index.d.ts +1 -1
  36. package/dist/agent-chat/index.d.ts.map +1 -1
  37. package/dist/agent-chat/relative-time.d.ts +0 -9
  38. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  39. package/dist/agent-chat/relative-time.js +2 -1
  40. package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
  41. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-detail-panel.js +9 -6
  43. package/dist/agent-chat/thread-feed-row.d.ts +32 -6
  44. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  45. package/dist/agent-chat/thread-feed-row.js +61 -26
  46. package/dist/agent-chat/thread-feed.d.ts +13 -2
  47. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  48. package/dist/agent-chat/thread-feed.js +16 -7
  49. package/dist/agent-chat/thread-message.d.ts +4 -3
  50. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  51. package/dist/agent-chat/thread-message.js +70 -31
  52. package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
  53. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  54. package/dist/agent-chat/thread-outcome-card.js +9 -2
  55. package/dist/agent-chat/thread-workspace.d.ts +7 -2
  56. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  57. package/dist/agent-chat/thread-workspace.js +62 -29
  58. package/dist/agent-chat/tool-chip.d.ts +3 -2
  59. package/dist/agent-chat/tool-chip.d.ts.map +1 -1
  60. package/dist/agent-chat/tool-chip.js +7 -2
  61. package/dist/agent-chat/virtualize.d.ts +39 -0
  62. package/dist/agent-chat/virtualize.d.ts.map +1 -1
  63. package/dist/agent-chat/virtualize.js +15 -0
  64. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  65. package/dist/charts/hooks/useBandPlot.js +15 -8
  66. package/dist/charts/index.d.ts +11 -10
  67. package/dist/charts/index.d.ts.map +1 -1
  68. package/dist/charts/index.js +53 -9
  69. package/dist/charts/kinds/BandStrip.d.ts +10 -7
  70. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  71. package/dist/charts/kinds/BandStrip.js +12 -7
  72. package/dist/charts/kinds/Bars.d.ts +13 -9
  73. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  74. package/dist/charts/kinds/Bars.js +37 -11
  75. package/dist/charts/kinds/Donut.d.ts +6 -5
  76. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  77. package/dist/charts/kinds/Donut.js +19 -2
  78. package/dist/charts/kinds/DualPanel.d.ts +9 -6
  79. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  80. package/dist/charts/kinds/DualPanel.js +37 -9
  81. package/dist/charts/kinds/Heatmap.d.ts +10 -6
  82. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  83. package/dist/charts/kinds/Heatmap.js +58 -31
  84. package/dist/charts/kinds/MirroredBars.d.ts +6 -5
  85. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  86. package/dist/charts/kinds/MirroredBars.js +9 -2
  87. package/dist/charts/kinds/MultiLine.d.ts +11 -6
  88. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  89. package/dist/charts/kinds/MultiLine.js +24 -15
  90. package/dist/charts/kinds/StackedArea.d.ts +18 -8
  91. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  92. package/dist/charts/kinds/StackedArea.js +29 -4
  93. package/dist/charts/kinds/ZonedLine.d.ts +13 -11
  94. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  95. package/dist/charts/kinds/ZonedLine.js +33 -23
  96. package/dist/charts/layout/auto-margin.d.ts +65 -8
  97. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  98. package/dist/charts/layout/auto-margin.js +45 -10
  99. package/dist/charts/layout/log-ticks.d.ts +27 -0
  100. package/dist/charts/layout/log-ticks.d.ts.map +1 -0
  101. package/dist/charts/layout/log-ticks.js +42 -0
  102. package/dist/charts/primitives/Axes.d.ts +32 -4
  103. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  104. package/dist/charts/primitives/Axes.js +49 -7
  105. package/dist/charts/primitives/CartesianChart.d.ts +45 -7
  106. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  107. package/dist/charts/primitives/CartesianChart.js +86 -28
  108. package/dist/charts/primitives/ChartCard.d.ts +45 -4
  109. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  110. package/dist/charts/primitives/ChartCard.js +70 -21
  111. package/dist/charts/primitives/ChartFrame.d.ts +30 -4
  112. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  113. package/dist/charts/primitives/ChartFrame.js +52 -15
  114. package/dist/charts/primitives/ChartLegend.d.ts +13 -2
  115. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  116. package/dist/charts/primitives/ChartLegend.js +33 -13
  117. package/dist/charts/primitives/ChartPending.d.ts +85 -6
  118. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  119. package/dist/charts/primitives/ChartPending.js +100 -6
  120. package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
  121. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  122. package/dist/charts/primitives/ChartTooltip.js +18 -5
  123. package/dist/charts/primitives/Crosshair.d.ts +3 -0
  124. package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
  125. package/dist/charts/primitives/Crosshair.js +3 -1
  126. package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
  127. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
  128. package/dist/charts/primitives/chart-frame-layout.js +89 -0
  129. package/dist/charts/primitives/chart-tier.d.ts +16 -0
  130. package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
  131. package/dist/charts/primitives/chart-tier.js +20 -0
  132. package/dist/charts/series.d.ts +55 -1
  133. package/dist/charts/series.d.ts.map +1 -1
  134. package/dist/charts/series.js +27 -1
  135. package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
  136. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  137. package/dist/charts/sparklines/BarSparkline.js +9 -3
  138. package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
  139. package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
  140. package/dist/charts/sparklines/LineSparkline.js +15 -3
  141. package/dist/charts/sparklines/index.d.ts +2 -2
  142. package/dist/charts/sparklines/index.d.ts.map +1 -1
  143. package/dist/charts/utils/format.d.ts +7 -4
  144. package/dist/charts/utils/format.d.ts.map +1 -1
  145. package/dist/charts/utils/format.js +17 -24
  146. package/dist/charts/utils/ticks.d.ts +81 -2
  147. package/dist/charts/utils/ticks.d.ts.map +1 -1
  148. package/dist/charts/utils/ticks.js +54 -4
  149. package/dist/cli/audit-allows.d.ts +24 -0
  150. package/dist/cli/audit-allows.d.ts.map +1 -0
  151. package/dist/cli/audit-allows.js +240 -0
  152. package/dist/cli/check-theme.d.ts +10 -0
  153. package/dist/cli/check-theme.d.ts.map +1 -0
  154. package/dist/cli/check-theme.js +139 -0
  155. package/dist/cli/doctor.d.ts +47 -0
  156. package/dist/cli/doctor.d.ts.map +1 -0
  157. package/dist/cli/doctor.js +197 -0
  158. package/dist/cli/guard-hook.d.ts +9 -0
  159. package/dist/cli/guard-hook.d.ts.map +1 -0
  160. package/dist/cli/guard-hook.js +93 -0
  161. package/dist/cli/help.d.ts +9 -0
  162. package/dist/cli/help.d.ts.map +1 -0
  163. package/dist/cli/help.js +53 -0
  164. package/dist/cli/index.d.ts +360 -270
  165. package/dist/cli/index.d.ts.map +1 -1
  166. package/dist/cli/index.js +85 -1901
  167. package/dist/cli/init.d.ts +4 -0
  168. package/dist/cli/init.d.ts.map +1 -0
  169. package/dist/cli/init.js +165 -0
  170. package/dist/cli/sync.d.ts +43 -0
  171. package/dist/cli/sync.d.ts.map +1 -0
  172. package/dist/cli/sync.js +206 -0
  173. package/dist/cli/tokens-css.d.ts +51 -0
  174. package/dist/cli/tokens-css.d.ts.map +1 -0
  175. package/dist/cli/tokens-css.js +165 -0
  176. package/dist/commands/ShortcutsHelp.d.ts +3 -2
  177. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  178. package/dist/commands/ShortcutsHelp.js +8 -2
  179. package/dist/commands/define-commands.d.ts.map +1 -1
  180. package/dist/commands/define-commands.js +2 -2
  181. package/dist/commands/define-overlays.d.ts +102 -0
  182. package/dist/commands/define-overlays.d.ts.map +1 -1
  183. package/dist/commands/define-overlays.js +102 -2
  184. package/dist/commands/index.d.ts +12 -2
  185. package/dist/commands/index.d.ts.map +1 -1
  186. package/dist/commands/index.js +9 -1
  187. package/dist/commands/overlays-mount.d.ts +16 -11
  188. package/dist/commands/overlays-mount.d.ts.map +1 -1
  189. package/dist/commands/overlays-mount.js +23 -14
  190. package/dist/commands/shell-bridge.d.ts +57 -0
  191. package/dist/commands/shell-bridge.d.ts.map +1 -0
  192. package/dist/commands/shell-bridge.js +32 -0
  193. package/dist/common/errors.d.ts +69 -0
  194. package/dist/common/errors.d.ts.map +1 -0
  195. package/dist/common/errors.js +64 -0
  196. package/dist/common/index.d.ts +17 -0
  197. package/dist/common/index.d.ts.map +1 -0
  198. package/dist/common/index.js +35 -0
  199. package/dist/common/is-dev.d.ts +3 -0
  200. package/dist/common/is-dev.d.ts.map +1 -0
  201. package/dist/common/is-dev.js +6 -0
  202. package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
  203. package/dist/common/motion.d.ts.map +1 -0
  204. package/dist/common/props.d.ts +63 -0
  205. package/dist/common/props.d.ts.map +1 -0
  206. package/dist/common/props.js +6 -0
  207. package/dist/common/query-state-like.d.ts +43 -0
  208. package/dist/common/query-state-like.d.ts.map +1 -0
  209. package/dist/common/query-state-like.js +20 -0
  210. package/dist/common/refs.d.ts +29 -0
  211. package/dist/common/refs.d.ts.map +1 -0
  212. package/dist/common/refs.js +29 -0
  213. package/dist/common/scroll-parent.d.ts +23 -0
  214. package/dist/common/scroll-parent.d.ts.map +1 -0
  215. package/dist/common/scroll-parent.js +21 -0
  216. package/dist/common/validate.d.ts +38 -0
  217. package/dist/common/validate.d.ts.map +1 -0
  218. package/dist/common/validate.js +36 -0
  219. package/dist/content/article-layout.module.css +11 -4
  220. package/dist/content/callout.d.ts +8 -4
  221. package/dist/content/callout.d.ts.map +1 -1
  222. package/dist/content/callout.js +15 -8
  223. package/dist/content/guide.d.ts +5 -4
  224. package/dist/content/guide.d.ts.map +1 -1
  225. package/dist/content/guide.js +13 -4
  226. package/dist/content/heading-anchor.d.ts +24 -2
  227. package/dist/content/heading-anchor.d.ts.map +1 -1
  228. package/dist/content/heading-anchor.js +23 -9
  229. package/dist/content/index.d.ts.map +1 -1
  230. package/dist/content/markdown.d.ts +3 -4
  231. package/dist/content/markdown.d.ts.map +1 -1
  232. package/dist/content/markdown.js +3 -4
  233. package/dist/content/prose.module.css +9 -7
  234. package/dist/content/reading-progress.d.ts +10 -4
  235. package/dist/content/reading-progress.d.ts.map +1 -1
  236. package/dist/content/reading-progress.js +26 -10
  237. package/dist/content/slug.d.ts +4 -0
  238. package/dist/content/slug.d.ts.map +1 -1
  239. package/dist/content/toc.d.ts +11 -0
  240. package/dist/content/toc.d.ts.map +1 -1
  241. package/dist/content/toc.js +23 -10
  242. package/dist/controls/actions.d.ts +50 -7
  243. package/dist/controls/actions.d.ts.map +1 -1
  244. package/dist/controls/actions.js +39 -8
  245. package/dist/controls/compare-filter.d.ts +3 -2
  246. package/dist/controls/compare-filter.d.ts.map +1 -1
  247. package/dist/controls/compare-filter.js +17 -2
  248. package/dist/controls/control-group.d.ts +3 -3
  249. package/dist/controls/control-group.d.ts.map +1 -1
  250. package/dist/controls/control-group.js +9 -2
  251. package/dist/controls/controls.module.css +169 -186
  252. package/dist/controls/controls.module.css.d.ts +9 -11
  253. package/dist/controls/enum-filter.d.ts +3 -2
  254. package/dist/controls/enum-filter.d.ts.map +1 -1
  255. package/dist/controls/enum-filter.js +28 -13
  256. package/dist/controls/filter-context.d.ts +8 -4
  257. package/dist/controls/filter-context.d.ts.map +1 -1
  258. package/dist/controls/filter-pill.d.ts +3 -3
  259. package/dist/controls/filter-pill.d.ts.map +1 -1
  260. package/dist/controls/filter-pill.js +4 -1
  261. package/dist/controls/filter-set.d.ts +3 -2
  262. package/dist/controls/filter-set.d.ts.map +1 -1
  263. package/dist/controls/filter-set.js +66 -41
  264. package/dist/controls/filter-sheet.d.ts +20 -80
  265. package/dist/controls/filter-sheet.d.ts.map +1 -1
  266. package/dist/controls/filter-sheet.js +9 -95
  267. package/dist/controls/index.d.ts +2 -0
  268. package/dist/controls/index.d.ts.map +1 -1
  269. package/dist/controls/index.js +4 -0
  270. package/dist/controls/multi-select-filter.d.ts +18 -2
  271. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  272. package/dist/controls/multi-select-filter.js +122 -20
  273. package/dist/controls/number-filter.d.ts +7 -5
  274. package/dist/controls/number-filter.d.ts.map +1 -1
  275. package/dist/controls/number-filter.js +40 -11
  276. package/dist/controls/panel-row.d.ts +132 -0
  277. package/dist/controls/panel-row.d.ts.map +1 -0
  278. package/dist/controls/panel-row.js +173 -0
  279. package/dist/controls/panel-row.module.css +86 -0
  280. package/dist/controls/panel-row.module.css.d.ts +13 -0
  281. package/dist/controls/range-filter.d.ts +2 -1
  282. package/dist/controls/range-filter.d.ts.map +1 -1
  283. package/dist/controls/range-filter.js +57 -35
  284. package/dist/controls/search-filter.d.ts +3 -2
  285. package/dist/controls/search-filter.d.ts.map +1 -1
  286. package/dist/controls/search-filter.js +25 -10
  287. package/dist/controls/select-filter.d.ts +2 -1
  288. package/dist/controls/select-filter.d.ts.map +1 -1
  289. package/dist/controls/select-filter.js +4 -0
  290. package/dist/controls/slider-control.d.ts +36 -0
  291. package/dist/controls/slider-control.d.ts.map +1 -0
  292. package/dist/controls/slider-control.js +64 -0
  293. package/dist/controls/sync-button.d.ts +3 -2
  294. package/dist/controls/sync-button.d.ts.map +1 -1
  295. package/dist/controls/sync-button.js +16 -3
  296. package/dist/controls/toggle-filter.d.ts +3 -2
  297. package/dist/controls/toggle-filter.d.ts.map +1 -1
  298. package/dist/controls/toggle-filter.js +28 -11
  299. package/dist/controls/view-tabs.d.ts +3 -2
  300. package/dist/controls/view-tabs.d.ts.map +1 -1
  301. package/dist/controls/view-tabs.js +43 -11
  302. package/dist/controls-dates/date-range-picker.d.ts +1 -1
  303. package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
  304. package/dist/controls-dates/date-range-picker.js +6 -1
  305. package/dist/dashboard/delta-badge.d.ts +18 -6
  306. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  307. package/dist/dashboard/delta-badge.js +33 -1
  308. package/dist/dashboard/empty-state.d.ts +8 -4
  309. package/dist/dashboard/empty-state.d.ts.map +1 -1
  310. package/dist/dashboard/empty-state.js +36 -6
  311. package/dist/dashboard/index.d.ts +9 -5
  312. package/dist/dashboard/index.d.ts.map +1 -1
  313. package/dist/dashboard/index.js +13 -1
  314. package/dist/dashboard/query-state.d.ts +39 -27
  315. package/dist/dashboard/query-state.d.ts.map +1 -1
  316. package/dist/dashboard/query-state.js +109 -58
  317. package/dist/dashboard/section.d.ts +58 -0
  318. package/dist/dashboard/section.d.ts.map +1 -0
  319. package/dist/dashboard/section.js +118 -0
  320. package/dist/{section → dashboard}/section.module.css +11 -0
  321. package/dist/dashboard/settings-section.d.ts +19 -9
  322. package/dist/dashboard/settings-section.d.ts.map +1 -1
  323. package/dist/dashboard/settings-section.js +75 -26
  324. package/dist/dashboard/stat-card.d.ts +44 -38
  325. package/dist/dashboard/stat-card.d.ts.map +1 -1
  326. package/dist/dashboard/stat-card.js +56 -43
  327. package/dist/dashboard/stat-card.module.css +46 -31
  328. package/dist/dashboard/stat-card.module.css.d.ts +1 -0
  329. package/dist/dashboard/stat-group.d.ts +41 -0
  330. package/dist/dashboard/stat-group.d.ts.map +1 -0
  331. package/dist/dashboard/stat-group.js +30 -0
  332. package/dist/dashboard/stat-group.module.css +66 -0
  333. package/dist/dashboard/stat-group.module.css.d.ts +8 -0
  334. package/dist/dashboard/widget-grid.d.ts +27 -0
  335. package/dist/dashboard/widget-grid.d.ts.map +1 -0
  336. package/dist/dashboard/widget-grid.js +41 -0
  337. package/dist/dashboard/widget-grid.module.css +53 -0
  338. package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
  339. package/dist/dashboard/widget-header.d.ts +132 -0
  340. package/dist/dashboard/widget-header.d.ts.map +1 -0
  341. package/dist/{widget-header → dashboard}/widget-header.js +42 -27
  342. package/dist/{widget-header → dashboard}/widget-header.module.css +72 -7
  343. package/dist/data/data-table.d.ts +151 -12
  344. package/dist/data/data-table.d.ts.map +1 -1
  345. package/dist/data/data-table.js +385 -139
  346. package/dist/data/data-table.module.css +64 -0
  347. package/dist/data/data-table.module.css.d.ts +11 -0
  348. package/dist/data/query-branch.d.ts +28 -0
  349. package/dist/data/query-branch.d.ts.map +1 -0
  350. package/dist/data/query-branch.js +10 -0
  351. package/dist/data/table.d.ts +2 -2
  352. package/dist/data/table.d.ts.map +1 -1
  353. package/dist/data/table.js +2 -16
  354. package/dist/data/virtual-list.d.ts +65 -6
  355. package/dist/data/virtual-list.d.ts.map +1 -1
  356. package/dist/data/virtual-list.js +89 -38
  357. package/dist/data/virtual.d.ts +4 -4
  358. package/dist/data/virtual.d.ts.map +1 -1
  359. package/dist/dev-dock.d.ts +28 -0
  360. package/dist/dev-dock.d.ts.map +1 -0
  361. package/dist/dev-dock.js +85 -0
  362. package/dist/format/date.d.ts +62 -0
  363. package/dist/format/date.d.ts.map +1 -0
  364. package/dist/format/date.js +106 -0
  365. package/dist/format/index.d.ts +39 -0
  366. package/dist/format/index.d.ts.map +1 -0
  367. package/dist/format/index.js +54 -0
  368. package/dist/format/measure.d.ts +15 -0
  369. package/dist/format/measure.d.ts.map +1 -0
  370. package/dist/format/measure.js +16 -0
  371. package/dist/format/number.d.ts +69 -0
  372. package/dist/format/number.d.ts.map +1 -0
  373. package/dist/format/number.js +62 -0
  374. package/dist/format/shared.d.ts +27 -0
  375. package/dist/format/shared.d.ts.map +1 -0
  376. package/dist/format/shared.js +29 -0
  377. package/dist/forms/FormErrorSummary.d.ts +4 -3
  378. package/dist/forms/FormErrorSummary.d.ts.map +1 -1
  379. package/dist/forms/FormErrorSummary.js +6 -4
  380. package/dist/forms/create-form.d.ts +2 -2
  381. package/dist/forms/field.d.ts +33 -9
  382. package/dist/forms/field.d.ts.map +1 -1
  383. package/dist/forms/field.js +7 -6
  384. package/dist/forms/form-layout.d.ts +142 -0
  385. package/dist/forms/form-layout.d.ts.map +1 -0
  386. package/dist/forms/form-layout.js +110 -0
  387. package/dist/forms/form-layout.module.css +145 -0
  388. package/dist/forms/form-layout.module.css.d.ts +20 -0
  389. package/dist/forms/form-state.d.ts +42 -0
  390. package/dist/forms/form-state.d.ts.map +1 -0
  391. package/dist/forms/form-state.js +26 -0
  392. package/dist/forms/index.d.ts +22 -6
  393. package/dist/forms/index.d.ts.map +1 -1
  394. package/dist/forms/index.js +19 -4
  395. package/dist/forms/use-field-array.d.ts +60 -0
  396. package/dist/forms/use-field-array.d.ts.map +1 -0
  397. package/dist/forms/use-field-array.js +20 -0
  398. package/dist/forms/use-form-draft.d.ts +25 -10
  399. package/dist/forms/use-form-draft.d.ts.map +1 -1
  400. package/dist/forms/use-form-draft.js +55 -18
  401. package/dist/forms/use-form-submit.d.ts +88 -0
  402. package/dist/forms/use-form-submit.d.ts.map +1 -0
  403. package/dist/forms/use-form-submit.js +63 -0
  404. package/dist/guard/index.d.ts +2 -9
  405. package/dist/guard/index.d.ts.map +1 -1
  406. package/dist/guard/index.js +38 -28
  407. package/dist/guard/types.d.ts +8 -10
  408. package/dist/guard/types.d.ts.map +1 -1
  409. package/dist/index.d.ts +21 -14
  410. package/dist/index.d.ts.map +1 -1
  411. package/dist/index.js +40 -6
  412. package/dist/notifications/bell.d.ts +3 -2
  413. package/dist/notifications/bell.d.ts.map +1 -1
  414. package/dist/notifications/bell.js +73 -56
  415. package/dist/notifications/center.d.ts +15 -4
  416. package/dist/notifications/center.d.ts.map +1 -1
  417. package/dist/notifications/center.js +5 -2
  418. package/dist/notifications/define-notifications.d.ts +56 -14
  419. package/dist/notifications/define-notifications.d.ts.map +1 -1
  420. package/dist/notifications/define-notifications.js +2 -1
  421. package/dist/notifications/index.d.ts +11 -7
  422. package/dist/notifications/index.d.ts.map +1 -1
  423. package/dist/notifications/index.js +5 -3
  424. package/dist/notifications/mount-guard.d.ts +8 -0
  425. package/dist/notifications/mount-guard.d.ts.map +1 -0
  426. package/dist/notifications/mount-guard.js +19 -0
  427. package/dist/notifications/notify.d.ts +58 -1
  428. package/dist/notifications/notify.d.ts.map +1 -1
  429. package/dist/notifications/notify.js +46 -2
  430. package/dist/notifications/store.d.ts +7 -0
  431. package/dist/notifications/store.d.ts.map +1 -1
  432. package/dist/provider/connectivity-indicator.d.ts +13 -0
  433. package/dist/provider/connectivity-indicator.d.ts.map +1 -0
  434. package/dist/{connectivity → provider}/connectivity-indicator.js +3 -1
  435. package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
  436. package/dist/provider/connectivity-provider.d.ts.map +1 -0
  437. package/dist/{connectivity → provider}/connectivity-provider.js +4 -1
  438. package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +3 -3
  439. package/dist/provider/connectivity-types.d.ts.map +1 -0
  440. package/dist/provider/error-fallback.module.css +21 -0
  441. package/dist/provider/error-fallback.module.css.d.ts +9 -0
  442. package/dist/provider/index.d.ts +29 -13
  443. package/dist/provider/index.d.ts.map +1 -1
  444. package/dist/provider/index.js +82 -34
  445. package/dist/provider/lab-theme.d.ts.map +1 -1
  446. package/dist/provider/lab-theme.js +2 -1
  447. package/dist/provider/use-connectivity.d.ts +3 -0
  448. package/dist/provider/use-connectivity.d.ts.map +1 -0
  449. package/dist/query-client.d.ts +63 -0
  450. package/dist/query-client.d.ts.map +1 -0
  451. package/dist/query-client.js +33 -0
  452. package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
  453. package/dist/query-devtools.d.ts.map +1 -0
  454. package/dist/{query/devtools.js → query-devtools.js} +1 -0
  455. package/dist/register.d.ts +17 -3
  456. package/dist/register.d.ts.map +1 -1
  457. package/dist/router-tanstack/index.d.ts +4 -7
  458. package/dist/router-tanstack/index.d.ts.map +1 -1
  459. package/dist/router-tanstack/index.js +1 -9
  460. package/dist/router-tanstack/nav.d.ts +1 -1
  461. package/dist/router-tanstack/nav.d.ts.map +1 -1
  462. package/dist/router-tanstack/search-store.d.ts +3 -6
  463. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  464. package/dist/router-tanstack/search-store.js +1 -1
  465. package/dist/router-tanstack/use-nav.d.ts +1 -1
  466. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  467. package/dist/router-tanstack/use-nav.js +1 -1
  468. package/dist/shell/account-types.d.ts +5 -2
  469. package/dist/shell/account-types.d.ts.map +1 -1
  470. package/dist/shell/app-brand.d.ts +13 -0
  471. package/dist/shell/app-brand.d.ts.map +1 -0
  472. package/dist/shell/app-brand.js +94 -0
  473. package/dist/shell/app-brand.module.css +109 -0
  474. package/dist/shell/app-brand.module.css.d.ts +14 -0
  475. package/dist/shell/app-breadcrumbs.d.ts +5 -3
  476. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  477. package/dist/shell/app-breadcrumbs.js +33 -22
  478. package/dist/shell/app-header.module.css +10 -5
  479. package/dist/shell/app-main.module.css +118 -0
  480. package/dist/shell/app-main.module.css.d.ts +11 -0
  481. package/dist/shell/app-mobile-nav.d.ts +4 -3
  482. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  483. package/dist/shell/app-mobile-nav.js +53 -17
  484. package/dist/shell/app-mobile-nav.module.css +48 -27
  485. package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
  486. package/dist/shell/app-sidebar-account.d.ts +2 -1
  487. package/dist/shell/app-sidebar-account.d.ts.map +1 -1
  488. package/dist/shell/app-sidebar-account.js +57 -22
  489. package/dist/shell/app-sidebar.d.ts +10 -4
  490. package/dist/shell/app-sidebar.d.ts.map +1 -1
  491. package/dist/shell/app-sidebar.js +78 -103
  492. package/dist/shell/app-sidebar.module.css +63 -96
  493. package/dist/shell/app-sidebar.module.css.d.ts +0 -5
  494. package/dist/shell/index.d.ts +24 -10
  495. package/dist/shell/index.d.ts.map +1 -1
  496. package/dist/shell/index.js +60 -29
  497. package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
  498. package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
  499. package/dist/shell/isomorphic-layout-effect.js +5 -0
  500. package/dist/shell/mobile-nav-model.d.ts +1 -1
  501. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  502. package/dist/shell/mobile-nav-model.js +27 -6
  503. package/dist/shell/nav-count-badge.d.ts +5 -3
  504. package/dist/shell/nav-count-badge.d.ts.map +1 -1
  505. package/dist/shell/nav-count-badge.js +3 -1
  506. package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +16 -7
  507. package/dist/shell/nav-types.d.ts.map +1 -0
  508. package/dist/shell/page-aside.d.ts +43 -0
  509. package/dist/shell/page-aside.d.ts.map +1 -0
  510. package/dist/shell/page-aside.js +175 -0
  511. package/dist/shell/page-aside.module.css +174 -0
  512. package/dist/shell/page-aside.module.css.d.ts +14 -0
  513. package/dist/shell/page-bar.d.ts +55 -15
  514. package/dist/shell/page-bar.d.ts.map +1 -1
  515. package/dist/shell/page-bar.js +130 -41
  516. package/dist/shell/page-bar.module.css +107 -13
  517. package/dist/shell/page-bar.module.css.d.ts +4 -1
  518. package/dist/shell/page-title.d.ts +14 -0
  519. package/dist/shell/page-title.d.ts.map +1 -0
  520. package/dist/shell/page-title.js +25 -0
  521. package/dist/shell/page-title.module.css +33 -0
  522. package/dist/shell/page-title.module.css.d.ts +11 -0
  523. package/dist/shell/sidebar-block-model.d.ts +1 -1
  524. package/dist/shell/sidebar-block-model.d.ts.map +1 -1
  525. package/dist/shell/sidebar-blocks.d.ts +1 -1
  526. package/dist/shell/sidebar-blocks.d.ts.map +1 -1
  527. package/dist/shell/sidebar-search.d.ts +4 -2
  528. package/dist/shell/sidebar-search.d.ts.map +1 -1
  529. package/dist/shell/sidebar-search.js +9 -4
  530. package/dist/shell/sidebar-search.module.css +1 -1
  531. package/dist/shell/use-breakpoint.d.ts +11 -0
  532. package/dist/shell/use-breakpoint.d.ts.map +1 -0
  533. package/dist/shell/use-breakpoint.js +33 -0
  534. package/dist/state/fields.d.ts +26 -22
  535. package/dist/state/fields.d.ts.map +1 -1
  536. package/dist/state/fields.js +19 -18
  537. package/dist/state/persisted-or-local.d.ts +9 -0
  538. package/dist/state/persisted-or-local.d.ts.map +1 -0
  539. package/dist/state/persisted-or-local.js +24 -0
  540. package/dist/state.d.ts +1 -1
  541. package/dist/state.d.ts.map +1 -1
  542. package/dist/state.js +2 -6
  543. package/dist/styles.css +36 -0
  544. package/dist/surfaces.d.ts +25 -47
  545. package/dist/surfaces.d.ts.map +1 -1
  546. package/dist/surfaces.js +72 -55
  547. package/dist/theme/ctl-theme.d.ts.map +1 -1
  548. package/dist/theme/index.d.ts.map +1 -1
  549. package/dist/theme/index.js +102 -17
  550. package/dist/theme/nav-link.module.css +37 -26
  551. package/dist/theme/segmented-control.module.css +22 -0
  552. package/dist/theme/segmented-control.module.css.d.ts +1 -0
  553. package/dist/theme/theme-toggle.d.ts +9 -0
  554. package/dist/theme/theme-toggle.d.ts.map +1 -0
  555. package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +9 -3
  556. package/dist/theme-lab/boot.d.ts +66 -0
  557. package/dist/theme-lab/boot.d.ts.map +1 -0
  558. package/dist/theme-lab/boot.js +82 -0
  559. package/dist/theme-lab/index.d.ts +4 -43
  560. package/dist/theme-lab/index.d.ts.map +1 -1
  561. package/dist/theme-lab/index.js +9 -69
  562. package/dist/tokens/index.d.ts +43 -0
  563. package/dist/tokens/index.d.ts.map +1 -1
  564. package/dist/tokens/index.js +45 -6
  565. package/dist/tokens/palette.d.ts +57 -25
  566. package/dist/tokens/palette.d.ts.map +1 -1
  567. package/dist/tokens/palette.js +44 -15
  568. package/dist/tokens.css +5 -5
  569. package/llms.txt +11 -26
  570. package/package.json +54 -34
  571. package/src/agent/ai-sdk-transport.ts +11 -4
  572. package/src/agent/coalesce.ts +2 -1
  573. package/src/agent/merge.ts +2 -1
  574. package/src/agent/part-list.tsx +2 -0
  575. package/src/agent/stick-to-bottom.tsx +9 -11
  576. package/src/agent/transport.ts +15 -2
  577. package/src/agent/use-agent-thread-runs.ts +65 -15
  578. package/src/agent-chat/composer.tsx +124 -103
  579. package/src/agent-chat/index.ts +6 -4
  580. package/src/agent-chat/relative-time.ts +2 -1
  581. package/src/agent-chat/thread-detail-panel.tsx +10 -6
  582. package/src/agent-chat/thread-feed-row.tsx +117 -34
  583. package/src/agent-chat/thread-feed.tsx +59 -27
  584. package/src/agent-chat/thread-message.tsx +48 -13
  585. package/src/agent-chat/thread-outcome-card.tsx +9 -1
  586. package/src/agent-chat/thread-workspace.tsx +72 -33
  587. package/src/agent-chat/tool-chip.tsx +8 -2
  588. package/src/agent-chat/virtualize.ts +48 -0
  589. package/src/charts/hooks/useBandPlot.ts +26 -8
  590. package/src/charts/index.ts +46 -14
  591. package/src/charts/kinds/BandStrip.tsx +36 -14
  592. package/src/charts/kinds/Bars.tsx +68 -20
  593. package/src/charts/kinds/Donut.tsx +30 -3
  594. package/src/charts/kinds/DualPanel.tsx +60 -13
  595. package/src/charts/kinds/Heatmap.tsx +90 -33
  596. package/src/charts/kinds/MirroredBars.tsx +21 -3
  597. package/src/charts/kinds/MultiLine.tsx +61 -18
  598. package/src/charts/kinds/StackedArea.tsx +77 -6
  599. package/src/charts/kinds/ZonedLine.tsx +72 -35
  600. package/src/charts/layout/auto-margin.ts +134 -21
  601. package/src/charts/layout/log-ticks.ts +90 -0
  602. package/src/charts/primitives/Axes.tsx +82 -5
  603. package/src/charts/primitives/CartesianChart.tsx +194 -41
  604. package/src/charts/primitives/ChartCard.tsx +111 -27
  605. package/src/charts/primitives/ChartFrame.tsx +111 -26
  606. package/src/charts/primitives/ChartLegend.tsx +43 -13
  607. package/src/charts/primitives/ChartPending.tsx +194 -10
  608. package/src/charts/primitives/ChartTooltip.tsx +37 -5
  609. package/src/charts/primitives/Crosshair.tsx +6 -3
  610. package/src/charts/primitives/chart-frame-layout.ts +212 -0
  611. package/src/charts/primitives/chart-tier.tsx +37 -0
  612. package/src/charts/series.ts +77 -1
  613. package/src/charts/sparklines/BarSparkline.tsx +11 -3
  614. package/src/charts/sparklines/LineSparkline.tsx +18 -4
  615. package/src/charts/sparklines/index.ts +2 -2
  616. package/src/charts/utils/format.ts +19 -27
  617. package/src/charts/utils/ticks.ts +149 -4
  618. package/src/cli/audit-allows.ts +371 -0
  619. package/src/cli/check-theme.ts +197 -0
  620. package/src/cli/doctor.ts +331 -0
  621. package/src/cli/guard-hook.ts +115 -0
  622. package/src/cli/help.ts +101 -0
  623. package/src/cli/index.ts +269 -3434
  624. package/src/cli/init.ts +247 -0
  625. package/src/cli/sync.ts +324 -0
  626. package/src/cli/tokens-css.ts +252 -0
  627. package/src/commands/ShortcutsHelp.tsx +10 -3
  628. package/src/commands/define-commands.ts +2 -2
  629. package/src/commands/define-overlays.ts +216 -1
  630. package/src/commands/index.ts +22 -1
  631. package/src/commands/overlays-mount.tsx +63 -31
  632. package/src/commands/shell-bridge.ts +86 -0
  633. package/src/common/errors.ts +143 -0
  634. package/src/common/index.ts +23 -0
  635. package/src/common/is-dev.ts +4 -0
  636. package/src/common/props.ts +71 -0
  637. package/src/common/query-state-like.ts +64 -0
  638. package/src/common/refs.ts +58 -0
  639. package/src/common/scroll-parent.ts +42 -0
  640. package/src/common/validate.ts +94 -0
  641. package/src/content/article-layout.module.css +11 -4
  642. package/src/content/callout.tsx +13 -7
  643. package/src/content/guide.tsx +14 -4
  644. package/src/content/heading-anchor.tsx +14 -3
  645. package/src/content/index.ts +2 -0
  646. package/src/content/markdown.tsx +6 -8
  647. package/src/content/prose.module.css +9 -7
  648. package/src/content/reading-progress.tsx +40 -15
  649. package/src/content/slug.ts +4 -0
  650. package/src/content/toc.tsx +47 -10
  651. package/src/controls/actions.tsx +90 -10
  652. package/src/controls/compare-filter.tsx +20 -3
  653. package/src/controls/control-group.tsx +11 -4
  654. package/src/controls/controls.module.css +169 -186
  655. package/src/controls/controls.module.css.d.ts +9 -11
  656. package/src/controls/enum-filter.tsx +32 -16
  657. package/src/controls/filter-context.tsx +14 -6
  658. package/src/controls/filter-pill.tsx +6 -3
  659. package/src/controls/filter-set.tsx +32 -13
  660. package/src/controls/filter-sheet.tsx +44 -194
  661. package/src/controls/index.ts +7 -0
  662. package/src/controls/multi-select-filter.tsx +177 -27
  663. package/src/controls/number-filter.tsx +61 -23
  664. package/src/controls/panel-row.module.css +86 -0
  665. package/src/controls/panel-row.module.css.d.ts +13 -0
  666. package/src/controls/panel-row.tsx +392 -0
  667. package/src/controls/range-filter.tsx +76 -39
  668. package/src/controls/search-filter.tsx +30 -12
  669. package/src/controls/select-filter.tsx +11 -1
  670. package/src/controls/slider-control.tsx +159 -0
  671. package/src/controls/sync-button.tsx +13 -3
  672. package/src/controls/toggle-filter.tsx +36 -16
  673. package/src/controls/view-tabs.tsx +63 -11
  674. package/src/controls-dates/date-range-picker.tsx +8 -1
  675. package/src/dashboard/delta-badge.tsx +75 -4
  676. package/src/dashboard/empty-state.tsx +67 -24
  677. package/src/dashboard/index.ts +24 -3
  678. package/src/dashboard/query-state.tsx +142 -85
  679. package/src/{section → dashboard}/section.module.css +11 -0
  680. package/src/dashboard/section.tsx +287 -0
  681. package/src/dashboard/settings-section.tsx +88 -32
  682. package/src/dashboard/stat-card.module.css +46 -31
  683. package/src/dashboard/stat-card.module.css.d.ts +1 -0
  684. package/src/dashboard/stat-card.tsx +186 -128
  685. package/src/dashboard/stat-group.module.css +66 -0
  686. package/src/dashboard/stat-group.module.css.d.ts +8 -0
  687. package/src/dashboard/stat-group.tsx +74 -0
  688. package/src/dashboard/widget-grid.module.css +53 -0
  689. package/src/dashboard/widget-grid.module.css.d.ts +9 -0
  690. package/src/dashboard/widget-grid.tsx +127 -0
  691. package/src/{widget-header → dashboard}/widget-header.module.css +72 -7
  692. package/src/dashboard/widget-header.tsx +292 -0
  693. package/src/data/data-table.module.css +64 -0
  694. package/src/data/data-table.module.css.d.ts +11 -0
  695. package/src/data/data-table.tsx +921 -348
  696. package/src/data/query-branch.ts +53 -0
  697. package/src/data/table.ts +9 -23
  698. package/src/data/virtual-list.tsx +190 -56
  699. package/src/data/virtual.ts +8 -4
  700. package/src/dev-dock.tsx +159 -0
  701. package/src/format/date.ts +184 -0
  702. package/src/format/index.ts +69 -0
  703. package/src/format/measure.ts +31 -0
  704. package/src/format/number.ts +129 -0
  705. package/src/format/shared.ts +63 -0
  706. package/src/forms/FormErrorSummary.tsx +13 -6
  707. package/src/forms/create-form.ts +2 -2
  708. package/src/forms/field.ts +42 -13
  709. package/src/forms/form-layout.module.css +145 -0
  710. package/src/forms/form-layout.module.css.d.ts +20 -0
  711. package/src/forms/form-layout.tsx +305 -0
  712. package/src/forms/form-state.tsx +94 -0
  713. package/src/forms/index.ts +37 -7
  714. package/src/forms/use-field-array.ts +94 -0
  715. package/src/forms/use-form-draft.ts +145 -53
  716. package/src/forms/use-form-submit.ts +199 -0
  717. package/src/guard/index.ts +99 -70
  718. package/src/guard/types.ts +7 -10
  719. package/src/index.ts +92 -19
  720. package/src/notifications/bell.tsx +18 -4
  721. package/src/notifications/center.tsx +18 -5
  722. package/src/notifications/define-notifications.ts +64 -18
  723. package/src/notifications/index.ts +17 -8
  724. package/src/notifications/mount-guard.ts +39 -0
  725. package/src/notifications/notify.tsx +122 -3
  726. package/src/notifications/store.ts +7 -0
  727. package/src/{connectivity → provider}/connectivity-indicator.tsx +7 -2
  728. package/src/{connectivity → provider}/connectivity-provider.tsx +5 -2
  729. package/src/{connectivity/types.ts → provider/connectivity-types.ts} +5 -2
  730. package/src/provider/error-fallback.module.css +21 -0
  731. package/src/provider/error-fallback.module.css.d.ts +9 -0
  732. package/src/provider/index.tsx +116 -28
  733. package/src/provider/lab-theme.ts +2 -1
  734. package/src/{connectivity → provider}/use-connectivity.ts +4 -3
  735. package/src/query-client.ts +100 -0
  736. package/src/{query/devtools.tsx → query-devtools.tsx} +6 -4
  737. package/src/register.ts +20 -4
  738. package/src/router-tanstack/index.ts +4 -15
  739. package/src/router-tanstack/nav.ts +1 -1
  740. package/src/router-tanstack/search-store.ts +6 -14
  741. package/src/router-tanstack/use-nav.ts +19 -14
  742. package/src/shell/account-types.ts +5 -2
  743. package/src/shell/app-brand.module.css +109 -0
  744. package/src/shell/app-brand.module.css.d.ts +14 -0
  745. package/src/shell/app-brand.tsx +140 -0
  746. package/src/shell/app-breadcrumbs.tsx +20 -10
  747. package/src/shell/app-header.module.css +10 -5
  748. package/src/shell/app-main.module.css +118 -0
  749. package/src/shell/app-main.module.css.d.ts +11 -0
  750. package/src/shell/app-mobile-nav.module.css +48 -27
  751. package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
  752. package/src/shell/app-mobile-nav.tsx +71 -12
  753. package/src/shell/app-sidebar-account.tsx +29 -6
  754. package/src/shell/app-sidebar.module.css +63 -96
  755. package/src/shell/app-sidebar.module.css.d.ts +0 -5
  756. package/src/shell/app-sidebar.tsx +61 -105
  757. package/src/shell/index.tsx +179 -93
  758. package/src/shell/isomorphic-layout-effect.ts +14 -0
  759. package/src/shell/mobile-nav-model.ts +67 -11
  760. package/src/shell/nav-count-badge.tsx +6 -1
  761. package/src/{nav/types.ts → shell/nav-types.ts} +15 -6
  762. package/src/shell/page-aside.module.css +174 -0
  763. package/src/shell/page-aside.module.css.d.ts +14 -0
  764. package/src/shell/page-aside.tsx +327 -0
  765. package/src/shell/page-bar.module.css +107 -13
  766. package/src/shell/page-bar.module.css.d.ts +4 -1
  767. package/src/shell/page-bar.tsx +294 -94
  768. package/src/shell/page-title.module.css +33 -0
  769. package/src/shell/page-title.module.css.d.ts +11 -0
  770. package/src/shell/page-title.tsx +57 -0
  771. package/src/shell/sidebar-block-model.ts +1 -1
  772. package/src/shell/sidebar-blocks.tsx +1 -1
  773. package/src/shell/sidebar-search.module.css +1 -1
  774. package/src/shell/sidebar-search.tsx +13 -4
  775. package/src/shell/use-breakpoint.ts +73 -0
  776. package/src/state/fields.ts +50 -53
  777. package/src/state/persisted-or-local.ts +50 -0
  778. package/src/state.ts +0 -9
  779. package/src/styles.css +36 -0
  780. package/src/surfaces.ts +72 -57
  781. package/src/theme/ctl-theme.tsx +10 -0
  782. package/src/theme/index.ts +136 -16
  783. package/src/theme/nav-link.module.css +37 -26
  784. package/src/theme/segmented-control.module.css +22 -0
  785. package/src/theme/segmented-control.module.css.d.ts +1 -0
  786. package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +11 -4
  787. package/src/theme-lab/boot.ts +140 -0
  788. package/src/theme-lab/index.tsx +21 -112
  789. package/src/tokens/index.ts +60 -6
  790. package/src/tokens/palette.ts +70 -30
  791. package/dist/agent/adapter.test.d.ts +0 -2
  792. package/dist/agent/adapter.test.d.ts.map +0 -1
  793. package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
  794. package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
  795. package/dist/agent/coalesce.test.d.ts +0 -2
  796. package/dist/agent/coalesce.test.d.ts.map +0 -1
  797. package/dist/agent/foreign.test.d.ts +0 -2
  798. package/dist/agent/foreign.test.d.ts.map +0 -1
  799. package/dist/agent/id.test.d.ts +0 -2
  800. package/dist/agent/id.test.d.ts.map +0 -1
  801. package/dist/agent/merge.test.d.ts +0 -2
  802. package/dist/agent/merge.test.d.ts.map +0 -1
  803. package/dist/agent/part-list.test.d.ts +0 -2
  804. package/dist/agent/part-list.test.d.ts.map +0 -1
  805. package/dist/agent/parts.test.d.ts +0 -2
  806. package/dist/agent/parts.test.d.ts.map +0 -1
  807. package/dist/agent/thread.test.d.ts +0 -2
  808. package/dist/agent/thread.test.d.ts.map +0 -1
  809. package/dist/agent/transport.test.d.ts +0 -2
  810. package/dist/agent/transport.test.d.ts.map +0 -1
  811. package/dist/agent/use-agent-stream.test.d.ts +0 -2
  812. package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
  813. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
  814. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
  815. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
  816. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
  817. package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
  818. package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
  819. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
  820. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
  821. package/dist/agent-chat/composer.test.d.ts +0 -2
  822. package/dist/agent-chat/composer.test.d.ts.map +0 -1
  823. package/dist/agent-chat/relative-time.test.d.ts +0 -2
  824. package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
  825. package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
  826. package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
  827. package/dist/agent-chat/thread-feed.test.d.ts +0 -2
  828. package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
  829. package/dist/agent-chat/thread-message.test.d.ts +0 -2
  830. package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
  831. package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
  832. package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
  833. package/dist/agent-chat/tool-chip.test.d.ts +0 -2
  834. package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
  835. package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
  836. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
  837. package/dist/charts/cursor/resolve.test.d.ts +0 -2
  838. package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
  839. package/dist/charts/cursor/store.test.d.ts +0 -2
  840. package/dist/charts/cursor/store.test.d.ts.map +0 -1
  841. package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
  842. package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
  843. package/dist/charts/kinds/Bars.test.d.ts +0 -2
  844. package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
  845. package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
  846. package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
  847. package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
  848. package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
  849. package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
  850. package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
  851. package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
  852. package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
  853. package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
  854. package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
  855. package/dist/charts/layout/auto-margin.test.d.ts +0 -2
  856. package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
  857. package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
  858. package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
  859. package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
  860. package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
  861. package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
  862. package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
  863. package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
  864. package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
  865. package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
  866. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
  867. package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
  868. package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
  869. package/dist/charts/series.test.d.ts +0 -2
  870. package/dist/charts/series.test.d.ts.map +0 -1
  871. package/dist/charts/utils/domain.test.d.ts +0 -2
  872. package/dist/charts/utils/domain.test.d.ts.map +0 -1
  873. package/dist/charts/utils/measure-text.test.d.ts +0 -2
  874. package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
  875. package/dist/cli/check-coverage.test.d.ts +0 -2
  876. package/dist/cli/check-coverage.test.d.ts.map +0 -1
  877. package/dist/cli/check-theme.test.d.ts +0 -2
  878. package/dist/cli/check-theme.test.d.ts.map +0 -1
  879. package/dist/cli/help.test.d.ts +0 -2
  880. package/dist/cli/help.test.d.ts.map +0 -1
  881. package/dist/cli/init-sync.test.d.ts +0 -2
  882. package/dist/cli/init-sync.test.d.ts.map +0 -1
  883. package/dist/cli/placement-engine.test.d.ts +0 -2
  884. package/dist/cli/placement-engine.test.d.ts.map +0 -1
  885. package/dist/cli/shipped-versions.test.d.ts +0 -2
  886. package/dist/cli/shipped-versions.test.d.ts.map +0 -1
  887. package/dist/cli/tarball-parity.test.d.ts +0 -2
  888. package/dist/cli/tarball-parity.test.d.ts.map +0 -1
  889. package/dist/cli/template-format.test.d.ts +0 -2
  890. package/dist/cli/template-format.test.d.ts.map +0 -1
  891. package/dist/cli/template-ledger.test.d.ts +0 -2
  892. package/dist/cli/template-ledger.test.d.ts.map +0 -1
  893. package/dist/cli/tokens-css.test.d.ts +0 -2
  894. package/dist/cli/tokens-css.test.d.ts.map +0 -1
  895. package/dist/cli/toolchain-wiring.test.d.ts +0 -2
  896. package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
  897. package/dist/cli/waiver-audit.test.d.ts +0 -2
  898. package/dist/cli/waiver-audit.test.d.ts.map +0 -1
  899. package/dist/connectivity/connectivity-indicator.d.ts +0 -11
  900. package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
  901. package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
  902. package/dist/connectivity/index.d.ts +0 -13
  903. package/dist/connectivity/index.d.ts.map +0 -1
  904. package/dist/connectivity/index.js +0 -8
  905. package/dist/connectivity/types.d.ts.map +0 -1
  906. package/dist/connectivity/use-connectivity.d.ts +0 -3
  907. package/dist/connectivity/use-connectivity.d.ts.map +0 -1
  908. package/dist/content/article-actions.test.d.ts +0 -2
  909. package/dist/content/article-actions.test.d.ts.map +0 -1
  910. package/dist/content/article-model.test.d.ts +0 -2
  911. package/dist/content/article-model.test.d.ts.map +0 -1
  912. package/dist/content/block-split.test.d.ts +0 -2
  913. package/dist/content/block-split.test.d.ts.map +0 -1
  914. package/dist/content/heading-components.test.d.ts +0 -2
  915. package/dist/content/heading-components.test.d.ts.map +0 -1
  916. package/dist/content/markdown.test.d.ts +0 -2
  917. package/dist/content/markdown.test.d.ts.map +0 -1
  918. package/dist/content/prose.module.css.test.d.ts +0 -2
  919. package/dist/content/prose.module.css.test.d.ts.map +0 -1
  920. package/dist/content/sanitize.test.d.ts +0 -2
  921. package/dist/content/sanitize.test.d.ts.map +0 -1
  922. package/dist/content/slug.test.d.ts +0 -2
  923. package/dist/content/slug.test.d.ts.map +0 -1
  924. package/dist/controls/actions.test.d.ts +0 -2
  925. package/dist/controls/actions.test.d.ts.map +0 -1
  926. package/dist/controls/actions.type-guard.test.d.ts +0 -2
  927. package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
  928. package/dist/controls/control-group.test.d.ts +0 -2
  929. package/dist/controls/control-group.test.d.ts.map +0 -1
  930. package/dist/controls/controls.router.test.d.ts +0 -2
  931. package/dist/controls/controls.router.test.d.ts.map +0 -1
  932. package/dist/controls/filter-set.test.d.ts +0 -2
  933. package/dist/controls/filter-set.test.d.ts.map +0 -1
  934. package/dist/controls/peer-boundary.test.d.ts +0 -2
  935. package/dist/controls/peer-boundary.test.d.ts.map +0 -1
  936. package/dist/controls/props-contract.test.d.ts +0 -2
  937. package/dist/controls/props-contract.test.d.ts.map +0 -1
  938. package/dist/controls/range-filter.type-guard.test.d.ts +0 -2
  939. package/dist/controls/range-filter.type-guard.test.d.ts.map +0 -1
  940. package/dist/controls/sync-button.test.d.ts +0 -2
  941. package/dist/controls/sync-button.test.d.ts.map +0 -1
  942. package/dist/dashboard/query-state.test.d.ts +0 -2
  943. package/dist/dashboard/query-state.test.d.ts.map +0 -1
  944. package/dist/dashboard/settings-section.test.d.ts +0 -2
  945. package/dist/dashboard/settings-section.test.d.ts.map +0 -1
  946. package/dist/dashboard/stat-card.test.d.ts +0 -2
  947. package/dist/dashboard/stat-card.test.d.ts.map +0 -1
  948. package/dist/data/data-table.test.d.ts +0 -2
  949. package/dist/data/data-table.test.d.ts.map +0 -1
  950. package/dist/data/index.d.ts +0 -38
  951. package/dist/data/index.d.ts.map +0 -1
  952. package/dist/data/index.js +0 -2
  953. package/dist/guard/check-source.test.d.ts +0 -2
  954. package/dist/guard/check-source.test.d.ts.map +0 -1
  955. package/dist/guard/grace.test.d.ts +0 -20
  956. package/dist/guard/grace.test.d.ts.map +0 -1
  957. package/dist/guard/guard-hook.test.d.ts +0 -2
  958. package/dist/guard/guard-hook.test.d.ts.map +0 -1
  959. package/dist/motion/index.d.ts.map +0 -1
  960. package/dist/nav/types.d.ts.map +0 -1
  961. package/dist/notifications/overlay.d.ts +0 -20
  962. package/dist/notifications/overlay.d.ts.map +0 -1
  963. package/dist/notifications/overlay.js +0 -12
  964. package/dist/provider/index.test.d.ts +0 -2
  965. package/dist/provider/index.test.d.ts.map +0 -1
  966. package/dist/provider/lab-theme.test.d.ts +0 -2
  967. package/dist/provider/lab-theme.test.d.ts.map +0 -1
  968. package/dist/query/devtools.d.ts.map +0 -1
  969. package/dist/query/error-message.d.ts +0 -41
  970. package/dist/query/error-message.d.ts.map +0 -1
  971. package/dist/query/error-message.js +0 -30
  972. package/dist/query/error-message.test.d.ts +0 -2
  973. package/dist/query/error-message.test.d.ts.map +0 -1
  974. package/dist/query/index.d.ts +0 -47
  975. package/dist/query/index.d.ts.map +0 -1
  976. package/dist/query/index.js +0 -56
  977. package/dist/router-tanstack/field.d.ts +0 -14
  978. package/dist/router-tanstack/field.d.ts.map +0 -1
  979. package/dist/router-tanstack/field.js +0 -4
  980. package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
  981. package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
  982. package/dist/router-tanstack/multi-search-param-store.js +0 -30
  983. package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
  984. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
  985. package/dist/router-tanstack/nav.test.d.ts +0 -2
  986. package/dist/router-tanstack/nav.test.d.ts.map +0 -1
  987. package/dist/router-tanstack/search-param-store.d.ts +0 -42
  988. package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
  989. package/dist/router-tanstack/search-param-store.js +0 -25
  990. package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
  991. package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
  992. package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
  993. package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
  994. package/dist/router-tanstack/search-store.test.d.ts +0 -2
  995. package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
  996. package/dist/router-tanstack/use-nav.test.d.ts +0 -2
  997. package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
  998. package/dist/section/index.d.ts +0 -2
  999. package/dist/section/index.d.ts.map +0 -1
  1000. package/dist/section/index.js +0 -4
  1001. package/dist/section/section.d.ts +0 -24
  1002. package/dist/section/section.d.ts.map +0 -1
  1003. package/dist/section/section.js +0 -126
  1004. package/dist/section/section.test.d.ts +0 -2
  1005. package/dist/section/section.test.d.ts.map +0 -1
  1006. package/dist/shell/app-mobile-nav.test.d.ts +0 -2
  1007. package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
  1008. package/dist/shell/app-sidebar.test.d.ts +0 -2
  1009. package/dist/shell/app-sidebar.test.d.ts.map +0 -1
  1010. package/dist/shell/index.test.d.ts +0 -2
  1011. package/dist/shell/index.test.d.ts.map +0 -1
  1012. package/dist/shell/mobile-nav-model.test.d.ts +0 -2
  1013. package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
  1014. package/dist/shell/page-bar.test.d.ts +0 -2
  1015. package/dist/shell/page-bar.test.d.ts.map +0 -1
  1016. package/dist/state/fields.test.d.ts +0 -2
  1017. package/dist/state/fields.test.d.ts.map +0 -1
  1018. package/dist/state/persisted.test.d.ts +0 -2
  1019. package/dist/state/persisted.test.d.ts.map +0 -1
  1020. package/dist/styles.floor.test.d.ts +0 -2
  1021. package/dist/styles.floor.test.d.ts.map +0 -1
  1022. package/dist/theme/border-coverage.test.d.ts +0 -18
  1023. package/dist/theme/border-coverage.test.d.ts.map +0 -1
  1024. package/dist/theme/contrast.test.d.ts +0 -2
  1025. package/dist/theme/contrast.test.d.ts.map +0 -1
  1026. package/dist/theme/ctl-theme.test.d.ts +0 -2
  1027. package/dist/theme/ctl-theme.test.d.ts.map +0 -1
  1028. package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
  1029. package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
  1030. package/dist/theme/density-option.test.d.ts +0 -2
  1031. package/dist/theme/density-option.test.d.ts.map +0 -1
  1032. package/dist/theme/density-relations.test.d.ts +0 -2
  1033. package/dist/theme/density-relations.test.d.ts.map +0 -1
  1034. package/dist/theme/derive-theme.test.d.ts +0 -2
  1035. package/dist/theme/derive-theme.test.d.ts.map +0 -1
  1036. package/dist/theme/divider-law.test.d.ts +0 -2
  1037. package/dist/theme/divider-law.test.d.ts.map +0 -1
  1038. package/dist/theme/fonts-option.test.d.ts +0 -2
  1039. package/dist/theme/fonts-option.test.d.ts.map +0 -1
  1040. package/dist/theme/icon-slot.test.d.ts +0 -2
  1041. package/dist/theme/icon-slot.test.d.ts.map +0 -1
  1042. package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
  1043. package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
  1044. package/dist/theme/radius-option.test.d.ts +0 -2
  1045. package/dist/theme/radius-option.test.d.ts.map +0 -1
  1046. package/dist/theme/radius.test.d.ts +0 -2
  1047. package/dist/theme/radius.test.d.ts.map +0 -1
  1048. package/dist/theme/shadow-surfaces.test.d.ts +0 -26
  1049. package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
  1050. package/dist/theme/spacing.test.d.ts +0 -2
  1051. package/dist/theme/spacing.test.d.ts.map +0 -1
  1052. package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
  1053. package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
  1054. package/dist/theme-lab/derive-controls.test.d.ts +0 -2
  1055. package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
  1056. package/dist/theme-lab/derive-state.test.d.ts +0 -2
  1057. package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
  1058. package/dist/theme-lab/theme-lab.test.d.ts +0 -2
  1059. package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
  1060. package/dist/theme-toggle/index.d.ts +0 -8
  1061. package/dist/theme-toggle/index.d.ts.map +0 -1
  1062. package/dist/tokens/build-fonts-css.test.d.ts +0 -2
  1063. package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
  1064. package/dist/tokens/build-radius-css.test.d.ts +0 -2
  1065. package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
  1066. package/dist/tokens/chart-margin.test.d.ts +0 -2
  1067. package/dist/tokens/chart-margin.test.d.ts.map +0 -1
  1068. package/dist/tokens/density.test.d.ts +0 -2
  1069. package/dist/tokens/density.test.d.ts.map +0 -1
  1070. package/dist/tokens/derive-radius.test.d.ts +0 -2
  1071. package/dist/tokens/derive-radius.test.d.ts.map +0 -1
  1072. package/dist/tokens/derive.test.d.ts +0 -2
  1073. package/dist/tokens/derive.test.d.ts.map +0 -1
  1074. package/dist/tokens/palette.test.d.ts +0 -2
  1075. package/dist/tokens/palette.test.d.ts.map +0 -1
  1076. package/dist/tokens/text-scale.test.d.ts +0 -2
  1077. package/dist/tokens/text-scale.test.d.ts.map +0 -1
  1078. package/dist/vite-color-mix.test.d.ts +0 -2
  1079. package/dist/vite-color-mix.test.d.ts.map +0 -1
  1080. package/dist/vite.test.d.ts +0 -2
  1081. package/dist/vite.test.d.ts.map +0 -1
  1082. package/dist/widget-header/delta-badge.d.ts +0 -13
  1083. package/dist/widget-header/delta-badge.d.ts.map +0 -1
  1084. package/dist/widget-header/delta-badge.js +0 -29
  1085. package/dist/widget-header/delta-badge.test.d.ts +0 -2
  1086. package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
  1087. package/dist/widget-header/index.d.ts +0 -3
  1088. package/dist/widget-header/index.d.ts.map +0 -1
  1089. package/dist/widget-header/index.js +0 -6
  1090. package/dist/widget-header/no-mantine.test.d.ts +0 -2
  1091. package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
  1092. package/dist/widget-header/widget-header.d.ts +0 -67
  1093. package/dist/widget-header/widget-header.d.ts.map +0 -1
  1094. package/dist/widget-header/widget-header.test.d.ts +0 -2
  1095. package/dist/widget-header/widget-header.test.d.ts.map +0 -1
  1096. package/src/agent/adapter.test.ts +0 -1640
  1097. package/src/agent/ai-sdk-transport.test.ts +0 -658
  1098. package/src/agent/coalesce.test.ts +0 -387
  1099. package/src/agent/foreign.test.ts +0 -51
  1100. package/src/agent/id.test.ts +0 -169
  1101. package/src/agent/merge.test.ts +0 -206
  1102. package/src/agent/part-list.test.tsx +0 -112
  1103. package/src/agent/parts.test.ts +0 -720
  1104. package/src/agent/thread.test.ts +0 -220
  1105. package/src/agent/transport.test.ts +0 -70
  1106. package/src/agent/use-agent-stream.test.tsx +0 -287
  1107. package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
  1108. package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
  1109. package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
  1110. package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
  1111. package/src/agent-chat/composer.test.tsx +0 -924
  1112. package/src/agent-chat/relative-time.test.ts +0 -95
  1113. package/src/agent-chat/thread-feed-row.test.tsx +0 -510
  1114. package/src/agent-chat/thread-feed.test.tsx +0 -241
  1115. package/src/agent-chat/thread-message.test.tsx +0 -1285
  1116. package/src/agent-chat/thread-workspace.test.tsx +0 -169
  1117. package/src/agent-chat/tool-chip.test.tsx +0 -139
  1118. package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
  1119. package/src/charts/cursor/resolve.test.ts +0 -151
  1120. package/src/charts/cursor/store.test.ts +0 -64
  1121. package/src/charts/kinds/BandStrip.test.tsx +0 -282
  1122. package/src/charts/kinds/Bars.test.tsx +0 -183
  1123. package/src/charts/kinds/DualPanel.test.tsx +0 -527
  1124. package/src/charts/kinds/MirroredBars.test.tsx +0 -232
  1125. package/src/charts/kinds/MultiLine.test.tsx +0 -218
  1126. package/src/charts/kinds/StackedArea.test.tsx +0 -114
  1127. package/src/charts/kinds/ZonedLine.test.tsx +0 -147
  1128. package/src/charts/layout/auto-margin.test.ts +0 -42
  1129. package/src/charts/primitives/CartesianChart.test.tsx +0 -647
  1130. package/src/charts/primitives/ChartCard.test.tsx +0 -66
  1131. package/src/charts/primitives/ChartFrame.test.tsx +0 -142
  1132. package/src/charts/primitives/ChartLegend.test.tsx +0 -78
  1133. package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
  1134. package/src/charts/primitives/XZoneRects.test.tsx +0 -123
  1135. package/src/charts/series.test.ts +0 -112
  1136. package/src/charts/utils/domain.test.ts +0 -54
  1137. package/src/charts/utils/measure-text.test.ts +0 -36
  1138. package/src/cli/check-coverage.test.ts +0 -297
  1139. package/src/cli/check-theme.test.ts +0 -837
  1140. package/src/cli/help.test.ts +0 -170
  1141. package/src/cli/init-sync.test.ts +0 -343
  1142. package/src/cli/placement-engine.test.ts +0 -298
  1143. package/src/cli/shipped-versions.test.ts +0 -83
  1144. package/src/cli/tarball-parity.test.ts +0 -44
  1145. package/src/cli/template-format.test.ts +0 -64
  1146. package/src/cli/template-ledger.test.ts +0 -219
  1147. package/src/cli/tokens-css.test.ts +0 -249
  1148. package/src/cli/toolchain-wiring.test.ts +0 -1358
  1149. package/src/cli/waiver-audit.test.ts +0 -215
  1150. package/src/connectivity/index.ts +0 -18
  1151. package/src/content/article-actions.test.ts +0 -78
  1152. package/src/content/article-model.test.ts +0 -119
  1153. package/src/content/block-split.test.ts +0 -75
  1154. package/src/content/heading-components.test.tsx +0 -65
  1155. package/src/content/markdown.test.tsx +0 -603
  1156. package/src/content/prose.module.css.test.ts +0 -39
  1157. package/src/content/sanitize.test.ts +0 -659
  1158. package/src/content/slug.test.ts +0 -62
  1159. package/src/controls/actions.test.tsx +0 -252
  1160. package/src/controls/actions.type-guard.test.ts +0 -63
  1161. package/src/controls/control-group.test.tsx +0 -229
  1162. package/src/controls/controls.router.test.tsx +0 -1053
  1163. package/src/controls/filter-set.test.tsx +0 -472
  1164. package/src/controls/peer-boundary.test.ts +0 -85
  1165. package/src/controls/props-contract.test.tsx +0 -137
  1166. package/src/controls/range-filter.type-guard.test.tsx +0 -122
  1167. package/src/controls/sync-button.test.tsx +0 -183
  1168. package/src/dashboard/query-state.test.tsx +0 -152
  1169. package/src/dashboard/settings-section.test.tsx +0 -56
  1170. package/src/dashboard/stat-card.test.tsx +0 -385
  1171. package/src/data/data-table.test.tsx +0 -434
  1172. package/src/data/index.ts +0 -41
  1173. package/src/guard/check-source.test.ts +0 -3388
  1174. package/src/guard/grace.test.ts +0 -158
  1175. package/src/guard/guard-hook.test.ts +0 -203
  1176. package/src/notifications/overlay.tsx +0 -64
  1177. package/src/provider/index.test.tsx +0 -101
  1178. package/src/provider/lab-theme.test.ts +0 -147
  1179. package/src/query/error-message.test.ts +0 -67
  1180. package/src/query/error-message.ts +0 -70
  1181. package/src/query/index.ts +0 -97
  1182. package/src/router-tanstack/field.ts +0 -35
  1183. package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
  1184. package/src/router-tanstack/multi-search-param-store.ts +0 -106
  1185. package/src/router-tanstack/nav.test.ts +0 -111
  1186. package/src/router-tanstack/search-param-store.test.ts +0 -135
  1187. package/src/router-tanstack/search-param-store.ts +0 -100
  1188. package/src/router-tanstack/search-store.router.test.tsx +0 -882
  1189. package/src/router-tanstack/search-store.test.ts +0 -578
  1190. package/src/router-tanstack/use-nav.test.tsx +0 -187
  1191. package/src/section/index.ts +0 -1
  1192. package/src/section/section.test.tsx +0 -204
  1193. package/src/section/section.tsx +0 -220
  1194. package/src/shell/app-mobile-nav.test.tsx +0 -403
  1195. package/src/shell/app-sidebar.test.tsx +0 -509
  1196. package/src/shell/index.test.tsx +0 -428
  1197. package/src/shell/mobile-nav-model.test.ts +0 -517
  1198. package/src/shell/page-bar.test.tsx +0 -316
  1199. package/src/state/fields.test.tsx +0 -512
  1200. package/src/state/persisted.test.ts +0 -91
  1201. package/src/styles.floor.test.ts +0 -36
  1202. package/src/theme/border-coverage.test.ts +0 -272
  1203. package/src/theme/contrast.test.ts +0 -354
  1204. package/src/theme/ctl-theme.test.tsx +0 -275
  1205. package/src/theme/ctl-tier-coverage.test.ts +0 -170
  1206. package/src/theme/density-option.test.ts +0 -96
  1207. package/src/theme/density-relations.test.ts +0 -506
  1208. package/src/theme/derive-theme.test.ts +0 -101
  1209. package/src/theme/divider-law.test.ts +0 -259
  1210. package/src/theme/fonts-option.test.ts +0 -75
  1211. package/src/theme/icon-slot.test.tsx +0 -275
  1212. package/src/theme/layout-rhythm-css.test.ts +0 -72
  1213. package/src/theme/radius-option.test.ts +0 -86
  1214. package/src/theme/radius.test.ts +0 -142
  1215. package/src/theme/shadow-surfaces.test.ts +0 -488
  1216. package/src/theme/spacing.test.ts +0 -495
  1217. package/src/theme/use-basalt-spacing.test.tsx +0 -55
  1218. package/src/theme-lab/derive-controls.test.ts +0 -94
  1219. package/src/theme-lab/derive-state.test.ts +0 -71
  1220. package/src/theme-lab/theme-lab.test.ts +0 -46
  1221. package/src/tokens/build-fonts-css.test.ts +0 -52
  1222. package/src/tokens/build-radius-css.test.ts +0 -25
  1223. package/src/tokens/chart-margin.test.ts +0 -39
  1224. package/src/tokens/density.test.ts +0 -259
  1225. package/src/tokens/derive-radius.test.ts +0 -62
  1226. package/src/tokens/derive.test.ts +0 -30
  1227. package/src/tokens/palette.test.ts +0 -114
  1228. package/src/tokens/text-scale.test.ts +0 -51
  1229. package/src/vite-color-mix.test.ts +0 -46
  1230. package/src/vite.test.ts +0 -423
  1231. package/src/widget-header/delta-badge.test.tsx +0 -60
  1232. package/src/widget-header/delta-badge.tsx +0 -57
  1233. package/src/widget-header/index.ts +0 -2
  1234. package/src/widget-header/no-mantine.test.ts +0 -33
  1235. package/src/widget-header/widget-header.test.tsx +0 -187
  1236. package/src/widget-header/widget-header.tsx +0 -213
  1237. /package/dist/{motion/index.js → common/motion.js} +0 -0
  1238. /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
  1239. /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  1240. /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
  1241. /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
  1242. /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
  1243. /package/src/{motion/index.ts → common/motion.ts} +0 -0
  1244. /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
  1245. /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
@@ -9,7 +9,7 @@
9
9
  * exactly as before.
10
10
  *
11
11
  * @example
12
- * import { BasaltDataTable } from 'basalt-ui/data'
12
+ * import { BasaltDataTable } from 'basalt-ui/data/table'
13
13
  * import { createColumnHelper } from '@tanstack/react-table'
14
14
  *
15
15
  * type Row = { name: string; age: number }
@@ -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
@@ -66,7 +88,12 @@ import { MultiSelectFilter } from '../controls/multi-select-filter'
66
88
  import { CtlSlot } from '../theme'
67
89
  import { alpha, VX } from '../tokens'
68
90
  import type { EnumField, FieldHandle, MultiField } from '../state'
69
- import { WidgetHeader } from '../widget-header'
91
+ import { WidgetHeader } from '../dashboard'
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 '../common/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',
@@ -204,7 +252,7 @@ function facetEnumHandle<T>(
204
252
  */
205
253
  function facetMultiHandle<T>(
206
254
  column: Column<T, unknown>,
207
- facet: DataTableFacet,
255
+ facet: DataTableFacet<T>,
208
256
  ): FieldHandle<MultiField<string>> {
209
257
  return {
210
258
  kind: 'multi',
@@ -227,8 +275,106 @@ function facetMultiHandle<T>(
227
275
  }
228
276
  }
229
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
+
230
369
  // ── Props ─────────────────────────────────────────────────────────────────────
231
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
+
232
378
  /**
233
379
  * Props for {@link BasaltDataTable}.
234
380
  *
@@ -242,268 +388,383 @@ function facetMultiHandle<T>(
242
388
  * emptyState: <Text c="dimmed">No results found.</Text>,
243
389
  * }
244
390
  */
245
- export type BasaltDataTableProps<T> = {
246
- // ── Header (WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ──────────────────────────
247
-
248
- /**
249
- * Optional heading rendered above the toolbar via `WidgetHeader tier="widget"`. `count` always
250
- * reads `table.getRowCount()` (C11) — never a raw `data.length` — so it tracks the row model
251
- * (post-filter/-pagination) rather than the unfiltered input.
252
- */
253
- title?: string
254
- /** Optional leading icon, forwarded to `WidgetHeader`. */
255
- icon?: ReactNode
256
- /** Optional muted line rendered below the title row. */
257
- subtitle?: string
258
-
259
- /** Row data array. */
260
- data: T[]
261
- /**
262
- * Column definitions. Use `createColumnHelper<T>()` from `@tanstack/react-table`
263
- * for a typed accessor builder — `col.accessor('field', …)` columns sort by value.
264
- */
265
- // The cell-value type varies per column, so the array is heterogeneous in TValue. `any` here
266
- // is the TanStack-idiomatic escape (accessor columns infer their own TValue); row typing stays
267
- // exact via the `T` data generic.
268
- // oxlint-disable-next-line typescript/no-explicit-any -- TanStack-idiomatic heterogeneous column array
269
- columns: ColumnDef<T, any>[]
270
- /**
271
- * Enable column sorting via clickable headers. Client-side — the rows in `data` are what get
272
- * reordered.
273
- *
274
- * Under `manualPagination` that is one page, so leaving this on without also passing
275
- * `manualSorting` is a contract violation and throws in dev — see `manualPagination`.
276
- * @default true
277
- */
278
- enableSorting?: boolean
279
- /**
280
- * Hand sorting to the server: the table stops reordering `data` locally and renders it in the
281
- * order given, while the headers keep toggling `SortingState` and reporting it through
282
- * `onSortingChange` — which is where you re-request the sorted page.
283
- *
284
- * This is the required companion to `manualPagination` whenever sort headers are live; without
285
- * it a server-paginated table sorts one page and the "of N" bar presents that as a sort of all N.
286
- * @default false
287
- * @example
288
- * <BasaltDataTable
289
- * data={page.rows}
290
- * columns={columns}
291
- * enablePagination
292
- * manualPagination
293
- * manualSorting
294
- * rowCount={page.total}
295
- * onSortingChange={(s) => refetch({ sort: s[0] })}
296
- * />
297
- */
298
- manualSorting?: boolean
299
- /** Stripe alternate rows. Forwarded to Mantine `Table` — `'odd'`/`'even'` pick the phase. */
300
- striped?: boolean | 'odd' | 'even'
301
- /** Highlight hovered rows. Forwarded to Mantine `Table`. */
302
- highlightOnHover?: boolean
303
- /**
304
- * Rendered when no rows are VISIBLE — `data` is empty, or a filter/page matches none of it.
305
- * Falls back to a simple message when omitted. (Before 1.25.0 this tracked `data.length` alone,
306
- * so a search matching nothing rendered a blank `<tbody>` with no message at all.)
307
- */
308
- emptyState?: ReactNode
309
- /**
310
- * When true, renders skeleton placeholder rows instead of the empty-state branch.
311
- * The header remains visible. Use while async data is loading.
312
- */
313
- isLoading?: boolean
314
- /**
315
- * Number of skeleton rows to render when `isLoading` is true.
316
- * @default 5
317
- */
318
- skeletonRows?: number
319
- /**
320
- * Initial sorting state. Drives `useState` initial value — useful for restoring
321
- * sort order from URL search params (e.g. `initialSorting={Route.useSearch().sorting}`).
322
- * @example
323
- * // URL-sync pattern with TanStack Router
324
- * const { sorting } = Route.useSearch()
325
- * <BasaltDataTable initialSorting={sorting} onSortingChange={(s) => navigate({ search: { sorting: s } })} … />
326
- */
327
- initialSorting?: SortingState
328
- /**
329
- * Called whenever the internal sorting state changes. Receives the new `SortingState`.
330
- * The table continues to manage sorting internally (uncontrolled) when this is omitted.
331
- */
332
- onSortingChange?: (sorting: SortingState) => void
333
-
334
- // ── Toolbar (search + facets + actions) ──────────────────────────────────────
335
-
336
- /**
337
- * Shows a global search `TextInput` above the table, wired to TanStack's `globalFilter` state
338
- * (substring match against every column's stringified cell value).
339
- * @default false
340
- * @example
341
- * <BasaltDataTable data={rows} columns={columns} enableGlobalFilter searchIcon={<IconSearch size={14} />} />
342
- */
343
- enableGlobalFilter?: boolean
344
- /**
345
- * Placeholder for the global search input.
346
- * @default 'Search…'
347
- */
348
- globalFilterPlaceholder?: string
349
- /**
350
- * Leading icon rendered inside the global search input (Mantine `leftSection`). basalt-ui ships
351
- * no icon set — pass any `ReactNode` (e.g. `<IconSearch size={14} />`).
352
- */
353
- searchIcon?: ReactNode
354
- /**
355
- * Initial global filter value. Drives `useState` initial value — mirrors `initialSorting` for
356
- * restoring the search term from a URL search param.
357
- */
358
- initialGlobalFilter?: string
359
- /** Called whenever the internal global filter value changes. */
360
- onGlobalFilterChange?: (value: string) => void
361
- /**
362
- * Faceted column filters, rendered in the toolbar as `EnumFilter`/`MultiSelectFilter` pills
363
- * inside a `FilterSet`, wired to TanStack's per-column `columnFilters` state. The toolbar
364
- * renders whenever this array is non-empty, `enableGlobalFilter` is set, or `actions` is passed.
365
- * @example
366
- * facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
367
- */
368
- facets?: DataTableFacet[]
369
- /** Called whenever the faceted column filters change — the seam for server-side faceting. */
370
- onColumnFiltersChange?: (filters: ColumnFiltersState) => void
371
- /**
372
- * Hand filtering to the server: the table stops narrowing `data` locally and renders every row
373
- * given, while the search input and facets keep reporting through `onGlobalFilterChange` /
374
- * `onColumnFiltersChange` — which is where you re-request the filtered page.
375
- *
376
- * Required alongside `manualPagination` whenever `enableGlobalFilter` or `facets` is set;
377
- * without it the controls narrow one page while "of N" keeps counting the whole set.
378
- * @default false
379
- */
380
- manualFiltering?: boolean
381
- /**
382
- * Right-aligned toolbar slot (e.g. an "Export" button). Renders the toolbar row even when no
383
- * search input or facets are configured. Renamed from `toolbarActions` — the toolbar (search +
384
- * facets + this slot) is wrapped in `CtlSlot` (C1/C5), so its controls resolve to the `ctl` tier.
385
- */
386
- actions?: ReactNode
387
-
388
- // ── Pagination ────────────────────────────────────────────────────────────────
389
-
390
- /**
391
- * Enables the bottom pagination bar ("Showing X–Y of N", rows-per-page `Select`, Mantine
392
- * `Pagination`) wired to TanStack's `pagination` state. Client-side by default — see
393
- * `manualPagination` for server-driven pagination.
394
- * @default false
395
- */
396
- enablePagination?: boolean
397
- /**
398
- * Rows-per-page choices offered in the pagination bar's Select.
399
- * @default [10, 25, 50, 100]
400
- */
401
- pageSizeOptions?: number[]
402
- /**
403
- * Initial pagination state. Drives `useState` initial value — mirrors `initialSorting` for
404
- * restoring the page/size from a URL search param.
405
- */
406
- initialPagination?: PaginationState
407
- /** Called whenever the internal pagination state changes. */
408
- onPaginationChange?: (pagination: PaginationState) => void
409
- /**
410
- * Disables local pagination slicing — pass the already-paginated page of `data` plus `rowCount`
411
- * (and optionally `pageCount`) for server-driven pagination. `onPaginationChange` is where you
412
- * fetch the next page.
413
- *
414
- * **It imposes a contract, enforced at render.** `data` is now one page, but the pagination bar
415
- * says "Showing 1–25 of 412" — so every OTHER client-side control becomes a claim about 412 rows
416
- * that it can only make about 25. Each has to be resolved explicitly, because only the call site
417
- * knows whether the server does the work:
418
- *
419
- * - `rowCount` (or `pageCount`) must be given, or "of N" counts the page and the pager collapses
420
- * to a single page nobody can leave;
421
- * - sorting must be handed over with `manualSorting`, or switched off with `enableSorting={false}`;
422
- * - `enableGlobalFilter` / `facets`, if used, must be handed over with `manualFiltering`;
423
- * - `enablePagination` must be on, or `manualPagination` is inert and the page renders as if it
424
- * were the whole table.
425
- *
426
- * Unresolved, each throws in dev and degrades to the honest render in production (no sort
427
- * headers, no filter controls, no "of N") — never to the plausible wrong answer.
428
- * @default false
429
- * @example
430
- * <BasaltDataTable
431
- * data={page.rows}
432
- * columns={columns}
433
- * enablePagination
434
- * manualPagination
435
- * rowCount={page.total}
436
- * initialPagination={{ pageIndex: page.index, pageSize: page.size }}
437
- * onPaginationChange={(p) => fetchPage(p)}
438
- * />
439
- */
440
- manualPagination?: boolean
441
- /** Total row count across all pages. Required for `manualPagination` to render "of N" and compute page count. */
442
- rowCount?: number
443
- /** Total page count, when known. Only consulted under `manualPagination`; omit to derive it from `rowCount`/`pageSize`. */
444
- pageCount?: number
445
-
446
- // ── Column pinning ────────────────────────────────────────────────────────────
447
-
448
- /**
449
- * Enables sticky left/right column pinning — pinned columns stick to the edge with a panel
450
- * background and a hairline shadow separator while the table scrolls horizontally. Pin columns
451
- * via `initialColumnPinning` (`{ left: string[], right: string[] }`).
452
- * @default false
453
- * @example
454
- * <BasaltDataTable data={rows} columns={columns} enablePinning initialColumnPinning={{ left: ['name'] }} />
455
- */
456
- enablePinning?: boolean
457
- /** Initial column-pinning state — which column ids stick to the left/right edge. */
458
- initialColumnPinning?: ColumnPinningState
459
-
460
- // ── Body chrome ─────────────────────────────────────────────────────────────
461
-
462
- /**
463
- * Caps the scrolling body (px number or CSS length) so a long table cannot blow a card out
464
- * vertically — the header stays put and the rows scroll under it. Renders Mantine's
465
- * `Table.ScrollContainer type="native"`, which is the same node the docs sanction as the raw
466
- * escape for a bespoke table: the blessed lane and the escape hatch produce identical DOM, so
467
- * adopting one does not contradict the other.
468
- *
469
- * `type="native"` is required rather than preferred: `ScrollArea`'s custom viewport is the
470
- * positioning context a sticky `<thead>` resolves against, so the default `'scrollarea'` type
471
- * pins the header to the page viewport instead of the table's box.
472
- */
473
- maxHeight?: number | string
474
- /**
475
- * `min-width` below which the body scrolls horizontally. Setting either this or `maxHeight`
476
- * turns on the scroll container; `maxHeight` alone implies `minWidth: 0`.
477
- */
478
- minWidth?: number | string
479
- /** Sticky header row. Pair with `maxHeight` to stick within the table rather than the page. */
480
- stickyHeader?: boolean
481
- /** Offset for a sticky header sitting under a fixed app header (px number or CSS length). */
482
- stickyHeaderOffset?: number | string
483
- /** Vertical cell padding. Forwarded to Mantine `Table`. */
484
- verticalSpacing?: MantineSpacing
485
- /** Horizontal cell padding. Forwarded to Mantine `Table`. */
486
- horizontalSpacing?: MantineSpacing
487
- /** Row separators. Forwarded to Mantine `Table`. @default true (Mantine's own default) */
488
- withRowBorders?: boolean
489
- /**
490
- * Outer table border — a full box around the table.
491
- *
492
- * **`false` by default since this minor** (it was `true`). Three separate faults, all visible on
493
- * the playground's `Controls (mobile)` page at 390px: its TOP edge sat directly under the table's
494
- * own `WidgetHeader`, separating a header from its own content; its BOTTOM edge landed on the last
495
- * row's own hairline, two rules at zero distance; and its left/right edges carried no information
496
- * at all on a table that fills the page (`docs/DESIGN-SPEC.md` §8 — a border is a layout divider,
497
- * and there is nothing here to divide). The head-row rule and the between-row rules stay, so the
498
- * table still reads as a table.
499
- *
500
- * Pass `true` for a table that genuinely needs a frame — a small table floating in whitespace
501
- * beside other content, where the box IS the grouping.
502
- *
503
- * @default false
504
- */
505
- withTableBorder?: boolean
506
- }
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
+ }
507
768
 
508
769
  // ── Sort indicator ────────────────────────────────────────────────────────────
509
770
 
@@ -538,19 +799,38 @@ const RANGE_LABEL_STYLE: CSSProperties = {
538
799
  const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]
539
800
 
540
801
  /**
541
- * The header row's two halves. The lead SHRINKS and the toolbar does not — a title can ellipsize, a
542
- * 220px search field with a facet pill beside it cannot, and when both were shrinkable flex split
543
- * 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.
544
814
  */
545
815
  const TABLE_HEADER_LEAD_STYLE: CSSProperties = { flex: '1 1 auto', minWidth: 0 }
546
- const TABLE_TOOLBAR_STYLE: CSSProperties = { flex: '0 0 auto' }
816
+ const TABLE_TOOLBAR_STYLE: CSSProperties = { flex: '0 1 auto', minWidth: 0 }
547
817
  /**
548
818
  * The toolbar search's width. Mantine's Input grows to its container, which in a `justify:
549
819
  * space-between` row means "whatever is left" — so the field was 600px on a wide table and 180px on a
550
820
  * narrow one, for the same one-word query. A stated width also makes the toolbar's own minimum width
551
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.
552
827
  */
553
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
+ }
554
834
  /** The rows-per-page Select's width — see `SEARCH_WIDTH` for why a toolbar field states one. */
555
835
  const PAGE_SIZE_WIDTH = 116
556
836
 
@@ -606,13 +886,6 @@ function getPinnedCellStyle<T>(
606
886
 
607
887
  // ── The manual-pagination contract ────────────────────────────────────────────
608
888
 
609
- /** The house dev gate (`src/provider`, `src/charts/kinds/BandStrip` use the same expression):
610
- * `basaltViteConfig` defines `process.env.NODE_ENV`, so a production bundle constant-folds this to
611
- * `false` and drops the throw. Read per call, never hoisted, so a test can flip it. */
612
- function isDev(): boolean {
613
- return process.env['NODE_ENV'] !== 'production'
614
- }
615
-
616
889
  /** One client-side control that `manualPagination` turns into a claim the table cannot support. */
617
890
  type ManualPaginationBreach = 'inert' | 'total' | 'sorting' | 'filtering'
618
891
 
@@ -695,6 +968,99 @@ function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): vo
695
968
  console.error(message)
696
969
  }
697
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
+
698
1064
  // ── Component ─────────────────────────────────────────────────────────────────
699
1065
 
700
1066
  /**
@@ -722,48 +1088,63 @@ function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): vo
722
1088
  * emptyState={<Text c="dimmed">No users found.</Text>}
723
1089
  * />
724
1090
  */
725
- export function BasaltDataTable<T>({
726
- title,
727
- icon,
728
- subtitle,
729
- data,
730
- columns,
731
- enableSorting = true,
732
- manualSorting = false,
733
- striped,
734
- highlightOnHover,
735
- emptyState,
736
- isLoading = false,
737
- skeletonRows = 5,
738
- initialSorting,
739
- onSortingChange,
740
- enableGlobalFilter = false,
741
- globalFilterPlaceholder = 'Search…',
742
- searchIcon,
743
- initialGlobalFilter,
744
- onGlobalFilterChange,
745
- facets,
746
- onColumnFiltersChange,
747
- manualFiltering = false,
748
- actions,
749
- enablePagination = false,
750
- pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
751
- initialPagination,
752
- onPaginationChange,
753
- manualPagination = false,
754
- rowCount,
755
- pageCount,
756
- enablePinning = false,
757
- initialColumnPinning,
758
- maxHeight,
759
- minWidth,
760
- stickyHeader,
761
- stickyHeaderOffset,
762
- verticalSpacing,
763
- horizontalSpacing,
764
- withRowBorders,
765
- withTableBorder = false,
766
- }: 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
767
1148
  const [sorting, setSorting] = useState<SortingState>(initialSorting ?? [])
768
1149
  const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter ?? '')
769
1150
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
@@ -771,6 +1152,51 @@ export function BasaltDataTable<T>({
771
1152
  initialPagination ?? { pageIndex: 0, pageSize: pageSizeOptions[0] ?? 10 },
772
1153
  )
773
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
+ )
774
1200
 
775
1201
  const handleSortingChange = useCallback(
776
1202
  (updater: Updater<SortingState>) => {
@@ -816,10 +1242,29 @@ export function BasaltDataTable<T>({
816
1242
  [onPaginationChange],
817
1243
  )
818
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
+
819
1264
  // Facets are TanStack `columnFilters` entries under the hood — inject an exact-match (or
820
1265
  // any-of, for `multiple`) filterFn onto the matching column so the built-in `includesString`
821
1266
  // auto-filter (substring match) never misfires against a Select's exact option value.
822
- const tableColumns = useMemo(() => {
1267
+ const facetColumns = useMemo(() => {
823
1268
  if (!facets || facets.length === 0) return columns
824
1269
  const facetById = new Map(facets.map((facet) => [facet.columnId, facet]))
825
1270
  return columns.map((column) => {
@@ -844,6 +1289,14 @@ export function BasaltDataTable<T>({
844
1289
  })
845
1290
  }, [columns, facets])
846
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
+
847
1300
  // Enforced before the table is built, so the degraded options below are the ones TanStack gets —
848
1301
  // a degraded table must not merely hide a control, it must not compute the answer either.
849
1302
  const hasFacets = Boolean(facets && facets.length > 0)
@@ -874,12 +1327,18 @@ export function BasaltDataTable<T>({
874
1327
  columnFilters,
875
1328
  columnPinning,
876
1329
  ...(enablePagination && { pagination }),
1330
+ ...(enableRowSelection && { rowSelection: rowSelectionState }),
877
1331
  },
878
1332
  onSortingChange: handleSortingChange,
879
1333
  onGlobalFilterChange: handleGlobalFilterChange,
880
1334
  onColumnFiltersChange: handleColumnFiltersChange,
881
1335
  onColumnPinningChange: setColumnPinning,
882
1336
  ...(enablePagination && { onPaginationChange: handlePaginationChange }),
1337
+ ...(enableRowSelection && {
1338
+ enableRowSelection: true,
1339
+ onRowSelectionChange: handleRowSelectionChange,
1340
+ }),
1341
+ ...(getRowId !== undefined && { getRowId }),
883
1342
  getCoreRowModel: getCoreRowModel(),
884
1343
  getSortedRowModel: getSortedRowModel(),
885
1344
  getFilteredRowModel: getFilteredRowModel(),
@@ -903,17 +1362,76 @@ export function BasaltDataTable<T>({
903
1362
  const showSearch = enableGlobalFilter && filteringEnabled
904
1363
  const showFacets = hasFacets && filteringEnabled
905
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
+ )
906
1396
  const headerGroups = enablePinning ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
907
1397
  // The rows actually rendered, not `data` — a filter or a page index that matches nothing leaves
908
1398
  // `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
909
1399
  const rows = table.getRowModel().rows
910
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
+
911
1426
  const tableNode = (
912
1427
  <Table
1428
+ className={cx(classNames?.table)}
913
1429
  {...(striped !== undefined && { striped })}
914
1430
  {...(highlightOnHover !== undefined && { highlightOnHover })}
915
1431
  {...(stickyHeader !== undefined && { stickyHeader })}
916
- {...(stickyHeaderOffset !== undefined && { stickyHeaderOffset })}
1432
+ {...(resolvedStickyHeaderOffset !== undefined && {
1433
+ stickyHeaderOffset: resolvedStickyHeaderOffset,
1434
+ })}
917
1435
  {...(verticalSpacing !== undefined && { verticalSpacing })}
918
1436
  {...(horizontalSpacing !== undefined && { horizontalSpacing })}
919
1437
  {...(withRowBorders !== undefined && { withRowBorders })}
@@ -970,10 +1488,25 @@ export function BasaltDataTable<T>({
970
1488
  ))}
971
1489
  </Table.Thead>
972
1490
  <Table.Tbody>
973
- {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 ? (
974
1507
  Array.from({ length: skeletonRows }, (_, rowIndex) => (
975
1508
  <Table.Tr key={`skeleton-${rowIndex}`}>
976
- {columns.map((_, colIndex) => (
1509
+ {Array.from({ length: columnCount }, (__, colIndex) => (
977
1510
  <Table.Td key={`skeleton-${rowIndex}-${colIndex}`}>
978
1511
  <Skeleton height={16} radius="sm" />
979
1512
  </Table.Td>
@@ -982,7 +1515,7 @@ export function BasaltDataTable<T>({
982
1515
  ))
983
1516
  ) : rows.length === 0 ? (
984
1517
  <Table.Tr>
985
- <Table.Td colSpan={columns.length}>
1518
+ <Table.Td colSpan={columnCount}>
986
1519
  {emptyState ?? (
987
1520
  <Text c="dimmed" ta="center" size="sm" py="sm">
988
1521
  No data to display.
@@ -1000,7 +1533,28 @@ export function BasaltDataTable<T>({
1000
1533
  ]
1001
1534
  : row.getVisibleCells()
1002
1535
  return (
1003
- <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
+ >
1004
1558
  {cells.map((cell) => {
1005
1559
  const pinnedStyle = enablePinning
1006
1560
  ? getPinnedCellStyle(table, cell.column)
@@ -1030,10 +1584,6 @@ export function BasaltDataTable<T>({
1030
1584
  </Table>
1031
1585
  )
1032
1586
 
1033
- // A capped body or a horizontal floor both need the same native scroll node; pinning keeps its
1034
- // simpler overflow-x Box when neither is set.
1035
- const scrolls = maxHeight !== undefined || minWidth !== undefined
1036
-
1037
1587
  const paginationState = table.getState().pagination
1038
1588
  // The CURRENT page size is always an option, even when it is not in `pageSizeOptions`. Mantine's
1039
1589
  // Select renders EMPTY when its `value` matches no row, so a table opened at
@@ -1047,7 +1597,7 @@ export function BasaltDataTable<T>({
1047
1597
  const rangeEnd = Math.min((paginationState.pageIndex + 1) * paginationState.pageSize, total)
1048
1598
 
1049
1599
  return (
1050
- <>
1600
+ <div className={cx(classNames?.root, className)} {...(style !== undefined && { style })}>
1051
1601
  {/*
1052
1602
  * ONE header row: the `WidgetHeader` (title · count) on the left, the toolbar right-aligned in
1053
1603
  * the SAME row. It was two stacked rows with `mb="xs"` on each, so a titled table with search
@@ -1074,11 +1624,17 @@ export function BasaltDataTable<T>({
1074
1624
  ) : null}
1075
1625
  {showToolbar && (
1076
1626
  <CtlSlot>
1077
- <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
+ >
1078
1634
  {showSearch && (
1079
1635
  <TextInput
1080
1636
  radius="md"
1081
- w={SEARCH_WIDTH}
1637
+ style={TABLE_SEARCH_STYLE}
1082
1638
  placeholder={globalFilterPlaceholder}
1083
1639
  leftSection={searchIcon}
1084
1640
  value={globalFilter}
@@ -1088,7 +1644,7 @@ export function BasaltDataTable<T>({
1088
1644
  {showFacets && (
1089
1645
  <FilterSet>
1090
1646
  {facets?.map((facet) => {
1091
- const column = table.getColumn(facet.columnId)
1647
+ const column = resolveFacetColumn(table, facet)
1092
1648
  if (!column) return null
1093
1649
  return facet.multiple ? (
1094
1650
  <MultiSelectFilter
@@ -1106,13 +1662,27 @@ export function BasaltDataTable<T>({
1106
1662
  })}
1107
1663
  </FilterSet>
1108
1664
  )}
1109
- {actions}
1665
+ {actions !== undefined && <BarActionSlot actions={actions} />}
1110
1666
  </Group>
1111
1667
  </CtlSlot>
1112
1668
  )}
1113
1669
  </Group>
1114
1670
  )}
1115
- {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
+ ) : (
1116
1686
  <Table.ScrollContainer
1117
1687
  type="native"
1118
1688
  minWidth={minWidth ?? 0}
@@ -1120,13 +1690,16 @@ export function BasaltDataTable<T>({
1120
1690
  >
1121
1691
  {tableNode}
1122
1692
  </Table.ScrollContainer>
1123
- ) : enablePinning ? (
1124
- <Box style={{ overflowX: 'auto' }}>{tableNode}</Box>
1125
- ) : (
1126
- tableNode
1127
1693
  )}
1128
1694
  {enablePagination && (
1129
- <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
+ >
1130
1703
  <Text style={RANGE_LABEL_STYLE}>
1131
1704
  Showing {rangeStart}–{rangeEnd}
1132
1705
  {totalIsAuthoritative ? ` of ${total}` : ''}
@@ -1151,6 +1724,6 @@ export function BasaltDataTable<T>({
1151
1724
  </Group>
1152
1725
  </Group>
1153
1726
  )}
1154
- </>
1727
+ </div>
1155
1728
  )
1156
1729
  }