basalt-ui 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1063) hide show
  1. package/AGENTS.md +23 -23
  2. package/CHANGELOG.md +59 -0
  3. package/MIGRATING.md +613 -20
  4. package/README.md +33 -3
  5. package/agent/rules/basalt-batteries.md +25 -10
  6. package/agent/rules/basalt-controls.md +56 -31
  7. package/agent/rules/basalt-mantine.md +17 -6
  8. package/agent/rules/basalt-tokens.md +1 -1
  9. package/configs/oxlint-plugin.js +1056 -28
  10. package/configs/oxlint-plugin.test.ts +1066 -0
  11. package/configs/oxlint.json +7 -0
  12. package/dist/agent/coalesce.d.ts.map +1 -1
  13. package/dist/agent/coalesce.js +2 -1
  14. package/dist/agent/merge.d.ts.map +1 -1
  15. package/dist/agent/merge.js +2 -1
  16. package/dist/agent/part-list.d.ts.map +1 -1
  17. package/dist/agent/part-list.js +2 -0
  18. package/dist/agent/stick-to-bottom.d.ts +3 -6
  19. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  20. package/dist/agent/stick-to-bottom.js +6 -3
  21. package/dist/agent-chat/composer.d.ts +6 -2
  22. package/dist/agent-chat/composer.d.ts.map +1 -1
  23. package/dist/agent-chat/composer.js +49 -33
  24. package/dist/agent-chat/relative-time.d.ts +0 -9
  25. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  26. package/dist/agent-chat/relative-time.js +2 -1
  27. package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
  28. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  29. package/dist/agent-chat/thread-detail-panel.js +8 -5
  30. package/dist/agent-chat/thread-feed-row.d.ts +2 -1
  31. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  32. package/dist/agent-chat/thread-feed-row.js +92 -92
  33. package/dist/agent-chat/thread-feed.d.ts +3 -2
  34. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  35. package/dist/agent-chat/thread-feed.js +8 -3
  36. package/dist/agent-chat/thread-message.d.ts +4 -3
  37. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  38. package/dist/agent-chat/thread-message.js +70 -31
  39. package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
  40. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  41. package/dist/agent-chat/thread-outcome-card.js +9 -2
  42. package/dist/agent-chat/thread-workspace.d.ts +7 -2
  43. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  44. package/dist/agent-chat/thread-workspace.js +62 -29
  45. package/dist/agent-chat/tool-chip.d.ts +3 -2
  46. package/dist/agent-chat/tool-chip.d.ts.map +1 -1
  47. package/dist/agent-chat/tool-chip.js +7 -2
  48. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  49. package/dist/charts/hooks/useBandPlot.js +15 -8
  50. package/dist/charts/index.d.ts +8 -7
  51. package/dist/charts/index.d.ts.map +1 -1
  52. package/dist/charts/index.js +45 -5
  53. package/dist/charts/kinds/BandStrip.d.ts +10 -7
  54. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  55. package/dist/charts/kinds/BandStrip.js +12 -7
  56. package/dist/charts/kinds/Bars.d.ts +12 -6
  57. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  58. package/dist/charts/kinds/Bars.js +37 -11
  59. package/dist/charts/kinds/Donut.d.ts +6 -5
  60. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  61. package/dist/charts/kinds/Donut.js +19 -2
  62. package/dist/charts/kinds/DualPanel.d.ts +6 -5
  63. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  64. package/dist/charts/kinds/DualPanel.js +36 -8
  65. package/dist/charts/kinds/Heatmap.d.ts +10 -6
  66. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  67. package/dist/charts/kinds/Heatmap.js +58 -31
  68. package/dist/charts/kinds/MirroredBars.d.ts +6 -5
  69. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  70. package/dist/charts/kinds/MirroredBars.js +9 -2
  71. package/dist/charts/kinds/MultiLine.d.ts +11 -6
  72. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  73. package/dist/charts/kinds/MultiLine.js +24 -15
  74. package/dist/charts/kinds/StackedArea.d.ts +18 -8
  75. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  76. package/dist/charts/kinds/StackedArea.js +29 -4
  77. package/dist/charts/kinds/ZonedLine.d.ts +12 -8
  78. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  79. package/dist/charts/kinds/ZonedLine.js +33 -23
  80. package/dist/charts/layout/auto-margin.d.ts +65 -8
  81. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  82. package/dist/charts/layout/auto-margin.js +45 -10
  83. package/dist/charts/layout/log-ticks.d.ts +27 -0
  84. package/dist/charts/layout/log-ticks.d.ts.map +1 -0
  85. package/dist/charts/layout/log-ticks.js +42 -0
  86. package/dist/charts/primitives/Axes.d.ts +11 -2
  87. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  88. package/dist/charts/primitives/Axes.js +15 -5
  89. package/dist/charts/primitives/CartesianChart.d.ts +45 -7
  90. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  91. package/dist/charts/primitives/CartesianChart.js +86 -28
  92. package/dist/charts/primitives/ChartCard.d.ts +27 -2
  93. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  94. package/dist/charts/primitives/ChartCard.js +37 -17
  95. package/dist/charts/primitives/ChartFrame.d.ts +30 -4
  96. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  97. package/dist/charts/primitives/ChartFrame.js +45 -15
  98. package/dist/charts/primitives/ChartLegend.d.ts +13 -2
  99. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  100. package/dist/charts/primitives/ChartLegend.js +33 -13
  101. package/dist/charts/primitives/ChartPending.d.ts +77 -4
  102. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  103. package/dist/charts/primitives/ChartPending.js +100 -6
  104. package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
  105. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  106. package/dist/charts/primitives/ChartTooltip.js +18 -5
  107. package/dist/charts/primitives/Crosshair.d.ts +3 -0
  108. package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
  109. package/dist/charts/primitives/Crosshair.js +3 -1
  110. package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
  111. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
  112. package/dist/charts/primitives/chart-frame-layout.js +89 -0
  113. package/dist/charts/primitives/chart-tier.d.ts +16 -0
  114. package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
  115. package/dist/charts/primitives/chart-tier.js +20 -0
  116. package/dist/charts/series.d.ts +55 -1
  117. package/dist/charts/series.d.ts.map +1 -1
  118. package/dist/charts/series.js +27 -1
  119. package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
  120. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  121. package/dist/charts/sparklines/BarSparkline.js +9 -3
  122. package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
  123. package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
  124. package/dist/charts/sparklines/LineSparkline.js +15 -3
  125. package/dist/charts/sparklines/index.d.ts +2 -2
  126. package/dist/charts/sparklines/index.d.ts.map +1 -1
  127. package/dist/charts/utils/format.d.ts +92 -2
  128. package/dist/charts/utils/format.d.ts.map +1 -1
  129. package/dist/charts/utils/format.js +64 -1
  130. package/dist/charts/utils/ticks.d.ts +81 -2
  131. package/dist/charts/utils/ticks.d.ts.map +1 -1
  132. package/dist/charts/utils/ticks.js +54 -4
  133. package/dist/cli/index.d.ts.map +1 -1
  134. package/dist/cli/index.js +2 -2
  135. package/dist/commands/ShortcutsHelp.d.ts +3 -2
  136. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  137. package/dist/commands/ShortcutsHelp.js +8 -2
  138. package/dist/commands/define-commands.d.ts.map +1 -1
  139. package/dist/commands/define-commands.js +2 -2
  140. package/dist/commands/define-overlays.d.ts +102 -0
  141. package/dist/commands/define-overlays.d.ts.map +1 -1
  142. package/dist/commands/define-overlays.js +102 -2
  143. package/dist/commands/index.d.ts +10 -1
  144. package/dist/commands/index.d.ts.map +1 -1
  145. package/dist/commands/index.js +3 -1
  146. package/dist/commands/overlays-mount.d.ts +13 -6
  147. package/dist/commands/overlays-mount.d.ts.map +1 -1
  148. package/dist/commands/overlays-mount.js +23 -14
  149. package/dist/common/errors.d.ts +34 -0
  150. package/dist/common/errors.d.ts.map +1 -0
  151. package/dist/common/errors.js +36 -0
  152. package/dist/common/index.d.ts +15 -0
  153. package/dist/common/index.d.ts.map +1 -0
  154. package/dist/common/index.js +29 -0
  155. package/dist/common/props.d.ts +63 -0
  156. package/dist/common/props.d.ts.map +1 -0
  157. package/dist/common/props.js +6 -0
  158. package/dist/common/query-state-like.d.ts +43 -0
  159. package/dist/common/query-state-like.d.ts.map +1 -0
  160. package/dist/common/query-state-like.js +20 -0
  161. package/dist/common/refs.d.ts +29 -0
  162. package/dist/common/refs.d.ts.map +1 -0
  163. package/dist/common/refs.js +29 -0
  164. package/dist/common/scroll-parent.d.ts +23 -0
  165. package/dist/common/scroll-parent.d.ts.map +1 -0
  166. package/dist/common/scroll-parent.js +21 -0
  167. package/dist/common/validate.d.ts +38 -0
  168. package/dist/common/validate.d.ts.map +1 -0
  169. package/dist/common/validate.js +36 -0
  170. package/dist/connectivity/connectivity-indicator.d.ts +3 -1
  171. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  172. package/dist/connectivity/connectivity-indicator.js +3 -1
  173. package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
  174. package/dist/connectivity/connectivity-provider.js +4 -1
  175. package/dist/connectivity/index.d.ts +1 -0
  176. package/dist/connectivity/index.d.ts.map +1 -1
  177. package/dist/connectivity/types.d.ts +2 -2
  178. package/dist/connectivity/types.d.ts.map +1 -1
  179. package/dist/connectivity/use-connectivity.d.ts.map +1 -1
  180. package/dist/content/article-layout.module.css +11 -4
  181. package/dist/content/callout.d.ts +8 -4
  182. package/dist/content/callout.d.ts.map +1 -1
  183. package/dist/content/callout.js +15 -8
  184. package/dist/content/guide.d.ts +5 -4
  185. package/dist/content/guide.d.ts.map +1 -1
  186. package/dist/content/guide.js +13 -4
  187. package/dist/content/heading-anchor.d.ts +24 -2
  188. package/dist/content/heading-anchor.d.ts.map +1 -1
  189. package/dist/content/heading-anchor.js +23 -9
  190. package/dist/content/index.d.ts.map +1 -1
  191. package/dist/content/markdown.d.ts.map +1 -1
  192. package/dist/content/markdown.js +2 -0
  193. package/dist/content/prose.module.css +9 -7
  194. package/dist/content/reading-progress.d.ts +10 -4
  195. package/dist/content/reading-progress.d.ts.map +1 -1
  196. package/dist/content/reading-progress.js +26 -10
  197. package/dist/content/slug.d.ts +4 -0
  198. package/dist/content/slug.d.ts.map +1 -1
  199. package/dist/content/toc.d.ts +11 -0
  200. package/dist/content/toc.d.ts.map +1 -1
  201. package/dist/content/toc.js +23 -10
  202. package/dist/controls/actions.d.ts +49 -6
  203. package/dist/controls/actions.d.ts.map +1 -1
  204. package/dist/controls/actions.js +39 -8
  205. package/dist/controls/compare-filter.d.ts +3 -2
  206. package/dist/controls/compare-filter.d.ts.map +1 -1
  207. package/dist/controls/compare-filter.js +17 -2
  208. package/dist/controls/control-group.d.ts +3 -3
  209. package/dist/controls/control-group.d.ts.map +1 -1
  210. package/dist/controls/control-group.js +9 -2
  211. package/dist/controls/controls.module.css +169 -186
  212. package/dist/controls/controls.module.css.d.ts +9 -11
  213. package/dist/controls/enum-filter.d.ts +3 -2
  214. package/dist/controls/enum-filter.d.ts.map +1 -1
  215. package/dist/controls/enum-filter.js +28 -13
  216. package/dist/controls/filter-context.d.ts +8 -4
  217. package/dist/controls/filter-context.d.ts.map +1 -1
  218. package/dist/controls/filter-pill.d.ts +3 -3
  219. package/dist/controls/filter-pill.d.ts.map +1 -1
  220. package/dist/controls/filter-pill.js +4 -1
  221. package/dist/controls/filter-set.d.ts +3 -2
  222. package/dist/controls/filter-set.d.ts.map +1 -1
  223. package/dist/controls/filter-set.js +66 -41
  224. package/dist/controls/filter-sheet.d.ts +20 -80
  225. package/dist/controls/filter-sheet.d.ts.map +1 -1
  226. package/dist/controls/filter-sheet.js +9 -95
  227. package/dist/controls/index.d.ts +2 -0
  228. package/dist/controls/index.d.ts.map +1 -1
  229. package/dist/controls/index.js +4 -0
  230. package/dist/controls/multi-select-filter.d.ts +18 -2
  231. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  232. package/dist/controls/multi-select-filter.js +122 -20
  233. package/dist/controls/number-filter.d.ts +7 -5
  234. package/dist/controls/number-filter.d.ts.map +1 -1
  235. package/dist/controls/number-filter.js +40 -11
  236. package/dist/controls/panel-row.d.ts +132 -0
  237. package/dist/controls/panel-row.d.ts.map +1 -0
  238. package/dist/controls/panel-row.js +173 -0
  239. package/dist/controls/panel-row.module.css +86 -0
  240. package/dist/controls/panel-row.module.css.d.ts +13 -0
  241. package/dist/controls/range-filter.d.ts +2 -1
  242. package/dist/controls/range-filter.d.ts.map +1 -1
  243. package/dist/controls/range-filter.js +57 -35
  244. package/dist/controls/search-filter.d.ts +3 -2
  245. package/dist/controls/search-filter.d.ts.map +1 -1
  246. package/dist/controls/search-filter.js +25 -10
  247. package/dist/controls/select-filter.d.ts +2 -1
  248. package/dist/controls/select-filter.d.ts.map +1 -1
  249. package/dist/controls/select-filter.js +4 -0
  250. package/dist/controls/slider-control.d.ts +36 -0
  251. package/dist/controls/slider-control.d.ts.map +1 -0
  252. package/dist/controls/slider-control.js +64 -0
  253. package/dist/controls/sync-button.d.ts +3 -2
  254. package/dist/controls/sync-button.d.ts.map +1 -1
  255. package/dist/controls/sync-button.js +16 -3
  256. package/dist/controls/toggle-filter.d.ts +3 -2
  257. package/dist/controls/toggle-filter.d.ts.map +1 -1
  258. package/dist/controls/toggle-filter.js +28 -11
  259. package/dist/controls/view-tabs.d.ts +3 -2
  260. package/dist/controls/view-tabs.d.ts.map +1 -1
  261. package/dist/controls/view-tabs.js +43 -11
  262. package/dist/controls-dates/date-range-picker.d.ts +1 -1
  263. package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
  264. package/dist/controls-dates/date-range-picker.js +6 -1
  265. package/dist/dashboard/delta-badge.d.ts +5 -1
  266. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  267. package/dist/dashboard/empty-state.d.ts +10 -3
  268. package/dist/dashboard/empty-state.d.ts.map +1 -1
  269. package/dist/dashboard/empty-state.js +37 -6
  270. package/dist/dashboard/index.d.ts +6 -4
  271. package/dist/dashboard/index.d.ts.map +1 -1
  272. package/dist/dashboard/index.js +7 -1
  273. package/dist/dashboard/query-state.d.ts +48 -27
  274. package/dist/dashboard/query-state.d.ts.map +1 -1
  275. package/dist/dashboard/query-state.js +111 -57
  276. package/dist/dashboard/settings-section.d.ts +19 -9
  277. package/dist/dashboard/settings-section.d.ts.map +1 -1
  278. package/dist/dashboard/settings-section.js +74 -25
  279. package/dist/dashboard/stat-card.d.ts +44 -38
  280. package/dist/dashboard/stat-card.d.ts.map +1 -1
  281. package/dist/dashboard/stat-card.js +55 -42
  282. package/dist/dashboard/stat-card.module.css +46 -31
  283. package/dist/dashboard/stat-card.module.css.d.ts +1 -0
  284. package/dist/dashboard/stat-group.d.ts +41 -0
  285. package/dist/dashboard/stat-group.d.ts.map +1 -0
  286. package/dist/dashboard/stat-group.js +30 -0
  287. package/dist/dashboard/stat-group.module.css +66 -0
  288. package/dist/dashboard/stat-group.module.css.d.ts +8 -0
  289. package/dist/dashboard/widget-grid.d.ts +27 -0
  290. package/dist/dashboard/widget-grid.d.ts.map +1 -0
  291. package/dist/dashboard/widget-grid.js +41 -0
  292. package/dist/dashboard/widget-grid.module.css +53 -0
  293. package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
  294. package/dist/data/data-table.d.ts +151 -12
  295. package/dist/data/data-table.d.ts.map +1 -1
  296. package/dist/data/data-table.js +384 -138
  297. package/dist/data/data-table.module.css +64 -0
  298. package/dist/data/data-table.module.css.d.ts +11 -0
  299. package/dist/data/index.d.ts +2 -2
  300. package/dist/data/index.d.ts.map +1 -1
  301. package/dist/data/index.js +24 -2
  302. package/dist/data/query-branch.d.ts +28 -0
  303. package/dist/data/query-branch.d.ts.map +1 -0
  304. package/dist/data/query-branch.js +10 -0
  305. package/dist/data/table.d.ts +2 -2
  306. package/dist/data/table.d.ts.map +1 -1
  307. package/dist/data/virtual-list.d.ts +65 -6
  308. package/dist/data/virtual-list.d.ts.map +1 -1
  309. package/dist/data/virtual-list.js +89 -38
  310. package/dist/data/virtual.d.ts +1 -1
  311. package/dist/data/virtual.d.ts.map +1 -1
  312. package/dist/forms/FormErrorSummary.d.ts +4 -3
  313. package/dist/forms/FormErrorSummary.d.ts.map +1 -1
  314. package/dist/forms/FormErrorSummary.js +6 -4
  315. package/dist/forms/create-form.d.ts +2 -2
  316. package/dist/forms/field.d.ts +46 -6
  317. package/dist/forms/field.d.ts.map +1 -1
  318. package/dist/forms/field.js +10 -5
  319. package/dist/forms/form-layout.d.ts +142 -0
  320. package/dist/forms/form-layout.d.ts.map +1 -0
  321. package/dist/forms/form-layout.js +110 -0
  322. package/dist/forms/form-layout.module.css +145 -0
  323. package/dist/forms/form-layout.module.css.d.ts +20 -0
  324. package/dist/forms/form-state.d.ts +42 -0
  325. package/dist/forms/form-state.d.ts.map +1 -0
  326. package/dist/forms/form-state.js +26 -0
  327. package/dist/forms/index.d.ts +22 -6
  328. package/dist/forms/index.d.ts.map +1 -1
  329. package/dist/forms/index.js +19 -3
  330. package/dist/forms/use-field-array.d.ts +60 -0
  331. package/dist/forms/use-field-array.d.ts.map +1 -0
  332. package/dist/forms/use-field-array.js +20 -0
  333. package/dist/forms/use-form-draft.d.ts +25 -10
  334. package/dist/forms/use-form-draft.d.ts.map +1 -1
  335. package/dist/forms/use-form-draft.js +55 -18
  336. package/dist/forms/use-form-submit.d.ts +88 -0
  337. package/dist/forms/use-form-submit.d.ts.map +1 -0
  338. package/dist/forms/use-form-submit.js +63 -0
  339. package/dist/guard/index.d.ts.map +1 -1
  340. package/dist/guard/index.js +26 -6
  341. package/dist/index.d.ts +10 -8
  342. package/dist/index.d.ts.map +1 -1
  343. package/dist/index.js +44 -4
  344. package/dist/nav/types.d.ts +15 -6
  345. package/dist/nav/types.d.ts.map +1 -1
  346. package/dist/notifications/bell.d.ts +3 -2
  347. package/dist/notifications/bell.d.ts.map +1 -1
  348. package/dist/notifications/bell.js +73 -56
  349. package/dist/notifications/center.d.ts +15 -4
  350. package/dist/notifications/center.d.ts.map +1 -1
  351. package/dist/notifications/center.js +5 -2
  352. package/dist/notifications/define-notifications.d.ts +56 -14
  353. package/dist/notifications/define-notifications.d.ts.map +1 -1
  354. package/dist/notifications/define-notifications.js +2 -1
  355. package/dist/notifications/index.d.ts +6 -2
  356. package/dist/notifications/index.d.ts.map +1 -1
  357. package/dist/notifications/index.js +5 -1
  358. package/dist/notifications/mount-guard.d.ts +8 -0
  359. package/dist/notifications/mount-guard.d.ts.map +1 -0
  360. package/dist/notifications/mount-guard.js +19 -0
  361. package/dist/notifications/notify.d.ts +58 -1
  362. package/dist/notifications/notify.d.ts.map +1 -1
  363. package/dist/notifications/notify.js +46 -2
  364. package/dist/notifications/overlay.d.ts.map +1 -1
  365. package/dist/notifications/overlay.js +2 -0
  366. package/dist/notifications/store.d.ts +7 -0
  367. package/dist/notifications/store.d.ts.map +1 -1
  368. package/dist/provider/index.d.ts +37 -5
  369. package/dist/provider/index.d.ts.map +1 -1
  370. package/dist/provider/index.js +66 -28
  371. package/dist/provider/lab-theme.d.ts.map +1 -1
  372. package/dist/provider/lab-theme.js +2 -1
  373. package/dist/query/devtools.d.ts.map +1 -1
  374. package/dist/query/devtools.js +1 -0
  375. package/dist/router-tanstack/field.d.ts +1 -1
  376. package/dist/router-tanstack/field.d.ts.map +1 -1
  377. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  378. package/dist/router-tanstack/search-store.js +1 -1
  379. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  380. package/dist/router-tanstack/use-nav.js +1 -1
  381. package/dist/section/index.d.ts +1 -1
  382. package/dist/section/index.d.ts.map +1 -1
  383. package/dist/section/section.d.ts +36 -2
  384. package/dist/section/section.d.ts.map +1 -1
  385. package/dist/section/section.js +49 -57
  386. package/dist/section/section.module.css +11 -0
  387. package/dist/shell/account-types.d.ts +5 -2
  388. package/dist/shell/account-types.d.ts.map +1 -1
  389. package/dist/shell/app-brand.d.ts +13 -0
  390. package/dist/shell/app-brand.d.ts.map +1 -0
  391. package/dist/shell/app-brand.js +94 -0
  392. package/dist/shell/app-brand.module.css +109 -0
  393. package/dist/shell/app-brand.module.css.d.ts +14 -0
  394. package/dist/shell/app-breadcrumbs.d.ts +4 -2
  395. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  396. package/dist/shell/app-breadcrumbs.js +33 -22
  397. package/dist/shell/app-header.module.css +10 -5
  398. package/dist/shell/app-main.module.css +107 -0
  399. package/dist/shell/app-main.module.css.d.ts +11 -0
  400. package/dist/shell/app-mobile-nav.d.ts +3 -2
  401. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  402. package/dist/shell/app-mobile-nav.js +29 -15
  403. package/dist/shell/app-mobile-nav.module.css +48 -27
  404. package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
  405. package/dist/shell/app-sidebar-account.d.ts +2 -1
  406. package/dist/shell/app-sidebar-account.d.ts.map +1 -1
  407. package/dist/shell/app-sidebar-account.js +57 -22
  408. package/dist/shell/app-sidebar.d.ts +9 -3
  409. package/dist/shell/app-sidebar.d.ts.map +1 -1
  410. package/dist/shell/app-sidebar.js +55 -109
  411. package/dist/shell/app-sidebar.module.css +53 -96
  412. package/dist/shell/app-sidebar.module.css.d.ts +0 -5
  413. package/dist/shell/index.d.ts +14 -6
  414. package/dist/shell/index.d.ts.map +1 -1
  415. package/dist/shell/index.js +47 -12
  416. package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
  417. package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
  418. package/dist/shell/isomorphic-layout-effect.js +5 -0
  419. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  420. package/dist/shell/mobile-nav-model.js +27 -6
  421. package/dist/shell/nav-count-badge.d.ts +5 -3
  422. package/dist/shell/nav-count-badge.d.ts.map +1 -1
  423. package/dist/shell/nav-count-badge.js +3 -1
  424. package/dist/shell/page-aside.d.ts +43 -0
  425. package/dist/shell/page-aside.d.ts.map +1 -0
  426. package/dist/shell/page-aside.js +202 -0
  427. package/dist/shell/page-aside.module.css +174 -0
  428. package/dist/shell/page-aside.module.css.d.ts +14 -0
  429. package/dist/shell/page-bar.d.ts +55 -15
  430. package/dist/shell/page-bar.d.ts.map +1 -1
  431. package/dist/shell/page-bar.js +130 -41
  432. package/dist/shell/page-bar.module.css +107 -13
  433. package/dist/shell/page-bar.module.css.d.ts +4 -1
  434. package/dist/shell/sidebar-search.d.ts +4 -2
  435. package/dist/shell/sidebar-search.d.ts.map +1 -1
  436. package/dist/shell/sidebar-search.js +9 -4
  437. package/dist/shell/sidebar-search.module.css +1 -1
  438. package/dist/state/fields.d.ts +23 -3
  439. package/dist/state/fields.d.ts.map +1 -1
  440. package/dist/state/fields.js +13 -2
  441. package/dist/state/persisted-or-local.d.ts +9 -0
  442. package/dist/state/persisted-or-local.d.ts.map +1 -0
  443. package/dist/state/persisted-or-local.js +24 -0
  444. package/dist/state.d.ts +1 -1
  445. package/dist/state.d.ts.map +1 -1
  446. package/dist/state.js +2 -6
  447. package/dist/surfaces.d.ts +12 -11
  448. package/dist/surfaces.d.ts.map +1 -1
  449. package/dist/surfaces.js +42 -12
  450. package/dist/theme/ctl-theme.d.ts.map +1 -1
  451. package/dist/theme/index.d.ts.map +1 -1
  452. package/dist/theme/index.js +102 -17
  453. package/dist/theme/nav-link.module.css +37 -26
  454. package/dist/theme/segmented-control.module.css +22 -0
  455. package/dist/theme/segmented-control.module.css.d.ts +1 -0
  456. package/dist/theme-lab/boot.d.ts +66 -0
  457. package/dist/theme-lab/boot.d.ts.map +1 -0
  458. package/dist/theme-lab/boot.js +82 -0
  459. package/dist/theme-lab/index.d.ts +4 -43
  460. package/dist/theme-lab/index.d.ts.map +1 -1
  461. package/dist/theme-lab/index.js +9 -69
  462. package/dist/theme-toggle/index.d.ts +3 -2
  463. package/dist/theme-toggle/index.d.ts.map +1 -1
  464. package/dist/theme-toggle/index.js +8 -2
  465. package/dist/tokens/index.d.ts +42 -0
  466. package/dist/tokens/index.d.ts.map +1 -1
  467. package/dist/tokens/index.js +35 -6
  468. package/dist/tokens/palette.d.ts +45 -25
  469. package/dist/tokens/palette.d.ts.map +1 -1
  470. package/dist/tokens/palette.js +31 -14
  471. package/dist/tokens.css +4 -5
  472. package/dist/utils/is-dev.d.ts +3 -0
  473. package/dist/utils/is-dev.d.ts.map +1 -0
  474. package/dist/utils/is-dev.js +6 -0
  475. package/dist/widget-header/delta-badge.d.ts +7 -1
  476. package/dist/widget-header/delta-badge.d.ts.map +1 -1
  477. package/dist/widget-header/delta-badge.js +8 -1
  478. package/dist/widget-header/index.d.ts +2 -2
  479. package/dist/widget-header/index.d.ts.map +1 -1
  480. package/dist/widget-header/index.js +3 -1
  481. package/dist/widget-header/widget-header.d.ts +69 -4
  482. package/dist/widget-header/widget-header.d.ts.map +1 -1
  483. package/dist/widget-header/widget-header.js +42 -27
  484. package/dist/widget-header/widget-header.module.css +72 -7
  485. package/llms.txt +6 -6
  486. package/package.json +49 -25
  487. package/src/agent/coalesce.ts +2 -1
  488. package/src/agent/merge.ts +2 -1
  489. package/src/agent/part-list.tsx +2 -0
  490. package/src/agent/stick-to-bottom.tsx +9 -11
  491. package/src/agent-chat/composer.tsx +124 -103
  492. package/src/agent-chat/relative-time.ts +2 -1
  493. package/src/agent-chat/thread-detail-panel.tsx +9 -5
  494. package/src/agent-chat/thread-feed-row.tsx +17 -9
  495. package/src/agent-chat/thread-feed.tsx +9 -3
  496. package/src/agent-chat/thread-message.tsx +48 -13
  497. package/src/agent-chat/thread-outcome-card.tsx +9 -1
  498. package/src/agent-chat/thread-workspace.tsx +72 -33
  499. package/src/agent-chat/tool-chip.tsx +8 -2
  500. package/src/charts/hooks/useBandPlot.ts +26 -8
  501. package/src/charts/index.ts +39 -4
  502. package/src/charts/kinds/BandStrip.tsx +36 -14
  503. package/src/charts/kinds/Bars.tsx +67 -16
  504. package/src/charts/kinds/Donut.tsx +30 -3
  505. package/src/charts/kinds/DualPanel.tsx +56 -10
  506. package/src/charts/kinds/Heatmap.tsx +90 -33
  507. package/src/charts/kinds/MirroredBars.tsx +21 -3
  508. package/src/charts/kinds/MultiLine.tsx +61 -18
  509. package/src/charts/kinds/StackedArea.tsx +77 -6
  510. package/src/charts/kinds/ZonedLine.tsx +71 -31
  511. package/src/charts/layout/auto-margin.ts +134 -21
  512. package/src/charts/layout/log-ticks.ts +90 -0
  513. package/src/charts/primitives/Axes.tsx +33 -5
  514. package/src/charts/primitives/CartesianChart.tsx +194 -41
  515. package/src/charts/primitives/ChartCard.tsx +66 -25
  516. package/src/charts/primitives/ChartFrame.tsx +107 -26
  517. package/src/charts/primitives/ChartLegend.tsx +43 -13
  518. package/src/charts/primitives/ChartPending.tsx +185 -7
  519. package/src/charts/primitives/ChartTooltip.tsx +37 -5
  520. package/src/charts/primitives/Crosshair.tsx +6 -3
  521. package/src/charts/primitives/chart-frame-layout.ts +212 -0
  522. package/src/charts/primitives/chart-tier.tsx +37 -0
  523. package/src/charts/series.ts +77 -1
  524. package/src/charts/sparklines/BarSparkline.tsx +11 -3
  525. package/src/charts/sparklines/LineSparkline.tsx +18 -4
  526. package/src/charts/sparklines/index.ts +2 -2
  527. package/src/charts/utils/format.ts +163 -2
  528. package/src/charts/utils/ticks.ts +149 -4
  529. package/src/cli/index.ts +8 -3
  530. package/src/commands/ShortcutsHelp.tsx +10 -3
  531. package/src/commands/define-commands.ts +2 -2
  532. package/src/commands/define-overlays.ts +216 -1
  533. package/src/commands/index.ts +12 -0
  534. package/src/commands/overlays-mount.tsx +60 -26
  535. package/src/common/errors.ts +73 -0
  536. package/src/common/index.ts +21 -0
  537. package/src/common/props.ts +71 -0
  538. package/src/common/query-state-like.ts +64 -0
  539. package/src/common/refs.ts +58 -0
  540. package/src/common/scroll-parent.ts +42 -0
  541. package/src/common/validate.ts +94 -0
  542. package/src/connectivity/connectivity-indicator.tsx +6 -1
  543. package/src/connectivity/connectivity-provider.tsx +4 -1
  544. package/src/connectivity/index.ts +1 -0
  545. package/src/connectivity/types.ts +5 -2
  546. package/src/connectivity/use-connectivity.ts +3 -2
  547. package/src/content/article-layout.module.css +11 -4
  548. package/src/content/callout.tsx +13 -7
  549. package/src/content/guide.tsx +14 -4
  550. package/src/content/heading-anchor.tsx +14 -3
  551. package/src/content/index.ts +2 -0
  552. package/src/content/markdown.tsx +2 -0
  553. package/src/content/prose.module.css +9 -7
  554. package/src/content/reading-progress.tsx +40 -15
  555. package/src/content/slug.ts +4 -0
  556. package/src/content/toc.tsx +47 -10
  557. package/src/controls/actions.tsx +89 -9
  558. package/src/controls/compare-filter.tsx +20 -3
  559. package/src/controls/control-group.tsx +11 -4
  560. package/src/controls/controls.module.css +169 -186
  561. package/src/controls/controls.module.css.d.ts +9 -11
  562. package/src/controls/enum-filter.tsx +32 -16
  563. package/src/controls/filter-context.tsx +14 -6
  564. package/src/controls/filter-pill.tsx +6 -3
  565. package/src/controls/filter-set.tsx +32 -13
  566. package/src/controls/filter-sheet.tsx +44 -194
  567. package/src/controls/index.ts +7 -0
  568. package/src/controls/multi-select-filter.tsx +177 -27
  569. package/src/controls/number-filter.tsx +61 -23
  570. package/src/controls/panel-row.module.css +86 -0
  571. package/src/controls/panel-row.module.css.d.ts +13 -0
  572. package/src/controls/panel-row.tsx +392 -0
  573. package/src/controls/range-filter.tsx +76 -39
  574. package/src/controls/search-filter.tsx +30 -12
  575. package/src/controls/select-filter.tsx +11 -1
  576. package/src/controls/slider-control.tsx +159 -0
  577. package/src/controls/sync-button.tsx +13 -3
  578. package/src/controls/toggle-filter.tsx +36 -16
  579. package/src/controls/view-tabs.tsx +63 -11
  580. package/src/controls-dates/date-range-picker.tsx +8 -1
  581. package/src/dashboard/delta-badge.tsx +5 -1
  582. package/src/dashboard/empty-state.tsx +72 -25
  583. package/src/dashboard/index.ts +13 -1
  584. package/src/dashboard/query-state.tsx +154 -84
  585. package/src/dashboard/settings-section.tsx +87 -31
  586. package/src/dashboard/stat-card.module.css +46 -31
  587. package/src/dashboard/stat-card.module.css.d.ts +1 -0
  588. package/src/dashboard/stat-card.tsx +185 -127
  589. package/src/dashboard/stat-group.module.css +66 -0
  590. package/src/dashboard/stat-group.module.css.d.ts +8 -0
  591. package/src/dashboard/stat-group.tsx +74 -0
  592. package/src/dashboard/widget-grid.module.css +53 -0
  593. package/src/dashboard/widget-grid.module.css.d.ts +9 -0
  594. package/src/dashboard/widget-grid.tsx +127 -0
  595. package/src/data/data-table.module.css +64 -0
  596. package/src/data/data-table.module.css.d.ts +11 -0
  597. package/src/data/data-table.tsx +919 -346
  598. package/src/data/index.ts +30 -2
  599. package/src/data/query-branch.ts +53 -0
  600. package/src/data/table.ts +5 -0
  601. package/src/data/virtual-list.tsx +189 -55
  602. package/src/data/virtual.ts +5 -1
  603. package/src/forms/FormErrorSummary.tsx +13 -6
  604. package/src/forms/create-form.ts +2 -2
  605. package/src/forms/field.ts +60 -11
  606. package/src/forms/form-layout.module.css +145 -0
  607. package/src/forms/form-layout.module.css.d.ts +20 -0
  608. package/src/forms/form-layout.tsx +305 -0
  609. package/src/forms/form-state.tsx +94 -0
  610. package/src/forms/index.ts +38 -7
  611. package/src/forms/use-field-array.ts +94 -0
  612. package/src/forms/use-form-draft.ts +145 -53
  613. package/src/forms/use-form-submit.ts +199 -0
  614. package/src/guard/index.ts +73 -18
  615. package/src/index.ts +74 -10
  616. package/src/nav/types.ts +15 -6
  617. package/src/notifications/bell.tsx +18 -4
  618. package/src/notifications/center.tsx +18 -5
  619. package/src/notifications/define-notifications.ts +64 -18
  620. package/src/notifications/index.ts +12 -1
  621. package/src/notifications/mount-guard.ts +41 -0
  622. package/src/notifications/notify.tsx +122 -3
  623. package/src/notifications/overlay.tsx +2 -0
  624. package/src/notifications/store.ts +7 -0
  625. package/src/provider/index.tsx +101 -13
  626. package/src/provider/lab-theme.ts +2 -1
  627. package/src/query/devtools.tsx +1 -0
  628. package/src/router-tanstack/field.ts +1 -0
  629. package/src/router-tanstack/search-store.ts +3 -10
  630. package/src/router-tanstack/use-nav.ts +18 -13
  631. package/src/section/index.ts +1 -1
  632. package/src/section/section.module.css +11 -0
  633. package/src/section/section.tsx +163 -96
  634. package/src/shell/account-types.ts +5 -2
  635. package/src/shell/app-brand.module.css +109 -0
  636. package/src/shell/app-brand.module.css.d.ts +14 -0
  637. package/src/shell/app-brand.tsx +140 -0
  638. package/src/shell/app-breadcrumbs.tsx +19 -9
  639. package/src/shell/app-header.module.css +10 -5
  640. package/src/shell/app-main.module.css +107 -0
  641. package/src/shell/app-main.module.css.d.ts +11 -0
  642. package/src/shell/app-mobile-nav.module.css +48 -27
  643. package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
  644. package/src/shell/app-mobile-nav.tsx +43 -11
  645. package/src/shell/app-sidebar-account.tsx +29 -6
  646. package/src/shell/app-sidebar.module.css +53 -96
  647. package/src/shell/app-sidebar.module.css.d.ts +0 -5
  648. package/src/shell/app-sidebar.tsx +40 -113
  649. package/src/shell/index.tsx +156 -65
  650. package/src/shell/isomorphic-layout-effect.ts +14 -0
  651. package/src/shell/mobile-nav-model.ts +66 -10
  652. package/src/shell/nav-count-badge.tsx +6 -1
  653. package/src/shell/page-aside.module.css +174 -0
  654. package/src/shell/page-aside.module.css.d.ts +14 -0
  655. package/src/shell/page-aside.tsx +377 -0
  656. package/src/shell/page-bar.module.css +107 -13
  657. package/src/shell/page-bar.module.css.d.ts +4 -1
  658. package/src/shell/page-bar.tsx +294 -94
  659. package/src/shell/sidebar-search.module.css +1 -1
  660. package/src/shell/sidebar-search.tsx +13 -4
  661. package/src/state/fields.ts +43 -10
  662. package/src/state/persisted-or-local.ts +50 -0
  663. package/src/state.ts +0 -9
  664. package/src/surfaces.ts +42 -12
  665. package/src/theme/ctl-theme.tsx +10 -0
  666. package/src/theme/index.ts +129 -16
  667. package/src/theme/nav-link.module.css +37 -26
  668. package/src/theme/segmented-control.module.css +22 -0
  669. package/src/theme/segmented-control.module.css.d.ts +1 -0
  670. package/src/theme-lab/boot.ts +140 -0
  671. package/src/theme-lab/index.tsx +21 -112
  672. package/src/theme-toggle/index.tsx +10 -3
  673. package/src/tokens/index.ts +50 -6
  674. package/src/tokens/palette.ts +58 -30
  675. package/src/utils/is-dev.ts +4 -0
  676. package/src/widget-header/delta-badge.tsx +25 -5
  677. package/src/widget-header/index.ts +10 -2
  678. package/src/widget-header/widget-header.module.css +72 -7
  679. package/src/widget-header/widget-header.tsx +150 -71
  680. package/dist/agent/adapter.test.d.ts +0 -2
  681. package/dist/agent/adapter.test.d.ts.map +0 -1
  682. package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
  683. package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
  684. package/dist/agent/coalesce.test.d.ts +0 -2
  685. package/dist/agent/coalesce.test.d.ts.map +0 -1
  686. package/dist/agent/foreign.test.d.ts +0 -2
  687. package/dist/agent/foreign.test.d.ts.map +0 -1
  688. package/dist/agent/id.test.d.ts +0 -2
  689. package/dist/agent/id.test.d.ts.map +0 -1
  690. package/dist/agent/merge.test.d.ts +0 -2
  691. package/dist/agent/merge.test.d.ts.map +0 -1
  692. package/dist/agent/part-list.test.d.ts +0 -2
  693. package/dist/agent/part-list.test.d.ts.map +0 -1
  694. package/dist/agent/parts.test.d.ts +0 -2
  695. package/dist/agent/parts.test.d.ts.map +0 -1
  696. package/dist/agent/thread.test.d.ts +0 -2
  697. package/dist/agent/thread.test.d.ts.map +0 -1
  698. package/dist/agent/transport.test.d.ts +0 -2
  699. package/dist/agent/transport.test.d.ts.map +0 -1
  700. package/dist/agent/use-agent-stream.test.d.ts +0 -2
  701. package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
  702. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
  703. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
  704. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
  705. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
  706. package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
  707. package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
  708. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
  709. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
  710. package/dist/agent-chat/composer.test.d.ts +0 -2
  711. package/dist/agent-chat/composer.test.d.ts.map +0 -1
  712. package/dist/agent-chat/relative-time.test.d.ts +0 -2
  713. package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
  714. package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
  715. package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
  716. package/dist/agent-chat/thread-feed.test.d.ts +0 -2
  717. package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
  718. package/dist/agent-chat/thread-message.test.d.ts +0 -2
  719. package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
  720. package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
  721. package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
  722. package/dist/agent-chat/tool-chip.test.d.ts +0 -2
  723. package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
  724. package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
  725. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
  726. package/dist/charts/cursor/resolve.test.d.ts +0 -2
  727. package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
  728. package/dist/charts/cursor/store.test.d.ts +0 -2
  729. package/dist/charts/cursor/store.test.d.ts.map +0 -1
  730. package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
  731. package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
  732. package/dist/charts/kinds/Bars.test.d.ts +0 -2
  733. package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
  734. package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
  735. package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
  736. package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
  737. package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
  738. package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
  739. package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
  740. package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
  741. package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
  742. package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
  743. package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
  744. package/dist/charts/layout/auto-margin.test.d.ts +0 -2
  745. package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
  746. package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
  747. package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
  748. package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
  749. package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
  750. package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
  751. package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
  752. package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
  753. package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
  754. package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
  755. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
  756. package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
  757. package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
  758. package/dist/charts/series.test.d.ts +0 -2
  759. package/dist/charts/series.test.d.ts.map +0 -1
  760. package/dist/charts/utils/domain.test.d.ts +0 -2
  761. package/dist/charts/utils/domain.test.d.ts.map +0 -1
  762. package/dist/charts/utils/measure-text.test.d.ts +0 -2
  763. package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
  764. package/dist/cli/check-coverage.test.d.ts +0 -2
  765. package/dist/cli/check-coverage.test.d.ts.map +0 -1
  766. package/dist/cli/check-theme.test.d.ts +0 -2
  767. package/dist/cli/check-theme.test.d.ts.map +0 -1
  768. package/dist/cli/help.test.d.ts +0 -2
  769. package/dist/cli/help.test.d.ts.map +0 -1
  770. package/dist/cli/init-sync.test.d.ts +0 -2
  771. package/dist/cli/init-sync.test.d.ts.map +0 -1
  772. package/dist/cli/placement-engine.test.d.ts +0 -2
  773. package/dist/cli/placement-engine.test.d.ts.map +0 -1
  774. package/dist/cli/shipped-versions.test.d.ts +0 -2
  775. package/dist/cli/shipped-versions.test.d.ts.map +0 -1
  776. package/dist/cli/tarball-parity.test.d.ts +0 -2
  777. package/dist/cli/tarball-parity.test.d.ts.map +0 -1
  778. package/dist/cli/template-format.test.d.ts +0 -2
  779. package/dist/cli/template-format.test.d.ts.map +0 -1
  780. package/dist/cli/template-ledger.test.d.ts +0 -2
  781. package/dist/cli/template-ledger.test.d.ts.map +0 -1
  782. package/dist/cli/tokens-css.test.d.ts +0 -2
  783. package/dist/cli/tokens-css.test.d.ts.map +0 -1
  784. package/dist/cli/toolchain-wiring.test.d.ts +0 -2
  785. package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
  786. package/dist/cli/waiver-audit.test.d.ts +0 -2
  787. package/dist/cli/waiver-audit.test.d.ts.map +0 -1
  788. package/dist/content/article-actions.test.d.ts +0 -2
  789. package/dist/content/article-actions.test.d.ts.map +0 -1
  790. package/dist/content/article-model.test.d.ts +0 -2
  791. package/dist/content/article-model.test.d.ts.map +0 -1
  792. package/dist/content/block-split.test.d.ts +0 -2
  793. package/dist/content/block-split.test.d.ts.map +0 -1
  794. package/dist/content/heading-components.test.d.ts +0 -2
  795. package/dist/content/heading-components.test.d.ts.map +0 -1
  796. package/dist/content/markdown.test.d.ts +0 -2
  797. package/dist/content/markdown.test.d.ts.map +0 -1
  798. package/dist/content/prose.module.css.test.d.ts +0 -2
  799. package/dist/content/prose.module.css.test.d.ts.map +0 -1
  800. package/dist/content/sanitize.test.d.ts +0 -2
  801. package/dist/content/sanitize.test.d.ts.map +0 -1
  802. package/dist/content/slug.test.d.ts +0 -2
  803. package/dist/content/slug.test.d.ts.map +0 -1
  804. package/dist/controls/actions.test.d.ts +0 -2
  805. package/dist/controls/actions.test.d.ts.map +0 -1
  806. package/dist/controls/actions.type-guard.test.d.ts +0 -2
  807. package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
  808. package/dist/controls/control-group.test.d.ts +0 -2
  809. package/dist/controls/control-group.test.d.ts.map +0 -1
  810. package/dist/controls/controls.router.test.d.ts +0 -2
  811. package/dist/controls/controls.router.test.d.ts.map +0 -1
  812. package/dist/controls/filter-set.test.d.ts +0 -2
  813. package/dist/controls/filter-set.test.d.ts.map +0 -1
  814. package/dist/controls/peer-boundary.test.d.ts +0 -2
  815. package/dist/controls/peer-boundary.test.d.ts.map +0 -1
  816. package/dist/controls/props-contract.test.d.ts +0 -2
  817. package/dist/controls/props-contract.test.d.ts.map +0 -1
  818. package/dist/controls/range-filter.type-guard.test.d.ts +0 -2
  819. package/dist/controls/range-filter.type-guard.test.d.ts.map +0 -1
  820. package/dist/controls/sync-button.test.d.ts +0 -2
  821. package/dist/controls/sync-button.test.d.ts.map +0 -1
  822. package/dist/dashboard/query-state.test.d.ts +0 -2
  823. package/dist/dashboard/query-state.test.d.ts.map +0 -1
  824. package/dist/dashboard/settings-section.test.d.ts +0 -2
  825. package/dist/dashboard/settings-section.test.d.ts.map +0 -1
  826. package/dist/dashboard/stat-card.test.d.ts +0 -2
  827. package/dist/dashboard/stat-card.test.d.ts.map +0 -1
  828. package/dist/data/data-table.test.d.ts +0 -2
  829. package/dist/data/data-table.test.d.ts.map +0 -1
  830. package/dist/guard/check-source.test.d.ts +0 -2
  831. package/dist/guard/check-source.test.d.ts.map +0 -1
  832. package/dist/guard/grace.test.d.ts +0 -20
  833. package/dist/guard/grace.test.d.ts.map +0 -1
  834. package/dist/guard/guard-hook.test.d.ts +0 -2
  835. package/dist/guard/guard-hook.test.d.ts.map +0 -1
  836. package/dist/provider/index.test.d.ts +0 -2
  837. package/dist/provider/index.test.d.ts.map +0 -1
  838. package/dist/provider/lab-theme.test.d.ts +0 -2
  839. package/dist/provider/lab-theme.test.d.ts.map +0 -1
  840. package/dist/query/error-message.test.d.ts +0 -2
  841. package/dist/query/error-message.test.d.ts.map +0 -1
  842. package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
  843. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
  844. package/dist/router-tanstack/nav.test.d.ts +0 -2
  845. package/dist/router-tanstack/nav.test.d.ts.map +0 -1
  846. package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
  847. package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
  848. package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
  849. package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
  850. package/dist/router-tanstack/search-store.test.d.ts +0 -2
  851. package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
  852. package/dist/router-tanstack/use-nav.test.d.ts +0 -2
  853. package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
  854. package/dist/section/section.test.d.ts +0 -2
  855. package/dist/section/section.test.d.ts.map +0 -1
  856. package/dist/shell/app-mobile-nav.test.d.ts +0 -2
  857. package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
  858. package/dist/shell/app-sidebar.test.d.ts +0 -2
  859. package/dist/shell/app-sidebar.test.d.ts.map +0 -1
  860. package/dist/shell/index.test.d.ts +0 -2
  861. package/dist/shell/index.test.d.ts.map +0 -1
  862. package/dist/shell/mobile-nav-model.test.d.ts +0 -2
  863. package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
  864. package/dist/shell/page-bar.test.d.ts +0 -2
  865. package/dist/shell/page-bar.test.d.ts.map +0 -1
  866. package/dist/state/fields.test.d.ts +0 -2
  867. package/dist/state/fields.test.d.ts.map +0 -1
  868. package/dist/state/persisted.test.d.ts +0 -2
  869. package/dist/state/persisted.test.d.ts.map +0 -1
  870. package/dist/styles.floor.test.d.ts +0 -2
  871. package/dist/styles.floor.test.d.ts.map +0 -1
  872. package/dist/theme/border-coverage.test.d.ts +0 -18
  873. package/dist/theme/border-coverage.test.d.ts.map +0 -1
  874. package/dist/theme/contrast.test.d.ts +0 -2
  875. package/dist/theme/contrast.test.d.ts.map +0 -1
  876. package/dist/theme/ctl-theme.test.d.ts +0 -2
  877. package/dist/theme/ctl-theme.test.d.ts.map +0 -1
  878. package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
  879. package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
  880. package/dist/theme/density-option.test.d.ts +0 -2
  881. package/dist/theme/density-option.test.d.ts.map +0 -1
  882. package/dist/theme/density-relations.test.d.ts +0 -2
  883. package/dist/theme/density-relations.test.d.ts.map +0 -1
  884. package/dist/theme/derive-theme.test.d.ts +0 -2
  885. package/dist/theme/derive-theme.test.d.ts.map +0 -1
  886. package/dist/theme/divider-law.test.d.ts +0 -2
  887. package/dist/theme/divider-law.test.d.ts.map +0 -1
  888. package/dist/theme/fonts-option.test.d.ts +0 -2
  889. package/dist/theme/fonts-option.test.d.ts.map +0 -1
  890. package/dist/theme/icon-slot.test.d.ts +0 -2
  891. package/dist/theme/icon-slot.test.d.ts.map +0 -1
  892. package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
  893. package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
  894. package/dist/theme/radius-option.test.d.ts +0 -2
  895. package/dist/theme/radius-option.test.d.ts.map +0 -1
  896. package/dist/theme/radius.test.d.ts +0 -2
  897. package/dist/theme/radius.test.d.ts.map +0 -1
  898. package/dist/theme/shadow-surfaces.test.d.ts +0 -26
  899. package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
  900. package/dist/theme/spacing.test.d.ts +0 -2
  901. package/dist/theme/spacing.test.d.ts.map +0 -1
  902. package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
  903. package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
  904. package/dist/theme-lab/derive-controls.test.d.ts +0 -2
  905. package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
  906. package/dist/theme-lab/derive-state.test.d.ts +0 -2
  907. package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
  908. package/dist/theme-lab/theme-lab.test.d.ts +0 -2
  909. package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
  910. package/dist/tokens/build-fonts-css.test.d.ts +0 -2
  911. package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
  912. package/dist/tokens/build-radius-css.test.d.ts +0 -2
  913. package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
  914. package/dist/tokens/chart-margin.test.d.ts +0 -2
  915. package/dist/tokens/chart-margin.test.d.ts.map +0 -1
  916. package/dist/tokens/density.test.d.ts +0 -2
  917. package/dist/tokens/density.test.d.ts.map +0 -1
  918. package/dist/tokens/derive-radius.test.d.ts +0 -2
  919. package/dist/tokens/derive-radius.test.d.ts.map +0 -1
  920. package/dist/tokens/derive.test.d.ts +0 -2
  921. package/dist/tokens/derive.test.d.ts.map +0 -1
  922. package/dist/tokens/palette.test.d.ts +0 -2
  923. package/dist/tokens/palette.test.d.ts.map +0 -1
  924. package/dist/tokens/text-scale.test.d.ts +0 -2
  925. package/dist/tokens/text-scale.test.d.ts.map +0 -1
  926. package/dist/vite-color-mix.test.d.ts +0 -2
  927. package/dist/vite-color-mix.test.d.ts.map +0 -1
  928. package/dist/vite.test.d.ts +0 -2
  929. package/dist/vite.test.d.ts.map +0 -1
  930. package/dist/widget-header/delta-badge.test.d.ts +0 -2
  931. package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
  932. package/dist/widget-header/no-mantine.test.d.ts +0 -2
  933. package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
  934. package/dist/widget-header/widget-header.test.d.ts +0 -2
  935. package/dist/widget-header/widget-header.test.d.ts.map +0 -1
  936. package/src/agent/adapter.test.ts +0 -1640
  937. package/src/agent/ai-sdk-transport.test.ts +0 -658
  938. package/src/agent/coalesce.test.ts +0 -387
  939. package/src/agent/foreign.test.ts +0 -51
  940. package/src/agent/id.test.ts +0 -169
  941. package/src/agent/merge.test.ts +0 -206
  942. package/src/agent/part-list.test.tsx +0 -112
  943. package/src/agent/parts.test.ts +0 -720
  944. package/src/agent/thread.test.ts +0 -220
  945. package/src/agent/transport.test.ts +0 -70
  946. package/src/agent/use-agent-stream.test.tsx +0 -287
  947. package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
  948. package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
  949. package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
  950. package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
  951. package/src/agent-chat/composer.test.tsx +0 -924
  952. package/src/agent-chat/relative-time.test.ts +0 -95
  953. package/src/agent-chat/thread-feed-row.test.tsx +0 -510
  954. package/src/agent-chat/thread-feed.test.tsx +0 -241
  955. package/src/agent-chat/thread-message.test.tsx +0 -1285
  956. package/src/agent-chat/thread-workspace.test.tsx +0 -169
  957. package/src/agent-chat/tool-chip.test.tsx +0 -139
  958. package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
  959. package/src/charts/cursor/resolve.test.ts +0 -151
  960. package/src/charts/cursor/store.test.ts +0 -64
  961. package/src/charts/kinds/BandStrip.test.tsx +0 -282
  962. package/src/charts/kinds/Bars.test.tsx +0 -183
  963. package/src/charts/kinds/DualPanel.test.tsx +0 -527
  964. package/src/charts/kinds/MirroredBars.test.tsx +0 -232
  965. package/src/charts/kinds/MultiLine.test.tsx +0 -218
  966. package/src/charts/kinds/StackedArea.test.tsx +0 -114
  967. package/src/charts/kinds/ZonedLine.test.tsx +0 -147
  968. package/src/charts/layout/auto-margin.test.ts +0 -42
  969. package/src/charts/primitives/CartesianChart.test.tsx +0 -647
  970. package/src/charts/primitives/ChartCard.test.tsx +0 -66
  971. package/src/charts/primitives/ChartFrame.test.tsx +0 -142
  972. package/src/charts/primitives/ChartLegend.test.tsx +0 -78
  973. package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
  974. package/src/charts/primitives/XZoneRects.test.tsx +0 -123
  975. package/src/charts/series.test.ts +0 -112
  976. package/src/charts/utils/domain.test.ts +0 -54
  977. package/src/charts/utils/measure-text.test.ts +0 -36
  978. package/src/cli/check-coverage.test.ts +0 -297
  979. package/src/cli/check-theme.test.ts +0 -837
  980. package/src/cli/help.test.ts +0 -170
  981. package/src/cli/init-sync.test.ts +0 -343
  982. package/src/cli/placement-engine.test.ts +0 -298
  983. package/src/cli/shipped-versions.test.ts +0 -83
  984. package/src/cli/tarball-parity.test.ts +0 -44
  985. package/src/cli/template-format.test.ts +0 -64
  986. package/src/cli/template-ledger.test.ts +0 -219
  987. package/src/cli/tokens-css.test.ts +0 -249
  988. package/src/cli/toolchain-wiring.test.ts +0 -1358
  989. package/src/cli/waiver-audit.test.ts +0 -215
  990. package/src/content/article-actions.test.ts +0 -78
  991. package/src/content/article-model.test.ts +0 -119
  992. package/src/content/block-split.test.ts +0 -75
  993. package/src/content/heading-components.test.tsx +0 -65
  994. package/src/content/markdown.test.tsx +0 -603
  995. package/src/content/prose.module.css.test.ts +0 -39
  996. package/src/content/sanitize.test.ts +0 -659
  997. package/src/content/slug.test.ts +0 -62
  998. package/src/controls/actions.test.tsx +0 -252
  999. package/src/controls/actions.type-guard.test.ts +0 -63
  1000. package/src/controls/control-group.test.tsx +0 -229
  1001. package/src/controls/controls.router.test.tsx +0 -1053
  1002. package/src/controls/filter-set.test.tsx +0 -472
  1003. package/src/controls/peer-boundary.test.ts +0 -85
  1004. package/src/controls/props-contract.test.tsx +0 -137
  1005. package/src/controls/range-filter.type-guard.test.tsx +0 -122
  1006. package/src/controls/sync-button.test.tsx +0 -183
  1007. package/src/dashboard/query-state.test.tsx +0 -152
  1008. package/src/dashboard/settings-section.test.tsx +0 -56
  1009. package/src/dashboard/stat-card.test.tsx +0 -385
  1010. package/src/data/data-table.test.tsx +0 -434
  1011. package/src/guard/check-source.test.ts +0 -3388
  1012. package/src/guard/grace.test.ts +0 -158
  1013. package/src/guard/guard-hook.test.ts +0 -203
  1014. package/src/provider/index.test.tsx +0 -101
  1015. package/src/provider/lab-theme.test.ts +0 -147
  1016. package/src/query/error-message.test.ts +0 -67
  1017. package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
  1018. package/src/router-tanstack/nav.test.ts +0 -111
  1019. package/src/router-tanstack/search-param-store.test.ts +0 -135
  1020. package/src/router-tanstack/search-store.router.test.tsx +0 -882
  1021. package/src/router-tanstack/search-store.test.ts +0 -578
  1022. package/src/router-tanstack/use-nav.test.tsx +0 -187
  1023. package/src/section/section.test.tsx +0 -204
  1024. package/src/shell/app-mobile-nav.test.tsx +0 -403
  1025. package/src/shell/app-sidebar.test.tsx +0 -509
  1026. package/src/shell/index.test.tsx +0 -428
  1027. package/src/shell/mobile-nav-model.test.ts +0 -517
  1028. package/src/shell/page-bar.test.tsx +0 -316
  1029. package/src/state/fields.test.tsx +0 -512
  1030. package/src/state/persisted.test.ts +0 -91
  1031. package/src/styles.floor.test.ts +0 -36
  1032. package/src/theme/border-coverage.test.ts +0 -272
  1033. package/src/theme/contrast.test.ts +0 -354
  1034. package/src/theme/ctl-theme.test.tsx +0 -275
  1035. package/src/theme/ctl-tier-coverage.test.ts +0 -170
  1036. package/src/theme/density-option.test.ts +0 -96
  1037. package/src/theme/density-relations.test.ts +0 -506
  1038. package/src/theme/derive-theme.test.ts +0 -101
  1039. package/src/theme/divider-law.test.ts +0 -259
  1040. package/src/theme/fonts-option.test.ts +0 -75
  1041. package/src/theme/icon-slot.test.tsx +0 -275
  1042. package/src/theme/layout-rhythm-css.test.ts +0 -72
  1043. package/src/theme/radius-option.test.ts +0 -86
  1044. package/src/theme/radius.test.ts +0 -142
  1045. package/src/theme/shadow-surfaces.test.ts +0 -488
  1046. package/src/theme/spacing.test.ts +0 -495
  1047. package/src/theme/use-basalt-spacing.test.tsx +0 -55
  1048. package/src/theme-lab/derive-controls.test.ts +0 -94
  1049. package/src/theme-lab/derive-state.test.ts +0 -71
  1050. package/src/theme-lab/theme-lab.test.ts +0 -46
  1051. package/src/tokens/build-fonts-css.test.ts +0 -52
  1052. package/src/tokens/build-radius-css.test.ts +0 -25
  1053. package/src/tokens/chart-margin.test.ts +0 -39
  1054. package/src/tokens/density.test.ts +0 -259
  1055. package/src/tokens/derive-radius.test.ts +0 -62
  1056. package/src/tokens/derive.test.ts +0 -30
  1057. package/src/tokens/palette.test.ts +0 -114
  1058. package/src/tokens/text-scale.test.ts +0 -51
  1059. package/src/vite-color-mix.test.ts +0 -46
  1060. package/src/vite.test.ts +0 -423
  1061. package/src/widget-header/delta-badge.test.tsx +0 -60
  1062. package/src/widget-header/no-mantine.test.ts +0 -33
  1063. package/src/widget-header/widget-header.test.tsx +0 -187
