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
@@ -80,16 +80,30 @@ import { fileURLToPath } from 'node:url'
80
80
 
81
81
  // ── Shared helpers ──────────────────────────────────────────────────────────────────────────────
82
82
 
83
+ /**
84
+ * Guard kinds and plugin rules RETIRED — kept recognized by `KNOWN_RULE_IDS` (below) and the
85
+ * guard's own `PLUGIN_RULE_IDS` copy so an existing `theme-allow <id>` reads as a dead waiver via
86
+ * `check-theme --audit-allows`, never as a typo.
87
+ *
88
+ * - `unframed-chart` (regex: a `<ChartLegend items={[...]}>` array literal) →
89
+ * `basalt/chart-legend-literal`, which also catches the `.map()`-over-a-non-series form the
90
+ * regex never could reach.
91
+ * - `duplicate-notifications-mount` — its only case, `<BasaltNotifications />` mounted beside
92
+ * `<BasaltOverlays notifications>`, is unreachable now that `BasaltNotifications` no longer
93
+ * exists as a standalone export; the rule had nothing left to fire on.
94
+ */
95
+ export const RETIRED_RULE_IDS = new Set(['unframed-chart', 'duplicate-notifications-mount'])
96
+
83
97
  /**
84
98
  * Rule ids a `theme-allow` may name — this plugin's own rules plus `src/guard`'s kinds. Duplicated
85
99
  * by hand for the same reason `majorOf` is: this file loads standalone out of a consumer's
86
100
  * node_modules and must not import from the package.
87
101
  *
88
102
  * Exported, and `oxlint-plugin.test.ts` asserts it is EXACTLY the plugin's own rule ids plus
89
- * `GUARD_RULES`'s keys. Forgetting an entry here is not a cosmetic omission — an id this set does
90
- * not know is treated as a typo, so the annotation naming it waives nothing (see
91
- * {@link parseThemeAllow}); before the fail-closed change it was worse still, silently widening to
92
- * a blanket waiver.
103
+ * `GUARD_RULES`'s keys plus `RETIRED_RULE_IDS`. Forgetting an entry here is not a cosmetic
104
+ * omission — an id this set does not know is treated as a typo, so the annotation naming it waives
105
+ * nothing (see {@link parseThemeAllow}); before the fail-closed change it was worse still, silently
106
+ * widening to a blanket waiver.
93
107
  */
94
108
  export const KNOWN_RULE_IDS = new Set([
95
109
  // this plugin
@@ -104,16 +118,23 @@ export const KNOWN_RULE_IDS = new Set([
104
118
  'raw-scroll-container',
105
119
  'hand-rolled-filter',
106
120
  'control-outside-home',
121
+ 'bound-control-outside-home',
107
122
  'control-size-literal',
108
123
  'page-bar-budget',
109
124
  'responsive-twin',
110
125
  'search-literal-link',
111
126
  'use-search-from-literal',
127
+ 'provider-above-router',
128
+ 'query-dual-import',
129
+ 'query-fn-unwrap',
130
+ 'deprecated-export',
131
+ 'forms-field-key',
112
132
  // `in-body-page-title` is BOTH a plugin rule and a guard kind — one id, two lanes, so one
113
133
  // annotation waives both. It is listed once, below, with the guard kinds.
114
134
  'visx-boundary',
115
135
  'visx-tooltip',
116
136
  'token-layer-boundary',
137
+ 'no-import-meta-env',
117
138
  // These three honour `basalt-agent-allow`, never `theme-allow` — but they are real ids, so an
118
139
  // annotation naming one must parse as a (useless) scoped annotation rather than as prose.
119
140
  'agent-resume-guard',
@@ -135,7 +156,6 @@ export const KNOWN_RULE_IDS = new Set([
135
156
  'inline-display',
136
157
  'raw-visx-axis',
137
158
  'raw-motion-value',
138
- 'unframed-chart',
139
159
  'chart-missing-aria-label',
140
160
  'raw-form-control',
141
161
  'sub-16-input-font',
@@ -147,6 +167,9 @@ export const KNOWN_RULE_IDS = new Set([
147
167
  'hidden-inline-style',
148
168
  'in-body-page-title',
149
169
  'raw-selection-control',
170
+ // Retired guard kinds — no longer in GUARD_RULES, but a `theme-allow` naming one must still
171
+ // parse as a real (dead) waiver rather than an unknown-id typo. See RETIRED_RULE_IDS above.
172
+ ...RETIRED_RULE_IDS,
150
173
  ])
151
174
 
152
175
  const ALLOW_RULE_TOKEN = /^(?:basalt\/)?([a-z][a-z0-9-]*)(?=$|[\s,:—–])/
@@ -443,6 +466,16 @@ function isInStyleContext(context, node) {
443
466
  return false
444
467
  }
445
468
 
469
+ /**
470
+ * No `/src/tokens/` exemption, and none is needed: both rules below only ever visit `JSXAttribute`/
471
+ * `Property` nodes, and `src/tokens/**` is pure data — `.ts`, never `.tsx`, zero React — so the
472
+ * visitor structurally cannot fire there. A path-based exemption used to sit here gated on
473
+ * `isBasaltScopedFile`, which tests "does the nearest package.json depend on basalt-ui", not
474
+ * "is this file basalt's own" — a consumer package that merely depends on basalt-ui could put raw
475
+ * literals under its own `src/tokens/` and silence both rules. Deleting the exemption is simpler
476
+ * than re-gating it on package identity: the simplest gate is no gate.
477
+ */
478
+ // Ships: error
446
479
  const noRawFontSize = {
447
480
  meta: {
448
481
  type: 'suggestion',
@@ -452,7 +485,7 @@ const noRawFontSize = {
452
485
  schema: [],
453
486
  },
454
487
  create(context) {
455
- if (getFilename(context).includes('/src/tokens/') || isTestFile(context)) return {}
488
+ if (isTestFile(context)) return {}
456
489
 
457
490
  return {
458
491
  JSXAttribute(node) {
@@ -496,16 +529,19 @@ const SIZE_ATTRS = new Set(['size', 'fz', 'fontSize'])
496
529
  * which is not always a font size — on `Text` it is, on `ThemeIcon`/`ActionIcon` it is a box
497
530
  * dimension — so one shared id would mean a consumer silencing the icon case also silences every
498
531
  * off-scale font size, the bundled-rule mistake the three boundary rules were split apart to avoid.
499
- * And a separate id can carry its own severity: this rule ships `warn` in the consumer preset for
500
- * one minor (the grace-minor doctrine in the package CLAUDE.md — it rejects code that previously
501
- * passed, and majors are banned here, so a consumer has no semver channel warning them). Widening
502
- * `no-raw-font-size` in place would have promoted it to `error` on upgrade with no such runway.
532
+ * And a separate id can carry its own severity: this rule ships `error` in the consumer preset —
533
+ * it is a NEW form the sibling rule never caught (a numeric-only check let every string form walk
534
+ * straight past it), not a narrowing of code that previously passed, so the grace-minor doctrine in
535
+ * the package CLAUDE.md does not apply to it. Widening `no-raw-font-size` in place would have meant
536
+ * the widened check inherited whatever level the sibling id already carried, with no way to give
537
+ * this specific form its own severity.
503
538
  *
504
539
  * Numeric `size={32}` is deliberately NOT flagged. It is the documented Mantine idiom for icon
505
540
  * dimensions and flagging it would fire on nearly every icon in an app for no design-system gain —
506
541
  * the token system has an opinion about type scale and spacing, not about how many pixels wide one
507
542
  * glyph box is.
508
543
  */
544
+ // Ships: error
509
545
  const rawSizeLiteral = {
510
546
  meta: {
511
547
  type: 'suggestion',
@@ -515,7 +551,7 @@ const rawSizeLiteral = {
515
551
  schema: [],
516
552
  },
517
553
  create(context) {
518
- if (getFilename(context).includes('/src/tokens/')) return {}
554
+ if (isTestFile(context)) return {}
519
555
 
520
556
  return {
521
557
  JSXAttribute(node) {
@@ -546,6 +582,7 @@ function attrValue(attr) {
546
582
  return unwrapExpressionContainer(attr.value)
547
583
  }
548
584
 
585
+ // Ships: error
549
586
  const cardInset = {
550
587
  meta: {
551
588
  type: 'suggestion',
@@ -635,6 +672,7 @@ function subtreeHasChart(node) {
635
672
  return false
636
673
  }
637
674
 
675
+ // Ships: error
638
676
  const chartInRawSurface = {
639
677
  meta: {
640
678
  type: 'suggestion',
@@ -720,6 +758,7 @@ function enclosingHomeOwner(node) {
720
758
  * code is small enough to fix rather than schedule. A consumer who disagrees turns the whole rule
721
759
  * down in their own config.
722
760
  */
761
+ // Ships: error
723
762
  const rawScrollContainer = {
724
763
  meta: {
725
764
  type: 'suggestion',
@@ -854,6 +893,7 @@ function boundaryVisitor(reportFor) {
854
893
  // segment named `charts` is deliberately permissive, not restrictive — it only ever widens where
855
894
  // visx is allowed, never narrows where it's flagged elsewhere. "Your visx lives in a charts/ dir"
856
895
  // is the intended convention.
896
+ // Ships: error
857
897
  const visxBoundary = {
858
898
  meta: {
859
899
  type: 'suggestion',
@@ -885,6 +925,7 @@ const visxBoundary = {
885
925
  // Rule 6 — visx-tooltip: @visx/tooltip banned everywhere, including inside charts. Fires ahead of
886
926
  // (and instead of) basalt/visx-boundary for that one specifier — basalt's own src/ never imports
887
927
  // @visx/tooltip, so no theme-allow-style exemption is needed here either.
928
+ // Ships: error
888
929
  const visxTooltip = {
889
930
  meta: {
890
931
  type: 'suggestion',
@@ -908,6 +949,7 @@ const visxTooltip = {
908
949
  // @mantine/* installed (see the file-header comment for the cycle/fork + packaging rationale).
909
950
  // Repo-local only — never registered in the shipped consumer preset, since this governs basalt's
910
951
  // own internal layering/packaging, not a consumer contract.
952
+ // Ships: repo-local only
911
953
  const tokenLayerBoundary = {
912
954
  meta: {
913
955
  type: 'suggestion',
@@ -969,6 +1011,7 @@ function propertyKeyName(node) {
969
1011
 
970
1012
  // Deliberately NO `hasThemeAllow` escape — see basalt/visx-boundary's comment for why these
971
1013
  // guards use their own token (`basalt-agent-allow`, see the file-header comment) instead.
1014
+ // Ships: error
972
1015
  const agentResumeGuard = {
973
1016
  meta: {
974
1017
  type: 'problem',
@@ -1015,6 +1058,7 @@ const RAW_USE_CHAT_SOURCES = new Set(['@ai-sdk/react', 'ai/react'])
1015
1058
  const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion'])
1016
1059
 
1017
1060
  // Deliberately NO `hasThemeAllow` escape — see the file-header comment.
1061
+ // Ships: error
1018
1062
  const agentNoRawUseChat = {
1019
1063
  meta: {
1020
1064
  type: 'problem',
@@ -1210,6 +1254,7 @@ function aiSdkMajorMessage(consumerMajor) {
1210
1254
  // version skew is still a fact about two package.json files, but a written, intentional
1211
1255
  // producer/consumer pair (see `doctor`'s `aiMajorSkewReason`) needs a way to say so at the import
1212
1256
  // site too, not just be permanently blocked.
1257
+ // Ships: error
1213
1258
  const aiSdkMajor = {
1214
1259
  meta: {
1215
1260
  type: 'problem',
@@ -1302,6 +1347,7 @@ const HAND_ROLLED_PLOT_MESSAGE =
1302
1347
  'the file. To waive just THIS node and stay policed on the rest, write `theme-allow ' +
1303
1348
  'hand-rolled-plot — <why>` on its own line above it. (basalt/hand-rolled-plot)'
1304
1349
 
1350
+ // Ships: error
1305
1351
  const handRolledPlot = {
1306
1352
  meta: {
1307
1353
  type: 'suggestion',
@@ -1412,6 +1458,7 @@ function isMapCall(node) {
1412
1458
  )
1413
1459
  }
1414
1460
 
1461
+ // Ships: error
1415
1462
  const chartLegendLiteral = {
1416
1463
  meta: {
1417
1464
  type: 'suggestion',
@@ -1678,6 +1725,7 @@ function shadowBasaltExportMessage(name) {
1678
1725
  * tokens does not. `warn`, not `error`: a name collision is evidence, not proof, and a consumer
1679
1726
  * with a genuinely different `Composer` should be told once, not blocked.
1680
1727
  */
1728
+ // Ships: warn (advisory)
1681
1729
  const shadowBasaltExport = {
1682
1730
  meta: {
1683
1731
  type: 'suggestion',
@@ -1806,6 +1854,7 @@ const HAND_ROLLED_SHELL_MESSAGE =
1806
1854
  * it. The module that DEFINES `BasaltShell` is exempt by declaration, not by path — a rule saying
1807
1855
  * "compose X" cannot fire inside X.
1808
1856
  */
1857
+ // Ships: error
1809
1858
  const handRolledShell = {
1810
1859
  meta: {
1811
1860
  type: 'suggestion',
@@ -1928,11 +1977,26 @@ const BOUND_TAGS = new Set([
1928
1977
  * The names whose DECLARATION means this file defines a basalt control rather than consuming one —
1929
1978
  * the owner exemption, the same shape as `hand-rolled-plot`'s `notesOwnerDefinition`: a rule that
1930
1979
  * says "use a bound control" cannot fire inside the module that IS the bound control.
1980
+ *
1981
+ * **The declaration alone is not enough, and the second half is what makes the set safe to widen.**
1982
+ * `PanelRow`, `EnumFilter` and `SliderControl` are ordinary names a consumer writes for their own
1983
+ * helpers, and a bare-name match let one local `function PanelRow(…)` switch `control-outside-home`,
1984
+ * `bound-control-outside-home` and `responsive-twin` off for that WHOLE file. So the exemption also
1985
+ * requires the file to import NOTHING from `basalt-ui*`: basalt's own control sources import each
1986
+ * other RELATIVELY, while a consumer of basalt always names the package. A file that consumes
1987
+ * basalt is not the module that defines its controls, whatever it calls its helpers.
1931
1988
  */
1932
1989
  const CONTROL_OWNER_NAMES = new Set([
1933
1990
  ...BOUND_TAGS,
1934
1991
  'FilterSet',
1935
1992
  'FilterPill',
1993
+ // The INTERNAL control modules, listed for the same reason the public names are: `EnumFilter` is
1994
+ // the shared body behind SelectFilter/CompareFilter/NumberFilter, and `PanelRow`/`SliderControl`
1995
+ // are the aside's row primitives (`docs/ASIDE-SPEC.md` §3). A module that IS a bound control
1996
+ // cannot be told to use one. This is a RELAXATION, so it needs no grace entry (C16).
1997
+ 'EnumFilter',
1998
+ 'PanelRow',
1999
+ 'SliderControl',
1936
2000
  'SyncButton',
1937
2001
  'ActionGroup',
1938
2002
  'OverflowMenu',
@@ -1940,19 +2004,25 @@ const CONTROL_OWNER_NAMES = new Set([
1940
2004
  ])
1941
2005
 
1942
2006
  /**
1943
- * Where a raw selection control legitimately lives outside a TIERED home: a settings row, an
1944
- * overlay, a composer. Every one of these is a place the tier does not apply, so the advice ("move
1945
- * it into a home slot") would be wrong rather than merely unwelcome.
2007
+ * Where a raw selection control legitimately lives outside a TIERED home: a settings row, a form
2008
+ * row, an overlay, a composer. Every one of these is a place the tier does not apply, so the advice
2009
+ * ("move it into a home slot") would be wrong rather than merely unwelcome.
1946
2010
  *
1947
2011
  * `SettingsRow` is the form row — law C1's third home, not an exception to C1 — so
1948
2012
  * `control-outside-home` treating it as a home is the whole of the enforcement a settings page
1949
2013
  * gets, and the two tier rules deliberately do not reach inside it (see {@link SLOT_ATTRS}).
2014
+ * `FormRow`/`FormGroup` (`basalt-ui/forms`, `src/forms/form-layout.tsx`) are the SAME home written
2015
+ * for a `<form>` rather than a settings page — a `TextInput` bound to a field is read and typed
2016
+ * into at Mantine's own tier, not a 30px chrome affordance — so they are added here rather than
2017
+ * given a second walk.
1950
2018
  *
1951
2019
  * Declared with the shared slot walk rather than beside `control-outside-home` because three rules
1952
2020
  * read it: that rule's ancestry exemption, and {@link hostedInsideSlot} for the two tier rules.
1953
2021
  */
1954
2022
  const CONTROL_HOST_TAGS = new Set([
1955
2023
  'SettingsRow',
2024
+ 'FormRow',
2025
+ 'FormGroup',
1956
2026
  'Modal',
1957
2027
  'Drawer',
1958
2028
  'Popover.Dropdown',
@@ -1960,6 +2030,24 @@ const CONTROL_HOST_TAGS = new Set([
1960
2030
  'Composer',
1961
2031
  ])
1962
2032
 
2033
+ /**
2034
+ * The basalt elements whose CHILDREN are a home — the SUBTREE homes, where the body IS the surface
2035
+ * (`docs/ASIDE-SPEC.md` §3). `FilterSet` mounts the pill surface, `PageAside` the panel surface,
2036
+ * `PanelRow` the row itself (its `end` prop resolves here too: the attribute hangs off `PanelRow`'s
2037
+ * own opening element, so the ancestry walk reaches the element).
2038
+ *
2039
+ * Read by BOTH C1 rules, because the two must not disagree about what a home IS — before this set
2040
+ * existed `bound-control-outside-home` treated all three as homes and `control-outside-home` knew
2041
+ * none of them, so a raw `<Select>` inside a `<PageAside><PanelRow>` reported while the bound
2042
+ * control beside it did not.
2043
+ *
2044
+ * Unlike {@link CONTROL_HOST_TAGS} these are PROVENANCE-gated, the same test
2045
+ * {@link SLOT_OWNER_TAGS} applies and for the same reason: `PanelRow` is an ordinary name for a
2046
+ * consumer's own layout helper, and reading one as a basalt home would exempt a genuinely homeless
2047
+ * control. The overlay hosts stay name-matched because `Modal`/`Drawer` are Mantine's, not basalt's.
2048
+ */
2049
+ const BASALT_HOST_TAGS = new Set(['FilterSet', 'PageAside', 'PanelRow'])
2050
+
1963
2051
  /**
1964
2052
  * A file whose NAME declares it is an overlay's or a form's own body — the cross-file half of
1965
2053
  * {@link CONTROL_HOST_TAGS}, and the one exemption here that is a CONVENTION rather than a fact
@@ -1978,10 +2066,32 @@ const CONTROL_HOST_TAGS = new Set([
1978
2066
  * with 9 known false positives. `-panel.tsx` is the loosest member and the one to reconsider first
1979
2067
  * if the set ever needs narrowing.
1980
2068
  *
2069
+ * TWO dialects, because a repo picks one file-naming law and the convention has to survive either:
2070
+ * `edit-session-modal.tsx` (kebab) and `CbbiPanel.tsx` (PascalCase — what a repo mandating
2071
+ * `PascalCase.tsx` for component files writes, basalt's own root `CLAUDE.md` included). The kebab
2072
+ * form alone could never match a component file in such a repo, which is exactly how the CBBI panel
2073
+ * — a `PageAside` child whose `<PageAside>` is in the PARENT page — collected four warns that no
2074
+ * ancestry walk in that file could ever have avoided.
2075
+ *
2076
+ * Both dialects require the leading SUBJECT: `Panel.tsx` and `modal.tsx` are page modules in every
2077
+ * consumer that has one, and a bare-noun exemption would switch the rules off wholesale.
2078
+ *
1981
2079
  * Basename only — a `modal/` DIRECTORY is not the convention, because a directory of modals holds
1982
2080
  * the page pieces around them too.
1983
2081
  */
1984
- const OVERLAY_CONVENTION_FILE = /(?:^|\/)[^/]*-(?:modal|drawer|popover|panel|form)\.[jt]sx$/
2082
+ const OVERLAY_CONVENTION_KEBAB = /^[^/]*-(?:modal|drawer|popover|panel|form)\.[jt]sx$/
2083
+ const OVERLAY_CONVENTION_PASCAL = /^[A-Z]\w*(?:Modal|Drawer|Popover|Panel|Form)\.[jt]sx$/
2084
+
2085
+ /**
2086
+ * The single predicate both `control-outside-home` and `bound-control-outside-home` ask — see
2087
+ * {@link OVERLAY_CONVENTION_KEBAB} for the whole trade. One function, so the two rules can never
2088
+ * disagree about what the convention IS; `src/guard/index.ts` carries the text-lane copy of the
2089
+ * same two dialects (`raw-selection-control`), pinned by both test files.
2090
+ */
2091
+ function isOverlayConventionFile(filename) {
2092
+ const basename = filename.slice(filename.lastIndexOf('/') + 1)
2093
+ return OVERLAY_CONVENTION_KEBAB.test(basename) || OVERLAY_CONVENTION_PASCAL.test(basename)
2094
+ }
1985
2095
 
1986
2096
  /** How far an ancestry walk climbs before giving up — a JSX tree this deep is not a slot value. */
1987
2097
  const ANCESTRY_MAX_DEPTH = 60
@@ -2190,6 +2300,34 @@ function hasAncestorTag(node, tags) {
2190
2300
  return false
2191
2301
  }
2192
2302
 
2303
+ /**
2304
+ * The BINDINGS of every JSX ancestor of `node`, nearest first — {@link hasAncestorTag} for a
2305
+ * PROVENANCE-gated set. It returns names rather than a boolean for the same reason
2306
+ * {@link slotOwnerBinding} does: whether that `PanelRow` is basalt's is answered by the import list,
2307
+ * which is only complete at `Program:exit`, while the ancestry is only walkable during the visit.
2308
+ *
2309
+ * `tags` is OPTIONAL, and omitting it is the second real use rather than a convenience: a rule
2310
+ * whose ancestor set is not known until the import list is complete cannot filter by tag NAME at
2311
+ * all (`RouterProvider as Router` is an ordinary thing to write), so it takes every binding and
2312
+ * resolves them at exit. `provider-above-router` is that caller.
2313
+ */
2314
+ function ancestorTagBindings(node, tags) {
2315
+ const found = []
2316
+ let current = node.parent
2317
+ for (let depth = 0; current !== null && current !== undefined; depth++) {
2318
+ if (depth > ANCESTRY_MAX_DEPTH || current.type === 'Program') break
2319
+ if (
2320
+ current.type === 'JSXElement' &&
2321
+ (tags === undefined || tags.has(jsxTagName(current.openingElement?.name) ?? ''))
2322
+ ) {
2323
+ const root = jsxRootName(current.openingElement?.name)
2324
+ if (root !== undefined) found.push(root)
2325
+ }
2326
+ current = current.parent
2327
+ }
2328
+ return found
2329
+ }
2330
+
2193
2331
  /** Identifiers a slot attribute's value resolves through — `filters={pills}`, `actions={[a, b]}`. */
2194
2332
  function collectSlotValueIdentifiers(node, into, depth = 0) {
2195
2333
  if (node === null || node === undefined || depth > 8) return
@@ -2239,6 +2377,14 @@ function createSlotContext(context) {
2239
2377
  * depending on JSX order.
2240
2378
  */
2241
2379
  const slotBoundIdentifiers = new Map()
2380
+ /**
2381
+ * Hoisted identifier → every SUBTREE-home BINDING whose children it was rendered as.
2382
+ *
2383
+ * The same hop `slotBoundIdentifiers` makes, one home shape over: `const rows = <SelectFilter/>`
2384
+ * handed to `<PageAside>{rows}</PageAside>` is inside that aside, and the ancestry walk cannot
2385
+ * see it because at the `const` there is no `PageAside` above the node at all.
2386
+ */
2387
+ const subtreeHostedIdentifiers = new Map()
2242
2388
  return {
2243
2389
  /** Feed every ImportDeclaration here — this is where the owner provenance comes from. */
2244
2390
  noteImport(node) {
@@ -2256,12 +2402,32 @@ function createSlotContext(context) {
2256
2402
  else owners.add(owner)
2257
2403
  }
2258
2404
  },
2405
+ /**
2406
+ * Feed every JSXExpressionContainer here — a `{expr}` CHILD of a subtree home
2407
+ * ({@link BASALT_HOST_TAGS}) hosts the identifiers that expression resolves through.
2408
+ */
2409
+ noteChild(node) {
2410
+ const parent = node.parent
2411
+ if (parent === null || parent === undefined || parent.type !== 'JSXElement') return
2412
+ const name = parent.openingElement?.name
2413
+ if (!BASALT_HOST_TAGS.has(jsxTagName(name) ?? '')) return
2414
+ const owner = jsxRootName(name)
2415
+ if (owner === undefined) return
2416
+ const identifiers = new Set()
2417
+ collectSlotValueIdentifiers(node.expression, identifiers)
2418
+ for (const id of identifiers) {
2419
+ const owners = subtreeHostedIdentifiers.get(id)
2420
+ if (owners === undefined) subtreeHostedIdentifiers.set(id, new Set([owner]))
2421
+ else owners.add(owner)
2422
+ }
2423
+ },
2259
2424
  /** Snapshot `node`'s ancestry while it is walkable — the owner NAME, resolved later. */
2260
2425
  capture(node) {
2261
2426
  const attr = enclosingSlotAttribute(node)
2262
2427
  return {
2263
2428
  directOwner: attr === undefined ? undefined : slotOwnerBinding(attr),
2264
2429
  declaratorName: enclosingDeclaratorName(node),
2430
+ subtreeHosts: ancestorTagBindings(node, BASALT_HOST_TAGS),
2265
2431
  }
2266
2432
  },
2267
2433
  /** Is a captured node inside a BASALT home slot — directly, or via a hoisted binding? */
@@ -2281,6 +2447,21 @@ function createSlotContext(context) {
2281
2447
  isHome(name) {
2282
2448
  return name !== undefined && homes.has(name)
2283
2449
  },
2450
+ /**
2451
+ * Is a captured node inside a basalt SUBTREE home — directly by ancestry, or via a hoisted
2452
+ * binding rendered as one's `{expr}` child? Both halves resolve the binding through `homes`,
2453
+ * so a consumer's own `PanelRow` is not one.
2454
+ */
2455
+ inSubtreeHome(captured) {
2456
+ for (const binding of captured.subtreeHosts) {
2457
+ if (homes.has(binding)) return true
2458
+ }
2459
+ if (captured.declaratorName === undefined) return false
2460
+ for (const owner of subtreeHostedIdentifiers.get(captured.declaratorName) ?? []) {
2461
+ if (homes.has(owner)) return true
2462
+ }
2463
+ return false
2464
+ },
2284
2465
  /**
2285
2466
  * EVERY basalt EXPORT whose slot a captured node sits in — `['ChartCard']` for
2286
2467
  * `<ChartCard actions={…}>` and for `<Card actions={…}>` under an alias, `[]` when the node is in
@@ -2303,14 +2484,24 @@ function createSlotContext(context) {
2303
2484
  }
2304
2485
  }
2305
2486
 
2306
- /** Collects an owner-definition flag for the control layer — see {@link CONTROL_OWNER_NAMES}. */
2487
+ /** Collects the owner-definition verdict for the control layer — see {@link CONTROL_OWNER_NAMES}. */
2307
2488
  function createControlOwnerProbe() {
2308
- const state = { definesControl: false }
2489
+ const state = { definesControl: false, importsBasalt: false }
2309
2490
  const note = (name) => {
2310
2491
  if (typeof name === 'string' && CONTROL_OWNER_NAMES.has(name)) state.definesControl = true
2311
2492
  }
2312
2493
  return {
2313
- state,
2494
+ /** Feed every ImportDeclaration here — a `basalt-ui*` import disqualifies the exemption. */
2495
+ noteImport(node) {
2496
+ const source = node.source?.value
2497
+ if (typeof source === 'string' && BASALT_IMPORT_SOURCE.test(source)) {
2498
+ state.importsBasalt = true
2499
+ }
2500
+ },
2501
+ /** Does this file DEFINE a basalt control rather than consume one? Both halves are required. */
2502
+ isOwner() {
2503
+ return state.definesControl && !state.importsBasalt
2504
+ },
2314
2505
  visitors: {
2315
2506
  FunctionDeclaration(node) {
2316
2507
  note(node.id?.name)
@@ -2343,6 +2534,7 @@ const HAND_ROLLED_FILTER_MESSAGE =
2343
2534
  * `SettingsRow.control`, the form-row home — see {@link SLOT_ATTRS} for why a raw input is right
2344
2535
  * there.
2345
2536
  */
2537
+ // Ships: error
2346
2538
  const handRolledFilter = {
2347
2539
  meta: {
2348
2540
  type: 'problem',
@@ -2392,9 +2584,9 @@ const handRolledFilter = {
2392
2584
  const CONTROL_OUTSIDE_HOME_MESSAGE =
2393
2585
  'Raw Mantine selection control with no home — a filter, tab or action belongs in exactly one of ' +
2394
2586
  'the three homes (a PageBar / Section / WidgetHeader slot, or a form row), and a home is entered ' +
2395
- 'through a slot prop (law C1). A settings row, an overlay (Modal/Drawer/Popover/Menu) and a ' +
2396
- 'form (@mantine/form) are the declared non-homes and never report. ' +
2397
- '(basalt/control-outside-home)'
2587
+ 'through a slot prop (law C1). A settings row, a form row (FormRow/FormGroup), an overlay ' +
2588
+ '(Modal/Drawer/Popover/Menu) and a form (@mantine/form) are the declared non-homes and never ' +
2589
+ 'report. (basalt/control-outside-home)'
2398
2590
 
2399
2591
  /**
2400
2592
  * The cross-file half of C1, and the one rule here that is openly a HEURISTIC: "this control has
@@ -2402,11 +2594,13 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
2402
2594
  * and stays warn until the playground and the five consumer repos run it with ≤3 waivers
2403
2595
  * (docs/CONTROLS-SPEC.md §9 wave 7).
2404
2596
  *
2405
- * Four exemptions carry the false-positive load: an overlay/settings-row ancestor, an
2406
- * {@link OVERLAY_CONVENTION_FILE} basename (the CROSS-FILE case — the `<Modal>` lives in the
2407
- * parent), a file that imports `@mantine/form` (a form is the third home and its inputs are not
2408
- * filters), and the owner exemption — a file DEFINING a basalt control cannot be told to use one.
2597
+ * Four exemptions carry the false-positive load: an overlay/settings-row/form-row ancestor
2598
+ * ({@link CONTROL_HOST_TAGS}, `FormRow`/`FormGroup` included), an {@link isOverlayConventionFile}
2599
+ * basename (the CROSS-FILE case — the `<Modal>` lives in the parent), a file that imports
2600
+ * `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
2601
+ * exemption — a file DEFINING a basalt control cannot be told to use one.
2409
2602
  */
2603
+ // Ships: warn (grace → 1.30.0)
2410
2604
  const controlOutsideHome = {
2411
2605
  meta: {
2412
2606
  type: 'suggestion',
@@ -2415,7 +2609,7 @@ const controlOutsideHome = {
2415
2609
  },
2416
2610
  create(context) {
2417
2611
  if (isTestFile(context)) return {}
2418
- if (OVERLAY_CONVENTION_FILE.test(getFilename(context))) return {}
2612
+ if (isOverlayConventionFile(getFilename(context))) return {}
2419
2613
  const slots = createSlotContext(context)
2420
2614
  const owner = createControlOwnerProbe()
2421
2615
  const mantineImports = new Map()
@@ -2427,11 +2621,15 @@ const controlOutsideHome = {
2427
2621
  ImportDeclaration(node) {
2428
2622
  collectMantineImports(node, mantineImports)
2429
2623
  slots.noteImport(node)
2624
+ owner.noteImport(node)
2430
2625
  if ((node.source?.value ?? '') === '@mantine/form') importsMantineForm = true
2431
2626
  },
2432
2627
  JSXAttribute(node) {
2433
2628
  slots.note(node)
2434
2629
  },
2630
+ JSXExpressionContainer(node) {
2631
+ slots.noteChild(node)
2632
+ },
2435
2633
  JSXOpeningElement(node) {
2436
2634
  // Cheap pre-filter: the tag as WRITTEN, or any Mantine binding (an alias resolves at exit).
2437
2635
  const root = jsxRootName(node.name)
@@ -2444,11 +2642,11 @@ const controlOutsideHome = {
2444
2642
  })
2445
2643
  },
2446
2644
  'Program:exit'() {
2447
- if (importsMantineForm || owner.state.definesControl) return
2645
+ if (importsMantineForm || owner.isOwner()) return
2448
2646
  for (const { node, name, captured, hosted } of candidates) {
2449
2647
  const tag = resolveMantineTag(name, mantineImports)
2450
2648
  if (tag === undefined || !RAW_FILTER_TAGS.has(tag)) continue
2451
- if (hosted || slots.resolve(captured)) continue
2649
+ if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
2452
2650
  if (hasThemeAllow(context, node, 'control-outside-home')) continue
2453
2651
  context.report({ node, message: CONTROL_OUTSIDE_HOME_MESSAGE })
2454
2652
  }
@@ -2536,6 +2734,7 @@ export const CTL_THEME_TAGS = new Set([
2536
2734
  */
2537
2735
  const TIERLESS_SLOT_OWNERS = new Set(['ChartCard'])
2538
2736
 
2737
+ // Ships: error
2539
2738
  const controlSizeLiteral = {
2540
2739
  meta: {
2541
2740
  type: 'suggestion',
@@ -2655,6 +2854,7 @@ function actionsAttributeValue(node) {
2655
2854
  * The four countable halves of C6. `error` from the start: every one of them is arithmetic over a
2656
2855
  * literal the file wrote down, with no heuristic and no layout intent to guess at.
2657
2856
  */
2857
+ // Ships: error
2658
2858
  const pageBarBudget = {
2659
2859
  meta: {
2660
2860
  type: 'problem',
@@ -2770,6 +2970,7 @@ function titleOrderOf(node) {
2770
2970
  * lanes on purpose, so one `theme-allow in-body-page-title — <why>` waives both rather than
2771
2971
  * needing a different word per enforcement surface.
2772
2972
  */
2973
+ // Ships: error
2773
2974
  const inBodyPageTitle = {
2774
2975
  meta: {
2775
2976
  type: 'suggestion',
@@ -2842,6 +3043,7 @@ function subtreeControlTags(node, into = new Set(), depth = 0) {
2842
3043
  * subtrees both render the same control tag. A file that DEFINES a basalt control is exempt — the
2843
3044
  * one legitimate double mount is the CSS swap inside the control itself.
2844
3045
  */
3046
+ // Ships: error
2845
3047
  const responsiveTwin = {
2846
3048
  meta: {
2847
3049
  type: 'suggestion',
@@ -2878,10 +3080,13 @@ const responsiveTwin = {
2878
3080
 
2879
3081
  return {
2880
3082
  ...owner.visitors,
3083
+ ImportDeclaration(node) {
3084
+ owner.noteImport(node)
3085
+ },
2881
3086
  JSXElement: inspectChildren,
2882
3087
  JSXFragment: inspectChildren,
2883
3088
  'Program:exit'() {
2884
- if (owner.state.definesControl) return
3089
+ if (owner.isOwner()) return
2885
3090
  for (const node of twins) {
2886
3091
  if (hasThemeAllow(context, node, 'responsive-twin')) continue
2887
3092
  context.report({ node, message: RESPONSIVE_TWIN_MESSAGE })
@@ -2920,6 +3125,7 @@ function insideNavDefinition(node) {
2920
3125
  return false
2921
3126
  }
2922
3127
 
3128
+ // Ships: error
2923
3129
  const searchLiteralLink = {
2924
3130
  meta: {
2925
3131
  type: 'suggestion',
@@ -2953,6 +3159,7 @@ const USE_SEARCH_FROM_LITERAL_MESSAGE =
2953
3159
  '(law C10) — take the value as a prop, or read it through the store field. ' +
2954
3160
  '(basalt/use-search-from-literal)'
2955
3161
 
3162
+ // Ships: error
2956
3163
  const useSearchFromLiteral = {
2957
3164
  meta: {
2958
3165
  type: 'suggestion',
@@ -2979,6 +3186,648 @@ const useSearchFromLiteral = {
2979
3186
  },
2980
3187
  }
2981
3188
 
3189
+ // ── Rule 24 — bound-control-outside-home ────────────────────────────────────────────────────────
3190
+
3191
+ /**
3192
+ * A bound basalt control with no home — the pill-shaped controls, and `SliderControl` is
3193
+ * deliberately NOT among them: it has no pill form at all. It always renders a `PanelRow`
3194
+ * (`docs/ASIDE-SPEC.md` §3), so a `<Section>` body or a plain page stack is a legitimate place to
3195
+ * write one and "this renders as a stray pill" would be false there. {@link BOUND_TAGS} is the
3196
+ * whole set; every member is a `FieldHandle`-bound `basalt-ui/controls` export, so unlike
3197
+ * {@link RAW_FILTER_TAGS} there is no question of what it is FOR — only of where it was written.
3198
+ */
3199
+ const BOUND_CONTROL_OUTSIDE_HOME_MESSAGE =
3200
+ 'Bound basalt control with no home — a SelectFilter / ToggleFilter / RangeFilter / ' +
3201
+ 'MultiSelectFilter / NumberFilter / SearchFilter / ViewTabs renders as a stray pill when it is ' +
3202
+ 'written into a Section body or a page stack. It belongs in a home slot (`filters` / `tabs` / ' +
3203
+ '`filtersEnd` / `actions` / `sync` / `control`), inside a <FilterSet>, or inside a <PageAside> ' +
3204
+ '— where the panel surface renders it as a row instead (law C1, docs/ASIDE-SPEC.md §3). ' +
3205
+ 'SliderControl is not policed: it renders its own PanelRow and has no pill form. An overlay ' +
3206
+ '(Modal/Drawer/Popover/Menu), a settings row and a form row (FormRow/FormGroup) are the ' +
3207
+ 'declared non-homes and never report. (basalt/bound-control-outside-home)'
3208
+
3209
+ /**
3210
+ * A BOUND basalt control that is in no home at all — ledger G5 (`docs/ASIDE-SPEC.md` §2), the half
3211
+ * of law C1 that had no guard: `control-outside-home` matches raw Mantine tags only, so a
3212
+ * `<SelectFilter field={…}/>` dropped into a `Section` body passed silently and shipped as a stray
3213
+ * pill.
3214
+ *
3215
+ * A NEW id rather than a widening of `control-outside-home`, per C16: a level is per-id, so
3216
+ * widening that rule would have landed this form at whatever level it holds, with no grace of its
3217
+ * own — and widening it in a minor would restart its grace besides.
3218
+ *
3219
+ * Provenance is the tag's basalt IMPORT ({@link collectBasaltImportMap}), never its bare name: a
3220
+ * consumer's own `SelectFilter` is not this one. The exemptions are the sibling rule's minus the
3221
+ * `@mantine/form` one — a form input is a raw input, and a `FieldHandle`-bound filter in a form
3222
+ * module is homeless there exactly as it is anywhere else.
3223
+ *
3224
+ * **There is deliberately NO text-lane twin in `src/guard`.** `raw-selection-control` can key on a
3225
+ * bare Mantine tag name because that name is Mantine's whatever the file does with it; a bound
3226
+ * control's name says nothing without the import graph and the ancestry, and a regex over a
3227
+ * 12-line window has neither. A text heuristic here would report a consumer's own `SelectFilter` in
3228
+ * its own `filters` slot — signal the plugin already has, noise the guard would add.
3229
+ */
3230
+ // Ships: warn (grace → 1.30.0)
3231
+ const boundControlOutsideHome = {
3232
+ meta: {
3233
+ type: 'suggestion',
3234
+ docs: { description: 'Warn on a bound basalt control that is in no basalt home.' },
3235
+ schema: [],
3236
+ },
3237
+ create(context) {
3238
+ if (isTestFile(context)) return {}
3239
+ if (isOverlayConventionFile(getFilename(context))) return {}
3240
+ const slots = createSlotContext(context)
3241
+ const owner = createControlOwnerProbe()
3242
+ const controls = new Map()
3243
+ const ownTree = isBasaltOwnSource(getFilename(context))
3244
+ const candidates = []
3245
+
3246
+ return {
3247
+ ...owner.visitors,
3248
+ ImportDeclaration(node) {
3249
+ collectBasaltImportMap(node, controls, ownTree)
3250
+ slots.noteImport(node)
3251
+ owner.noteImport(node)
3252
+ },
3253
+ JSXAttribute(node) {
3254
+ slots.note(node)
3255
+ },
3256
+ JSXExpressionContainer(node) {
3257
+ slots.noteChild(node)
3258
+ },
3259
+ JSXOpeningElement(node) {
3260
+ // Cheap pre-filter on the tag as WRITTEN; an alias resolves through `controls` at exit.
3261
+ const root = jsxRootName(node.name)
3262
+ if (root === undefined) return
3263
+ if (!BOUND_TAGS.has(jsxTagName(node.name) ?? '') && !controls.has(root)) return
3264
+ candidates.push({
3265
+ node,
3266
+ root,
3267
+ captured: slots.capture(node),
3268
+ hosted: hasAncestorTag(node, CONTROL_HOST_TAGS),
3269
+ })
3270
+ },
3271
+ 'Program:exit'() {
3272
+ if (owner.isOwner()) return
3273
+ for (const { node, root, captured, hosted } of candidates) {
3274
+ const exported = controls.get(root)
3275
+ if (exported === undefined || !BOUND_TAGS.has(exported)) continue
3276
+ if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
3277
+ if (hasThemeAllow(context, node, 'bound-control-outside-home')) continue
3278
+ context.report({ node, message: BOUND_CONTROL_OUTSIDE_HOME_MESSAGE })
3279
+ }
3280
+ },
3281
+ }
3282
+ },
3283
+ }
3284
+
3285
+ // ── Rule 25 — provider-above-router ─────────────────────────────────────────────────────────────
3286
+
3287
+ /** `@tanstack/react-router` and its subpaths — never `@tanstack/react-router-devtools`. */
3288
+ const TANSTACK_ROUTER_SOURCE = /^@tanstack\/react-router(?:\/|$)/
3289
+
3290
+ const PROVIDER_ABOVE_ROUTER_MESSAGE =
3291
+ '<BasaltProvider> is rendered INSIDE <RouterProvider> — invert them. Mantine context has to ' +
3292
+ 'exist before any route renders, so the mount order is BasaltProvider → the data layer → ' +
3293
+ 'RouterProvider (agent/rules/basalt-mantine.md). A route that renders a themed component ' +
3294
+ 'before the provider mounts reads Mantine defaults, not the basalt theme. ' +
3295
+ '(basalt/provider-above-router)'
3296
+
3297
+ /**
3298
+ * `BasaltProvider` mounted BELOW a TanStack `RouterProvider` — F5, the first of the three
3299
+ * `basalt-mantine.md` laws that shipped under a `not guarded: —` banner.
3300
+ *
3301
+ * Both tags are provenance-gated: `BasaltProvider` through {@link collectBasaltImportMap} (so a
3302
+ * consumer's own component of that name is not this one) and `RouterProvider` through an
3303
+ * `@tanstack/react-router` import, by its LOCAL binding rather than its written name. The inverse
3304
+ * nesting — the correct one — is not a finding, and neither is either tag alone.
3305
+ *
3306
+ * **STATIC JSX ancestry only, and that is the whole heuristic.** The two providers have to be
3307
+ * written in one JSX tree in one file for this to see anything. The common real shape (`main.tsx`
3308
+ * renders `<BasaltProvider><App/></BasaltProvider>` and `App` renders `<RouterProvider/>`) is
3309
+ * INVERTED cross-file and correct; the inverted-and-wrong cross-file shape is equally invisible.
3310
+ * A rule that followed the composition would need the module graph, which an oxlint rule does not
3311
+ * have — so this catches the one-file case and the law stays advisory for the rest. That is why it
3312
+ * lands at `warn`.
3313
+ */
3314
+ // Ships: warn (grace → 1.30.0)
3315
+ const providerAboveRouter = {
3316
+ meta: {
3317
+ type: 'suggestion',
3318
+ docs: { description: 'Warn on a BasaltProvider rendered inside a TanStack RouterProvider.' },
3319
+ schema: [],
3320
+ },
3321
+ create(context) {
3322
+ if (isTestFile(context)) return {}
3323
+ const ownTree = isBasaltOwnSource(getFilename(context))
3324
+ const basalt = new Map()
3325
+ const routers = new Set()
3326
+ const candidates = []
3327
+
3328
+ return {
3329
+ ImportDeclaration(node) {
3330
+ collectBasaltImportMap(node, basalt, ownTree)
3331
+ const source = node.source?.value
3332
+ if (typeof source !== 'string' || !TANSTACK_ROUTER_SOURCE.test(source)) return
3333
+ for (const spec of node.specifiers ?? []) {
3334
+ const imported = spec.imported?.name ?? spec.imported?.value
3335
+ if (imported === 'RouterProvider' && typeof spec.local?.name === 'string') {
3336
+ routers.add(spec.local.name)
3337
+ }
3338
+ }
3339
+ },
3340
+ JSXOpeningElement(node) {
3341
+ const root = jsxRootName(node.name)
3342
+ if (root === undefined || !basalt.has(root)) return
3343
+ candidates.push({ node, root, ancestors: ancestorTagBindings(node) })
3344
+ },
3345
+ 'Program:exit'() {
3346
+ for (const { node, root, ancestors } of candidates) {
3347
+ if (basalt.get(root) !== 'BasaltProvider') continue
3348
+ if (!ancestors.some((name) => routers.has(name))) continue
3349
+ if (hasThemeAllow(context, node, 'provider-above-router')) continue
3350
+ context.report({ node, message: PROVIDER_ABOVE_ROUTER_MESSAGE })
3351
+ }
3352
+ },
3353
+ }
3354
+ },
3355
+ }
3356
+
3357
+ // ── Rule 27 — query-dual-import ─────────────────────────────────────────────────────────────────
3358
+
3359
+ /** `@tanstack/react-query` and its subpaths — never `@tanstack/react-query-devtools`. */
3360
+ const TANSTACK_QUERY_SOURCE = /^@tanstack\/react-query(?:\/|$)/
3361
+
3362
+ /** The `basalt-ui/query` subpath, exactly — the seam the hooks are supposed to arrive through. */
3363
+ const BASALT_QUERY_SOURCE = 'basalt-ui/query'
3364
+
3365
+ /** The root barrel — where `QueryState` and the dashboard composites live. */
3366
+ const BASALT_ROOT_SOURCE = 'basalt-ui'
3367
+
3368
+ const QUERY_DUAL_IMPORT_MESSAGE =
3369
+ 'Dual query import — this file imports from @tanstack/react-query AND from basalt-ui/query. ' +
3370
+ 'Take the hooks, the client factory and `unwrap` from basalt-ui/query only: two entry points to ' +
3371
+ 'one library is how a second QueryClient, a second devtools bundle and a duplicated ' +
3372
+ '`toErrorMessage` reach an app (agent/rules/basalt-batteries.md). ' +
3373
+ '(basalt/query-dual-import)'
3374
+
3375
+ const QUERY_ROOT_IMPORT_MESSAGE =
3376
+ 'Raw @tanstack/react-query import in a basalt file — the hooks come through basalt-ui/query, ' +
3377
+ 'which re-exports them beside `createBasaltQueryClient`, `unwrap`, `toErrorMessage` and the lazy ' +
3378
+ 'devtools (agent/rules/basalt-batteries.md). Import from basalt-ui/query instead. ' +
3379
+ '(basalt/query-dual-import)'
3380
+
3381
+ /**
3382
+ * The `basalt-batteries.md` law "import query hooks from `basalt-ui/query`, never dual-import
3383
+ * `@tanstack/react-query`" — F5, and one of the two `./query` surfaces that shipped advisory.
3384
+ *
3385
+ * TWO forms, one id, because they are one law read at two strengths. The DUAL form (both
3386
+ * specifiers present) is the law verbatim. The softer form — a raw `@tanstack/react-query` import
3387
+ * in a file that also imports the basalt root barrel — is a heuristic: it says "this file is
3388
+ * already a basalt file, so the query seam is right there", and it is the form that actually
3389
+ * catches the drift, since a file that has never heard of `basalt-ui/query` cannot dual-import.
3390
+ * The softer form is why the whole rule lands at `warn` rather than `error`.
3391
+ *
3392
+ * Gated on the LITERAL specifier, never on `ownTree` relative resolution: basalt's own
3393
+ * `src/query/**` and `src/dashboard/query-state.tsx` import `@tanstack/react-query` because they
3394
+ * ARE the seam, and they reach their siblings relatively. A file that writes `basalt-ui/query` is
3395
+ * by definition a consumer of the published surface.
3396
+ */
3397
+ // Ships: warn (grace → 1.30.0)
3398
+ const queryDualImport = {
3399
+ meta: {
3400
+ type: 'suggestion',
3401
+ docs: { description: 'Warn on a raw @tanstack/react-query import beside a basalt import.' },
3402
+ schema: [],
3403
+ },
3404
+ create(context) {
3405
+ if (isTestFile(context)) return {}
3406
+ const raw = []
3407
+ let basaltQuery = false
3408
+ let basaltRoot = false
3409
+
3410
+ return {
3411
+ ImportDeclaration(node) {
3412
+ const source = node.source?.value
3413
+ if (typeof source !== 'string') return
3414
+ if (source === BASALT_QUERY_SOURCE) basaltQuery = true
3415
+ else if (source === BASALT_ROOT_SOURCE) basaltRoot = true
3416
+ // `import type { UseQueryResult } from '@tanstack/react-query'` is erased at compile time
3417
+ // — there is no runtime import to route through the seam, and typing against the library's
3418
+ // own result type is exactly what `QueryStateLike` documents as legitimate. Same skip
3419
+ // `agent-no-raw-usechat` and `ai-sdk-major` take.
3420
+ else if (TANSTACK_QUERY_SOURCE.test(source) && node.importKind !== 'type') raw.push(node)
3421
+ },
3422
+ 'Program:exit'() {
3423
+ if (!basaltQuery && !basaltRoot) return
3424
+ const message = basaltQuery ? QUERY_DUAL_IMPORT_MESSAGE : QUERY_ROOT_IMPORT_MESSAGE
3425
+ for (const node of raw) {
3426
+ if (hasThemeAllow(context, node, 'query-dual-import')) continue
3427
+ context.report({ node, message })
3428
+ }
3429
+ },
3430
+ }
3431
+ },
3432
+ }
3433
+
3434
+ // ── Rule 28 — query-fn-unwrap ───────────────────────────────────────────────────────────────────
3435
+
3436
+ const QUERY_FN_UNWRAP_MESSAGE =
3437
+ 'queryFn fetches without unwrap() — `unwrap` throws on the error branch AND on a null data with ' +
3438
+ 'no error (a 204, a silent transport failure), and both are caller bugs that have to surface as ' +
3439
+ 'a rejected query rather than as `data: null` rendered through the success branch ' +
3440
+ '(agent/rules/basalt-batteries.md). Wrap the call: `queryFn: () => unwrap(client.thing.get())`. ' +
3441
+ '(basalt/query-fn-unwrap)'
3442
+
3443
+ /** A bare `fetch(` / `unwrap(` call, as written — the two halves of this rule's text heuristic. */
3444
+ const FETCH_CALL = /\bfetch\s*\(/
3445
+ const UNWRAP_CALL = /\bunwrap\s*\(/
3446
+
3447
+ /**
3448
+ * A `queryFn` that fetches and never `unwrap`s it — F5, the second unguarded `./query` law.
3449
+ *
3450
+ * **The heuristic is deliberately one sentence long, and it is a TEXT test inside a SYNTACTIC
3451
+ * range**: the rule takes the source span of the `queryFn` property's own function value and fires
3452
+ * only when `fetch(` appears in it and `unwrap(` does not. Nothing resolves, nothing follows a
3453
+ * helper out of the span.
3454
+ *
3455
+ * What that buys and what it costs, both stated so neither is a surprise. It sees the two shapes
3456
+ * the doctrine is actually written against — `queryFn: () => fetch(url)` and
3457
+ * `queryFn: async () => (await fetch(url)).json()`. It does NOT see a `queryFn: fetchThing`
3458
+ * reference, an Eden/Treaty call (`client.thing.get()` contains no `fetch(`), or an `unwrap`
3459
+ * applied one frame out; and it counts an `unwrap(` anywhere in the span, including one applied to
3460
+ * something else. A stricter version would need the call graph. The narrow version fires on the
3461
+ * form a consumer writes by reflex on day one, which is the one the law exists for.
3462
+ *
3463
+ * Scoped to files importing `basalt-ui/query` literally, for the reason {@link queryDualImport}
3464
+ * gives: without that gate it is an opinion about `fetch`, which basalt does not have.
3465
+ */
3466
+ // Ships: warn (grace → 1.30.0)
3467
+ const queryFnUnwrap = {
3468
+ meta: {
3469
+ type: 'suggestion',
3470
+ docs: { description: 'Warn on a queryFn that fetches without wrapping the call in unwrap().' },
3471
+ schema: [],
3472
+ },
3473
+ create(context) {
3474
+ if (isTestFile(context)) return {}
3475
+ const candidates = []
3476
+ let basaltQuery = false
3477
+
3478
+ return {
3479
+ ImportDeclaration(node) {
3480
+ if (node.source?.value === BASALT_QUERY_SOURCE) basaltQuery = true
3481
+ },
3482
+ Property(node) {
3483
+ if (propertyKeyName(node) !== 'queryFn') return
3484
+ const value = node.value
3485
+ if (value?.type !== 'ArrowFunctionExpression' && value?.type !== 'FunctionExpression') {
3486
+ return
3487
+ }
3488
+ candidates.push({ node, value })
3489
+ },
3490
+ 'Program:exit'() {
3491
+ if (!basaltQuery) return
3492
+ const text = context.sourceCode?.text ?? ''
3493
+ for (const { node, value } of candidates) {
3494
+ const [start, end] = value.range ?? []
3495
+ if (typeof start !== 'number' || typeof end !== 'number') continue
3496
+ const body = text.slice(start, end)
3497
+ if (!FETCH_CALL.test(body) || UNWRAP_CALL.test(body)) continue
3498
+ if (hasThemeAllow(context, node, 'query-fn-unwrap')) continue
3499
+ context.report({ node, message: QUERY_FN_UNWRAP_MESSAGE })
3500
+ }
3501
+ },
3502
+ }
3503
+ },
3504
+ }
3505
+
3506
+ // ── Rule 29 — deprecated-export ─────────────────────────────────────────────────────────────────
3507
+
3508
+ /**
3509
+ * @typedef {{
3510
+ * subpath: string
3511
+ * name: string
3512
+ * prop?: string
3513
+ * replacement: string
3514
+ * removeIn: string
3515
+ * fix?: false
3516
+ * }} DeprecatedExport
3517
+ */
3518
+
3519
+ /**
3520
+ * The deprecation ledger — every export that is `@deprecated` but still SHIPPED, with what to
3521
+ * write instead and the minor it goes away in. B4: majors are banned here, so a rename cannot be
3522
+ * signalled by a version number; the lifecycle is instead "ship the alias, lint the old name, write
3523
+ * the MIGRATING row, remove it a minor later" (`../CLAUDE.md`, "Deprecation lifecycle").
3524
+ *
3525
+ * A row without `prop` is a NAMED IMPORT from `subpath` — reported on the specifier, with an
3526
+ * autofix that rewrites it to `{ <replacement> as <local> }` so no call site has to move in the
3527
+ * same edit. **That autofix is the row's contract**: only a DROP-IN rename belongs here. A
3528
+ * deprecation whose replacement takes a different shape (`createSearchParamStore` →
3529
+ * `createSearchStore` + a `fields` object) would need the fix suppressed before it could be a row,
3530
+ * and until then it lives in `MIGRATING.md` alone. A row WITH `prop` is a JSX ATTRIBUTE on the component `name`, imported from `subpath`
3531
+ * — no autofix, because the three connectivity props collapse into ONE object prop and a
3532
+ * per-attribute rewrite would produce three `connectivity=` attributes on one tag.
3533
+ *
3534
+ * **Adding a row is the whole job.** Deprecate the export in its JSDoc, add the row here with a
3535
+ * `removeIn` at least one minor out, add the `MIGRATING.md` row, and this rule starts nudging.
3536
+ * `oxlint-plugin.test.ts` scans every `src/**\/index.ts(x)` barrel for a JSDoc `@deprecated` and
3537
+ * fails when one has no row here, so the barrel half cannot be forgotten. Removing the export is a
3538
+ * separate commit that deletes the row in the same change.
3539
+ *
3540
+ * @type {readonly DeprecatedExport[]}
3541
+ */
3542
+ export const DEPRECATED_EXPORTS = [
3543
+ // Empty since 1.29.0: every 1.28.x deprecation was removed on schedule. The next
3544
+ // deprecation adds a row here in the same commit as its @deprecated JSDoc and MIGRATING entry.
3545
+ ]
3546
+
3547
+ function deprecatedExportMessage(row) {
3548
+ const what =
3549
+ row.prop === undefined
3550
+ ? `\`${row.name}\` from ${row.subpath}`
3551
+ : `\`${row.prop}\` on <${row.name}>`
3552
+ return (
3553
+ `Deprecated basalt export — ${what} is superseded by \`${row.replacement}\` and is removed in ` +
3554
+ `${row.removeIn}. It still resolves today, so nothing is broken yet; migrate before that minor ` +
3555
+ '(packages/basalt-ui/MIGRATING.md carries the row). ' +
3556
+ '(basalt/deprecated-export)'
3557
+ )
3558
+ }
3559
+
3560
+ /**
3561
+ * A shipped-but-`@deprecated` export still being imported or written — B4, the sunset half of the
3562
+ * lifecycle whose absence the blueprint audit found (§8): the grace mechanism covers a rule getting
3563
+ * STRICTER, and nothing covered an export going away.
3564
+ *
3565
+ * Permanently `warn` ({@link PLUGIN_RULE_ADVISORY}), for the same reason `shadow-basalt-export` is:
3566
+ * a deprecation is a schedule, not a defect. The code compiles and runs; promoting this to `error`
3567
+ * would break a consumer's build for something that still works, which is the one thing the
3568
+ * no-majors doctrine exists to avoid. The `removeIn` minor is where it becomes an error — by the
3569
+ * export ceasing to exist.
3570
+ */
3571
+ // Ships: warn (advisory)
3572
+ const deprecatedExport = {
3573
+ meta: {
3574
+ type: 'suggestion',
3575
+ docs: { description: 'Warn on an import or prop that basalt has deprecated but still ships.' },
3576
+ schema: [],
3577
+ fixable: 'code',
3578
+ },
3579
+ create(context) {
3580
+ if (isTestFile(context)) return {}
3581
+ if (isBasaltOwnSource(getFilename(context))) return {}
3582
+ const importRows = DEPRECATED_EXPORTS.filter((row) => row.prop === undefined)
3583
+ const propRows = DEPRECATED_EXPORTS.filter((row) => row.prop !== undefined)
3584
+ const components = new Map()
3585
+ const candidates = []
3586
+
3587
+ return {
3588
+ ImportDeclaration(node) {
3589
+ const source = node.source?.value
3590
+ if (typeof source !== 'string') return
3591
+ for (const spec of node.specifiers ?? []) {
3592
+ const imported = spec.imported?.name ?? spec.imported?.value
3593
+ const local = spec.local?.name
3594
+ if (typeof imported !== 'string' || typeof local !== 'string') continue
3595
+ for (const row of propRows) {
3596
+ if (source === row.subpath && imported === row.name) components.set(local, row.name)
3597
+ }
3598
+ const row = importRows.find((r) => r.subpath === source && r.name === imported)
3599
+ if (row === undefined) continue
3600
+ if (hasThemeAllow(context, spec, 'deprecated-export')) continue
3601
+ context.report({
3602
+ node: spec,
3603
+ message: deprecatedExportMessage(row),
3604
+ ...(row.fix === false
3605
+ ? {}
3606
+ : { fix: (fixer) => fixer.replaceText(spec, `${row.replacement} as ${local}`) }),
3607
+ })
3608
+ }
3609
+ },
3610
+ JSXAttribute(node) {
3611
+ const name = node.name?.name
3612
+ if (typeof name !== 'string') return
3613
+ const owner = node.parent
3614
+ if (owner === null || owner === undefined || owner.type !== 'JSXOpeningElement') return
3615
+ const root = jsxRootName(owner.name)
3616
+ if (root === undefined) return
3617
+ candidates.push({ node, root, name })
3618
+ },
3619
+ 'Program:exit'() {
3620
+ for (const { node, root, name } of candidates) {
3621
+ const component = components.get(root)
3622
+ if (component === undefined) continue
3623
+ const row = propRows.find((r) => r.name === component && r.prop === name)
3624
+ if (row === undefined) continue
3625
+ if (hasThemeAllow(context, node, 'deprecated-export')) continue
3626
+ context.report({ node, message: deprecatedExportMessage(row) })
3627
+ }
3628
+ },
3629
+ }
3630
+ },
3631
+ }
3632
+
3633
+ // ── Rule 30 — forms-field-key ───────────────────────────────────────────────────────────────────
3634
+
3635
+ /** The subpath the forms idiom is imported from — the only provenance this rule accepts. */
3636
+ const FORMS_IMPORT_SOURCE = 'basalt-ui/forms'
3637
+
3638
+ /** The three names this rule resolves through the local→imported map. */
3639
+ const FORMS_SPREAD_NAMES = new Set(['inputProps', 'field'])
3640
+
3641
+ const FORMS_FIELD_KEY_MESSAGES = {
3642
+ missing:
3643
+ 'A spread `inputProps(…)` with no sibling `key`. The field loses its reconciler key, so an ' +
3644
+ 'uncontrolled input keeps its old text through `form.reset()`, `form.setValues()` and a ' +
3645
+ 'removed list row — nothing throws and nothing type-checks differently. Write ' +
3646
+ '`key={fieldKey(form, path)} {...inputProps(form, path)}`: `inputProps` stopped returning ' +
3647
+ '`key` because a spread `key` is a React 19 warning, and a call site that merely kept the ' +
3648
+ 'spread compiles unchanged and silently stops resetting. ' +
3649
+ '(basalt/forms-field-key)',
3650
+ deprecatedAlias:
3651
+ 'A spread `field(…)`. That is the @deprecated 1.27 alias and it still bundles `key` INTO the ' +
3652
+ 'returned object, so it still logs React 19\'s `A props object containing a "key" prop is ' +
3653
+ 'being spread into JSX` on every render — kept byte-identical on purpose, because a ' +
3654
+ 'deprecation is a schedule and not a silent behaviour change. Migrate to ' +
3655
+ '`key={fieldKey(form, path)} {...inputProps(form, path)}` (packages/basalt-ui/MIGRATING.md ' +
3656
+ 'carries the row); the alias itself is removed in 1.29.0. ' +
3657
+ '(basalt/forms-field-key)',
3658
+ }
3659
+
3660
+ /** The `key` attribute among a JSXOpeningElement's attributes, if it has one written literally. */
3661
+ function hasKeyAttribute(opening) {
3662
+ for (const attr of opening?.attributes ?? []) {
3663
+ if (attr?.type === 'JSXAttribute' && attr.name?.name === 'key') return true
3664
+ }
3665
+ return false
3666
+ }
3667
+
3668
+ /**
3669
+ * A field spread that lost its reconciler key — the guard for the one change in the 1.28.0 forms
3670
+ * minor that a consumer's compiler cannot see.
3671
+ *
3672
+ * `inputProps` used to return `getInputProps(path)` AND `key` in one object, so
3673
+ * `<TextInput {...inputProps(form, 'x')} />` covered both. It no longer returns `key` (a spread
3674
+ * `key` is a React 19 warning), and that exact call site still type-checks, still renders and still
3675
+ * validates — it just stops resetting. There is no compile error, no throw and no console line to
3676
+ * find it by, which is precisely the profile a lint rule exists for.
3677
+ *
3678
+ * TWO forms under this one id, because they are one law read from either end:
3679
+ *
3680
+ * - `{...inputProps(…)}` with no sibling `key` — the post-migration mistake. Autofixed: the `key`
3681
+ * attribute is inserted with the SAME arguments, and `fieldKey` is added to the existing
3682
+ * `basalt-ui/forms` import when it is not already there, so `--fix` produces working code rather
3683
+ * than an unresolved identifier.
3684
+ * - `{...field(…)}` — the pre-migration call site. Reported ALWAYS, key or no key, and deliberately
3685
+ * NOT autofixed: `field` still returns the bundle, so inserting a second `key` would be wrong,
3686
+ * and the honest remedy is the rename `basalt/deprecated-export` already nudges. This arm is a
3687
+ * pointer at the migration, one lane over from that rule's import-level nudge.
3688
+ *
3689
+ * Provenance is the `basalt-ui/forms` specifier (plus a relative import inside basalt's own `src/`,
3690
+ * or the dogfood surface goes silent — the same gate `collectBasaltImports` uses), read through the
3691
+ * local→imported map so `import { inputProps as f }` is covered. `inputProps` is an ordinary enough
3692
+ * name that firing on an unimported one would be an opinion about a consumer's own helper.
3693
+ */
3694
+ // Ships: warn (grace → 1.30.0)
3695
+ const formsFieldKey = {
3696
+ meta: {
3697
+ type: 'suggestion',
3698
+ docs: { description: 'Warn on a spread field object that lost (or still bundles) its key.' },
3699
+ schema: [],
3700
+ fixable: 'code',
3701
+ },
3702
+ create(context) {
3703
+ if (isTestFile(context)) return {}
3704
+ const ownTree = isBasaltOwnSource(getFilename(context))
3705
+ /** local name → the name it was imported as, for the three forms names. */
3706
+ const imported = new Map()
3707
+ /** The forms ImportDeclaration the `fieldKey` specifier is added to, if one exists. */
3708
+ let formsImport
3709
+ const candidates = []
3710
+
3711
+ return {
3712
+ ImportDeclaration(node) {
3713
+ const source = node.source?.value
3714
+ if (typeof source !== 'string') return
3715
+ if (source !== FORMS_IMPORT_SOURCE && !(ownTree && RELATIVE_IMPORT_SOURCE.test(source))) {
3716
+ return
3717
+ }
3718
+ for (const spec of node.specifiers ?? []) {
3719
+ const local = spec.local?.name
3720
+ const name = spec.imported?.name ?? spec.imported?.value
3721
+ if (typeof local !== 'string' || typeof name !== 'string') continue
3722
+ if (name === 'fieldKey' || FORMS_SPREAD_NAMES.has(name)) {
3723
+ imported.set(local, name)
3724
+ if (source === FORMS_IMPORT_SOURCE) formsImport = node
3725
+ }
3726
+ }
3727
+ },
3728
+ JSXSpreadAttribute(node) {
3729
+ const call = node.argument
3730
+ if (call?.type !== 'CallExpression' || call.callee?.type !== 'Identifier') return
3731
+ const opening = node.parent
3732
+ if (opening === null || opening === undefined || opening.type !== 'JSXOpeningElement')
3733
+ return
3734
+ candidates.push({ node, call, local: call.callee.name, opening })
3735
+ },
3736
+ 'Program:exit'() {
3737
+ const text = context.sourceCode?.text ?? ''
3738
+ const hasFieldKey = [...imported.values()].includes('fieldKey')
3739
+ let importFixed = false
3740
+
3741
+ for (const { node, call, local, opening } of candidates) {
3742
+ const name = imported.get(local)
3743
+ if (name === undefined) continue
3744
+ if (hasThemeAllow(context, node, 'forms-field-key')) continue
3745
+
3746
+ if (name === 'field') {
3747
+ context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.deprecatedAlias })
3748
+ continue
3749
+ }
3750
+ if (hasKeyAttribute(opening)) continue
3751
+
3752
+ const args = call.arguments ?? []
3753
+ const first = args[0]?.range
3754
+ const last = args[args.length - 1]?.range
3755
+ // No arguments to copy means no honest `fieldKey(…)` to write — report without a fix
3756
+ // rather than emit one that does not compile.
3757
+ if (first === undefined || last === undefined) {
3758
+ context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.missing })
3759
+ continue
3760
+ }
3761
+ const argsText = text.slice(first[0], last[1])
3762
+ // The import specifier is added by the FIRST fixed occurrence only: three fields in one
3763
+ // file would otherwise emit three overlapping edits to one import, and an overlapping
3764
+ // fix is dropped rather than merged.
3765
+ const needsImport = !hasFieldKey && !importFixed && formsImport !== undefined
3766
+ const lastSpec = needsImport
3767
+ ? formsImport.specifiers?.[(formsImport.specifiers?.length ?? 0) - 1]
3768
+ : undefined
3769
+ if (needsImport && lastSpec !== undefined) importFixed = true
3770
+
3771
+ context.report({
3772
+ node,
3773
+ message: FORMS_FIELD_KEY_MESSAGES.missing,
3774
+ fix: (fixer) => {
3775
+ const fixes = [fixer.insertTextBefore(node, `key={fieldKey(${argsText})} `)]
3776
+ if (lastSpec !== undefined) fixes.push(fixer.insertTextAfter(lastSpec, ', fieldKey'))
3777
+ return fixes
3778
+ },
3779
+ })
3780
+ }
3781
+ },
3782
+ }
3783
+ },
3784
+ }
3785
+
3786
+ // ── Rule 31 — no-import-meta-env ────────────────────────────────────────────────────────────────
3787
+
3788
+ const NO_IMPORT_META_ENV_MESSAGE =
3789
+ 'import.meta.env is banned in shipped basalt-ui source — this package is consumed both as a ' +
3790
+ 'Vite-bundled dep AND resolved directly by a consumer whose own bundler may define no ' +
3791
+ 'import.meta.env at all (basaltViteConfig defines process.env.NODE_ENV precisely so basalt ' +
3792
+ 'code never needs it). Use process.env.NODE_ENV instead. (basalt/no-import-meta-env)'
3793
+
3794
+ /**
3795
+ * `import.meta.env` — a MemberExpression whose object is the `import.meta` MetaProperty and whose
3796
+ * property is `env`. Repo-local only: a CONSUMER's own app code legitimately uses
3797
+ * `import.meta.env.VITE_*` for its own config, so this must never reach the shipped preset — only
3798
+ * `/.oxlintrc.json` wires it, scoped to `packages/basalt-ui/src/**` the same way the Mantine-free
3799
+ * boundaries above are scoped, rather than through path logic in the rule itself (unlike
3800
+ * `token-layer-boundary`, which every package resolves the plugin from and so has to self-scope).
3801
+ */
3802
+ // Ships: repo-local only
3803
+ const noImportMetaEnv = {
3804
+ meta: {
3805
+ type: 'suggestion',
3806
+ docs: {
3807
+ description: 'Disallow import.meta.env — basalt-ui source must use process.env.NODE_ENV.',
3808
+ },
3809
+ schema: [],
3810
+ },
3811
+ create(context) {
3812
+ if (isTestFile(context)) return {}
3813
+
3814
+ return {
3815
+ MemberExpression(node) {
3816
+ const object = node.object
3817
+ if (
3818
+ object?.type !== 'MetaProperty' ||
3819
+ object.meta?.name !== 'import' ||
3820
+ object.property?.name !== 'meta'
3821
+ )
3822
+ return
3823
+ if (node.computed || node.property?.name !== 'env') return
3824
+ if (hasThemeAllow(context, node, 'no-import-meta-env')) return
3825
+ context.report({ node, message: NO_IMPORT_META_ENV_MESSAGE })
3826
+ },
3827
+ }
3828
+ },
3829
+ }
3830
+
2982
3831
  // ── Grace ledger ────────────────────────────────────────────────────────────────────────────────
2983
3832
 
2984
3833
  /**
@@ -3012,7 +3861,7 @@ const useSearchFromLiteral = {
3012
3861
  export const PLUGIN_RULE_GRACE = {
3013
3862
  'control-outside-home': {
3014
3863
  since: '1.26.0',
3015
- promote: '1.28.0',
3864
+ promote: '1.30.0',
3016
3865
  why:
3017
3866
  'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
3018
3867
  'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
@@ -3022,11 +3871,76 @@ export const PLUGIN_RULE_GRACE = {
3022
3871
  'cautious: the wave-7 run found 9 remaining warns in argo, every one of them a control in a ' +
3023
3872
  'modal/form module whose `<Modal>` is rendered by the PARENT — the cross-file case law C1 is ' +
3024
3873
  'explicitly advisory about, which no ancestry walk inside one file can ever see. ' +
3025
- '{@link OVERLAY_CONVENTION_FILE} now exempts the declared naming convention for exactly ' +
3026
- 'that shape; 1.28.0 is when the remainder is re-measured. A file outside the convention ' +
3027
- "that is still an overlay's own body declares it with " +
3874
+ '{@link isOverlayConventionFile} exempts that declared naming convention, in BOTH the kebab ' +
3875
+ '(`edit-session-modal.tsx`) and the PascalCase (`CbbiPanel.tsx`) dialect. Re-dated ' +
3876
+ '2026-08-28 — the argo wave-7 migration has not run; the PascalCase overlay convention (this ' +
3877
+ 'change) is expected to clear most of the 9. 1.30.0 is when the remainder is re-measured. A ' +
3878
+ "file outside the convention that is still an overlay's own body declares it with " +
3028
3879
  '`theme-allow-file control-outside-home — overlay`.',
3029
3880
  },
3881
+ 'bound-control-outside-home': {
3882
+ since: '1.28.0',
3883
+ promote: '1.30.0',
3884
+ why:
3885
+ 'ASIDE-SPEC G5 — a bound control in a section body rendered as a stray pill and nothing saw ' +
3886
+ 'it. `control-outside-home` matches raw Mantine tags only, so the whole basalt half of law ' +
3887
+ 'C1 was unguarded: `<SelectFilter field={…}/>` written into a `Section` body or a bare page ' +
3888
+ 'stack passed every lane. A NEW id rather than a widening (C16 — a level is per-id), landing ' +
3889
+ 'warn because it inherits the sibling rule\'s heuristic: "this control has no home" is a ' +
3890
+ 'claim about layout intent, and the ancestry it reads now spans four subtree homes ' +
3891
+ '(FilterSet / PageAside / PanelRow / the overlay hosts) that only shipped in 1.27.0. 1.30.0 ' +
3892
+ 'is when the incumbent count across argo, linewatch, image-share, rb, image-gen and the ' +
3893
+ 'playground is re-measured.',
3894
+ },
3895
+ 'provider-above-router': {
3896
+ since: '1.28.0',
3897
+ promote: '1.30.0',
3898
+ why:
3899
+ 'F5 — one of the three basalt-mantine.md laws that shipped under a `not guarded: —` banner. ' +
3900
+ 'It reads STATIC JSX ancestry inside one file, which is a genuine fraction of the law: the ' +
3901
+ 'usual real shape composes the two providers across modules, where no oxlint rule can see ' +
3902
+ 'the nesting at all. Landing it at `error` would state a certainty the implementation does ' +
3903
+ 'not have. 1.30.0 is when the incumbent count across argo, linewatch, image-share, rb, ' +
3904
+ 'image-gen and the playground is measured — every one of them mounts a router, so the ' +
3905
+ 'false-positive question has a real sample.',
3906
+ },
3907
+ 'query-dual-import': {
3908
+ since: '1.28.0',
3909
+ promote: '1.30.0',
3910
+ why:
3911
+ 'F5 — the basalt-batteries.md law with no guard. TWO forms under one id: the dual import ' +
3912
+ '(both `@tanstack/react-query` and `basalt-ui/query` in one file) is the law verbatim, and ' +
3913
+ 'the softer form — a raw `@tanstack/react-query` import in a file that also imports the ' +
3914
+ 'basalt root barrel — is the heuristic that actually catches the drift, since a file that ' +
3915
+ 'never names `basalt-ui/query` cannot dual-import. The soft form is what holds the whole ' +
3916
+ 'rule at `warn`: "this file is already a basalt file" is a claim about intent. 1.30.0 is ' +
3917
+ 'when the consumer count is measured and the soft form is either kept or split off.',
3918
+ },
3919
+ 'forms-field-key': {
3920
+ since: '1.28.0',
3921
+ promote: '1.30.0',
3922
+ why:
3923
+ "the 1.28.0 forms minor took `key` out of `inputProps`' return, and the OLD call site " +
3924
+ "(`<TextInput {...inputProps(form, 'x')} />`) survives that change with no compile error, " +
3925
+ 'no throw and no console line — it just stops resetting. This rule is the only thing that ' +
3926
+ 'reports it, which is an argument for `error`, not `warn`; it lands at `warn` anyway ' +
3927
+ 'because it fires on code every consumer is CURRENTLY shipping, and the grace minor is what ' +
3928
+ 'turns "your build is broken by the upgrade" into "your editor lists the eleven lines". ' +
3929
+ 'The `field(` arm is the same law read from the pre-migration end, and that alias is removed ' +
3930
+ 'in 1.29.0 anyway. 1.30.0 is when argo, linewatch, image-share, rb, image-gen and the ' +
3931
+ 'playground have all been through the autofix and the remainder is measured.',
3932
+ },
3933
+ 'query-fn-unwrap': {
3934
+ since: '1.28.0',
3935
+ promote: '1.30.0',
3936
+ why:
3937
+ 'F5 — the second unguarded ./query law. Openly a TEXT heuristic inside a syntactic range: ' +
3938
+ '`fetch(` present in the queryFn span and `unwrap(` absent. It cannot see a `queryFn: ' +
3939
+ 'fetchThing` reference, an Eden/Treaty call, or an unwrap one frame out, and it counts an ' +
3940
+ '`unwrap(` applied to anything. That is a deliberate floor rather than a first draft, and a ' +
3941
+ 'rule whose stated reach is this narrow has no business failing a build. 1.30.0 is when the ' +
3942
+ 'measured false-positive load decides whether it promotes or stays advisory.',
3943
+ },
3030
3944
  }
3031
3945
 
3032
3946
  /**
@@ -3050,6 +3964,16 @@ export const PLUGIN_RULE_ADVISORY = {
3050
3964
  'strong evidence and not proof, and renaming defeats it outright, so this stays a permanent ' +
3051
3965
  'warn rather than a grace entry with no honest promote date.',
3052
3966
  },
3967
+ 'deprecated-export': {
3968
+ since: '1.28.0',
3969
+ why:
3970
+ 'B4, the sunset half of the deprecation lifecycle (`../CLAUDE.md`). A deprecation is a ' +
3971
+ 'SCHEDULE, not a defect: the code compiles, runs and is correct until the `removeIn` minor, ' +
3972
+ 'so promoting this to `error` would fail a consumer build over something that still works — ' +
3973
+ 'the one outcome the no-majors doctrine exists to avoid. There is no version at which that ' +
3974
+ 'stops being true, which is what makes it advisory rather than a grace entry: enforcement ' +
3975
+ 'arrives by the export ceasing to exist, not by this rule changing level.',
3976
+ },
3053
3977
  }
3054
3978
 
3055
3979
  // ── Plugin export ───────────────────────────────────────────────────────────────────────────────
@@ -3068,17 +3992,24 @@ export default {
3068
3992
  'raw-scroll-container': rawScrollContainer,
3069
3993
  'hand-rolled-filter': handRolledFilter,
3070
3994
  'control-outside-home': controlOutsideHome,
3995
+ 'bound-control-outside-home': boundControlOutsideHome,
3071
3996
  'control-size-literal': controlSizeLiteral,
3072
3997
  'page-bar-budget': pageBarBudget,
3073
3998
  'in-body-page-title': inBodyPageTitle,
3074
3999
  'responsive-twin': responsiveTwin,
3075
4000
  'search-literal-link': searchLiteralLink,
3076
4001
  'use-search-from-literal': useSearchFromLiteral,
4002
+ 'provider-above-router': providerAboveRouter,
4003
+ 'query-dual-import': queryDualImport,
4004
+ 'query-fn-unwrap': queryFnUnwrap,
4005
+ 'deprecated-export': deprecatedExport,
4006
+ 'forms-field-key': formsFieldKey,
3077
4007
  'visx-boundary': visxBoundary,
3078
4008
  'visx-tooltip': visxTooltip,
3079
4009
  'token-layer-boundary': tokenLayerBoundary,
3080
4010
  'agent-resume-guard': agentResumeGuard,
3081
4011
  'agent-no-raw-usechat': agentNoRawUseChat,
3082
4012
  'ai-sdk-major': aiSdkMajor,
4013
+ 'no-import-meta-env': noImportMetaEnv,
3083
4014
  },
3084
4015
  }