@@ -14,6 +14,7 @@ import { dirname, resolve } from 'node:path'
14
14
  // eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
15
15
  import basaltPlugin, {
16
16
  CTL_THEME_TAGS,
17
+ DEPRECATED_EXPORTS,
17
18
  KNOWN_RULE_IDS,
18
19
  PLUGIN_RULE_ADVISORY,
19
20
  PLUGIN_RULE_GRACE,
@@ -35,7 +36,14 @@ beforeEach(() => {
35
36
  plugins: [],
36
37
  jsPlugins: [PLUGIN_PATH],
37
38
  rules: {
39
+ 'basalt/provider-above-router': 'error',
40
+ 'basalt/duplicate-notifications-mount': 'error',
41
+ 'basalt/query-dual-import': 'error',
42
+ 'basalt/query-fn-unwrap': 'error',
43
+ 'basalt/deprecated-export': 'error',
44
+ 'basalt/forms-field-key': 'error',
38
45
  'basalt/no-raw-font-size': 'error',
46
+ 'basalt/raw-size-literal': 'error',
39
47
  'basalt/card-inset': 'error',
40
48
  'basalt/chart-in-raw-surface': 'error',
41
49
  'basalt/hand-rolled-plot': 'error',
@@ -45,6 +53,7 @@ beforeEach(() => {
45
53
  'basalt/raw-scroll-container': 'error',
46
54
  'basalt/hand-rolled-filter': 'error',
47
55
  'basalt/control-outside-home': 'error',
56
+ 'basalt/bound-control-outside-home': 'error',
48
57
  'basalt/control-size-literal': 'error',
49
58
  'basalt/page-bar-budget': 'error',
50
59
  'basalt/in-body-page-title': 'error',
@@ -111,6 +120,76 @@ describe('basalt/no-raw-font-size', () => {
111
120
  expect(code).toBe(0)
112
121
  expect(rules).not.toContain('no-raw-font-size')
113
122
  })
123
+
124
+ // F25: symmetric with raw-size-literal's own test-file skip below — the doctrine is about
125
+ // shipped UI, not fixtures.
126
+ it('does NOT flag a numeric fz inside a *.test.tsx file', () => {
127
+ const { code, rules } = run(`export const C = () => <Text fz={10}>a</Text>\n`, 'lib.test.tsx')
128
+ expect(code).toBe(0)
129
+ expect(rules).not.toContain('no-raw-font-size')
130
+ })
131
+ })
132
+
133
+ // ── raw-size-literal ─────────────────────────────────────────────────────────
134
+
135
+ describe('basalt/raw-size-literal', () => {
136
+ it('flags a CSS-length string on size', () => {
137
+ const { code, rules } = run(`export const C = () => <ThemeIcon size="2rem" />\n`)
138
+ expect(code).toBe(1)
139
+ expect(rules).toContain('raw-size-literal')
140
+ })
141
+
142
+ it('flags a CSS-length string on fz', () => {
143
+ const { code, rules } = run(`export const C = () => <Text fz="0.8rem">a</Text>\n`)
144
+ expect(code).toBe(1)
145
+ expect(rules).toContain('raw-size-literal')
146
+ })
147
+
148
+ it('does NOT flag a scale token on size', () => {
149
+ const { code, rules } = run(`export const C = () => <ThemeIcon size="md" />\n`)
150
+ expect(code).toBe(0)
151
+ expect(rules).not.toContain('raw-size-literal')
152
+ })
153
+
154
+ it('does NOT flag a numeric size (the documented icon-dimension idiom)', () => {
155
+ const { code, rules } = run(`export const C = () => <ThemeIcon size={32} />\n`)
156
+ expect(code).toBe(0)
157
+ expect(rules).not.toContain('raw-size-literal')
158
+ })
159
+
160
+ it('does NOT flag with a same-line theme-allow comment', () => {
161
+ const { code, rules } = run(
162
+ `export const C = () => <Text fz="0.8rem" /* theme-allow: legacy */>a</Text>\n`,
163
+ )
164
+ expect(code).toBe(0)
165
+ expect(rules).not.toContain('raw-size-literal')
166
+ })
167
+
168
+ // F25: was the asymmetric half — no-raw-font-size already skipped test files, this rule did not.
169
+ it('does NOT flag a CSS-length string inside a *.test.tsx file', () => {
170
+ const { code, rules } = run(
171
+ `export const C = () => <Text fz="0.8rem">a</Text>\n`,
172
+ 'lib.test.tsx',
173
+ )
174
+ expect(code).toBe(0)
175
+ expect(rules).not.toContain('raw-size-literal')
176
+ })
177
+
178
+ // A3: the /src/tokens/ exemption used to be a bare path-substring check, then a check gated on
179
+ // `isBasaltScopedFile` — but that predicate tests "does the nearest package.json depend on
180
+ // basalt-ui", not "is this file basalt's own", so a CONSUMER package that merely depends on
181
+ // basalt-ui could still put raw literals under its own `src/tokens/` and silence the rule. The
182
+ // exemption is deleted outright rather than re-gated on package identity: both rules only ever
183
+ // visit JSX, and basalt's real `src/tokens/**` is pure `.ts` data with none, so no gate was ever
184
+ // load-bearing there. A `src/tokens/` path — basalt's own or a consumer's — now always fires.
185
+ it('DOES flag a CSS-length string inside a src/tokens/ file (no path exemption)', () => {
186
+ const { code, rules } = run(
187
+ `export const C = () => <ThemeIcon size="2rem" />\n`,
188
+ 'src/tokens/palette.tsx',
189
+ )
190
+ expect(code).toBe(1)
191
+ expect(rules).toContain('raw-size-literal')
192
+ })
114
193
  })
115
194
 
116
195
  // ── card-inset ───────────────────────────────────────────────────────────────
@@ -1296,6 +1375,30 @@ describe('PLUGIN_RULE_GRACE', () => {
1296
1375
  it('passes the C16 gate against the real ledger and package version', () => {
1297
1376
  expect(() => assertGraceLedger(Object.fromEntries(graceEntries), pkgVersion)).not.toThrow()
1298
1377
  })
1378
+
1379
+ // F27: a grace entry is written for a rule that lands in the NEXT release, so its `since` can sit
1380
+ // one minor ahead of `package.json`'s current version (`bound-control-outside-home` at 1.28.0
1381
+ // while the published version still reads 1.27.0, exactly like `control-outside-home` before it).
1382
+ // What this catches is a `since` that is malformed or planted further out than that — a value
1383
+ // static analysis has no way to verify is "the version this actually ships in", so the bound this
1384
+ // test enforces is the one thing that IS checkable: well-formed semver, no more than one minor
1385
+ // past what is currently published.
1386
+ it("every grace entry's since is valid semver, at most one minor past the published version", () => {
1387
+ const SEMVER = /^\d+\.\d+\.\d+$/
1388
+ const [major, minor] = pkgVersion.split('.').map(Number)
1389
+ const nextMinorCeiling = `${major}.${minor + 1}.0`
1390
+ for (const [id, entry] of graceEntries) {
1391
+ if (!SEMVER.test(entry.since)) {
1392
+ throw new Error(`${id}: \`since\` (${entry.since}) is not valid semver.`)
1393
+ }
1394
+ if (compareSemver(entry.since, nextMinorCeiling) > 0) {
1395
+ throw new Error(
1396
+ `${id}: \`since\` (${entry.since}) is more than one minor past the published version ` +
1397
+ `${pkgVersion}.`,
1398
+ )
1399
+ }
1400
+ }
1401
+ })
1299
1402
  })
1300
1403
 
1301
1404
  // ── chart-legend-literal: the .map() half ────────────────────────────────────
@@ -1681,6 +1784,28 @@ describe('basalt/control-outside-home', () => {
1681
1784
  },
1682
1785
  )
1683
1786
 
1787
+ // FormRow/FormGroup (`basalt-ui/forms`) are law C1's third home written for a `<form>` rather
1788
+ // than a settings page — the same non-home treatment SettingsRow gets, via the same
1789
+ // CONTROL_HOST_TAGS ancestry walk.
1790
+ it.each(['FormRow', 'FormGroup'])(
1791
+ 'does NOT flag a raw Select nested inside a %s imported from basalt-ui/forms',
1792
+ (host) => {
1793
+ const { code, rules } = run(
1794
+ `import { ${host} } from 'basalt-ui/forms'\n${MANTINE_IMPORT}` +
1795
+ `export const C = () => <${host} label="L"><Select data={[]} /></${host}>\n`,
1796
+ )
1797
+ expect(code).toBe(0)
1798
+ expect(rules).not.toContain('control-outside-home')
1799
+ },
1800
+ )
1801
+
1802
+ it('still flags a bare Select in a page body with no FormRow/FormGroup around it', () => {
1803
+ const { rules } = run(
1804
+ `${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`,
1805
+ )
1806
+ expect(rules).toContain('control-outside-home')
1807
+ })
1808
+
1684
1809
  it('does NOT flag one under a member-expression non-home (Menu.Dropdown)', () => {
1685
1810
  const { rules } = run(
1686
1811
  `${MANTINE_IMPORT}export const C = () => <Menu.Dropdown><Select data={[]} /></Menu.Dropdown>\n`,
@@ -1708,6 +1833,58 @@ describe('basalt/control-outside-home', () => {
1708
1833
  expect(rules).not.toContain('control-outside-home')
1709
1834
  })
1710
1835
 
1836
+ /**
1837
+ * The exemption's SECOND half. `CONTROL_OWNER_NAMES` carries generic names (`PanelRow`,
1838
+ * `EnumFilter`, `SliderControl`), so a bare-name match let one local helper switch this rule —
1839
+ * and `bound-control-outside-home` and `responsive-twin` with it — off for a whole consumer file.
1840
+ * A file that imports `basalt-ui*` is CONSUMING basalt; basalt's own control sources import each
1841
+ * other relatively and never name the package.
1842
+ */
1843
+ it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
1844
+ const { rules } = run(
1845
+ `import { Section } from 'basalt-ui'\n${MANTINE_IMPORT}` +
1846
+ `function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
1847
+ `export const C = () => (\n <Section title="x">\n <Select value={v} onChange={set} data={[]} />\n </Section>\n)\n`,
1848
+ )
1849
+ expect(rules).toContain('control-outside-home')
1850
+ })
1851
+
1852
+ /**
1853
+ * The basalt SUBTREE homes, which this rule did not know: `bound-control-outside-home` has read
1854
+ * `FilterSet` / `PageAside` / `PanelRow` as homes since it shipped, so a `<Select>` inside a
1855
+ * `<PageAside><PanelRow>` reported while the bound control beside it did not — two rules
1856
+ * answering one question two ways.
1857
+ */
1858
+ it('does NOT flag one inside a PageAside > PanelRow subtree', () => {
1859
+ const { code, rules } = run(
1860
+ `import { PageAside } from 'basalt-ui'\nimport { PanelRow } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
1861
+ `export const C = () => (\n <PageAside title="Filters">\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n </PageAside>\n)\n`,
1862
+ 'src/scale-page.tsx',
1863
+ )
1864
+ expect(code).toBe(0)
1865
+ expect(rules).not.toContain('control-outside-home')
1866
+ })
1867
+
1868
+ it('does NOT flag one inside a FilterSet subtree', () => {
1869
+ const { rules } = run(
1870
+ `import { FilterSet } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
1871
+ `export const C = () => (\n <FilterSet>\n <Select data={[]} />\n </FilterSet>\n)\n`,
1872
+ 'src/scale-page.tsx',
1873
+ )
1874
+ expect(rules).not.toContain('control-outside-home')
1875
+ })
1876
+
1877
+ // Provenance, the test `SLOT_OWNER_TAGS` already applies: a consumer's own `PanelRow` is a layout
1878
+ // helper, not a basalt home, so it exempts nothing.
1879
+ it("still flags one inside a consumer's OWN PanelRow", () => {
1880
+ const { rules } = run(
1881
+ `import { PanelRow } from './panel-row'\n${MANTINE_IMPORT}` +
1882
+ `export const C = () => (\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n)\n`,
1883
+ 'src/scale-page.tsx',
1884
+ )
1885
+ expect(rules).toContain('control-outside-home')
1886
+ })
1887
+
1711
1888
  it('flags an ALIASED Mantine import with no home', () => {
1712
1889
  const { code, rules } = run(
1713
1890
  `import { Select as MantineSelect } from '@mantine/core'\n` +
@@ -1770,6 +1947,252 @@ describe('basalt/control-outside-home', () => {
1770
1947
  const { rules } = run(SOURCE, 'src/modal.tsx')
1771
1948
  expect(rules).toContain('control-outside-home')
1772
1949
  })
1950
+
1951
+ // The SECOND dialect: a repo mandating `PascalCase.tsx` for component files (basalt's own root
1952
+ // CLAUDE.md included) can never write `foo-panel.tsx`, so the kebab form alone exempted nothing
1953
+ // there — which is how the CBBI panel collected four warns for a `<PageAside>` in its parent.
1954
+ it.each(['src/EditSessionModal.tsx', 'src/FiltersDrawer.tsx', 'src/CbbiPanel.tsx'])(
1955
+ 'does NOT flag %s — the PascalCase dialect of the same convention',
1956
+ (filename) => {
1957
+ const { code, rules } = run(SOURCE, filename)
1958
+ expect(code).toBe(0)
1959
+ expect(rules).not.toContain('control-outside-home')
1960
+ },
1961
+ )
1962
+
1963
+ /**
1964
+ * The trade, stated as a test rather than only as prose: a `FooPanel.tsx` that renders NO page
1965
+ * bar is exempt WHOLE-FILE, so a control that genuinely belongs in a bar goes unreported there.
1966
+ * Measured before keeping it — across argo, linewatch, image-share, rb and image-gen the
1967
+ * PascalCase dialect matches 9 files, all in image-gen, and NONE of them renders a `<PageBar>`
1968
+ * or sits under a `routes/` directory. Nine overlay bodies, zero pages, so the dialect stays
1969
+ * whole-file rather than growing a "does it render a page bar" predicate (CONTROLS-SPEC §6).
1970
+ */
1971
+ it('exempts a FooPanel.tsx that renders no page bar — the measured trade', () => {
1972
+ const { code, rules } = run(SOURCE, 'src/components/refine/CropPanel.tsx')
1973
+ expect(code).toBe(0)
1974
+ expect(rules).not.toContain('control-outside-home')
1975
+ })
1976
+
1977
+ it('needs the leading subject in the PascalCase dialect too', () => {
1978
+ const { rules } = run(SOURCE, 'src/Panel.tsx')
1979
+ expect(rules).toContain('control-outside-home')
1980
+ })
1981
+
1982
+ it('is CASE-sensitive — foopanel.tsx is neither dialect', () => {
1983
+ const { rules } = run(SOURCE, 'src/foopanel.tsx')
1984
+ expect(rules).toContain('control-outside-home')
1985
+ })
1986
+ })
1987
+ })
1988
+
1989
+ /**
1990
+ * Ledger G5 (`docs/ASIDE-SPEC.md` §2) — the basalt half of law C1. Mirrors the
1991
+ * `control-outside-home` block above case for case, because the two rules answer the same question
1992
+ * about a different tag set: that one matches raw Mantine, this one matches a bound basalt control.
1993
+ */
1994
+ describe('basalt/bound-control-outside-home', () => {
1995
+ const CONTROLS_IMPORT = `import { FilterSet, PanelRow, SelectFilter, SliderControl } from 'basalt-ui/controls'\n`
1996
+ const ASIDE_IMPORT = `import { PageAside, PageBar, Section } from 'basalt-ui'\n`
1997
+
1998
+ it("flags a bound control in a Section's BODY — the stray pill G5 named", () => {
1999
+ const { code, rules } = run(
2000
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Composition">\n <SelectFilter field={f} />\n </Section>\n)\n`,
2001
+ )
2002
+ expect(code).toBe(1)
2003
+ expect(rules).toContain('bound-control-outside-home')
2004
+ })
2005
+
2006
+ it('flags one in a bare page Stack', () => {
2007
+ const { rules } = run(
2008
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}` +
2009
+ `export const C = () => (\n <Stack>\n <SelectFilter field={f} />\n </Stack>\n)\n`,
2010
+ )
2011
+ expect(rules).toContain('bound-control-outside-home')
2012
+ })
2013
+
2014
+ it('does NOT flag one in a PageBar filters slot', () => {
2015
+ const { code, rules } = run(
2016
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => <PageBar filters={<SelectFilter field={f} />} />\n`,
2017
+ )
2018
+ expect(code).toBe(0)
2019
+ expect(rules).not.toContain('bound-control-outside-home')
2020
+ })
2021
+
2022
+ it('does NOT flag one inside a FilterSet subtree', () => {
2023
+ const { code, rules } = run(
2024
+ `${CONTROLS_IMPORT}export const C = () => (\n <FilterSet>\n <SelectFilter field={f} />\n </FilterSet>\n)\n`,
2025
+ )
2026
+ expect(code).toBe(0)
2027
+ expect(rules).not.toContain('bound-control-outside-home')
2028
+ })
2029
+
2030
+ // The aside is a shell REGION whose body IS a home — it scopes its children to the `panel`
2031
+ // surface, where the same control renders as a row rather than a pill (docs/ASIDE-SPEC.md §3).
2032
+ it('does NOT flag one inside a PageAside subtree, Section and all', () => {
2033
+ const { code, rules } = run(
2034
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <PageAside title="Filters">\n <Section title="Origin">\n <SelectFilter field={f} />\n </Section>\n </PageAside>\n)\n`,
2035
+ )
2036
+ expect(code).toBe(0)
2037
+ expect(rules).not.toContain('bound-control-outside-home')
2038
+ })
2039
+
2040
+ it("does NOT flag one as a PanelRow's `end` — the attribute hangs off PanelRow itself", () => {
2041
+ const { code, rules } = run(
2042
+ `${CONTROLS_IMPORT}export const C = () => <PanelRow label="Weight" end={<SelectFilter field={f} />} />\n`,
2043
+ )
2044
+ expect(code).toBe(0)
2045
+ expect(rules).not.toContain('bound-control-outside-home')
2046
+ })
2047
+
2048
+ it("does NOT flag one in a PanelRow's children — the row IS the home", () => {
2049
+ const { rules } = run(
2050
+ `${CONTROLS_IMPORT}export const C = () => (\n <PanelRow label="Origin">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
2051
+ )
2052
+ expect(rules).not.toContain('bound-control-outside-home')
2053
+ })
2054
+
2055
+ it('does NOT flag one inside an overlay host (Modal)', () => {
2056
+ const { rules } = run(
2057
+ `import { Modal } from '@mantine/core'\n${CONTROLS_IMPORT}` +
2058
+ `export const C = () => (\n <Modal opened>\n <SelectFilter field={f} />\n </Modal>\n)\n`,
2059
+ )
2060
+ expect(rules).not.toContain('bound-control-outside-home')
2061
+ })
2062
+
2063
+ // FormRow/FormGroup are the same declared non-home as SettingsRow — shared CONTROL_HOST_TAGS walk.
2064
+ it.each(['FormRow', 'FormGroup'])('does NOT flag one inside a %s', (host) => {
2065
+ const { code, rules } = run(
2066
+ `import { ${host} } from 'basalt-ui/forms'\n${CONTROLS_IMPORT}` +
2067
+ `export const C = () => <${host} label="L"><SelectFilter field={f} /></${host}>\n`,
2068
+ )
2069
+ expect(code).toBe(0)
2070
+ expect(rules).not.toContain('bound-control-outside-home')
2071
+ })
2072
+
2073
+ it.each(['src/cbbi-panel.tsx', 'src/CbbiPanel.tsx'])(
2074
+ 'does NOT flag %s — a panel body whose host lives in the parent, in either dialect',
2075
+ (filename) => {
2076
+ const { code, rules } = run(
2077
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2078
+ filename,
2079
+ )
2080
+ expect(code).toBe(0)
2081
+ expect(rules).not.toContain('bound-control-outside-home')
2082
+ },
2083
+ )
2084
+
2085
+ // Both dialects need the leading SUBJECT, and neither is case-insensitive — `foopanel.tsx` is a
2086
+ // page module spelled without a separator, not a declaration.
2087
+ it.each(['src/foopanel.tsx', 'src/Panel.tsx'])('still flags %s', (filename) => {
2088
+ const { rules } = run(
2089
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2090
+ filename,
2091
+ )
2092
+ expect(rules).toContain('bound-control-outside-home')
2093
+ })
2094
+
2095
+ it('honors a scoped theme-allow', () => {
2096
+ const { rules } = run(
2097
+ `${CONTROLS_IMPORT}export const C = () => (\n <div>\n {/* theme-allow bound-control-outside-home — a one-off embedded picker */}\n <SelectFilter field={f} />\n </div>\n)\n`,
2098
+ )
2099
+ expect(rules).not.toContain('bound-control-outside-home')
2100
+ })
2101
+
2102
+ it('honors a theme-allow-file declaration', () => {
2103
+ const { rules } = run(
2104
+ `// theme-allow-file bound-control-outside-home — a legacy page, migrating next sprint\n` +
2105
+ `${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
2106
+ )
2107
+ expect(rules).not.toContain('bound-control-outside-home')
2108
+ })
2109
+
2110
+ // Provenance, the same test the raw-filter rules apply the other way round: a consumer's own
2111
+ // `SelectFilter` is not basalt's, and basalt has no claim over where it is written.
2112
+ it('does NOT flag a locally-defined SelectFilter', () => {
2113
+ const { code, rules } = run(
2114
+ `import { SelectFilter } from './my-filters'\n` +
2115
+ `export const C = () => <div><SelectFilter field={f} /></div>\n`,
2116
+ )
2117
+ expect(code).toBe(0)
2118
+ expect(rules).not.toContain('bound-control-outside-home')
2119
+ })
2120
+
2121
+ it('flags an ALIASED basalt import with no home', () => {
2122
+ const { rules } = run(
2123
+ `import { SelectFilter as Picker } from 'basalt-ui/controls'\n` +
2124
+ `export const C = () => <div><Picker field={f} /></div>\n`,
2125
+ )
2126
+ expect(rules).toContain('bound-control-outside-home')
2127
+ })
2128
+
2129
+ /**
2130
+ * The owner exemption needs the file to import NOTHING from `basalt-ui*` — see
2131
+ * `control-outside-home`'s copy for why. For THIS rule that makes the exemption reachable only
2132
+ * inside basalt's own `src/`, which is exactly where it is meant to apply: provenance already
2133
+ * requires a basalt import (or a relative one inside that tree), so a consumer file can never be
2134
+ * both the owner and a place this rule could fire.
2135
+ */
2136
+ it('DOES flag a consumer file that declares its own PanelRow', () => {
2137
+ const { rules } = run(
2138
+ `${CONTROLS_IMPORT}function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
2139
+ `export const C = () => <div><SelectFilter field={f} /></div>\n`,
2140
+ )
2141
+ expect(rules).toContain('bound-control-outside-home')
2142
+ })
2143
+
2144
+ // Provenance on the HOME too, the same test `control-outside-home` applies: a consumer's own
2145
+ // `PanelRow` is a layout helper, not the aside's row primitive.
2146
+ it("still flags one inside a consumer's OWN PanelRow", () => {
2147
+ const { rules } = run(
2148
+ `import { SelectFilter } from 'basalt-ui/controls'\nimport { PanelRow } from './panel-row'\n` +
2149
+ `export const C = () => (\n <PanelRow label="Scale">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
2150
+ )
2151
+ expect(rules).toContain('bound-control-outside-home')
2152
+ })
2153
+
2154
+ /**
2155
+ * `SliderControl` is NOT policed: it renders its own `PanelRow` and has no pill form at all
2156
+ * (`docs/ASIDE-SPEC.md` §3), so "this renders as a stray pill" is false for it and a `Section`
2157
+ * body is a legitimate place to write one. The plugin has no per-tag home set, so the honest fix
2158
+ * is dropping it from the policed set rather than giving it a private exemption.
2159
+ */
2160
+ it('does NOT flag a SliderControl in a Section body — it has no pill form', () => {
2161
+ const { code, rules } = run(
2162
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Weights">\n <SliderControl field={f} label="Pi" />\n </Section>\n)\n`,
2163
+ )
2164
+ expect(code).toBe(0)
2165
+ expect(rules).not.toContain('bound-control-outside-home')
2166
+ })
2167
+
2168
+ it('resolves a hoisted binding handed to a slot, like its siblings do', () => {
2169
+ const { rules } = run(
2170
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}const pills = <SelectFilter field={f} />\n` +
2171
+ `export const C = () => <PageBar filters={pills} />\n`,
2172
+ )
2173
+ expect(rules).not.toContain('bound-control-outside-home')
2174
+ })
2175
+
2176
+ /**
2177
+ * …and one handed to a subtree home as its CHILDREN, which the ancestry walk cannot see: at the
2178
+ * `const` there is no `PageAside` above the node at all. The same `Program:exit` deferral the
2179
+ * slot lane uses, over the home's `{expr}` children instead of its attributes.
2180
+ */
2181
+ it('resolves a hoisted binding rendered as a PageAside CHILD', () => {
2182
+ const { code, rules } = run(
2183
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
2184
+ `export const C = () => <PageAside title="Filters">{rows}</PageAside>\n`,
2185
+ )
2186
+ expect(code).toBe(0)
2187
+ expect(rules).not.toContain('bound-control-outside-home')
2188
+ })
2189
+
2190
+ it('still flags the same hoisted binding under a bare Stack', () => {
2191
+ const { rules } = run(
2192
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
2193
+ `export const C = () => <Stack>{rows}</Stack>\n`,
2194
+ )
2195
+ expect(rules).toContain('bound-control-outside-home')
1773
2196
  })
1774
2197
  })
1775
2198
 
@@ -2159,6 +2582,17 @@ describe('basalt/responsive-twin', () => {
2159
2582
  expect(rules).not.toContain('responsive-twin')
2160
2583
  })
2161
2584
 
2585
+ // The owner exemption's second half, the same predicate its two siblings apply: a consumer file
2586
+ // that names a helper `PanelRow` and imports basalt is consuming basalt, not defining a control.
2587
+ it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
2588
+ const { rules } = run(
2589
+ `import { Section } from 'basalt-ui'\n` +
2590
+ `function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
2591
+ `export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
2592
+ )
2593
+ expect(rules).toContain('responsive-twin')
2594
+ })
2595
+
2162
2596
  it('honors a scoped theme-allow', () => {
2163
2597
  const { rules } = run(
2164
2598
  `export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n {/* theme-allow responsive-twin — two genuinely different data sets */}\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
@@ -2507,6 +2941,7 @@ describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
2507
2941
 
2508
2942
  it('gives ./controls the control-tier rules', () => {
2509
2943
  expect([...SURFACES['./controls'].pluginRules].toSorted()).toEqual([
2944
+ 'bound-control-outside-home',
2510
2945
  'control-outside-home',
2511
2946
  'control-size-literal',
2512
2947
  'hand-rolled-filter',
@@ -2514,3 +2949,634 @@ describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
2514
2949
  ])
2515
2950
  })
2516
2951
  })
2952
+
2953
+ // ── provider-above-router (F5) ───────────────────────────────────────────────
2954
+
2955
+ describe('basalt/provider-above-router', () => {
2956
+ const IMPORTS =
2957
+ `import { BasaltProvider } from 'basalt-ui'\n` +
2958
+ `import { RouterProvider } from '@tanstack/react-router'\n`
2959
+
2960
+ it('flags a BasaltProvider rendered inside a RouterProvider', () => {
2961
+ const { code, rules } = run(
2962
+ `${IMPORTS}export const App = () => (\n` +
2963
+ ` <RouterProvider router={r}>\n <BasaltProvider>{null}</BasaltProvider>\n` +
2964
+ ` </RouterProvider>\n)\n`,
2965
+ )
2966
+ expect(code).toBe(1)
2967
+ expect(rules).toContain('provider-above-router')
2968
+ })
2969
+
2970
+ it('does NOT flag the correct order — provider above router', () => {
2971
+ const { code, rules } = run(
2972
+ `${IMPORTS}export const App = () => (\n` +
2973
+ ` <BasaltProvider>\n <RouterProvider router={r} />\n </BasaltProvider>\n)\n`,
2974
+ )
2975
+ expect(code).toBe(0)
2976
+ expect(rules).not.toContain('provider-above-router')
2977
+ })
2978
+
2979
+ // Provenance both ways: the tag names alone are not the finding.
2980
+ it("does NOT flag a consumer's own BasaltProvider (no basalt import)", () => {
2981
+ const { code, rules } = run(
2982
+ `import { RouterProvider } from '@tanstack/react-router'\n` +
2983
+ `import { BasaltProvider } from './my-provider'\n` +
2984
+ `export const App = () => (\n <RouterProvider router={r}>\n` +
2985
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
2986
+ )
2987
+ expect(code).toBe(0)
2988
+ expect(rules).not.toContain('provider-above-router')
2989
+ })
2990
+
2991
+ it('does NOT flag a RouterProvider that is not TanStack s', () => {
2992
+ const { code, rules } = run(
2993
+ `import { BasaltProvider } from 'basalt-ui'\n` +
2994
+ `import { RouterProvider } from 'other-router'\n` +
2995
+ `export const App = () => (\n <RouterProvider router={r}>\n` +
2996
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
2997
+ )
2998
+ expect(code).toBe(0)
2999
+ expect(rules).not.toContain('provider-above-router')
3000
+ })
3001
+
3002
+ // The alias is why the ancestry is read by BINDING and not by written tag name.
3003
+ it('flags through an aliased RouterProvider import', () => {
3004
+ const { code, rules } = run(
3005
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3006
+ `import { RouterProvider as Router } from '@tanstack/react-router'\n` +
3007
+ `export const App = () => (\n <Router router={r}>\n` +
3008
+ ` <BasaltProvider>{null}</BasaltProvider>\n </Router>\n)\n`,
3009
+ )
3010
+ expect(code).toBe(1)
3011
+ expect(rules).toContain('provider-above-router')
3012
+ })
3013
+
3014
+ it('honours a theme-allow naming the rule', () => {
3015
+ const { code, rules } = run(
3016
+ `${IMPORTS}export const App = () => (\n <RouterProvider router={r}>\n` +
3017
+ ` {/* theme-allow provider-above-router — legacy bootstrap, ported next minor */}\n` +
3018
+ ` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
3019
+ )
3020
+ expect(code).toBe(0)
3021
+ expect(rules).not.toContain('provider-above-router')
3022
+ })
3023
+
3024
+ // The documented limit of the heuristic — stated in the rule's own JSDoc, pinned here so a
3025
+ // future widening has to move this test rather than discover the gap.
3026
+ it('does NOT see a cross-file composition (static JSX ancestry only)', () => {
3027
+ const { code, rules } = run(
3028
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3029
+ `import { Routes } from './routes'\n` +
3030
+ `export const App = () => (\n <Routes>\n <BasaltProvider>{null}</BasaltProvider>\n` +
3031
+ ` </Routes>\n)\n`,
3032
+ )
3033
+ expect(code).toBe(0)
3034
+ expect(rules).not.toContain('provider-above-router')
3035
+ })
3036
+ })
3037
+
3038
+ // ── duplicate-notifications-mount (F5) ───────────────────────────────────────
3039
+
3040
+ describe('basalt/duplicate-notifications-mount', () => {
3041
+ const IMPORTS =
3042
+ `import { BasaltOverlays } from 'basalt-ui/commands'\n` +
3043
+ `import { BasaltNotifications } from 'basalt-ui/notifications'\n`
3044
+
3045
+ it('flags both mounts in one file', () => {
3046
+ const { code, rules } = run(
3047
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
3048
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3049
+ )
3050
+ expect(code).toBe(1)
3051
+ expect(rules).toContain('duplicate-notifications-mount')
3052
+ })
3053
+
3054
+ // `notifications` defaults to TRUE, so the attribute being absent is the same defect.
3055
+ it('flags when BasaltOverlays writes no notifications prop at all', () => {
3056
+ const { code, rules } = run(
3057
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays>\n` +
3058
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3059
+ )
3060
+ expect(code).toBe(1)
3061
+ expect(rules).toContain('duplicate-notifications-mount')
3062
+ })
3063
+
3064
+ it('does NOT flag when BasaltOverlays disables its notifications layer', () => {
3065
+ const { code, rules } = run(
3066
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications={false}>\n` +
3067
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3068
+ )
3069
+ expect(code).toBe(0)
3070
+ expect(rules).not.toContain('duplicate-notifications-mount')
3071
+ })
3072
+
3073
+ it('does NOT flag a standalone BasaltNotifications on its own', () => {
3074
+ const { code, rules } = run(
3075
+ `import { BasaltNotifications } from 'basalt-ui/notifications'\n` +
3076
+ `export const App = () => <BasaltNotifications />\n`,
3077
+ )
3078
+ expect(code).toBe(0)
3079
+ expect(rules).not.toContain('duplicate-notifications-mount')
3080
+ })
3081
+
3082
+ it("does NOT flag a consumer's own components of the same names", () => {
3083
+ const { code, rules } = run(
3084
+ `import { BasaltOverlays } from './overlays'\n` +
3085
+ `import { BasaltNotifications } from './notifications'\n` +
3086
+ `export const App = () => (\n <BasaltOverlays>\n <BasaltNotifications />\n` +
3087
+ ` </BasaltOverlays>\n)\n`,
3088
+ )
3089
+ expect(code).toBe(0)
3090
+ expect(rules).not.toContain('duplicate-notifications-mount')
3091
+ })
3092
+
3093
+ it('honours a theme-allow naming the rule', () => {
3094
+ const { code, rules } = run(
3095
+ `${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
3096
+ ` {/* theme-allow duplicate-notifications-mount — two roots, never both mounted */}\n` +
3097
+ ` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
3098
+ )
3099
+ expect(code).toBe(0)
3100
+ expect(rules).not.toContain('duplicate-notifications-mount')
3101
+ })
3102
+
3103
+ // Two mounts WRITTEN, one mount RENDERED — the ternary is the shape a consumer reaches for while
3104
+ // migrating from the standalone layer to the composed one, and it was never a double mount.
3105
+ it('does NOT flag the two branches of one ternary', () => {
3106
+ const { code, rules } = run(
3107
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3108
+ ` <div>{composed ? <BasaltOverlays /> : <BasaltNotifications />}</div>\n)\n`,
3109
+ )
3110
+ expect(code).toBe(0)
3111
+ expect(rules).not.toContain('duplicate-notifications-mount')
3112
+ })
3113
+
3114
+ it('does NOT flag the two operands of one logical expression', () => {
3115
+ const { code, rules } = run(
3116
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3117
+ ` <div>{(composed && <BasaltOverlays />) || <BasaltNotifications />}</div>\n)\n`,
3118
+ )
3119
+ expect(code).toBe(0)
3120
+ expect(rules).not.toContain('duplicate-notifications-mount')
3121
+ })
3122
+
3123
+ // The exemption is the NEAREST common ancestor only. Two separate `&&` guards can both hold, so
3124
+ // this stays a finding — the pair is conditional, not exclusive.
3125
+ it('DOES flag two independently-guarded mounts under one parent', () => {
3126
+ const { code, rules } = run(
3127
+ `${IMPORTS}export const App = ({ a, b }) => (\n` +
3128
+ ` <div>{a && <BasaltOverlays />}{b && <BasaltNotifications />}</div>\n)\n`,
3129
+ )
3130
+ expect(code).toBe(1)
3131
+ expect(rules).toContain('duplicate-notifications-mount')
3132
+ })
3133
+
3134
+ // One exclusive pair does not excuse a second, unconditional overlays mount.
3135
+ it('DOES flag when a NON-exclusive overlays mount also exists', () => {
3136
+ const { code, rules } = run(
3137
+ `${IMPORTS}export const App = ({ composed }) => (\n` +
3138
+ ` <BasaltOverlays>\n` +
3139
+ ` {composed ? <BasaltOverlays /> : <BasaltNotifications />}\n` +
3140
+ ` </BasaltOverlays>\n)\n`,
3141
+ )
3142
+ expect(code).toBe(1)
3143
+ expect(rules).toContain('duplicate-notifications-mount')
3144
+ })
3145
+ })
3146
+
3147
+ // ── query-dual-import (F5) ───────────────────────────────────────────────────
3148
+
3149
+ describe('basalt/query-dual-import', () => {
3150
+ it('flags a file importing from both @tanstack/react-query and basalt-ui/query', () => {
3151
+ const { code, rules, output } = run(
3152
+ `import { useQuery } from '@tanstack/react-query'\n` +
3153
+ `import { unwrap } from 'basalt-ui/query'\n` +
3154
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3155
+ )
3156
+ expect(code).toBe(1)
3157
+ expect(rules).toContain('query-dual-import')
3158
+ expect(output).toContain('Dual query import')
3159
+ })
3160
+
3161
+ // The softer half — the one that actually catches the drift, and why the rule is `warn`.
3162
+ it('flags a raw @tanstack/react-query import beside a basalt root import', () => {
3163
+ const { code, rules, output } = run(
3164
+ `import { useQuery } from '@tanstack/react-query'\n` +
3165
+ `import { QueryState } from 'basalt-ui'\n` +
3166
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3167
+ )
3168
+ expect(code).toBe(1)
3169
+ expect(rules).toContain('query-dual-import')
3170
+ expect(output).toContain('Raw @tanstack/react-query import')
3171
+ })
3172
+
3173
+ it('does NOT flag basalt-ui/query on its own', () => {
3174
+ const { code, rules } = run(
3175
+ `import { useQuery, unwrap } from 'basalt-ui/query'\n` +
3176
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3177
+ )
3178
+ expect(code).toBe(0)
3179
+ expect(rules).not.toContain('query-dual-import')
3180
+ })
3181
+
3182
+ // `import type` is erased at compile time — there is no runtime import to route through the
3183
+ // seam, and typing against the library's own result type is what `QueryStateLike` documents as
3184
+ // legitimate. Same skip `agent-no-raw-usechat` and `ai-sdk-major` take.
3185
+ it('does NOT flag a type-only @tanstack/react-query import beside basalt-ui/query', () => {
3186
+ const { code, rules } = run(
3187
+ `import type { UseQueryResult } from '@tanstack/react-query'\n` +
3188
+ `import { unwrap } from 'basalt-ui/query'\n` +
3189
+ `export const pick = (q: UseQueryResult<number>) => unwrap(q.data)\n`,
3190
+ )
3191
+ expect(code).toBe(0)
3192
+ expect(rules).not.toContain('query-dual-import')
3193
+ })
3194
+
3195
+ it('does NOT flag @tanstack/react-query in a file with no basalt import', () => {
3196
+ const { code, rules } = run(
3197
+ `import { useQuery } from '@tanstack/react-query'\n` +
3198
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3199
+ )
3200
+ expect(code).toBe(0)
3201
+ expect(rules).not.toContain('query-dual-import')
3202
+ })
3203
+
3204
+ // The devtools package is a different package, not a subpath.
3205
+ it('does NOT flag @tanstack/react-query-devtools', () => {
3206
+ const { code, rules } = run(
3207
+ `import { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n` +
3208
+ `import { unwrap } from 'basalt-ui/query'\n` +
3209
+ `export const D = () => <ReactQueryDevtools />\n`,
3210
+ )
3211
+ expect(code).toBe(0)
3212
+ expect(rules).not.toContain('query-dual-import')
3213
+ })
3214
+
3215
+ it('honours a theme-allow naming the rule', () => {
3216
+ const { code, rules } = run(
3217
+ `// theme-allow query-dual-import — the persister API has no basalt re-export yet\n` +
3218
+ `import { useQuery } from '@tanstack/react-query'\n` +
3219
+ `import { unwrap } from 'basalt-ui/query'\n` +
3220
+ `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(g()) })\n`,
3221
+ )
3222
+ expect(code).toBe(0)
3223
+ expect(rules).not.toContain('query-dual-import')
3224
+ })
3225
+ })
3226
+
3227
+ // ── query-fn-unwrap (F5) ─────────────────────────────────────────────────────
3228
+
3229
+ describe('basalt/query-fn-unwrap', () => {
3230
+ const IMPORT = `import { unwrap } from 'basalt-ui/query'\n`
3231
+
3232
+ it('flags a queryFn that fetches with no unwrap', () => {
3233
+ const { code, rules } = run(
3234
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
3235
+ )
3236
+ expect(code).toBe(1)
3237
+ expect(rules).toContain('query-fn-unwrap')
3238
+ })
3239
+
3240
+ it('flags the async .json() form', () => {
3241
+ const { code, rules } = run(
3242
+ `${IMPORT}export const o = {\n queryKey: ['t'],\n` +
3243
+ ` queryFn: async () => (await fetch('/api/t')).json(),\n}\n`,
3244
+ )
3245
+ expect(code).toBe(1)
3246
+ expect(rules).toContain('query-fn-unwrap')
3247
+ })
3248
+
3249
+ it('does NOT flag a queryFn that wraps the call', () => {
3250
+ const { code, rules } = run(
3251
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => unwrap(fetch('/api/t')) }\n`,
3252
+ )
3253
+ expect(code).toBe(0)
3254
+ expect(rules).not.toContain('query-fn-unwrap')
3255
+ })
3256
+
3257
+ it('does NOT flag a queryFn with no fetch in it', () => {
3258
+ const { code, rules } = run(
3259
+ `${IMPORT}export const o = { queryKey: ['t'], queryFn: () => client.t.get() }\n`,
3260
+ )
3261
+ expect(code).toBe(0)
3262
+ expect(rules).not.toContain('query-fn-unwrap')
3263
+ })
3264
+
3265
+ // The scope gate: with no basalt-ui/query import this is an opinion about `fetch`, which basalt
3266
+ // does not have.
3267
+ it('does NOT flag a file that never imports basalt-ui/query', () => {
3268
+ const { code, rules } = run(
3269
+ `export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
3270
+ )
3271
+ expect(code).toBe(0)
3272
+ expect(rules).not.toContain('query-fn-unwrap')
3273
+ })
3274
+
3275
+ // The documented blind spot — a reference, not a function literal. Pinned so a widening moves it.
3276
+ it('does NOT see a queryFn passed as a reference', () => {
3277
+ const { code, rules } = run(
3278
+ `${IMPORT}const load = () => fetch('/api/t')\n` +
3279
+ `export const o = { queryKey: ['t'], queryFn: load }\n`,
3280
+ )
3281
+ expect(code).toBe(0)
3282
+ expect(rules).not.toContain('query-fn-unwrap')
3283
+ })
3284
+
3285
+ it('honours a theme-allow naming the rule', () => {
3286
+ const { code, rules } = run(
3287
+ `${IMPORT}export const o = {\n queryKey: ['t'],\n` +
3288
+ ` // theme-allow query-fn-unwrap — raw Response is the point, this reads a blob\n` +
3289
+ ` queryFn: () => fetch('/api/t'),\n}\n`,
3290
+ )
3291
+ expect(code).toBe(0)
3292
+ expect(rules).not.toContain('query-fn-unwrap')
3293
+ })
3294
+ })
3295
+
3296
+ // ── deprecated-export (B4) ───────────────────────────────────────────────────
3297
+
3298
+ describe('basalt/deprecated-export', () => {
3299
+ it('flags a deprecated named import', () => {
3300
+ const { code, rules, output } = run(
3301
+ `import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
3302
+ )
3303
+ expect(code).toBe(1)
3304
+ expect(rules).toContain('deprecated-export')
3305
+ expect(output).toContain('inputProps')
3306
+ })
3307
+
3308
+ it('does NOT flag the replacement', () => {
3309
+ const { code, rules } = run(
3310
+ `import { inputProps } from 'basalt-ui/forms'\nexport const f = inputProps\n`,
3311
+ )
3312
+ expect(code).toBe(0)
3313
+ expect(rules).not.toContain('deprecated-export')
3314
+ })
3315
+
3316
+ it('does NOT flag a same-named import from somewhere else', () => {
3317
+ const { code, rules } = run(`import { field } from 'basalt-ui/state'\nexport const f = field\n`)
3318
+ expect(code).toBe(0)
3319
+ expect(rules).not.toContain('deprecated-export')
3320
+ })
3321
+
3322
+ it('flags each deprecated BasaltProvider connectivity prop', () => {
3323
+ const { code, rules, output } = run(
3324
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3325
+ `export const App = () => (\n` +
3326
+ ` <BasaltProvider sseUrl="/e" healthUrl="/h" healthIntervalMs={5000}>{null}</BasaltProvider>\n)\n`,
3327
+ )
3328
+ expect(code).toBe(1)
3329
+ expect(rules).toContain('deprecated-export')
3330
+ expect(output).toContain('connectivity')
3331
+ })
3332
+
3333
+ it('does NOT flag the connectivity object prop', () => {
3334
+ const { code, rules } = run(
3335
+ `import { BasaltProvider } from 'basalt-ui'\n` +
3336
+ `export const App = () => <BasaltProvider connectivity={{ sseUrl: '/e' }}>{null}</BasaltProvider>\n`,
3337
+ )
3338
+ expect(code).toBe(0)
3339
+ expect(rules).not.toContain('deprecated-export')
3340
+ })
3341
+
3342
+ it("does NOT flag an sseUrl on a consumer's own component", () => {
3343
+ const { code, rules } = run(
3344
+ `import { BasaltProvider } from './provider'\n` +
3345
+ `export const App = () => <BasaltProvider sseUrl="/e">{null}</BasaltProvider>\n`,
3346
+ )
3347
+ expect(code).toBe(0)
3348
+ expect(rules).not.toContain('deprecated-export')
3349
+ })
3350
+
3351
+ it('honours a theme-allow naming the rule', () => {
3352
+ const { code, rules } = run(
3353
+ `// theme-allow deprecated-export — pinned to 1.27.0 until the forms port lands\n` +
3354
+ `import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
3355
+ )
3356
+ expect(code).toBe(0)
3357
+ expect(rules).not.toContain('deprecated-export')
3358
+ })
3359
+
3360
+ // `field` → `inputProps` is NOT a drop-in rename any more (`inputProps` returns no `key`), so
3361
+ // the row carries `fix: false`: the diagnostic still fires, but `--fix` must leave the import
3362
+ // alone — the rewrite belongs to `basalt/forms-field-key`, which also inserts the key.
3363
+ it('leaves a row with `fix: false` untouched under --fix while still reporting it', () => {
3364
+ const source = `import { field } from 'basalt-ui/forms'\nexport const f = field\n`
3365
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3366
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3367
+ expect(readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')).toBe(source)
3368
+ const { rules } = run(source)
3369
+ expect(rules).toContain('deprecated-export')
3370
+ })
3371
+ })
3372
+
3373
+ // ── forms-field-key ──────────────────────────────────────────────────────────
3374
+
3375
+ describe('basalt/forms-field-key', () => {
3376
+ const FORMS = `import { inputProps } from 'basalt-ui/forms'\n`
3377
+
3378
+ it('flags a spread inputProps() with no sibling key', () => {
3379
+ const { code, rules, output } = run(
3380
+ `${FORMS}export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
3381
+ )
3382
+ expect(code).toBe(1)
3383
+ expect(rules).toContain('forms-field-key')
3384
+ expect(output).toContain('no sibling `key`')
3385
+ })
3386
+
3387
+ it('does NOT flag the documented two-call idiom', () => {
3388
+ const { code, rules } = run(
3389
+ `import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
3390
+ `export const C = () => <TextInput key={fieldKey(form, 'e')} {...inputProps(form, 'e')} />\n`,
3391
+ )
3392
+ expect(code).toBe(0)
3393
+ expect(rules).not.toContain('forms-field-key')
3394
+ })
3395
+
3396
+ // An `inputProps` a consumer wrote themselves is not basalt's — the same provenance gate the
3397
+ // control and chart rules take, one lane over.
3398
+ it('does NOT flag a same-named helper imported from somewhere else', () => {
3399
+ const { code, rules } = run(
3400
+ `import { inputProps } from './my-helpers'\n` +
3401
+ `export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
3402
+ )
3403
+ expect(code).toBe(0)
3404
+ expect(rules).not.toContain('forms-field-key')
3405
+ })
3406
+
3407
+ it('resolves an aliased import — `inputProps as f` is still inputProps', () => {
3408
+ const { code, rules } = run(
3409
+ `import { inputProps as f } from 'basalt-ui/forms'\n` +
3410
+ `export const C = () => <TextInput {...f(form, 'email')} />\n`,
3411
+ )
3412
+ expect(code).toBe(1)
3413
+ expect(rules).toContain('forms-field-key')
3414
+ })
3415
+
3416
+ // The second message: `field` still RETURNS `key`, so it is reported whether or not the element
3417
+ // carries one — the remedy is the rename, never an added attribute.
3418
+ it('flags a spread field() even when the element already has a key', () => {
3419
+ const { code, rules, output } = run(
3420
+ `import { field } from 'basalt-ui/forms'\n` +
3421
+ `export const C = () => <TextInput key="e" {...field(form, 'email')} />\n`,
3422
+ )
3423
+ expect(code).toBe(1)
3424
+ expect(rules).toContain('forms-field-key')
3425
+ expect(output).toContain('@deprecated 1.27 alias')
3426
+ })
3427
+
3428
+ it('honours a theme-allow naming the rule', () => {
3429
+ const { code, rules } = run(
3430
+ `${FORMS}export const C = () => (\n` +
3431
+ ` // theme-allow forms-field-key — this input is remounted by its parent\n` +
3432
+ ` <TextInput {...inputProps(form, 'email')} />\n)\n`,
3433
+ )
3434
+ expect(code).toBe(0)
3435
+ expect(rules).not.toContain('forms-field-key')
3436
+ })
3437
+
3438
+ // The autofix has to produce COMPILING code: an inserted `fieldKey(…)` with no import is a worse
3439
+ // outcome than the missing key it replaced.
3440
+ it('autofixes the key in, with the same arguments, and adds fieldKey to the import once', () => {
3441
+ const source =
3442
+ `${FORMS}export const C = () => (\n` +
3443
+ ` <form>\n` +
3444
+ ` <TextInput {...inputProps(form, 'email')} label="Email" />\n` +
3445
+ ` <TextInput {...inputProps(form, 'name')} />\n` +
3446
+ ` </form>\n)\n`
3447
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3448
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3449
+ const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
3450
+ expect(fixed).toContain(`import { inputProps, fieldKey } from 'basalt-ui/forms'`)
3451
+ expect(fixed).toContain(`key={fieldKey(form, 'email')} {...inputProps(form, 'email')}`)
3452
+ expect(fixed).toContain(`key={fieldKey(form, 'name')} {...inputProps(form, 'name')}`)
3453
+ // Once, not twice: two overlapping edits to one import declaration would drop one of them.
3454
+ expect(fixed.match(/fieldKey,|, fieldKey/g)).toHaveLength(1)
3455
+ })
3456
+
3457
+ it('does not re-add fieldKey when it is already imported', () => {
3458
+ const source =
3459
+ `import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
3460
+ `export const C = () => <TextInput {...inputProps(form, 'email')} />\n`
3461
+ writeFileSync(resolve(dir, 'fixture.tsx'), source)
3462
+ Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
3463
+ const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
3464
+ expect(fixed).toContain(`import { fieldKey, inputProps } from 'basalt-ui/forms'`)
3465
+ expect(fixed).toContain(`key={fieldKey(form, 'email')}`)
3466
+ })
3467
+ })
3468
+
3469
+ // ── the deprecation ledger (B4) ──────────────────────────────────────────────
3470
+
3471
+ describe('DEPRECATED_EXPORTS', () => {
3472
+ const pkgVersion = (
3473
+ JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
3474
+ version: string
3475
+ }
3476
+ ).version
3477
+
3478
+ it('carries a replacement and a removeIn at least one minor out on every row', () => {
3479
+ const [major, minor] = pkgVersion.split('.').map(Number)
3480
+ const floor = `${major}.${(minor as number) + 1}.0`
3481
+ for (const row of DEPRECATED_EXPORTS) {
3482
+ expect([row.name, row.replacement.length > 0]).toEqual([row.name, true])
3483
+ expect([row.name, /^\d+\.\d+\.\d+$/.test(row.removeIn)]).toEqual([row.name, true])
3484
+ expect([row.name, compareSemver(row.removeIn, floor) >= 0]).toEqual([row.name, true])
3485
+ }
3486
+ })
3487
+
3488
+ it('names a real published subpath on every row', () => {
3489
+ const exports = Object.keys(
3490
+ (
3491
+ JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
3492
+ exports: Record<string, unknown>
3493
+ }
3494
+ ).exports,
3495
+ )
3496
+ for (const row of DEPRECATED_EXPORTS) {
3497
+ const subpath = row.subpath === 'basalt-ui' ? '.' : row.subpath.replace('basalt-ui', '.')
3498
+ expect([row.subpath, exports.includes(subpath)]).toEqual([row.subpath, true])
3499
+ }
3500
+ })
3501
+
3502
+ /**
3503
+ * The barrel half of the lifecycle: a JSDoc `@deprecated` on a published barrel with no ledger
3504
+ * row is an export nothing nudges anyone off. Deliberately scoped to `src/**' + '/index.ts(x)` —
3505
+ * the files a consumer's import resolves through. To add one: write the `@deprecated` JSDoc, add
3506
+ * the `DEPRECATED_EXPORTS` row, add the `MIGRATING.md` row.
3507
+ */
3508
+ it('has a row for every @deprecated JSDoc tag on a published barrel', () => {
3509
+ const barrels = [
3510
+ ...new Bun.Glob('src/**/index.{ts,tsx}').scanSync({
3511
+ cwd: resolve(import.meta.dirname, '..'),
3512
+ absolute: true,
3513
+ }),
3514
+ ]
3515
+ const names = new Set(DEPRECATED_EXPORTS.map((row) => row.prop ?? row.name))
3516
+ for (const file of barrels) {
3517
+ const lines = readFileSync(file, 'utf8').split('\n')
3518
+ for (const [index, line] of lines.entries()) {
3519
+ if (!/^\s*\*\s*@deprecated\b/.test(line)) continue
3520
+ const declared = lines
3521
+ .slice(index, index + 12)
3522
+ .map((l) => /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*[:(]/.exec(l)?.[1])
3523
+ .find((name) => name !== undefined)
3524
+ expect([file, declared, declared !== undefined && names.has(declared)]).toEqual([
3525
+ file,
3526
+ declared,
3527
+ true,
3528
+ ])
3529
+ }
3530
+ }
3531
+ })
3532
+ })
3533
+
3534
+ // ── the `Ships:` claim above each rule (F26) ─────────────────────────────────
3535
+
3536
+ /**
3537
+ * F26 was a rule's own doc comment claiming a level it did not ship at, with nothing able to catch
3538
+ * it: the C16 ledger is asserted against the preset, and a COMMENT is outside the ledger. So the
3539
+ * claim is now one normalised line — `// Ships: <level>` directly above each rule's `const` — and
3540
+ * this is the assertion that makes it as checkable as the ledger it restates.
3541
+ */
3542
+ describe('the `Ships:` line above each rule', () => {
3543
+ const source = readFileSync(resolve(import.meta.dirname, 'oxlint-plugin.js'), 'utf8')
3544
+ const lines = source.split('\n')
3545
+ const shipped = JSON.parse(readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8')) as {
3546
+ rules: Record<string, unknown>
3547
+ }
3548
+ // The registry block maps rule id → the `const` implementing it.
3549
+ const registry = [
3550
+ ...source.slice(source.indexOf('export default {')).matchAll(/^ {4}'([\w-]+)': (\w+),$/gm),
3551
+ ].map(([, id, binding]) => ({ id: id as string, binding: binding as string }))
3552
+
3553
+ it('covers every registered rule', () => {
3554
+ expect(registry.map((r) => r.id).toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
3555
+ })
3556
+
3557
+ for (const { id, binding } of registry) {
3558
+ it(`${id}: the claim matches oxlint.json`, () => {
3559
+ const at = lines.indexOf(`const ${binding} = {`)
3560
+ expect([id, at]).not.toEqual([id, -1])
3561
+ const claim = /^\/\/ Ships: (.+)$/.exec(lines[at - 1] as string)?.[1]
3562
+ const level = shipped.rules[`basalt/${id}`]
3563
+ if (level === undefined) {
3564
+ expect([id, claim]).toEqual([id, 'repo-local only'])
3565
+ return
3566
+ }
3567
+ if (Object.hasOwn(PLUGIN_RULE_GRACE, id)) {
3568
+ const { promote } = PLUGIN_RULE_GRACE[id] as GraceEntry
3569
+ expect([id, claim]).toEqual([id, `warn (grace → ${promote})`])
3570
+ expect([id, level]).toEqual([id, 'warn'])
3571
+ return
3572
+ }
3573
+ if (Object.hasOwn(PLUGIN_RULE_ADVISORY, id)) {
3574
+ expect([id, claim]).toEqual([id, 'warn (advisory)'])
3575
+ expect([id, level]).toEqual([id, 'warn'])
3576
+ return
3577
+ }
3578
+ expect([id, claim]).toEqual([id, 'error'])
3579
+ expect([id, level]).toEqual([id, 'error'])
3580
+ })
3581
+ }
3582
+ })