basalt-ui 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1069) hide show
  1. package/AGENTS.md +23 -23
  2. package/CHANGELOG.md +67 -0
  3. package/MIGRATING.md +896 -37
  4. package/README.md +33 -3
  5. package/agent/rules/basalt-batteries.md +25 -10
  6. package/agent/rules/basalt-controls.md +73 -43
  7. package/agent/rules/basalt-mantine.md +17 -6
  8. package/agent/rules/basalt-state.md +5 -0
  9. package/agent/rules/basalt-tokens.md +1 -1
  10. package/configs/oxlint-plugin.js +1286 -73
  11. package/configs/oxlint-plugin.test.ts +1338 -29
  12. package/configs/oxlint.json +12 -5
  13. package/dist/agent/coalesce.d.ts.map +1 -1
  14. package/dist/agent/coalesce.js +2 -1
  15. package/dist/agent/merge.d.ts.map +1 -1
  16. package/dist/agent/merge.js +2 -1
  17. package/dist/agent/part-list.d.ts.map +1 -1
  18. package/dist/agent/part-list.js +2 -0
  19. package/dist/agent/stick-to-bottom.d.ts +3 -6
  20. package/dist/agent/stick-to-bottom.d.ts.map +1 -1
  21. package/dist/agent/stick-to-bottom.js +6 -3
  22. package/dist/agent-chat/composer.d.ts +6 -2
  23. package/dist/agent-chat/composer.d.ts.map +1 -1
  24. package/dist/agent-chat/composer.js +49 -33
  25. package/dist/agent-chat/relative-time.d.ts +0 -9
  26. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  27. package/dist/agent-chat/relative-time.js +2 -1
  28. package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
  29. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  30. package/dist/agent-chat/thread-detail-panel.js +8 -5
  31. package/dist/agent-chat/thread-feed-row.d.ts +2 -1
  32. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  33. package/dist/agent-chat/thread-feed-row.js +92 -92
  34. package/dist/agent-chat/thread-feed.d.ts +3 -2
  35. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  36. package/dist/agent-chat/thread-feed.js +8 -3
  37. package/dist/agent-chat/thread-message.d.ts +4 -3
  38. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  39. package/dist/agent-chat/thread-message.js +70 -31
  40. package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
  41. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-outcome-card.js +9 -2
  43. package/dist/agent-chat/thread-workspace.d.ts +7 -2
  44. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  45. package/dist/agent-chat/thread-workspace.js +62 -29
  46. package/dist/agent-chat/tool-chip.d.ts +3 -2
  47. package/dist/agent-chat/tool-chip.d.ts.map +1 -1
  48. package/dist/agent-chat/tool-chip.js +7 -2
  49. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  50. package/dist/charts/hooks/useBandPlot.js +15 -8
  51. package/dist/charts/index.d.ts +8 -7
  52. package/dist/charts/index.d.ts.map +1 -1
  53. package/dist/charts/index.js +45 -5
  54. package/dist/charts/kinds/BandStrip.d.ts +10 -7
  55. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  56. package/dist/charts/kinds/BandStrip.js +12 -7
  57. package/dist/charts/kinds/Bars.d.ts +12 -6
  58. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  59. package/dist/charts/kinds/Bars.js +37 -11
  60. package/dist/charts/kinds/Donut.d.ts +6 -5
  61. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  62. package/dist/charts/kinds/Donut.js +19 -2
  63. package/dist/charts/kinds/DualPanel.d.ts +6 -5
  64. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  65. package/dist/charts/kinds/DualPanel.js +36 -8
  66. package/dist/charts/kinds/Heatmap.d.ts +10 -6
  67. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  68. package/dist/charts/kinds/Heatmap.js +58 -31
  69. package/dist/charts/kinds/MirroredBars.d.ts +6 -5
  70. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  71. package/dist/charts/kinds/MirroredBars.js +9 -2
  72. package/dist/charts/kinds/MultiLine.d.ts +11 -6
  73. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  74. package/dist/charts/kinds/MultiLine.js +24 -15
  75. package/dist/charts/kinds/StackedArea.d.ts +18 -8
  76. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  77. package/dist/charts/kinds/StackedArea.js +29 -4
  78. package/dist/charts/kinds/ZonedLine.d.ts +12 -8
  79. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  80. package/dist/charts/kinds/ZonedLine.js +33 -23
  81. package/dist/charts/layout/auto-margin.d.ts +65 -8
  82. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  83. package/dist/charts/layout/auto-margin.js +45 -10
  84. package/dist/charts/layout/log-ticks.d.ts +27 -0
  85. package/dist/charts/layout/log-ticks.d.ts.map +1 -0
  86. package/dist/charts/layout/log-ticks.js +42 -0
  87. package/dist/charts/primitives/Axes.d.ts +11 -2
  88. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  89. package/dist/charts/primitives/Axes.js +15 -5
  90. package/dist/charts/primitives/CartesianChart.d.ts +45 -7
  91. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  92. package/dist/charts/primitives/CartesianChart.js +86 -28
  93. package/dist/charts/primitives/ChartCard.d.ts +27 -2
  94. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  95. package/dist/charts/primitives/ChartCard.js +37 -17
  96. package/dist/charts/primitives/ChartFrame.d.ts +30 -4
  97. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  98. package/dist/charts/primitives/ChartFrame.js +45 -15
  99. package/dist/charts/primitives/ChartLegend.d.ts +13 -2
  100. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  101. package/dist/charts/primitives/ChartLegend.js +33 -13
  102. package/dist/charts/primitives/ChartPending.d.ts +77 -4
  103. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  104. package/dist/charts/primitives/ChartPending.js +100 -6
  105. package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
  106. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  107. package/dist/charts/primitives/ChartTooltip.js +18 -5
  108. package/dist/charts/primitives/Crosshair.d.ts +3 -0
  109. package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
  110. package/dist/charts/primitives/Crosshair.js +3 -1
  111. package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
  112. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
  113. package/dist/charts/primitives/chart-frame-layout.js +89 -0
  114. package/dist/charts/primitives/chart-tier.d.ts +16 -0
  115. package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
  116. package/dist/charts/primitives/chart-tier.js +20 -0
  117. package/dist/charts/series.d.ts +55 -1
  118. package/dist/charts/series.d.ts.map +1 -1
  119. package/dist/charts/series.js +27 -1
  120. package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
  121. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  122. package/dist/charts/sparklines/BarSparkline.js +9 -3
  123. package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
  124. package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
  125. package/dist/charts/sparklines/LineSparkline.js +15 -3
  126. package/dist/charts/sparklines/index.d.ts +2 -2
  127. package/dist/charts/sparklines/index.d.ts.map +1 -1
  128. package/dist/charts/utils/format.d.ts +92 -2
  129. package/dist/charts/utils/format.d.ts.map +1 -1
  130. package/dist/charts/utils/format.js +64 -1
  131. package/dist/charts/utils/ticks.d.ts +81 -2
  132. package/dist/charts/utils/ticks.d.ts.map +1 -1
  133. package/dist/charts/utils/ticks.js +54 -4
  134. package/dist/cli/index.d.ts.map +1 -1
  135. package/dist/cli/index.js +2 -2
  136. package/dist/commands/ShortcutsHelp.d.ts +3 -2
  137. package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
  138. package/dist/commands/ShortcutsHelp.js +8 -2
  139. package/dist/commands/define-commands.d.ts.map +1 -1
  140. package/dist/commands/define-commands.js +2 -2
  141. package/dist/commands/define-overlays.d.ts +102 -0
  142. package/dist/commands/define-overlays.d.ts.map +1 -1
  143. package/dist/commands/define-overlays.js +102 -2
  144. package/dist/commands/index.d.ts +10 -1
  145. package/dist/commands/index.d.ts.map +1 -1
  146. package/dist/commands/index.js +3 -1
  147. package/dist/commands/overlays-mount.d.ts +13 -6
  148. package/dist/commands/overlays-mount.d.ts.map +1 -1
  149. package/dist/commands/overlays-mount.js +23 -14
  150. package/dist/common/errors.d.ts +34 -0
  151. package/dist/common/errors.d.ts.map +1 -0
  152. package/dist/common/errors.js +36 -0
  153. package/dist/common/index.d.ts +15 -0
  154. package/dist/common/index.d.ts.map +1 -0
  155. package/dist/common/index.js +29 -0
  156. package/dist/common/props.d.ts +63 -0
  157. package/dist/common/props.d.ts.map +1 -0
  158. package/dist/common/props.js +6 -0
  159. package/dist/common/query-state-like.d.ts +43 -0
  160. package/dist/common/query-state-like.d.ts.map +1 -0
  161. package/dist/common/query-state-like.js +20 -0
  162. package/dist/common/refs.d.ts +29 -0
  163. package/dist/common/refs.d.ts.map +1 -0
  164. package/dist/common/refs.js +29 -0
  165. package/dist/common/scroll-parent.d.ts +23 -0
  166. package/dist/common/scroll-parent.d.ts.map +1 -0
  167. package/dist/common/scroll-parent.js +21 -0
  168. package/dist/common/validate.d.ts +38 -0
  169. package/dist/common/validate.d.ts.map +1 -0
  170. package/dist/common/validate.js +36 -0
  171. package/dist/connectivity/connectivity-indicator.d.ts +3 -1
  172. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  173. package/dist/connectivity/connectivity-indicator.js +3 -1
  174. package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
  175. package/dist/connectivity/connectivity-provider.js +4 -1
  176. package/dist/connectivity/index.d.ts +1 -0
  177. package/dist/connectivity/index.d.ts.map +1 -1
  178. package/dist/connectivity/types.d.ts +2 -2
  179. package/dist/connectivity/types.d.ts.map +1 -1
  180. package/dist/connectivity/use-connectivity.d.ts.map +1 -1
  181. package/dist/content/article-layout.module.css +11 -4
  182. package/dist/content/callout.d.ts +8 -4
  183. package/dist/content/callout.d.ts.map +1 -1
  184. package/dist/content/callout.js +15 -8
  185. package/dist/content/guide.d.ts +5 -4
  186. package/dist/content/guide.d.ts.map +1 -1
  187. package/dist/content/guide.js +13 -4
  188. package/dist/content/heading-anchor.d.ts +24 -2
  189. package/dist/content/heading-anchor.d.ts.map +1 -1
  190. package/dist/content/heading-anchor.js +23 -9
  191. package/dist/content/index.d.ts.map +1 -1
  192. package/dist/content/markdown.d.ts.map +1 -1
  193. package/dist/content/markdown.js +2 -0
  194. package/dist/content/prose.module.css +9 -7
  195. package/dist/content/reading-progress.d.ts +10 -4
  196. package/dist/content/reading-progress.d.ts.map +1 -1
  197. package/dist/content/reading-progress.js +26 -10
  198. package/dist/content/slug.d.ts +4 -0
  199. package/dist/content/slug.d.ts.map +1 -1
  200. package/dist/content/toc.d.ts +11 -0
  201. package/dist/content/toc.d.ts.map +1 -1
  202. package/dist/content/toc.js +23 -10
  203. package/dist/controls/actions.d.ts +49 -6
  204. package/dist/controls/actions.d.ts.map +1 -1
  205. package/dist/controls/actions.js +39 -8
  206. package/dist/controls/compare-filter.d.ts +3 -2
  207. package/dist/controls/compare-filter.d.ts.map +1 -1
  208. package/dist/controls/compare-filter.js +17 -2
  209. package/dist/controls/control-group.d.ts +3 -3
  210. package/dist/controls/control-group.d.ts.map +1 -1
  211. package/dist/controls/control-group.js +9 -2
  212. package/dist/controls/controls.module.css +169 -186
  213. package/dist/controls/controls.module.css.d.ts +9 -11
  214. package/dist/controls/enum-filter.d.ts +13 -3
  215. package/dist/controls/enum-filter.d.ts.map +1 -1
  216. package/dist/controls/enum-filter.js +32 -15
  217. package/dist/controls/filter-context.d.ts +15 -6
  218. package/dist/controls/filter-context.d.ts.map +1 -1
  219. package/dist/controls/filter-pill.d.ts +3 -3
  220. package/dist/controls/filter-pill.d.ts.map +1 -1
  221. package/dist/controls/filter-pill.js +4 -1
  222. package/dist/controls/filter-set.d.ts +3 -2
  223. package/dist/controls/filter-set.d.ts.map +1 -1
  224. package/dist/controls/filter-set.js +66 -41
  225. package/dist/controls/filter-sheet.d.ts +20 -80
  226. package/dist/controls/filter-sheet.d.ts.map +1 -1
  227. package/dist/controls/filter-sheet.js +9 -95
  228. package/dist/controls/index.d.ts +3 -0
  229. package/dist/controls/index.d.ts.map +1 -1
  230. package/dist/controls/index.js +6 -0
  231. package/dist/controls/multi-select-filter.d.ts +18 -2
  232. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  233. package/dist/controls/multi-select-filter.js +124 -22
  234. package/dist/controls/number-filter.d.ts +28 -0
  235. package/dist/controls/number-filter.d.ts.map +1 -0
  236. package/dist/controls/number-filter.js +172 -0
  237. package/dist/controls/panel-row.d.ts +132 -0
  238. package/dist/controls/panel-row.d.ts.map +1 -0
  239. package/dist/controls/panel-row.js +173 -0
  240. package/dist/controls/panel-row.module.css +86 -0
  241. package/dist/controls/panel-row.module.css.d.ts +13 -0
  242. package/dist/controls/range-filter.d.ts +20 -5
  243. package/dist/controls/range-filter.d.ts.map +1 -1
  244. package/dist/controls/range-filter.js +75 -38
  245. package/dist/controls/search-filter.d.ts +3 -2
  246. package/dist/controls/search-filter.d.ts.map +1 -1
  247. package/dist/controls/search-filter.js +26 -11
  248. package/dist/controls/select-filter.d.ts +2 -1
  249. package/dist/controls/select-filter.d.ts.map +1 -1
  250. package/dist/controls/select-filter.js +4 -0
  251. package/dist/controls/slider-control.d.ts +36 -0
  252. package/dist/controls/slider-control.d.ts.map +1 -0
  253. package/dist/controls/slider-control.js +64 -0
  254. package/dist/controls/sync-button.d.ts +3 -2
  255. package/dist/controls/sync-button.d.ts.map +1 -1
  256. package/dist/controls/sync-button.js +16 -3
  257. package/dist/controls/toggle-filter.d.ts +3 -2
  258. package/dist/controls/toggle-filter.d.ts.map +1 -1
  259. package/dist/controls/toggle-filter.js +29 -12
  260. package/dist/controls/view-tabs.d.ts +3 -2
  261. package/dist/controls/view-tabs.d.ts.map +1 -1
  262. package/dist/controls/view-tabs.js +43 -11
  263. package/dist/controls-dates/date-range-picker.d.ts +1 -1
  264. package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
  265. package/dist/controls-dates/date-range-picker.js +6 -1
  266. package/dist/dashboard/delta-badge.d.ts +5 -1
  267. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  268. package/dist/dashboard/empty-state.d.ts +10 -3
  269. package/dist/dashboard/empty-state.d.ts.map +1 -1
  270. package/dist/dashboard/empty-state.js +37 -6
  271. package/dist/dashboard/index.d.ts +6 -4
  272. package/dist/dashboard/index.d.ts.map +1 -1
  273. package/dist/dashboard/index.js +10 -2
  274. package/dist/dashboard/query-state.d.ts +48 -27
  275. package/dist/dashboard/query-state.d.ts.map +1 -1
  276. package/dist/dashboard/query-state.js +111 -57
  277. package/dist/dashboard/settings-section.d.ts +19 -9
  278. package/dist/dashboard/settings-section.d.ts.map +1 -1
  279. package/dist/dashboard/settings-section.js +74 -25
  280. package/dist/dashboard/stat-card.d.ts +76 -19
  281. package/dist/dashboard/stat-card.d.ts.map +1 -1
  282. package/dist/dashboard/stat-card.js +78 -38
  283. package/dist/dashboard/stat-card.module.css +112 -26
  284. package/dist/dashboard/stat-card.module.css.d.ts +5 -0
  285. package/dist/dashboard/stat-group.d.ts +41 -0
  286. package/dist/dashboard/stat-group.d.ts.map +1 -0
  287. package/dist/dashboard/stat-group.js +30 -0
  288. package/dist/dashboard/stat-group.module.css +66 -0
  289. package/dist/dashboard/stat-group.module.css.d.ts +8 -0
  290. package/dist/dashboard/widget-grid.d.ts +27 -0
  291. package/dist/dashboard/widget-grid.d.ts.map +1 -0
  292. package/dist/dashboard/widget-grid.js +41 -0
  293. package/dist/dashboard/widget-grid.module.css +53 -0
  294. package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
  295. package/dist/data/data-table.d.ts +151 -12
  296. package/dist/data/data-table.d.ts.map +1 -1
  297. package/dist/data/data-table.js +392 -138
  298. package/dist/data/data-table.module.css +64 -0
  299. package/dist/data/data-table.module.css.d.ts +11 -0
  300. package/dist/data/index.d.ts +2 -2
  301. package/dist/data/index.d.ts.map +1 -1
  302. package/dist/data/index.js +24 -2
  303. package/dist/data/query-branch.d.ts +28 -0
  304. package/dist/data/query-branch.d.ts.map +1 -0
  305. package/dist/data/query-branch.js +10 -0
  306. package/dist/data/table.d.ts +2 -2
  307. package/dist/data/table.d.ts.map +1 -1
  308. package/dist/data/virtual-list.d.ts +65 -6
  309. package/dist/data/virtual-list.d.ts.map +1 -1
  310. package/dist/data/virtual-list.js +89 -38
  311. package/dist/data/virtual.d.ts +1 -1
  312. package/dist/data/virtual.d.ts.map +1 -1
  313. package/dist/forms/FormErrorSummary.d.ts +4 -3
  314. package/dist/forms/FormErrorSummary.d.ts.map +1 -1
  315. package/dist/forms/FormErrorSummary.js +6 -4
  316. package/dist/forms/create-form.d.ts +2 -2
  317. package/dist/forms/field.d.ts +46 -6
  318. package/dist/forms/field.d.ts.map +1 -1
  319. package/dist/forms/field.js +10 -5
  320. package/dist/forms/form-layout.d.ts +142 -0
  321. package/dist/forms/form-layout.d.ts.map +1 -0
  322. package/dist/forms/form-layout.js +110 -0
  323. package/dist/forms/form-layout.module.css +145 -0
  324. package/dist/forms/form-layout.module.css.d.ts +20 -0
  325. package/dist/forms/form-state.d.ts +42 -0
  326. package/dist/forms/form-state.d.ts.map +1 -0
  327. package/dist/forms/form-state.js +26 -0
  328. package/dist/forms/index.d.ts +22 -6
  329. package/dist/forms/index.d.ts.map +1 -1
  330. package/dist/forms/index.js +19 -3
  331. package/dist/forms/use-field-array.d.ts +60 -0
  332. package/dist/forms/use-field-array.d.ts.map +1 -0
  333. package/dist/forms/use-field-array.js +20 -0
  334. package/dist/forms/use-form-draft.d.ts +25 -10
  335. package/dist/forms/use-form-draft.d.ts.map +1 -1
  336. package/dist/forms/use-form-draft.js +55 -18
  337. package/dist/forms/use-form-submit.d.ts +88 -0
  338. package/dist/forms/use-form-submit.d.ts.map +1 -0
  339. package/dist/forms/use-form-submit.js +63 -0
  340. package/dist/guard/index.d.ts +1 -1
  341. package/dist/guard/index.d.ts.map +1 -1
  342. package/dist/guard/index.js +27 -11
  343. package/dist/index.d.ts +10 -8
  344. package/dist/index.d.ts.map +1 -1
  345. package/dist/index.js +44 -4
  346. package/dist/nav/types.d.ts +15 -6
  347. package/dist/nav/types.d.ts.map +1 -1
  348. package/dist/notifications/bell.d.ts +3 -2
  349. package/dist/notifications/bell.d.ts.map +1 -1
  350. package/dist/notifications/bell.js +73 -56
  351. package/dist/notifications/center.d.ts +15 -4
  352. package/dist/notifications/center.d.ts.map +1 -1
  353. package/dist/notifications/center.js +5 -2
  354. package/dist/notifications/define-notifications.d.ts +56 -14
  355. package/dist/notifications/define-notifications.d.ts.map +1 -1
  356. package/dist/notifications/define-notifications.js +2 -1
  357. package/dist/notifications/index.d.ts +6 -2
  358. package/dist/notifications/index.d.ts.map +1 -1
  359. package/dist/notifications/index.js +5 -1
  360. package/dist/notifications/mount-guard.d.ts +8 -0
  361. package/dist/notifications/mount-guard.d.ts.map +1 -0
  362. package/dist/notifications/mount-guard.js +19 -0
  363. package/dist/notifications/notify.d.ts +58 -1
  364. package/dist/notifications/notify.d.ts.map +1 -1
  365. package/dist/notifications/notify.js +46 -2
  366. package/dist/notifications/overlay.d.ts.map +1 -1
  367. package/dist/notifications/overlay.js +2 -0
  368. package/dist/notifications/store.d.ts +7 -0
  369. package/dist/notifications/store.d.ts.map +1 -1
  370. package/dist/provider/index.d.ts +37 -5
  371. package/dist/provider/index.d.ts.map +1 -1
  372. package/dist/provider/index.js +66 -28
  373. package/dist/provider/lab-theme.d.ts.map +1 -1
  374. package/dist/provider/lab-theme.js +2 -1
  375. package/dist/query/devtools.d.ts.map +1 -1
  376. package/dist/query/devtools.js +1 -0
  377. package/dist/router-tanstack/field.d.ts +1 -1
  378. package/dist/router-tanstack/field.d.ts.map +1 -1
  379. package/dist/router-tanstack/index.d.ts +1 -1
  380. package/dist/router-tanstack/index.d.ts.map +1 -1
  381. package/dist/router-tanstack/search-store.d.ts +4 -1
  382. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  383. package/dist/router-tanstack/search-store.js +92 -8
  384. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  385. package/dist/router-tanstack/use-nav.js +1 -1
  386. package/dist/section/index.d.ts +1 -1
  387. package/dist/section/index.d.ts.map +1 -1
  388. package/dist/section/section.d.ts +36 -2
  389. package/dist/section/section.d.ts.map +1 -1
  390. package/dist/section/section.js +49 -57
  391. package/dist/section/section.module.css +11 -0
  392. package/dist/shell/account-types.d.ts +5 -2
  393. package/dist/shell/account-types.d.ts.map +1 -1
  394. package/dist/shell/app-brand.d.ts +13 -0
  395. package/dist/shell/app-brand.d.ts.map +1 -0
  396. package/dist/shell/app-brand.js +94 -0
  397. package/dist/shell/app-brand.module.css +109 -0
  398. package/dist/shell/app-brand.module.css.d.ts +14 -0
  399. package/dist/shell/app-breadcrumbs.d.ts +4 -2
  400. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  401. package/dist/shell/app-breadcrumbs.js +33 -22
  402. package/dist/shell/app-header.module.css +10 -5
  403. package/dist/shell/app-main.module.css +107 -0
  404. package/dist/shell/app-main.module.css.d.ts +11 -0
  405. package/dist/shell/app-mobile-nav.d.ts +3 -2
  406. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  407. package/dist/shell/app-mobile-nav.js +29 -15
  408. package/dist/shell/app-mobile-nav.module.css +48 -27
  409. package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
  410. package/dist/shell/app-sidebar-account.d.ts +2 -1
  411. package/dist/shell/app-sidebar-account.d.ts.map +1 -1
  412. package/dist/shell/app-sidebar-account.js +57 -22
  413. package/dist/shell/app-sidebar.d.ts +9 -3
  414. package/dist/shell/app-sidebar.d.ts.map +1 -1
  415. package/dist/shell/app-sidebar.js +55 -109
  416. package/dist/shell/app-sidebar.module.css +53 -96
  417. package/dist/shell/app-sidebar.module.css.d.ts +0 -5
  418. package/dist/shell/index.d.ts +14 -6
  419. package/dist/shell/index.d.ts.map +1 -1
  420. package/dist/shell/index.js +47 -12
  421. package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
  422. package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
  423. package/dist/shell/isomorphic-layout-effect.js +5 -0
  424. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  425. package/dist/shell/mobile-nav-model.js +27 -6
  426. package/dist/shell/nav-count-badge.d.ts +5 -3
  427. package/dist/shell/nav-count-badge.d.ts.map +1 -1
  428. package/dist/shell/nav-count-badge.js +3 -1
  429. package/dist/shell/page-aside.d.ts +43 -0
  430. package/dist/shell/page-aside.d.ts.map +1 -0
  431. package/dist/shell/page-aside.js +202 -0
  432. package/dist/shell/page-aside.module.css +174 -0
  433. package/dist/shell/page-aside.module.css.d.ts +14 -0
  434. package/dist/shell/page-bar.d.ts +55 -15
  435. package/dist/shell/page-bar.d.ts.map +1 -1
  436. package/dist/shell/page-bar.js +130 -41
  437. package/dist/shell/page-bar.module.css +107 -13
  438. package/dist/shell/page-bar.module.css.d.ts +4 -1
  439. package/dist/shell/sidebar-search.d.ts +4 -2
  440. package/dist/shell/sidebar-search.d.ts.map +1 -1
  441. package/dist/shell/sidebar-search.js +9 -4
  442. package/dist/shell/sidebar-search.module.css +1 -1
  443. package/dist/state/fields.d.ts +227 -34
  444. package/dist/state/fields.d.ts.map +1 -1
  445. package/dist/state/fields.js +77 -30
  446. package/dist/state/persisted-or-local.d.ts +9 -0
  447. package/dist/state/persisted-or-local.d.ts.map +1 -0
  448. package/dist/state/persisted-or-local.js +24 -0
  449. package/dist/state/persisted.d.ts +12 -0
  450. package/dist/state/persisted.d.ts.map +1 -1
  451. package/dist/state/persisted.js +36 -18
  452. package/dist/state.d.ts +1 -1
  453. package/dist/state.d.ts.map +1 -1
  454. package/dist/state.js +2 -6
  455. package/dist/surfaces.d.ts +12 -11
  456. package/dist/surfaces.d.ts.map +1 -1
  457. package/dist/surfaces.js +43 -12
  458. package/dist/theme/ctl-theme.d.ts.map +1 -1
  459. package/dist/theme/index.d.ts.map +1 -1
  460. package/dist/theme/index.js +102 -17
  461. package/dist/theme/nav-link.module.css +37 -26
  462. package/dist/theme/segmented-control.module.css +22 -0
  463. package/dist/theme/segmented-control.module.css.d.ts +1 -0
  464. package/dist/theme-lab/boot.d.ts +66 -0
  465. package/dist/theme-lab/boot.d.ts.map +1 -0
  466. package/dist/theme-lab/boot.js +82 -0
  467. package/dist/theme-lab/index.d.ts +4 -43
  468. package/dist/theme-lab/index.d.ts.map +1 -1
  469. package/dist/theme-lab/index.js +9 -69
  470. package/dist/theme-toggle/index.d.ts +3 -2
  471. package/dist/theme-toggle/index.d.ts.map +1 -1
  472. package/dist/theme-toggle/index.js +8 -2
  473. package/dist/tokens/index.d.ts +42 -0
  474. package/dist/tokens/index.d.ts.map +1 -1
  475. package/dist/tokens/index.js +35 -6
  476. package/dist/tokens/palette.d.ts +45 -25
  477. package/dist/tokens/palette.d.ts.map +1 -1
  478. package/dist/tokens/palette.js +31 -14
  479. package/dist/tokens.css +4 -5
  480. package/dist/utils/is-dev.d.ts +3 -0
  481. package/dist/utils/is-dev.d.ts.map +1 -0
  482. package/dist/utils/is-dev.js +6 -0
  483. package/dist/widget-header/delta-badge.d.ts +7 -1
  484. package/dist/widget-header/delta-badge.d.ts.map +1 -1
  485. package/dist/widget-header/delta-badge.js +8 -1
  486. package/dist/widget-header/index.d.ts +2 -2
  487. package/dist/widget-header/index.d.ts.map +1 -1
  488. package/dist/widget-header/index.js +3 -1
  489. package/dist/widget-header/widget-header.d.ts +99 -4
  490. package/dist/widget-header/widget-header.d.ts.map +1 -1
  491. package/dist/widget-header/widget-header.js +45 -18
  492. package/dist/widget-header/widget-header.module.css +87 -7
  493. package/dist/widget-header/widget-header.module.css.d.ts +1 -0
  494. package/llms.txt +6 -6
  495. package/package.json +49 -25
  496. package/src/agent/coalesce.ts +2 -1
  497. package/src/agent/merge.ts +2 -1
  498. package/src/agent/part-list.tsx +2 -0
  499. package/src/agent/stick-to-bottom.tsx +9 -11
  500. package/src/agent-chat/composer.tsx +124 -103
  501. package/src/agent-chat/relative-time.ts +2 -1
  502. package/src/agent-chat/thread-detail-panel.tsx +9 -5
  503. package/src/agent-chat/thread-feed-row.tsx +17 -9
  504. package/src/agent-chat/thread-feed.tsx +9 -3
  505. package/src/agent-chat/thread-message.tsx +48 -13
  506. package/src/agent-chat/thread-outcome-card.tsx +9 -1
  507. package/src/agent-chat/thread-workspace.tsx +72 -33
  508. package/src/agent-chat/tool-chip.tsx +8 -2
  509. package/src/charts/hooks/useBandPlot.ts +26 -8
  510. package/src/charts/index.ts +39 -4
  511. package/src/charts/kinds/BandStrip.tsx +36 -14
  512. package/src/charts/kinds/Bars.tsx +67 -16
  513. package/src/charts/kinds/Donut.tsx +30 -3
  514. package/src/charts/kinds/DualPanel.tsx +56 -10
  515. package/src/charts/kinds/Heatmap.tsx +90 -33
  516. package/src/charts/kinds/MirroredBars.tsx +21 -3
  517. package/src/charts/kinds/MultiLine.tsx +61 -18
  518. package/src/charts/kinds/StackedArea.tsx +77 -6
  519. package/src/charts/kinds/ZonedLine.tsx +71 -31
  520. package/src/charts/layout/auto-margin.ts +134 -21
  521. package/src/charts/layout/log-ticks.ts +90 -0
  522. package/src/charts/primitives/Axes.tsx +33 -5
  523. package/src/charts/primitives/CartesianChart.tsx +194 -41
  524. package/src/charts/primitives/ChartCard.tsx +66 -25
  525. package/src/charts/primitives/ChartFrame.tsx +107 -26
  526. package/src/charts/primitives/ChartLegend.tsx +43 -13
  527. package/src/charts/primitives/ChartPending.tsx +185 -7
  528. package/src/charts/primitives/ChartTooltip.tsx +37 -5
  529. package/src/charts/primitives/Crosshair.tsx +6 -3
  530. package/src/charts/primitives/chart-frame-layout.ts +212 -0
  531. package/src/charts/primitives/chart-tier.tsx +37 -0
  532. package/src/charts/series.ts +77 -1
  533. package/src/charts/sparklines/BarSparkline.tsx +11 -3
  534. package/src/charts/sparklines/LineSparkline.tsx +18 -4
  535. package/src/charts/sparklines/index.ts +2 -2
  536. package/src/charts/utils/format.ts +163 -2
  537. package/src/charts/utils/ticks.ts +149 -4
  538. package/src/cli/index.ts +8 -3
  539. package/src/commands/ShortcutsHelp.tsx +10 -3
  540. package/src/commands/define-commands.ts +2 -2
  541. package/src/commands/define-overlays.ts +216 -1
  542. package/src/commands/index.ts +12 -0
  543. package/src/commands/overlays-mount.tsx +60 -26
  544. package/src/common/errors.ts +73 -0
  545. package/src/common/index.ts +21 -0
  546. package/src/common/props.ts +71 -0
  547. package/src/common/query-state-like.ts +64 -0
  548. package/src/common/refs.ts +58 -0
  549. package/src/common/scroll-parent.ts +42 -0
  550. package/src/common/validate.ts +94 -0
  551. package/src/connectivity/connectivity-indicator.tsx +6 -1
  552. package/src/connectivity/connectivity-provider.tsx +4 -1
  553. package/src/connectivity/index.ts +1 -0
  554. package/src/connectivity/types.ts +5 -2
  555. package/src/connectivity/use-connectivity.ts +3 -2
  556. package/src/content/article-layout.module.css +11 -4
  557. package/src/content/callout.tsx +13 -7
  558. package/src/content/guide.tsx +14 -4
  559. package/src/content/heading-anchor.tsx +14 -3
  560. package/src/content/index.ts +2 -0
  561. package/src/content/markdown.tsx +2 -0
  562. package/src/content/prose.module.css +9 -7
  563. package/src/content/reading-progress.tsx +40 -15
  564. package/src/content/slug.ts +4 -0
  565. package/src/content/toc.tsx +47 -10
  566. package/src/controls/actions.tsx +89 -9
  567. package/src/controls/compare-filter.tsx +20 -3
  568. package/src/controls/control-group.tsx +11 -4
  569. package/src/controls/controls.module.css +169 -186
  570. package/src/controls/controls.module.css.d.ts +9 -11
  571. package/src/controls/enum-filter.tsx +46 -19
  572. package/src/controls/filter-context.tsx +21 -8
  573. package/src/controls/filter-pill.tsx +6 -3
  574. package/src/controls/filter-set.tsx +32 -13
  575. package/src/controls/filter-sheet.tsx +44 -194
  576. package/src/controls/index.ts +8 -0
  577. package/src/controls/multi-select-filter.tsx +179 -29
  578. package/src/controls/number-filter.tsx +329 -0
  579. package/src/controls/panel-row.module.css +86 -0
  580. package/src/controls/panel-row.module.css.d.ts +13 -0
  581. package/src/controls/panel-row.tsx +392 -0
  582. package/src/controls/range-filter.tsx +133 -43
  583. package/src/controls/search-filter.tsx +31 -13
  584. package/src/controls/select-filter.tsx +11 -1
  585. package/src/controls/slider-control.tsx +159 -0
  586. package/src/controls/sync-button.tsx +13 -3
  587. package/src/controls/toggle-filter.tsx +37 -17
  588. package/src/controls/view-tabs.tsx +63 -11
  589. package/src/controls-dates/date-range-picker.tsx +8 -1
  590. package/src/dashboard/delta-badge.tsx +5 -1
  591. package/src/dashboard/empty-state.tsx +72 -25
  592. package/src/dashboard/index.ts +19 -2
  593. package/src/dashboard/query-state.tsx +154 -84
  594. package/src/dashboard/settings-section.tsx +87 -31
  595. package/src/dashboard/stat-card.module.css +112 -26
  596. package/src/dashboard/stat-card.module.css.d.ts +5 -0
  597. package/src/dashboard/stat-card.tsx +207 -68
  598. package/src/dashboard/stat-group.module.css +66 -0
  599. package/src/dashboard/stat-group.module.css.d.ts +8 -0
  600. package/src/dashboard/stat-group.tsx +74 -0
  601. package/src/dashboard/widget-grid.module.css +53 -0
  602. package/src/dashboard/widget-grid.module.css.d.ts +9 -0
  603. package/src/dashboard/widget-grid.tsx +127 -0
  604. package/src/data/data-table.module.css +64 -0
  605. package/src/data/data-table.module.css.d.ts +11 -0
  606. package/src/data/data-table.tsx +927 -346
  607. package/src/data/index.ts +30 -2
  608. package/src/data/query-branch.ts +53 -0
  609. package/src/data/table.ts +5 -0
  610. package/src/data/virtual-list.tsx +189 -55
  611. package/src/data/virtual.ts +5 -1
  612. package/src/forms/FormErrorSummary.tsx +13 -6
  613. package/src/forms/create-form.ts +2 -2
  614. package/src/forms/field.ts +60 -11
  615. package/src/forms/form-layout.module.css +145 -0
  616. package/src/forms/form-layout.module.css.d.ts +20 -0
  617. package/src/forms/form-layout.tsx +305 -0
  618. package/src/forms/form-state.tsx +94 -0
  619. package/src/forms/index.ts +38 -7
  620. package/src/forms/use-field-array.ts +94 -0
  621. package/src/forms/use-form-draft.ts +145 -53
  622. package/src/forms/use-form-submit.ts +199 -0
  623. package/src/guard/index.ts +91 -23
  624. package/src/index.ts +79 -10
  625. package/src/nav/types.ts +15 -6
  626. package/src/notifications/bell.tsx +18 -4
  627. package/src/notifications/center.tsx +18 -5
  628. package/src/notifications/define-notifications.ts +64 -18
  629. package/src/notifications/index.ts +12 -1
  630. package/src/notifications/mount-guard.ts +41 -0
  631. package/src/notifications/notify.tsx +122 -3
  632. package/src/notifications/overlay.tsx +2 -0
  633. package/src/notifications/store.ts +7 -0
  634. package/src/provider/index.tsx +101 -13
  635. package/src/provider/lab-theme.ts +2 -1
  636. package/src/query/devtools.tsx +1 -0
  637. package/src/router-tanstack/field.ts +5 -0
  638. package/src/router-tanstack/index.ts +4 -0
  639. package/src/router-tanstack/search-store.ts +171 -12
  640. package/src/router-tanstack/use-nav.ts +18 -13
  641. package/src/section/index.ts +1 -1
  642. package/src/section/section.module.css +11 -0
  643. package/src/section/section.tsx +163 -96
  644. package/src/shell/account-types.ts +5 -2
  645. package/src/shell/app-brand.module.css +109 -0
  646. package/src/shell/app-brand.module.css.d.ts +14 -0
  647. package/src/shell/app-brand.tsx +140 -0
  648. package/src/shell/app-breadcrumbs.tsx +19 -9
  649. package/src/shell/app-header.module.css +10 -5
  650. package/src/shell/app-main.module.css +107 -0
  651. package/src/shell/app-main.module.css.d.ts +11 -0
  652. package/src/shell/app-mobile-nav.module.css +48 -27
  653. package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
  654. package/src/shell/app-mobile-nav.tsx +43 -11
  655. package/src/shell/app-sidebar-account.tsx +29 -6
  656. package/src/shell/app-sidebar.module.css +53 -96
  657. package/src/shell/app-sidebar.module.css.d.ts +0 -5
  658. package/src/shell/app-sidebar.tsx +40 -113
  659. package/src/shell/index.tsx +156 -65
  660. package/src/shell/isomorphic-layout-effect.ts +14 -0
  661. package/src/shell/mobile-nav-model.ts +66 -10
  662. package/src/shell/nav-count-badge.tsx +6 -1
  663. package/src/shell/page-aside.module.css +174 -0
  664. package/src/shell/page-aside.module.css.d.ts +14 -0
  665. package/src/shell/page-aside.tsx +377 -0
  666. package/src/shell/page-bar.module.css +107 -13
  667. package/src/shell/page-bar.module.css.d.ts +4 -1
  668. package/src/shell/page-bar.tsx +294 -94
  669. package/src/shell/sidebar-search.module.css +1 -1
  670. package/src/shell/sidebar-search.tsx +13 -4
  671. package/src/state/fields.ts +360 -69
  672. package/src/state/persisted-or-local.ts +50 -0
  673. package/src/state/persisted.ts +76 -26
  674. package/src/state.ts +4 -7
  675. package/src/surfaces.ts +43 -12
  676. package/src/theme/ctl-theme.tsx +10 -0
  677. package/src/theme/index.ts +129 -16
  678. package/src/theme/nav-link.module.css +37 -26
  679. package/src/theme/segmented-control.module.css +22 -0
  680. package/src/theme/segmented-control.module.css.d.ts +1 -0
  681. package/src/theme-lab/boot.ts +140 -0
  682. package/src/theme-lab/index.tsx +21 -112
  683. package/src/theme-toggle/index.tsx +10 -3
  684. package/src/tokens/index.ts +50 -6
  685. package/src/tokens/palette.ts +58 -30
  686. package/src/utils/is-dev.ts +4 -0
  687. package/src/widget-header/delta-badge.tsx +25 -5
  688. package/src/widget-header/index.ts +10 -2
  689. package/src/widget-header/widget-header.module.css +87 -7
  690. package/src/widget-header/widget-header.module.css.d.ts +1 -0
  691. package/src/widget-header/widget-header.tsx +166 -42
  692. package/dist/agent/adapter.test.d.ts +0 -2
  693. package/dist/agent/adapter.test.d.ts.map +0 -1
  694. package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
  695. package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
  696. package/dist/agent/coalesce.test.d.ts +0 -2
  697. package/dist/agent/coalesce.test.d.ts.map +0 -1
  698. package/dist/agent/foreign.test.d.ts +0 -2
  699. package/dist/agent/foreign.test.d.ts.map +0 -1
  700. package/dist/agent/id.test.d.ts +0 -2
  701. package/dist/agent/id.test.d.ts.map +0 -1
  702. package/dist/agent/merge.test.d.ts +0 -2
  703. package/dist/agent/merge.test.d.ts.map +0 -1
  704. package/dist/agent/part-list.test.d.ts +0 -2
  705. package/dist/agent/part-list.test.d.ts.map +0 -1
  706. package/dist/agent/parts.test.d.ts +0 -2
  707. package/dist/agent/parts.test.d.ts.map +0 -1
  708. package/dist/agent/thread.test.d.ts +0 -2
  709. package/dist/agent/thread.test.d.ts.map +0 -1
  710. package/dist/agent/transport.test.d.ts +0 -2
  711. package/dist/agent/transport.test.d.ts.map +0 -1
  712. package/dist/agent/use-agent-stream.test.d.ts +0 -2
  713. package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
  714. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
  715. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
  716. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
  717. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
  718. package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
  719. package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
  720. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
  721. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
  722. package/dist/agent-chat/composer.test.d.ts +0 -2
  723. package/dist/agent-chat/composer.test.d.ts.map +0 -1
  724. package/dist/agent-chat/relative-time.test.d.ts +0 -2
  725. package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
  726. package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
  727. package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
  728. package/dist/agent-chat/thread-feed.test.d.ts +0 -2
  729. package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
  730. package/dist/agent-chat/thread-message.test.d.ts +0 -2
  731. package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
  732. package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
  733. package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
  734. package/dist/agent-chat/tool-chip.test.d.ts +0 -2
  735. package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
  736. package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
  737. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
  738. package/dist/charts/cursor/resolve.test.d.ts +0 -2
  739. package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
  740. package/dist/charts/cursor/store.test.d.ts +0 -2
  741. package/dist/charts/cursor/store.test.d.ts.map +0 -1
  742. package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
  743. package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
  744. package/dist/charts/kinds/Bars.test.d.ts +0 -2
  745. package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
  746. package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
  747. package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
  748. package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
  749. package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
  750. package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
  751. package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
  752. package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
  753. package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
  754. package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
  755. package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
  756. package/dist/charts/layout/auto-margin.test.d.ts +0 -2
  757. package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
  758. package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
  759. package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
  760. package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
  761. package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
  762. package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
  763. package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
  764. package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
  765. package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
  766. package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
  767. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
  768. package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
  769. package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
  770. package/dist/charts/series.test.d.ts +0 -2
  771. package/dist/charts/series.test.d.ts.map +0 -1
  772. package/dist/charts/utils/domain.test.d.ts +0 -2
  773. package/dist/charts/utils/domain.test.d.ts.map +0 -1
  774. package/dist/charts/utils/measure-text.test.d.ts +0 -2
  775. package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
  776. package/dist/cli/check-coverage.test.d.ts +0 -2
  777. package/dist/cli/check-coverage.test.d.ts.map +0 -1
  778. package/dist/cli/check-theme.test.d.ts +0 -2
  779. package/dist/cli/check-theme.test.d.ts.map +0 -1
  780. package/dist/cli/help.test.d.ts +0 -2
  781. package/dist/cli/help.test.d.ts.map +0 -1
  782. package/dist/cli/init-sync.test.d.ts +0 -2
  783. package/dist/cli/init-sync.test.d.ts.map +0 -1
  784. package/dist/cli/placement-engine.test.d.ts +0 -2
  785. package/dist/cli/placement-engine.test.d.ts.map +0 -1
  786. package/dist/cli/shipped-versions.test.d.ts +0 -2
  787. package/dist/cli/shipped-versions.test.d.ts.map +0 -1
  788. package/dist/cli/tarball-parity.test.d.ts +0 -2
  789. package/dist/cli/tarball-parity.test.d.ts.map +0 -1
  790. package/dist/cli/template-format.test.d.ts +0 -2
  791. package/dist/cli/template-format.test.d.ts.map +0 -1
  792. package/dist/cli/template-ledger.test.d.ts +0 -2
  793. package/dist/cli/template-ledger.test.d.ts.map +0 -1
  794. package/dist/cli/tokens-css.test.d.ts +0 -2
  795. package/dist/cli/tokens-css.test.d.ts.map +0 -1
  796. package/dist/cli/toolchain-wiring.test.d.ts +0 -2
  797. package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
  798. package/dist/cli/waiver-audit.test.d.ts +0 -2
  799. package/dist/cli/waiver-audit.test.d.ts.map +0 -1
  800. package/dist/content/article-actions.test.d.ts +0 -2
  801. package/dist/content/article-actions.test.d.ts.map +0 -1
  802. package/dist/content/article-model.test.d.ts +0 -2
  803. package/dist/content/article-model.test.d.ts.map +0 -1
  804. package/dist/content/block-split.test.d.ts +0 -2
  805. package/dist/content/block-split.test.d.ts.map +0 -1
  806. package/dist/content/heading-components.test.d.ts +0 -2
  807. package/dist/content/heading-components.test.d.ts.map +0 -1
  808. package/dist/content/markdown.test.d.ts +0 -2
  809. package/dist/content/markdown.test.d.ts.map +0 -1
  810. package/dist/content/prose.module.css.test.d.ts +0 -2
  811. package/dist/content/prose.module.css.test.d.ts.map +0 -1
  812. package/dist/content/sanitize.test.d.ts +0 -2
  813. package/dist/content/sanitize.test.d.ts.map +0 -1
  814. package/dist/content/slug.test.d.ts +0 -2
  815. package/dist/content/slug.test.d.ts.map +0 -1
  816. package/dist/controls/actions.test.d.ts +0 -2
  817. package/dist/controls/actions.test.d.ts.map +0 -1
  818. package/dist/controls/actions.type-guard.test.d.ts +0 -2
  819. package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
  820. package/dist/controls/control-group.test.d.ts +0 -2
  821. package/dist/controls/control-group.test.d.ts.map +0 -1
  822. package/dist/controls/controls.router.test.d.ts +0 -2
  823. package/dist/controls/controls.router.test.d.ts.map +0 -1
  824. package/dist/controls/filter-set.test.d.ts +0 -2
  825. package/dist/controls/filter-set.test.d.ts.map +0 -1
  826. package/dist/controls/peer-boundary.test.d.ts +0 -2
  827. package/dist/controls/peer-boundary.test.d.ts.map +0 -1
  828. package/dist/controls/props-contract.test.d.ts +0 -2
  829. package/dist/controls/props-contract.test.d.ts.map +0 -1
  830. package/dist/controls/sync-button.test.d.ts +0 -2
  831. package/dist/controls/sync-button.test.d.ts.map +0 -1
  832. package/dist/dashboard/query-state.test.d.ts +0 -2
  833. package/dist/dashboard/query-state.test.d.ts.map +0 -1
  834. package/dist/dashboard/settings-section.test.d.ts +0 -2
  835. package/dist/dashboard/settings-section.test.d.ts.map +0 -1
  836. package/dist/dashboard/stat-card.test.d.ts +0 -2
  837. package/dist/dashboard/stat-card.test.d.ts.map +0 -1
  838. package/dist/data/data-table.test.d.ts +0 -2
  839. package/dist/data/data-table.test.d.ts.map +0 -1
  840. package/dist/guard/check-source.test.d.ts +0 -2
  841. package/dist/guard/check-source.test.d.ts.map +0 -1
  842. package/dist/guard/grace.test.d.ts +0 -20
  843. package/dist/guard/grace.test.d.ts.map +0 -1
  844. package/dist/guard/guard-hook.test.d.ts +0 -2
  845. package/dist/guard/guard-hook.test.d.ts.map +0 -1
  846. package/dist/provider/index.test.d.ts +0 -2
  847. package/dist/provider/index.test.d.ts.map +0 -1
  848. package/dist/provider/lab-theme.test.d.ts +0 -2
  849. package/dist/provider/lab-theme.test.d.ts.map +0 -1
  850. package/dist/query/error-message.test.d.ts +0 -2
  851. package/dist/query/error-message.test.d.ts.map +0 -1
  852. package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
  853. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
  854. package/dist/router-tanstack/nav.test.d.ts +0 -2
  855. package/dist/router-tanstack/nav.test.d.ts.map +0 -1
  856. package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
  857. package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
  858. package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
  859. package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
  860. package/dist/router-tanstack/search-store.test.d.ts +0 -2
  861. package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
  862. package/dist/router-tanstack/use-nav.test.d.ts +0 -2
  863. package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
  864. package/dist/section/section.test.d.ts +0 -2
  865. package/dist/section/section.test.d.ts.map +0 -1
  866. package/dist/shell/app-mobile-nav.test.d.ts +0 -2
  867. package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
  868. package/dist/shell/app-sidebar.test.d.ts +0 -2
  869. package/dist/shell/app-sidebar.test.d.ts.map +0 -1
  870. package/dist/shell/index.test.d.ts +0 -2
  871. package/dist/shell/index.test.d.ts.map +0 -1
  872. package/dist/shell/mobile-nav-model.test.d.ts +0 -2
  873. package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
  874. package/dist/shell/page-bar.test.d.ts +0 -2
  875. package/dist/shell/page-bar.test.d.ts.map +0 -1
  876. package/dist/state/fields.test.d.ts +0 -2
  877. package/dist/state/fields.test.d.ts.map +0 -1
  878. package/dist/styles.floor.test.d.ts +0 -2
  879. package/dist/styles.floor.test.d.ts.map +0 -1
  880. package/dist/theme/border-coverage.test.d.ts +0 -18
  881. package/dist/theme/border-coverage.test.d.ts.map +0 -1
  882. package/dist/theme/contrast.test.d.ts +0 -2
  883. package/dist/theme/contrast.test.d.ts.map +0 -1
  884. package/dist/theme/ctl-theme.test.d.ts +0 -2
  885. package/dist/theme/ctl-theme.test.d.ts.map +0 -1
  886. package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
  887. package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
  888. package/dist/theme/density-option.test.d.ts +0 -2
  889. package/dist/theme/density-option.test.d.ts.map +0 -1
  890. package/dist/theme/density-relations.test.d.ts +0 -2
  891. package/dist/theme/density-relations.test.d.ts.map +0 -1
  892. package/dist/theme/derive-theme.test.d.ts +0 -2
  893. package/dist/theme/derive-theme.test.d.ts.map +0 -1
  894. package/dist/theme/divider-law.test.d.ts +0 -2
  895. package/dist/theme/divider-law.test.d.ts.map +0 -1
  896. package/dist/theme/fonts-option.test.d.ts +0 -2
  897. package/dist/theme/fonts-option.test.d.ts.map +0 -1
  898. package/dist/theme/icon-slot.test.d.ts +0 -2
  899. package/dist/theme/icon-slot.test.d.ts.map +0 -1
  900. package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
  901. package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
  902. package/dist/theme/radius-option.test.d.ts +0 -2
  903. package/dist/theme/radius-option.test.d.ts.map +0 -1
  904. package/dist/theme/radius.test.d.ts +0 -2
  905. package/dist/theme/radius.test.d.ts.map +0 -1
  906. package/dist/theme/shadow-surfaces.test.d.ts +0 -26
  907. package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
  908. package/dist/theme/spacing.test.d.ts +0 -2
  909. package/dist/theme/spacing.test.d.ts.map +0 -1
  910. package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
  911. package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
  912. package/dist/theme-lab/derive-controls.test.d.ts +0 -2
  913. package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
  914. package/dist/theme-lab/derive-state.test.d.ts +0 -2
  915. package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
  916. package/dist/theme-lab/theme-lab.test.d.ts +0 -2
  917. package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
  918. package/dist/tokens/build-fonts-css.test.d.ts +0 -2
  919. package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
  920. package/dist/tokens/build-radius-css.test.d.ts +0 -2
  921. package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
  922. package/dist/tokens/chart-margin.test.d.ts +0 -2
  923. package/dist/tokens/chart-margin.test.d.ts.map +0 -1
  924. package/dist/tokens/density.test.d.ts +0 -2
  925. package/dist/tokens/density.test.d.ts.map +0 -1
  926. package/dist/tokens/derive-radius.test.d.ts +0 -2
  927. package/dist/tokens/derive-radius.test.d.ts.map +0 -1
  928. package/dist/tokens/derive.test.d.ts +0 -2
  929. package/dist/tokens/derive.test.d.ts.map +0 -1
  930. package/dist/tokens/palette.test.d.ts +0 -2
  931. package/dist/tokens/palette.test.d.ts.map +0 -1
  932. package/dist/tokens/text-scale.test.d.ts +0 -2
  933. package/dist/tokens/text-scale.test.d.ts.map +0 -1
  934. package/dist/vite-color-mix.test.d.ts +0 -2
  935. package/dist/vite-color-mix.test.d.ts.map +0 -1
  936. package/dist/vite.test.d.ts +0 -2
  937. package/dist/vite.test.d.ts.map +0 -1
  938. package/dist/widget-header/delta-badge.test.d.ts +0 -2
  939. package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
  940. package/dist/widget-header/no-mantine.test.d.ts +0 -2
  941. package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
  942. package/dist/widget-header/widget-header.test.d.ts +0 -2
  943. package/dist/widget-header/widget-header.test.d.ts.map +0 -1
  944. package/src/agent/adapter.test.ts +0 -1640
  945. package/src/agent/ai-sdk-transport.test.ts +0 -658
  946. package/src/agent/coalesce.test.ts +0 -387
  947. package/src/agent/foreign.test.ts +0 -51
  948. package/src/agent/id.test.ts +0 -169
  949. package/src/agent/merge.test.ts +0 -206
  950. package/src/agent/part-list.test.tsx +0 -112
  951. package/src/agent/parts.test.ts +0 -720
  952. package/src/agent/thread.test.ts +0 -220
  953. package/src/agent/transport.test.ts +0 -70
  954. package/src/agent/use-agent-stream.test.tsx +0 -287
  955. package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
  956. package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
  957. package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
  958. package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
  959. package/src/agent-chat/composer.test.tsx +0 -924
  960. package/src/agent-chat/relative-time.test.ts +0 -95
  961. package/src/agent-chat/thread-feed-row.test.tsx +0 -510
  962. package/src/agent-chat/thread-feed.test.tsx +0 -241
  963. package/src/agent-chat/thread-message.test.tsx +0 -1285
  964. package/src/agent-chat/thread-workspace.test.tsx +0 -169
  965. package/src/agent-chat/tool-chip.test.tsx +0 -139
  966. package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
  967. package/src/charts/cursor/resolve.test.ts +0 -151
  968. package/src/charts/cursor/store.test.ts +0 -64
  969. package/src/charts/kinds/BandStrip.test.tsx +0 -282
  970. package/src/charts/kinds/Bars.test.tsx +0 -183
  971. package/src/charts/kinds/DualPanel.test.tsx +0 -527
  972. package/src/charts/kinds/MirroredBars.test.tsx +0 -232
  973. package/src/charts/kinds/MultiLine.test.tsx +0 -218
  974. package/src/charts/kinds/StackedArea.test.tsx +0 -114
  975. package/src/charts/kinds/ZonedLine.test.tsx +0 -147
  976. package/src/charts/layout/auto-margin.test.ts +0 -42
  977. package/src/charts/primitives/CartesianChart.test.tsx +0 -647
  978. package/src/charts/primitives/ChartCard.test.tsx +0 -66
  979. package/src/charts/primitives/ChartFrame.test.tsx +0 -142
  980. package/src/charts/primitives/ChartLegend.test.tsx +0 -78
  981. package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
  982. package/src/charts/primitives/XZoneRects.test.tsx +0 -123
  983. package/src/charts/series.test.ts +0 -112
  984. package/src/charts/utils/domain.test.ts +0 -54
  985. package/src/charts/utils/measure-text.test.ts +0 -36
  986. package/src/cli/check-coverage.test.ts +0 -297
  987. package/src/cli/check-theme.test.ts +0 -837
  988. package/src/cli/help.test.ts +0 -170
  989. package/src/cli/init-sync.test.ts +0 -343
  990. package/src/cli/placement-engine.test.ts +0 -298
  991. package/src/cli/shipped-versions.test.ts +0 -83
  992. package/src/cli/tarball-parity.test.ts +0 -44
  993. package/src/cli/template-format.test.ts +0 -64
  994. package/src/cli/template-ledger.test.ts +0 -219
  995. package/src/cli/tokens-css.test.ts +0 -249
  996. package/src/cli/toolchain-wiring.test.ts +0 -1358
  997. package/src/cli/waiver-audit.test.ts +0 -215
  998. package/src/content/article-actions.test.ts +0 -78
  999. package/src/content/article-model.test.ts +0 -119
  1000. package/src/content/block-split.test.ts +0 -75
  1001. package/src/content/heading-components.test.tsx +0 -65
  1002. package/src/content/markdown.test.tsx +0 -603
  1003. package/src/content/prose.module.css.test.ts +0 -39
  1004. package/src/content/sanitize.test.ts +0 -659
  1005. package/src/content/slug.test.ts +0 -62
  1006. package/src/controls/actions.test.tsx +0 -252
  1007. package/src/controls/actions.type-guard.test.ts +0 -63
  1008. package/src/controls/control-group.test.tsx +0 -229
  1009. package/src/controls/controls.router.test.tsx +0 -637
  1010. package/src/controls/filter-set.test.tsx +0 -472
  1011. package/src/controls/peer-boundary.test.ts +0 -85
  1012. package/src/controls/props-contract.test.tsx +0 -106
  1013. package/src/controls/sync-button.test.tsx +0 -183
  1014. package/src/dashboard/query-state.test.tsx +0 -152
  1015. package/src/dashboard/settings-section.test.tsx +0 -56
  1016. package/src/dashboard/stat-card.test.tsx +0 -154
  1017. package/src/data/data-table.test.tsx +0 -434
  1018. package/src/guard/check-source.test.ts +0 -3355
  1019. package/src/guard/grace.test.ts +0 -149
  1020. package/src/guard/guard-hook.test.ts +0 -203
  1021. package/src/provider/index.test.tsx +0 -101
  1022. package/src/provider/lab-theme.test.ts +0 -147
  1023. package/src/query/error-message.test.ts +0 -67
  1024. package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
  1025. package/src/router-tanstack/nav.test.ts +0 -111
  1026. package/src/router-tanstack/search-param-store.test.ts +0 -135
  1027. package/src/router-tanstack/search-store.router.test.tsx +0 -661
  1028. package/src/router-tanstack/search-store.test.ts +0 -418
  1029. package/src/router-tanstack/use-nav.test.tsx +0 -187
  1030. package/src/section/section.test.tsx +0 -204
  1031. package/src/shell/app-mobile-nav.test.tsx +0 -403
  1032. package/src/shell/app-sidebar.test.tsx +0 -509
  1033. package/src/shell/index.test.tsx +0 -428
  1034. package/src/shell/mobile-nav-model.test.ts +0 -517
  1035. package/src/shell/page-bar.test.tsx +0 -316
  1036. package/src/state/fields.test.tsx +0 -258
  1037. package/src/styles.floor.test.ts +0 -36
  1038. package/src/theme/border-coverage.test.ts +0 -272
  1039. package/src/theme/contrast.test.ts +0 -354
  1040. package/src/theme/ctl-theme.test.tsx +0 -275
  1041. package/src/theme/ctl-tier-coverage.test.ts +0 -170
  1042. package/src/theme/density-option.test.ts +0 -96
  1043. package/src/theme/density-relations.test.ts +0 -506
  1044. package/src/theme/derive-theme.test.ts +0 -101
  1045. package/src/theme/divider-law.test.ts +0 -259
  1046. package/src/theme/fonts-option.test.ts +0 -75
  1047. package/src/theme/icon-slot.test.tsx +0 -275
  1048. package/src/theme/layout-rhythm-css.test.ts +0 -72
  1049. package/src/theme/radius-option.test.ts +0 -86
  1050. package/src/theme/radius.test.ts +0 -142
  1051. package/src/theme/shadow-surfaces.test.ts +0 -488
  1052. package/src/theme/spacing.test.ts +0 -495
  1053. package/src/theme/use-basalt-spacing.test.tsx +0 -55
  1054. package/src/theme-lab/derive-controls.test.ts +0 -94
  1055. package/src/theme-lab/derive-state.test.ts +0 -71
  1056. package/src/theme-lab/theme-lab.test.ts +0 -46
  1057. package/src/tokens/build-fonts-css.test.ts +0 -52
  1058. package/src/tokens/build-radius-css.test.ts +0 -25
  1059. package/src/tokens/chart-margin.test.ts +0 -39
  1060. package/src/tokens/density.test.ts +0 -259
  1061. package/src/tokens/derive-radius.test.ts +0 -62
  1062. package/src/tokens/derive.test.ts +0 -30
  1063. package/src/tokens/palette.test.ts +0 -114
  1064. package/src/tokens/text-scale.test.ts +0 -51
  1065. package/src/vite-color-mix.test.ts +0 -46
  1066. package/src/vite.test.ts +0 -423
  1067. package/src/widget-header/delta-badge.test.tsx +0 -60
  1068. package/src/widget-header/no-mantine.test.ts +0 -33
  1069. package/src/widget-header/widget-header.test.tsx +0 -187
@@ -104,11 +104,18 @@ export const KNOWN_RULE_IDS = new Set([
104
104
  'raw-scroll-container',
105
105
  'hand-rolled-filter',
106
106
  'control-outside-home',
107
+ 'bound-control-outside-home',
107
108
  'control-size-literal',
108
109
  'page-bar-budget',
109
110
  'responsive-twin',
110
111
  'search-literal-link',
111
112
  'use-search-from-literal',
113
+ 'provider-above-router',
114
+ 'duplicate-notifications-mount',
115
+ 'query-dual-import',
116
+ 'query-fn-unwrap',
117
+ 'deprecated-export',
118
+ 'forms-field-key',
112
119
  // `in-body-page-title` is BOTH a plugin rule and a guard kind — one id, two lanes, so one
113
120
  // annotation waives both. It is listed once, below, with the guard kinds.
114
121
  'visx-boundary',
@@ -443,6 +450,16 @@ function isInStyleContext(context, node) {
443
450
  return false
444
451
  }
445
452
 
453
+ /**
454
+ * No `/src/tokens/` exemption, and none is needed: both rules below only ever visit `JSXAttribute`/
455
+ * `Property` nodes, and `src/tokens/**` is pure data — `.ts`, never `.tsx`, zero React — so the
456
+ * visitor structurally cannot fire there. A path-based exemption used to sit here gated on
457
+ * `isBasaltScopedFile`, which tests "does the nearest package.json depend on basalt-ui", not
458
+ * "is this file basalt's own" — a consumer package that merely depends on basalt-ui could put raw
459
+ * literals under its own `src/tokens/` and silence both rules. Deleting the exemption is simpler
460
+ * than re-gating it on package identity: the simplest gate is no gate.
461
+ */
462
+ // Ships: error
446
463
  const noRawFontSize = {
447
464
  meta: {
448
465
  type: 'suggestion',
@@ -452,7 +469,7 @@ const noRawFontSize = {
452
469
  schema: [],
453
470
  },
454
471
  create(context) {
455
- if (getFilename(context).includes('/src/tokens/') || isTestFile(context)) return {}
472
+ if (isTestFile(context)) return {}
456
473
 
457
474
  return {
458
475
  JSXAttribute(node) {
@@ -496,16 +513,19 @@ const SIZE_ATTRS = new Set(['size', 'fz', 'fontSize'])
496
513
  * which is not always a font size — on `Text` it is, on `ThemeIcon`/`ActionIcon` it is a box
497
514
  * dimension — so one shared id would mean a consumer silencing the icon case also silences every
498
515
  * off-scale font size, the bundled-rule mistake the three boundary rules were split apart to avoid.
499
- * And a separate id can carry its own severity: this rule ships `warn` in the consumer preset for
500
- * one minor (the grace-minor doctrine in the package CLAUDE.md — it rejects code that previously
501
- * passed, and majors are banned here, so a consumer has no semver channel warning them). Widening
502
- * `no-raw-font-size` in place would have promoted it to `error` on upgrade with no such runway.
516
+ * And a separate id can carry its own severity: this rule ships `error` in the consumer preset —
517
+ * it is a NEW form the sibling rule never caught (a numeric-only check let every string form walk
518
+ * straight past it), not a narrowing of code that previously passed, so the grace-minor doctrine in
519
+ * the package CLAUDE.md does not apply to it. Widening `no-raw-font-size` in place would have meant
520
+ * the widened check inherited whatever level the sibling id already carried, with no way to give
521
+ * this specific form its own severity.
503
522
  *
504
523
  * Numeric `size={32}` is deliberately NOT flagged. It is the documented Mantine idiom for icon
505
524
  * dimensions and flagging it would fire on nearly every icon in an app for no design-system gain —
506
525
  * the token system has an opinion about type scale and spacing, not about how many pixels wide one
507
526
  * glyph box is.
508
527
  */
528
+ // Ships: error
509
529
  const rawSizeLiteral = {
510
530
  meta: {
511
531
  type: 'suggestion',
@@ -515,7 +535,7 @@ const rawSizeLiteral = {
515
535
  schema: [],
516
536
  },
517
537
  create(context) {
518
- if (getFilename(context).includes('/src/tokens/')) return {}
538
+ if (isTestFile(context)) return {}
519
539
 
520
540
  return {
521
541
  JSXAttribute(node) {
@@ -546,6 +566,7 @@ function attrValue(attr) {
546
566
  return unwrapExpressionContainer(attr.value)
547
567
  }
548
568
 
569
+ // Ships: error
549
570
  const cardInset = {
550
571
  meta: {
551
572
  type: 'suggestion',
@@ -635,6 +656,7 @@ function subtreeHasChart(node) {
635
656
  return false
636
657
  }
637
658
 
659
+ // Ships: error
638
660
  const chartInRawSurface = {
639
661
  meta: {
640
662
  type: 'suggestion',
@@ -720,6 +742,7 @@ function enclosingHomeOwner(node) {
720
742
  * code is small enough to fix rather than schedule. A consumer who disagrees turns the whole rule
721
743
  * down in their own config.
722
744
  */
745
+ // Ships: error
723
746
  const rawScrollContainer = {
724
747
  meta: {
725
748
  type: 'suggestion',
@@ -854,6 +877,7 @@ function boundaryVisitor(reportFor) {
854
877
  // segment named `charts` is deliberately permissive, not restrictive — it only ever widens where
855
878
  // visx is allowed, never narrows where it's flagged elsewhere. "Your visx lives in a charts/ dir"
856
879
  // is the intended convention.
880
+ // Ships: error
857
881
  const visxBoundary = {
858
882
  meta: {
859
883
  type: 'suggestion',
@@ -885,6 +909,7 @@ const visxBoundary = {
885
909
  // Rule 6 — visx-tooltip: @visx/tooltip banned everywhere, including inside charts. Fires ahead of
886
910
  // (and instead of) basalt/visx-boundary for that one specifier — basalt's own src/ never imports
887
911
  // @visx/tooltip, so no theme-allow-style exemption is needed here either.
912
+ // Ships: error
888
913
  const visxTooltip = {
889
914
  meta: {
890
915
  type: 'suggestion',
@@ -908,6 +933,7 @@ const visxTooltip = {
908
933
  // @mantine/* installed (see the file-header comment for the cycle/fork + packaging rationale).
909
934
  // Repo-local only — never registered in the shipped consumer preset, since this governs basalt's
910
935
  // own internal layering/packaging, not a consumer contract.
936
+ // Ships: repo-local only
911
937
  const tokenLayerBoundary = {
912
938
  meta: {
913
939
  type: 'suggestion',
@@ -969,6 +995,7 @@ function propertyKeyName(node) {
969
995
 
970
996
  // Deliberately NO `hasThemeAllow` escape — see basalt/visx-boundary's comment for why these
971
997
  // guards use their own token (`basalt-agent-allow`, see the file-header comment) instead.
998
+ // Ships: error
972
999
  const agentResumeGuard = {
973
1000
  meta: {
974
1001
  type: 'problem',
@@ -1015,6 +1042,7 @@ const RAW_USE_CHAT_SOURCES = new Set(['@ai-sdk/react', 'ai/react'])
1015
1042
  const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion'])
1016
1043
 
1017
1044
  // Deliberately NO `hasThemeAllow` escape — see the file-header comment.
1045
+ // Ships: error
1018
1046
  const agentNoRawUseChat = {
1019
1047
  meta: {
1020
1048
  type: 'problem',
@@ -1210,6 +1238,7 @@ function aiSdkMajorMessage(consumerMajor) {
1210
1238
  // version skew is still a fact about two package.json files, but a written, intentional
1211
1239
  // producer/consumer pair (see `doctor`'s `aiMajorSkewReason`) needs a way to say so at the import
1212
1240
  // site too, not just be permanently blocked.
1241
+ // Ships: error
1213
1242
  const aiSdkMajor = {
1214
1243
  meta: {
1215
1244
  type: 'problem',
@@ -1302,6 +1331,7 @@ const HAND_ROLLED_PLOT_MESSAGE =
1302
1331
  'the file. To waive just THIS node and stay policed on the rest, write `theme-allow ' +
1303
1332
  'hand-rolled-plot — <why>` on its own line above it. (basalt/hand-rolled-plot)'
1304
1333
 
1334
+ // Ships: error
1305
1335
  const handRolledPlot = {
1306
1336
  meta: {
1307
1337
  type: 'suggestion',
@@ -1412,6 +1442,7 @@ function isMapCall(node) {
1412
1442
  )
1413
1443
  }
1414
1444
 
1445
+ // Ships: error
1415
1446
  const chartLegendLiteral = {
1416
1447
  meta: {
1417
1448
  type: 'suggestion',
@@ -1588,6 +1619,65 @@ const SHADOW_ALIASES = {
1588
1619
  JoinedButtons: 'ControlGroup',
1589
1620
  }
1590
1621
 
1622
+ /**
1623
+ * The basalt export NAMES a file imports, each mapped to the LOCAL bindings it arrived under —
1624
+ * `StatCard → {StatCard}` for `import { StatCard }`, `StatCard → {BaseStatCard}` for
1625
+ * `import { StatCard as BaseStatCard }`.
1626
+ *
1627
+ * The counterpart of {@link collectBasaltImports}, keyed the other way round: that one collects
1628
+ * LOCAL names (is this `<Section>` basalt's?), this one is keyed by the CANONICAL export (does this
1629
+ * file compose basalt's `StatCard` at all?). An alias is precisely the case that separates them —
1630
+ * a wrapper importing `StatCard as BaseStatCard` composes `StatCard`, and a set of local names
1631
+ * would answer `BaseStatCard`. The local half is carried alongside because an import alone is not
1632
+ * composition: the caller has to check the binding is REFERENCED (see
1633
+ * {@link isValueReferencePosition}).
1634
+ *
1635
+ * TYPE-ONLY specifiers are skipped, declaration-level and per-specifier, exactly as
1636
+ * `agent-no-raw-usechat` skips them: `import type { StatCard }` is erased at compile time, so a fork
1637
+ * copying the original's props through `ComponentProps<typeof StatCard>` composes nothing at all.
1638
+ *
1639
+ * Only the package name and its subpaths; no relative half, because `shadow-basalt-export` already
1640
+ * returns early inside basalt's own source, where a relative import would be the dogfood lane.
1641
+ */
1642
+ function collectBasaltImportedNames(node, into) {
1643
+ const source = node.source?.value
1644
+ if (typeof source !== 'string' || !BASALT_IMPORT_SOURCE.test(source)) return
1645
+ if (node.importKind === 'type') return
1646
+ for (const spec of node.specifiers ?? []) {
1647
+ if (spec.importKind === 'type') continue
1648
+ const imported = spec.imported?.name ?? spec.imported?.value
1649
+ const local = spec.local?.name
1650
+ if (typeof imported !== 'string' || typeof local !== 'string') continue
1651
+ const locals = into.get(imported)
1652
+ if (locals === undefined) into.set(imported, new Set([local]))
1653
+ else locals.add(local)
1654
+ }
1655
+ }
1656
+
1657
+ /** The positions an `Identifier`/`JSXIdentifier` occupies WITHOUT referencing a value. */
1658
+ const NON_VALUE_IDENTIFIER_PARENTS = new Set([
1659
+ // The binding's own declaration, not a use of it.
1660
+ 'ImportSpecifier',
1661
+ 'ImportDefaultSpecifier',
1662
+ 'ImportNamespaceSpecifier',
1663
+ // A JSX attribute's NAME (`value={…}`), not its value.
1664
+ 'JSXAttribute',
1665
+ ])
1666
+
1667
+ /**
1668
+ * Does this identifier REFERENCE a value, rather than declare or type-position it?
1669
+ *
1670
+ * The type half matters as much as the import half: `typeof StatCard` parses as a `TSTypeQuery`
1671
+ * around a plain `Identifier`, so a value import used only in a type is indistinguishable from a
1672
+ * rendered one without this test — and that shape (a fork copying the original's props) is the
1673
+ * whole reason the composition check exists.
1674
+ */
1675
+ function isValueReferencePosition(node) {
1676
+ const parentType = node.parent?.type
1677
+ if (typeof parentType !== 'string') return false
1678
+ return !parentType.startsWith('TS') && !NON_VALUE_IDENTIFIER_PARENTS.has(parentType)
1679
+ }
1680
+
1591
1681
  function shadowAliasMessage(name, canonical) {
1592
1682
  return (
1593
1683
  `Local component '${name}' is a renamed '${canonical}' — basalt ships that composite, and a ` +
@@ -1619,6 +1709,7 @@ function shadowBasaltExportMessage(name) {
1619
1709
  * tokens does not. `warn`, not `error`: a name collision is evidence, not proof, and a consumer
1620
1710
  * with a genuinely different `Composer` should be told once, not blocked.
1621
1711
  */
1712
+ // Ships: warn (advisory)
1622
1713
  const shadowBasaltExport = {
1623
1714
  meta: {
1624
1715
  type: 'suggestion',
@@ -1636,30 +1727,86 @@ const shadowBasaltExport = {
1636
1727
  // obsidian's React-free `obsidian-vault-core` took the report and had to waive it.
1637
1728
  if (filename.length === 0 || !isBasaltScopedFile(filename)) return {}
1638
1729
 
1639
- const report = (node, name) => {
1730
+ // Candidates are BUFFERED and judged at `Program:exit`, the same shape the chart and control
1731
+ // rules use: the alias exemption below needs the file's whole import list AND every reference in
1732
+ // the file, and a declaration hoisted above its imports would otherwise be judged against an
1733
+ // empty one.
1734
+ /** Canonical basalt export → the local bindings it was imported under (value imports only). */
1735
+ const basaltImportedNames = new Map()
1736
+ /** Every local name the file REFERENCES as a value — the composition half of the exemption. */
1737
+ const referencedLocals = new Set()
1738
+ const candidates = []
1739
+
1740
+ /** Does the file compose basalt's `canonical` — imported as a value AND referenced? */
1741
+ const composes = (canonical) => {
1742
+ const locals = basaltImportedNames.get(canonical)
1743
+ if (locals === undefined) return false
1744
+ for (const local of locals) {
1745
+ if (referencedLocals.has(local)) return true
1746
+ }
1747
+ return false
1748
+ }
1749
+
1750
+ const noteReference = (node) => {
1751
+ if (typeof node.name !== 'string' || !isValueReferencePosition(node)) return
1752
+ referencedLocals.add(node.name)
1753
+ }
1754
+
1755
+ const note = (node, name) => {
1640
1756
  if (typeof name !== 'string') return
1641
1757
  const alias = Object.hasOwn(SHADOW_ALIASES, name) ? SHADOW_ALIASES[name] : undefined
1642
1758
  if (!BASALT_EXPORTS.has(name) && alias === undefined) return
1643
- if (hasThemeAllow(context, node, 'shadow-basalt-export')) return
1644
- const message =
1645
- alias === undefined ? shadowBasaltExportMessage(name) : shadowAliasMessage(name, alias)
1646
- context.report({ node, message })
1759
+ candidates.push({ node, name, alias })
1647
1760
  }
1648
1761
 
1649
1762
  return {
1763
+ ImportDeclaration(node) {
1764
+ collectBasaltImportedNames(node, basaltImportedNames)
1765
+ },
1766
+ // Both halves are needed and neither subsumes the other: a JSX tag is a `JSXIdentifier`
1767
+ // (`<Base/>`), while every other use of the same binding is an `Identifier`
1768
+ // (`component={Base}`, `createElement(Base)`, `memo(Base)`).
1769
+ Identifier(node) {
1770
+ noteReference(node)
1771
+ },
1772
+ JSXIdentifier(node) {
1773
+ noteReference(node)
1774
+ },
1650
1775
  FunctionDeclaration(node) {
1651
- report(node, node.id?.name)
1776
+ note(node, node.id?.name)
1652
1777
  },
1653
1778
  VariableDeclarator(node) {
1654
1779
  if (!isComponentInit(node.init)) return
1655
- report(node, node.id?.name)
1780
+ note(node, node.id?.name)
1656
1781
  },
1657
1782
  // A class only counts when it EXTENDS something — a legacy class component. basalt ships no
1658
1783
  // class components, so a standalone `class X {}` sharing a name with one of its exports is a
1659
1784
  // collision between two unrelated things, not a fork.
1660
1785
  ClassDeclaration(node) {
1661
1786
  if (node.superClass === null || node.superClass === undefined) return
1662
- report(node, node.id?.name)
1787
+ note(node, node.id?.name)
1788
+ },
1789
+ 'Program:exit'() {
1790
+ for (const { node, name, alias } of candidates) {
1791
+ // A file that COMPOSES the export it renames is a wrapper, not a fork, and the advice
1792
+ // ("import StatCard instead of re-rolling it") is already followed — argo's three hero
1793
+ // cards each imported `StatCard` and wrapped it, and each took a warn for it.
1794
+ //
1795
+ // COMPOSES, not "imports": an import the file never references is not composition, and the
1796
+ // two shapes that reach here silently are the ones a fork actually writes — a type-only
1797
+ // `import type { StatCard }` feeding `ComponentProps<typeof StatCard>` (props copied,
1798
+ // nothing rendered) and a dead import left behind after the body was re-rolled. Both used
1799
+ // to exempt the whole file.
1800
+ //
1801
+ // The collision half is deliberately NOT exempted either way: a local `StatCard` beside an
1802
+ // `import { StatCard as Base }` is a fork that kept the name AND kept a piece of the
1803
+ // original, which is the shape this rule most wants to see.
1804
+ if (alias !== undefined && composes(alias)) continue
1805
+ if (hasThemeAllow(context, node, 'shadow-basalt-export')) continue
1806
+ const message =
1807
+ alias === undefined ? shadowBasaltExportMessage(name) : shadowAliasMessage(name, alias)
1808
+ context.report({ node, message })
1809
+ }
1663
1810
  },
1664
1811
  }
1665
1812
  },
@@ -1691,6 +1838,7 @@ const HAND_ROLLED_SHELL_MESSAGE =
1691
1838
  * it. The module that DEFINES `BasaltShell` is exempt by declaration, not by path — a rule saying
1692
1839
  * "compose X" cannot fire inside X.
1693
1840
  */
1841
+ // Ships: error
1694
1842
  const handRolledShell = {
1695
1843
  meta: {
1696
1844
  type: 'suggestion',
@@ -1803,6 +1951,7 @@ const BOUND_TAGS = new Set([
1803
1951
  'CompareFilter',
1804
1952
  'SelectFilter',
1805
1953
  'MultiSelectFilter',
1954
+ 'NumberFilter',
1806
1955
  'SearchFilter',
1807
1956
  'ToggleFilter',
1808
1957
  'ViewTabs',
@@ -1812,11 +1961,26 @@ const BOUND_TAGS = new Set([
1812
1961
  * The names whose DECLARATION means this file defines a basalt control rather than consuming one —
1813
1962
  * the owner exemption, the same shape as `hand-rolled-plot`'s `notesOwnerDefinition`: a rule that
1814
1963
  * says "use a bound control" cannot fire inside the module that IS the bound control.
1964
+ *
1965
+ * **The declaration alone is not enough, and the second half is what makes the set safe to widen.**
1966
+ * `PanelRow`, `EnumFilter` and `SliderControl` are ordinary names a consumer writes for their own
1967
+ * helpers, and a bare-name match let one local `function PanelRow(…)` switch `control-outside-home`,
1968
+ * `bound-control-outside-home` and `responsive-twin` off for that WHOLE file. So the exemption also
1969
+ * requires the file to import NOTHING from `basalt-ui*`: basalt's own control sources import each
1970
+ * other RELATIVELY, while a consumer of basalt always names the package. A file that consumes
1971
+ * basalt is not the module that defines its controls, whatever it calls its helpers.
1815
1972
  */
1816
1973
  const CONTROL_OWNER_NAMES = new Set([
1817
1974
  ...BOUND_TAGS,
1818
1975
  'FilterSet',
1819
1976
  'FilterPill',
1977
+ // The INTERNAL control modules, listed for the same reason the public names are: `EnumFilter` is
1978
+ // the shared body behind SelectFilter/CompareFilter/NumberFilter, and `PanelRow`/`SliderControl`
1979
+ // are the aside's row primitives (`docs/ASIDE-SPEC.md` §3). A module that IS a bound control
1980
+ // cannot be told to use one. This is a RELAXATION, so it needs no grace entry (C16).
1981
+ 'EnumFilter',
1982
+ 'PanelRow',
1983
+ 'SliderControl',
1820
1984
  'SyncButton',
1821
1985
  'ActionGroup',
1822
1986
  'OverflowMenu',
@@ -1824,19 +1988,25 @@ const CONTROL_OWNER_NAMES = new Set([
1824
1988
  ])
1825
1989
 
1826
1990
  /**
1827
- * Where a raw selection control legitimately lives outside a TIERED home: a settings row, an
1828
- * overlay, a composer. Every one of these is a place the tier does not apply, so the advice ("move
1829
- * it into a home slot") would be wrong rather than merely unwelcome.
1991
+ * Where a raw selection control legitimately lives outside a TIERED home: a settings row, a form
1992
+ * row, an overlay, a composer. Every one of these is a place the tier does not apply, so the advice
1993
+ * ("move it into a home slot") would be wrong rather than merely unwelcome.
1830
1994
  *
1831
1995
  * `SettingsRow` is the form row — law C1's third home, not an exception to C1 — so
1832
1996
  * `control-outside-home` treating it as a home is the whole of the enforcement a settings page
1833
1997
  * gets, and the two tier rules deliberately do not reach inside it (see {@link SLOT_ATTRS}).
1998
+ * `FormRow`/`FormGroup` (`basalt-ui/forms`, `src/forms/form-layout.tsx`) are the SAME home written
1999
+ * for a `<form>` rather than a settings page — a `TextInput` bound to a field is read and typed
2000
+ * into at Mantine's own tier, not a 30px chrome affordance — so they are added here rather than
2001
+ * given a second walk.
1834
2002
  *
1835
2003
  * Declared with the shared slot walk rather than beside `control-outside-home` because three rules
1836
2004
  * read it: that rule's ancestry exemption, and {@link hostedInsideSlot} for the two tier rules.
1837
2005
  */
1838
2006
  const CONTROL_HOST_TAGS = new Set([
1839
2007
  'SettingsRow',
2008
+ 'FormRow',
2009
+ 'FormGroup',
1840
2010
  'Modal',
1841
2011
  'Drawer',
1842
2012
  'Popover.Dropdown',
@@ -1844,6 +2014,69 @@ const CONTROL_HOST_TAGS = new Set([
1844
2014
  'Composer',
1845
2015
  ])
1846
2016
 
2017
+ /**
2018
+ * The basalt elements whose CHILDREN are a home — the SUBTREE homes, where the body IS the surface
2019
+ * (`docs/ASIDE-SPEC.md` §3). `FilterSet` mounts the pill surface, `PageAside` the panel surface,
2020
+ * `PanelRow` the row itself (its `end` prop resolves here too: the attribute hangs off `PanelRow`'s
2021
+ * own opening element, so the ancestry walk reaches the element).
2022
+ *
2023
+ * Read by BOTH C1 rules, because the two must not disagree about what a home IS — before this set
2024
+ * existed `bound-control-outside-home` treated all three as homes and `control-outside-home` knew
2025
+ * none of them, so a raw `<Select>` inside a `<PageAside><PanelRow>` reported while the bound
2026
+ * control beside it did not.
2027
+ *
2028
+ * Unlike {@link CONTROL_HOST_TAGS} these are PROVENANCE-gated, the same test
2029
+ * {@link SLOT_OWNER_TAGS} applies and for the same reason: `PanelRow` is an ordinary name for a
2030
+ * consumer's own layout helper, and reading one as a basalt home would exempt a genuinely homeless
2031
+ * control. The overlay hosts stay name-matched because `Modal`/`Drawer` are Mantine's, not basalt's.
2032
+ */
2033
+ const BASALT_HOST_TAGS = new Set(['FilterSet', 'PageAside', 'PanelRow'])
2034
+
2035
+ /**
2036
+ * A file whose NAME declares it is an overlay's or a form's own body — the cross-file half of
2037
+ * {@link CONTROL_HOST_TAGS}, and the one exemption here that is a CONVENTION rather than a fact
2038
+ * about the AST.
2039
+ *
2040
+ * It exists because law C1's cross-file case is explicitly advisory (`docs/CONTROLS-SPEC.md` §6,
2041
+ * "Honest coverage") and `control-outside-home` was paying for it in false positives: argo's
2042
+ * wave-7 run left 9 warns, all of them a `Select`/`SegmentedControl` inside a `*-modal.tsx` or
2043
+ * `*-form.tsx` whose `<Modal>` is rendered by the PARENT route. Nothing in the flagged file can see
2044
+ * the overlay, so no ancestry walk — however deep — will ever reach it, and the alternative is 9
2045
+ * `theme-allow-file` comments that say what the filename already says.
2046
+ *
2047
+ * The trade is stated plainly: this exempts a whole file on a naming convention, so a `Select` that
2048
+ * genuinely belongs in a page bar goes unreported if it is written in `filters-panel.tsx`. That is
2049
+ * the same bargain `@mantine/form` already buys and a strictly smaller one than promoting the rule
2050
+ * with 9 known false positives. `-panel.tsx` is the loosest member and the one to reconsider first
2051
+ * if the set ever needs narrowing.
2052
+ *
2053
+ * TWO dialects, because a repo picks one file-naming law and the convention has to survive either:
2054
+ * `edit-session-modal.tsx` (kebab) and `CbbiPanel.tsx` (PascalCase — what a repo mandating
2055
+ * `PascalCase.tsx` for component files writes, basalt's own root `CLAUDE.md` included). The kebab
2056
+ * form alone could never match a component file in such a repo, which is exactly how the CBBI panel
2057
+ * — a `PageAside` child whose `<PageAside>` is in the PARENT page — collected four warns that no
2058
+ * ancestry walk in that file could ever have avoided.
2059
+ *
2060
+ * Both dialects require the leading SUBJECT: `Panel.tsx` and `modal.tsx` are page modules in every
2061
+ * consumer that has one, and a bare-noun exemption would switch the rules off wholesale.
2062
+ *
2063
+ * Basename only — a `modal/` DIRECTORY is not the convention, because a directory of modals holds
2064
+ * the page pieces around them too.
2065
+ */
2066
+ const OVERLAY_CONVENTION_KEBAB = /^[^/]*-(?:modal|drawer|popover|panel|form)\.[jt]sx$/
2067
+ const OVERLAY_CONVENTION_PASCAL = /^[A-Z]\w*(?:Modal|Drawer|Popover|Panel|Form)\.[jt]sx$/
2068
+
2069
+ /**
2070
+ * The single predicate both `control-outside-home` and `bound-control-outside-home` ask — see
2071
+ * {@link OVERLAY_CONVENTION_KEBAB} for the whole trade. One function, so the two rules can never
2072
+ * disagree about what the convention IS; `src/guard/index.ts` carries the text-lane copy of the
2073
+ * same two dialects (`raw-selection-control`), pinned by both test files.
2074
+ */
2075
+ function isOverlayConventionFile(filename) {
2076
+ const basename = filename.slice(filename.lastIndexOf('/') + 1)
2077
+ return OVERLAY_CONVENTION_KEBAB.test(basename) || OVERLAY_CONVENTION_PASCAL.test(basename)
2078
+ }
2079
+
1847
2080
  /** How far an ancestry walk climbs before giving up — a JSX tree this deep is not a slot value. */
1848
2081
  const ANCESTRY_MAX_DEPTH = 60
1849
2082
 
@@ -1945,6 +2178,27 @@ function collectBasaltImports(node, into, ownTree) {
1945
2178
  }
1946
2179
  }
1947
2180
 
2181
+ /**
2182
+ * {@link collectBasaltImports} as a local→EXPORTED map — the same provenance test, keyed so a caller
2183
+ * can ask WHICH home it found rather than only whether it found one.
2184
+ *
2185
+ * `control-size-literal` needs the distinction: `ChartCard` is the one slot owner whose slot cannot
2186
+ * mount the tier theme, and `import { ChartCard as Card }` is a normal thing to write, so a
2187
+ * membership Set (which answers `Card`) could not express the exemption at all.
2188
+ */
2189
+ function collectBasaltImportMap(node, into, ownTree) {
2190
+ const source = node.source?.value
2191
+ if (typeof source !== 'string') return
2192
+ if (!BASALT_IMPORT_SOURCE.test(source) && !(ownTree && RELATIVE_IMPORT_SOURCE.test(source))) {
2193
+ return
2194
+ }
2195
+ for (const spec of node.specifiers ?? []) {
2196
+ const local = spec.local?.name
2197
+ if (typeof local !== 'string') continue
2198
+ into.set(local, spec.imported?.name ?? spec.imported?.value ?? local)
2199
+ }
2200
+ }
2201
+
1948
2202
  /**
1949
2203
  * The BINDING a home's slot prop hangs off — `PageBar` for `<PageBar filters={…}>` — or undefined
1950
2204
  * when `attr` is not a slot-shaped prop on a home-shaped tag.
@@ -2030,6 +2284,34 @@ function hasAncestorTag(node, tags) {
2030
2284
  return false
2031
2285
  }
2032
2286
 
2287
+ /**
2288
+ * The BINDINGS of every JSX ancestor of `node`, nearest first — {@link hasAncestorTag} for a
2289
+ * PROVENANCE-gated set. It returns names rather than a boolean for the same reason
2290
+ * {@link slotOwnerBinding} does: whether that `PanelRow` is basalt's is answered by the import list,
2291
+ * which is only complete at `Program:exit`, while the ancestry is only walkable during the visit.
2292
+ *
2293
+ * `tags` is OPTIONAL, and omitting it is the second real use rather than a convenience: a rule
2294
+ * whose ancestor set is not known until the import list is complete cannot filter by tag NAME at
2295
+ * all (`RouterProvider as Router` is an ordinary thing to write), so it takes every binding and
2296
+ * resolves them at exit. `provider-above-router` is that caller.
2297
+ */
2298
+ function ancestorTagBindings(node, tags) {
2299
+ const found = []
2300
+ let current = node.parent
2301
+ for (let depth = 0; current !== null && current !== undefined; depth++) {
2302
+ if (depth > ANCESTRY_MAX_DEPTH || current.type === 'Program') break
2303
+ if (
2304
+ current.type === 'JSXElement' &&
2305
+ (tags === undefined || tags.has(jsxTagName(current.openingElement?.name) ?? ''))
2306
+ ) {
2307
+ const root = jsxRootName(current.openingElement?.name)
2308
+ if (root !== undefined) found.push(root)
2309
+ }
2310
+ current = current.parent
2311
+ }
2312
+ return found
2313
+ }
2314
+
2033
2315
  /** Identifiers a slot attribute's value resolves through — `filters={pills}`, `actions={[a, b]}`. */
2034
2316
  function collectSlotValueIdentifiers(node, into, depth = 0) {
2035
2317
  if (node === null || node === undefined || depth > 8) return
@@ -2067,14 +2349,30 @@ function collectSlotValueIdentifiers(node, into, depth = 0) {
2067
2349
  * complete at exit.
2068
2350
  */
2069
2351
  function createSlotContext(context) {
2070
- const homes = new Set()
2352
+ /** Local binding → the basalt export it was imported under. Membership IS the home test. */
2353
+ const homes = new Map()
2071
2354
  const ownTree = isBasaltOwnSource(getFilename(context))
2072
- /** Hoisted identifier → the owner BINDING of the slot it was handed to. */
2355
+ /**
2356
+ * Hoisted identifier → EVERY owner BINDING whose slot it was handed to.
2357
+ *
2358
+ * A Set, not one binding: `const acts = <Button size="xs"/>` handed to both `<Section actions>` and
2359
+ * `<ChartCard actions>` is inside two slots at once, and a last-writer-wins map made the verdict
2360
+ * depend on which attribute came later in the file — the same code went silent or reported
2361
+ * depending on JSX order.
2362
+ */
2073
2363
  const slotBoundIdentifiers = new Map()
2364
+ /**
2365
+ * Hoisted identifier → every SUBTREE-home BINDING whose children it was rendered as.
2366
+ *
2367
+ * The same hop `slotBoundIdentifiers` makes, one home shape over: `const rows = <SelectFilter/>`
2368
+ * handed to `<PageAside>{rows}</PageAside>` is inside that aside, and the ancestry walk cannot
2369
+ * see it because at the `const` there is no `PageAside` above the node at all.
2370
+ */
2371
+ const subtreeHostedIdentifiers = new Map()
2074
2372
  return {
2075
2373
  /** Feed every ImportDeclaration here — this is where the owner provenance comes from. */
2076
2374
  noteImport(node) {
2077
- collectBasaltImports(node, homes, ownTree)
2375
+ collectBasaltImportMap(node, homes, ownTree)
2078
2376
  },
2079
2377
  /** Feed every JSXAttribute here — a slot attribute contributes its value's identifiers. */
2080
2378
  note(attr) {
@@ -2082,7 +2380,30 @@ function createSlotContext(context) {
2082
2380
  if (owner === undefined) return
2083
2381
  const identifiers = new Set()
2084
2382
  collectSlotValueIdentifiers(unwrapExpressionContainer(attr.value), identifiers)
2085
- for (const id of identifiers) slotBoundIdentifiers.set(id, owner)
2383
+ for (const id of identifiers) {
2384
+ const owners = slotBoundIdentifiers.get(id)
2385
+ if (owners === undefined) slotBoundIdentifiers.set(id, new Set([owner]))
2386
+ else owners.add(owner)
2387
+ }
2388
+ },
2389
+ /**
2390
+ * Feed every JSXExpressionContainer here — a `{expr}` CHILD of a subtree home
2391
+ * ({@link BASALT_HOST_TAGS}) hosts the identifiers that expression resolves through.
2392
+ */
2393
+ noteChild(node) {
2394
+ const parent = node.parent
2395
+ if (parent === null || parent === undefined || parent.type !== 'JSXElement') return
2396
+ const name = parent.openingElement?.name
2397
+ if (!BASALT_HOST_TAGS.has(jsxTagName(name) ?? '')) return
2398
+ const owner = jsxRootName(name)
2399
+ if (owner === undefined) return
2400
+ const identifiers = new Set()
2401
+ collectSlotValueIdentifiers(node.expression, identifiers)
2402
+ for (const id of identifiers) {
2403
+ const owners = subtreeHostedIdentifiers.get(id)
2404
+ if (owners === undefined) subtreeHostedIdentifiers.set(id, new Set([owner]))
2405
+ else owners.add(owner)
2406
+ }
2086
2407
  },
2087
2408
  /** Snapshot `node`'s ancestry while it is walkable — the owner NAME, resolved later. */
2088
2409
  capture(node) {
@@ -2090,30 +2411,81 @@ function createSlotContext(context) {
2090
2411
  return {
2091
2412
  directOwner: attr === undefined ? undefined : slotOwnerBinding(attr),
2092
2413
  declaratorName: enclosingDeclaratorName(node),
2414
+ subtreeHosts: ancestorTagBindings(node, BASALT_HOST_TAGS),
2093
2415
  }
2094
2416
  },
2095
2417
  /** Is a captured node inside a BASALT home slot — directly, or via a hoisted binding? */
2096
2418
  resolve(captured) {
2097
2419
  if (captured.directOwner !== undefined) return homes.has(captured.directOwner)
2098
2420
  if (captured.declaratorName === undefined) return false
2099
- const hoistedOwner = slotBoundIdentifiers.get(captured.declaratorName)
2100
- return hoistedOwner !== undefined && homes.has(hoistedOwner)
2421
+ const hoistedOwners = slotBoundIdentifiers.get(captured.declaratorName)
2422
+ if (hoistedOwners === undefined) return false
2423
+ // ANY basalt owner puts the binding in a home: a node handed to one basalt slot and one
2424
+ // consumer component still renders inside the basalt slot.
2425
+ for (const owner of hoistedOwners) {
2426
+ if (homes.has(owner)) return true
2427
+ }
2428
+ return false
2101
2429
  },
2102
2430
  /** Does `name` resolve to a basalt home component in this file? */
2103
2431
  isHome(name) {
2104
2432
  return name !== undefined && homes.has(name)
2105
2433
  },
2434
+ /**
2435
+ * Is a captured node inside a basalt SUBTREE home — directly by ancestry, or via a hoisted
2436
+ * binding rendered as one's `{expr}` child? Both halves resolve the binding through `homes`,
2437
+ * so a consumer's own `PanelRow` is not one.
2438
+ */
2439
+ inSubtreeHome(captured) {
2440
+ for (const binding of captured.subtreeHosts) {
2441
+ if (homes.has(binding)) return true
2442
+ }
2443
+ if (captured.declaratorName === undefined) return false
2444
+ for (const owner of subtreeHostedIdentifiers.get(captured.declaratorName) ?? []) {
2445
+ if (homes.has(owner)) return true
2446
+ }
2447
+ return false
2448
+ },
2449
+ /**
2450
+ * EVERY basalt EXPORT whose slot a captured node sits in — `['ChartCard']` for
2451
+ * `<ChartCard actions={…}>` and for `<Card actions={…}>` under an alias, `[]` when the node is in
2452
+ * no basalt slot. Resolves through the same hoisted-binding hop `resolve` does.
2453
+ *
2454
+ * A LIST, because one hoisted binding can be handed to several slots, and an exemption keyed on
2455
+ * the owner is only sound when it holds for all of them — a `size="xs"` shared between
2456
+ * `<ChartCard actions>` and `<Section actions>` really does render inside the Section's tiered
2457
+ * slot. Non-basalt owners are dropped: they are not homes, so they carry no tier either way.
2458
+ */
2459
+ ownerExports(captured) {
2460
+ const bindings =
2461
+ captured.directOwner !== undefined
2462
+ ? [captured.directOwner]
2463
+ : captured.declaratorName === undefined
2464
+ ? []
2465
+ : [...(slotBoundIdentifiers.get(captured.declaratorName) ?? [])]
2466
+ return bindings.filter((binding) => homes.has(binding)).map((binding) => homes.get(binding))
2467
+ },
2106
2468
  }
2107
2469
  }
2108
2470
 
2109
- /** Collects an owner-definition flag for the control layer — see {@link CONTROL_OWNER_NAMES}. */
2471
+ /** Collects the owner-definition verdict for the control layer — see {@link CONTROL_OWNER_NAMES}. */
2110
2472
  function createControlOwnerProbe() {
2111
- const state = { definesControl: false }
2473
+ const state = { definesControl: false, importsBasalt: false }
2112
2474
  const note = (name) => {
2113
2475
  if (typeof name === 'string' && CONTROL_OWNER_NAMES.has(name)) state.definesControl = true
2114
2476
  }
2115
2477
  return {
2116
- state,
2478
+ /** Feed every ImportDeclaration here — a `basalt-ui*` import disqualifies the exemption. */
2479
+ noteImport(node) {
2480
+ const source = node.source?.value
2481
+ if (typeof source === 'string' && BASALT_IMPORT_SOURCE.test(source)) {
2482
+ state.importsBasalt = true
2483
+ }
2484
+ },
2485
+ /** Does this file DEFINE a basalt control rather than consume one? Both halves are required. */
2486
+ isOwner() {
2487
+ return state.definesControl && !state.importsBasalt
2488
+ },
2117
2489
  visitors: {
2118
2490
  FunctionDeclaration(node) {
2119
2491
  note(node.id?.name)
@@ -2146,6 +2518,7 @@ const HAND_ROLLED_FILTER_MESSAGE =
2146
2518
  * `SettingsRow.control`, the form-row home — see {@link SLOT_ATTRS} for why a raw input is right
2147
2519
  * there.
2148
2520
  */
2521
+ // Ships: error
2149
2522
  const handRolledFilter = {
2150
2523
  meta: {
2151
2524
  type: 'problem',
@@ -2195,9 +2568,9 @@ const handRolledFilter = {
2195
2568
  const CONTROL_OUTSIDE_HOME_MESSAGE =
2196
2569
  'Raw Mantine selection control with no home — a filter, tab or action belongs in exactly one of ' +
2197
2570
  'the three homes (a PageBar / Section / WidgetHeader slot, or a form row), and a home is entered ' +
2198
- 'through a slot prop (law C1). A settings row, an overlay (Modal/Drawer/Popover/Menu) and a ' +
2199
- 'form (@mantine/form) are the declared non-homes and never report. ' +
2200
- '(basalt/control-outside-home)'
2571
+ 'through a slot prop (law C1). A settings row, a form row (FormRow/FormGroup), an overlay ' +
2572
+ '(Modal/Drawer/Popover/Menu) and a form (@mantine/form) are the declared non-homes and never ' +
2573
+ 'report. (basalt/control-outside-home)'
2201
2574
 
2202
2575
  /**
2203
2576
  * The cross-file half of C1, and the one rule here that is openly a HEURISTIC: "this control has
@@ -2205,10 +2578,13 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
2205
2578
  * and stays warn until the playground and the five consumer repos run it with ≤3 waivers
2206
2579
  * (docs/CONTROLS-SPEC.md §9 wave 7).
2207
2580
  *
2208
- * Three exemptions carry the false-positive load: an overlay/settings-row ancestor, a file that
2209
- * imports `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
2581
+ * Four exemptions carry the false-positive load: an overlay/settings-row/form-row ancestor
2582
+ * ({@link CONTROL_HOST_TAGS}, `FormRow`/`FormGroup` included), an {@link isOverlayConventionFile}
2583
+ * basename (the CROSS-FILE case — the `<Modal>` lives in the parent), a file that imports
2584
+ * `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
2210
2585
  * exemption — a file DEFINING a basalt control cannot be told to use one.
2211
2586
  */
2587
+ // Ships: warn (grace → 1.30.0)
2212
2588
  const controlOutsideHome = {
2213
2589
  meta: {
2214
2590
  type: 'suggestion',
@@ -2217,6 +2593,7 @@ const controlOutsideHome = {
2217
2593
  },
2218
2594
  create(context) {
2219
2595
  if (isTestFile(context)) return {}
2596
+ if (isOverlayConventionFile(getFilename(context))) return {}
2220
2597
  const slots = createSlotContext(context)
2221
2598
  const owner = createControlOwnerProbe()
2222
2599
  const mantineImports = new Map()
@@ -2228,11 +2605,15 @@ const controlOutsideHome = {
2228
2605
  ImportDeclaration(node) {
2229
2606
  collectMantineImports(node, mantineImports)
2230
2607
  slots.noteImport(node)
2608
+ owner.noteImport(node)
2231
2609
  if ((node.source?.value ?? '') === '@mantine/form') importsMantineForm = true
2232
2610
  },
2233
2611
  JSXAttribute(node) {
2234
2612
  slots.note(node)
2235
2613
  },
2614
+ JSXExpressionContainer(node) {
2615
+ slots.noteChild(node)
2616
+ },
2236
2617
  JSXOpeningElement(node) {
2237
2618
  // Cheap pre-filter: the tag as WRITTEN, or any Mantine binding (an alias resolves at exit).
2238
2619
  const root = jsxRootName(node.name)
@@ -2245,11 +2626,11 @@ const controlOutsideHome = {
2245
2626
  })
2246
2627
  },
2247
2628
  'Program:exit'() {
2248
- if (importsMantineForm || owner.state.definesControl) return
2629
+ if (importsMantineForm || owner.isOwner()) return
2249
2630
  for (const { node, name, captured, hosted } of candidates) {
2250
2631
  const tag = resolveMantineTag(name, mantineImports)
2251
2632
  if (tag === undefined || !RAW_FILTER_TAGS.has(tag)) continue
2252
- if (hosted || slots.resolve(captured)) continue
2633
+ if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
2253
2634
  if (hasThemeAllow(context, node, 'control-outside-home')) continue
2254
2635
  context.report({ node, message: CONTROL_OUTSIDE_HOME_MESSAGE })
2255
2636
  }
@@ -2319,6 +2700,25 @@ export const CTL_THEME_TAGS = new Set([
2319
2700
  * {@link CTL_THEME_TAGS} member, and skipped under an overlay ({@link hostedInsideSlot}) — see
2320
2701
  * `CTL_THEME_TAGS` for what firing on everything in the slot cost.
2321
2702
  */
2703
+ /**
2704
+ * The home whose slot cannot mount the tier theme, so the CONTROL there has to state its own size.
2705
+ *
2706
+ * `ChartCard` lives inside the Mantine-free chart layer (`src/charts/primitives/`), which means it
2707
+ * cannot render a `MantineThemeProvider` at all — it writes `data-basalt-tier="widget"` by hand and
2708
+ * nothing else (`../CLAUDE.md`, "A home sizes its own SLOT"; `docs/CONTROLS-SPEC.md` §5). So this
2709
+ * rule's whole message — "the HOME sets the tier, drop the prop" — is FALSE in exactly one slot: a
2710
+ * `Switch` in `ChartCard.actions` with no `size` renders at Mantine's default, not at `ctl`.
2711
+ *
2712
+ * It has to be an exemption rather than a waiver because the rule fires on the `size` ATTRIBUTE and
2713
+ * cannot tell a correct `size="ctl"` from the `size="xs"` it exists to catch. The playground carried
2714
+ * exactly one waiver for this and it was the honest kind — the rule was wrong, not the code.
2715
+ *
2716
+ * `hand-rolled-filter` is deliberately NOT exempted here: a raw `Select` in `ChartCard.actions` is
2717
+ * still a filter that should take a `field`, and the tier is not what makes that true.
2718
+ */
2719
+ const TIERLESS_SLOT_OWNERS = new Set(['ChartCard'])
2720
+
2721
+ // Ships: error
2322
2722
  const controlSizeLiteral = {
2323
2723
  meta: {
2324
2724
  type: 'suggestion',
@@ -2358,6 +2758,12 @@ const controlSizeLiteral = {
2358
2758
  CTL_THEME_TAGS.has(resolveMantineTag(name, mantineImports) ?? '')
2359
2759
  if (!tiered || hosted) continue
2360
2760
  if (!slots.resolve(captured)) continue
2761
+ // EVERY home the node was handed to has to be tierless. One hoisted `const acts =
2762
+ // <Button size="xs"/>` given to both `<ChartCard actions>` and `<Section actions>` renders
2763
+ // in a tiered slot too, and the prop is redundant there — so the exemption does not apply.
2764
+ const owners = slots.ownerExports(captured)
2765
+ if (owners.length > 0 && owners.every((owner) => TIERLESS_SLOT_OWNERS.has(owner)))
2766
+ continue
2361
2767
  if (hasThemeAllow(context, node, 'control-size-literal')) continue
2362
2768
  context.report({ node, message: CONTROL_SIZE_LITERAL_MESSAGE })
2363
2769
  }
@@ -2432,6 +2838,7 @@ function actionsAttributeValue(node) {
2432
2838
  * The four countable halves of C6. `error` from the start: every one of them is arithmetic over a
2433
2839
  * literal the file wrote down, with no heuristic and no layout intent to guess at.
2434
2840
  */
2841
+ // Ships: error
2435
2842
  const pageBarBudget = {
2436
2843
  meta: {
2437
2844
  type: 'problem',
@@ -2547,6 +2954,7 @@ function titleOrderOf(node) {
2547
2954
  * lanes on purpose, so one `theme-allow in-body-page-title — <why>` waives both rather than
2548
2955
  * needing a different word per enforcement surface.
2549
2956
  */
2957
+ // Ships: error
2550
2958
  const inBodyPageTitle = {
2551
2959
  meta: {
2552
2960
  type: 'suggestion',
@@ -2619,6 +3027,7 @@ function subtreeControlTags(node, into = new Set(), depth = 0) {
2619
3027
  * subtrees both render the same control tag. A file that DEFINES a basalt control is exempt — the
2620
3028
  * one legitimate double mount is the CSS swap inside the control itself.
2621
3029
  */
3030
+ // Ships: error
2622
3031
  const responsiveTwin = {
2623
3032
  meta: {
2624
3033
  type: 'suggestion',
@@ -2655,10 +3064,13 @@ const responsiveTwin = {
2655
3064
 
2656
3065
  return {
2657
3066
  ...owner.visitors,
3067
+ ImportDeclaration(node) {
3068
+ owner.noteImport(node)
3069
+ },
2658
3070
  JSXElement: inspectChildren,
2659
3071
  JSXFragment: inspectChildren,
2660
3072
  'Program:exit'() {
2661
- if (owner.state.definesControl) return
3073
+ if (owner.isOwner()) return
2662
3074
  for (const node of twins) {
2663
3075
  if (hasThemeAllow(context, node, 'responsive-twin')) continue
2664
3076
  context.report({ node, message: RESPONSIVE_TWIN_MESSAGE })
@@ -2697,6 +3109,7 @@ function insideNavDefinition(node) {
2697
3109
  return false
2698
3110
  }
2699
3111
 
3112
+ // Ships: error
2700
3113
  const searchLiteralLink = {
2701
3114
  meta: {
2702
3115
  type: 'suggestion',
@@ -2730,6 +3143,7 @@ const USE_SEARCH_FROM_LITERAL_MESSAGE =
2730
3143
  '(law C10) — take the value as a prop, or read it through the store field. ' +
2731
3144
  '(basalt/use-search-from-literal)'
2732
3145
 
3146
+ // Ships: error
2733
3147
  const useSearchFromLiteral = {
2734
3148
  meta: {
2735
3149
  type: 'suggestion',
@@ -2756,6 +3170,750 @@ const useSearchFromLiteral = {
2756
3170
  },
2757
3171
  }
2758
3172
 
3173
+ // ── Rule 24 — bound-control-outside-home ────────────────────────────────────────────────────────
3174
+
3175
+ /**
3176
+ * A bound basalt control with no home — the pill-shaped controls, and `SliderControl` is
3177
+ * deliberately NOT among them: it has no pill form at all. It always renders a `PanelRow`
3178
+ * (`docs/ASIDE-SPEC.md` §3), so a `<Section>` body or a plain page stack is a legitimate place to
3179
+ * write one and "this renders as a stray pill" would be false there. {@link BOUND_TAGS} is the
3180
+ * whole set; every member is a `FieldHandle`-bound `basalt-ui/controls` export, so unlike
3181
+ * {@link RAW_FILTER_TAGS} there is no question of what it is FOR — only of where it was written.
3182
+ */
3183
+ const BOUND_CONTROL_OUTSIDE_HOME_MESSAGE =
3184
+ 'Bound basalt control with no home — a SelectFilter / ToggleFilter / RangeFilter / ' +
3185
+ 'MultiSelectFilter / NumberFilter / SearchFilter / ViewTabs renders as a stray pill when it is ' +
3186
+ 'written into a Section body or a page stack. It belongs in a home slot (`filters` / `tabs` / ' +
3187
+ '`filtersEnd` / `actions` / `sync` / `control`), inside a <FilterSet>, or inside a <PageAside> ' +
3188
+ '— where the panel surface renders it as a row instead (law C1, docs/ASIDE-SPEC.md §3). ' +
3189
+ 'SliderControl is not policed: it renders its own PanelRow and has no pill form. An overlay ' +
3190
+ '(Modal/Drawer/Popover/Menu), a settings row and a form row (FormRow/FormGroup) are the ' +
3191
+ 'declared non-homes and never report. (basalt/bound-control-outside-home)'
3192
+
3193
+ /**
3194
+ * A BOUND basalt control that is in no home at all — ledger G5 (`docs/ASIDE-SPEC.md` §2), the half
3195
+ * of law C1 that had no guard: `control-outside-home` matches raw Mantine tags only, so a
3196
+ * `<SelectFilter field={…}/>` dropped into a `Section` body passed silently and shipped as a stray
3197
+ * pill.
3198
+ *
3199
+ * A NEW id rather than a widening of `control-outside-home`, per C16: a level is per-id, so
3200
+ * widening that rule would have landed this form at whatever level it holds, with no grace of its
3201
+ * own — and widening it in a minor would restart its grace besides.
3202
+ *
3203
+ * Provenance is the tag's basalt IMPORT ({@link collectBasaltImportMap}), never its bare name: a
3204
+ * consumer's own `SelectFilter` is not this one. The exemptions are the sibling rule's minus the
3205
+ * `@mantine/form` one — a form input is a raw input, and a `FieldHandle`-bound filter in a form
3206
+ * module is homeless there exactly as it is anywhere else.
3207
+ *
3208
+ * **There is deliberately NO text-lane twin in `src/guard`.** `raw-selection-control` can key on a
3209
+ * bare Mantine tag name because that name is Mantine's whatever the file does with it; a bound
3210
+ * control's name says nothing without the import graph and the ancestry, and a regex over a
3211
+ * 12-line window has neither. A text heuristic here would report a consumer's own `SelectFilter` in
3212
+ * its own `filters` slot — signal the plugin already has, noise the guard would add.
3213
+ */
3214
+ // Ships: warn (grace → 1.30.0)
3215
+ const boundControlOutsideHome = {
3216
+ meta: {
3217
+ type: 'suggestion',
3218
+ docs: { description: 'Warn on a bound basalt control that is in no basalt home.' },
3219
+ schema: [],
3220
+ },
3221
+ create(context) {
3222
+ if (isTestFile(context)) return {}
3223
+ if (isOverlayConventionFile(getFilename(context))) return {}
3224
+ const slots = createSlotContext(context)
3225
+ const owner = createControlOwnerProbe()
3226
+ const controls = new Map()
3227
+ const ownTree = isBasaltOwnSource(getFilename(context))
3228
+ const candidates = []
3229
+
3230
+ return {
3231
+ ...owner.visitors,
3232
+ ImportDeclaration(node) {
3233
+ collectBasaltImportMap(node, controls, ownTree)
3234
+ slots.noteImport(node)
3235
+ owner.noteImport(node)
3236
+ },
3237
+ JSXAttribute(node) {
3238
+ slots.note(node)
3239
+ },
3240
+ JSXExpressionContainer(node) {
3241
+ slots.noteChild(node)
3242
+ },
3243
+ JSXOpeningElement(node) {
3244
+ // Cheap pre-filter on the tag as WRITTEN; an alias resolves through `controls` at exit.
3245
+ const root = jsxRootName(node.name)
3246
+ if (root === undefined) return
3247
+ if (!BOUND_TAGS.has(jsxTagName(node.name) ?? '') && !controls.has(root)) return
3248
+ candidates.push({
3249
+ node,
3250
+ root,
3251
+ captured: slots.capture(node),
3252
+ hosted: hasAncestorTag(node, CONTROL_HOST_TAGS),
3253
+ })
3254
+ },
3255
+ 'Program:exit'() {
3256
+ if (owner.isOwner()) return
3257
+ for (const { node, root, captured, hosted } of candidates) {
3258
+ const exported = controls.get(root)
3259
+ if (exported === undefined || !BOUND_TAGS.has(exported)) continue
3260
+ if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
3261
+ if (hasThemeAllow(context, node, 'bound-control-outside-home')) continue
3262
+ context.report({ node, message: BOUND_CONTROL_OUTSIDE_HOME_MESSAGE })
3263
+ }
3264
+ },
3265
+ }
3266
+ },
3267
+ }
3268
+
3269
+ // ── Rule 25 — provider-above-router ─────────────────────────────────────────────────────────────
3270
+
3271
+ /** `@tanstack/react-router` and its subpaths — never `@tanstack/react-router-devtools`. */
3272
+ const TANSTACK_ROUTER_SOURCE = /^@tanstack\/react-router(?:\/|$)/
3273
+
3274
+ const PROVIDER_ABOVE_ROUTER_MESSAGE =
3275
+ '<BasaltProvider> is rendered INSIDE <RouterProvider> — invert them. Mantine context has to ' +
3276
+ 'exist before any route renders, so the mount order is BasaltProvider → the data layer → ' +
3277
+ 'RouterProvider (agent/rules/basalt-mantine.md). A route that renders a themed component ' +
3278
+ 'before the provider mounts reads Mantine defaults, not the basalt theme. ' +
3279
+ '(basalt/provider-above-router)'
3280
+
3281
+ /**
3282
+ * `BasaltProvider` mounted BELOW a TanStack `RouterProvider` — F5, the first of the three
3283
+ * `basalt-mantine.md` laws that shipped under a `not guarded: —` banner.
3284
+ *
3285
+ * Both tags are provenance-gated: `BasaltProvider` through {@link collectBasaltImportMap} (so a
3286
+ * consumer's own component of that name is not this one) and `RouterProvider` through an
3287
+ * `@tanstack/react-router` import, by its LOCAL binding rather than its written name. The inverse
3288
+ * nesting — the correct one — is not a finding, and neither is either tag alone.
3289
+ *
3290
+ * **STATIC JSX ancestry only, and that is the whole heuristic.** The two providers have to be
3291
+ * written in one JSX tree in one file for this to see anything. The common real shape (`main.tsx`
3292
+ * renders `<BasaltProvider><App/></BasaltProvider>` and `App` renders `<RouterProvider/>`) is
3293
+ * INVERTED cross-file and correct; the inverted-and-wrong cross-file shape is equally invisible.
3294
+ * A rule that followed the composition would need the module graph, which an oxlint rule does not
3295
+ * have — so this catches the one-file case and the law stays advisory for the rest. That is why it
3296
+ * lands at `warn`.
3297
+ */
3298
+ // Ships: warn (grace → 1.30.0)
3299
+ const providerAboveRouter = {
3300
+ meta: {
3301
+ type: 'suggestion',
3302
+ docs: { description: 'Warn on a BasaltProvider rendered inside a TanStack RouterProvider.' },
3303
+ schema: [],
3304
+ },
3305
+ create(context) {
3306
+ if (isTestFile(context)) return {}
3307
+ const ownTree = isBasaltOwnSource(getFilename(context))
3308
+ const basalt = new Map()
3309
+ const routers = new Set()
3310
+ const candidates = []
3311
+
3312
+ return {
3313
+ ImportDeclaration(node) {
3314
+ collectBasaltImportMap(node, basalt, ownTree)
3315
+ const source = node.source?.value
3316
+ if (typeof source !== 'string' || !TANSTACK_ROUTER_SOURCE.test(source)) return
3317
+ for (const spec of node.specifiers ?? []) {
3318
+ const imported = spec.imported?.name ?? spec.imported?.value
3319
+ if (imported === 'RouterProvider' && typeof spec.local?.name === 'string') {
3320
+ routers.add(spec.local.name)
3321
+ }
3322
+ }
3323
+ },
3324
+ JSXOpeningElement(node) {
3325
+ const root = jsxRootName(node.name)
3326
+ if (root === undefined || !basalt.has(root)) return
3327
+ candidates.push({ node, root, ancestors: ancestorTagBindings(node) })
3328
+ },
3329
+ 'Program:exit'() {
3330
+ for (const { node, root, ancestors } of candidates) {
3331
+ if (basalt.get(root) !== 'BasaltProvider') continue
3332
+ if (!ancestors.some((name) => routers.has(name))) continue
3333
+ if (hasThemeAllow(context, node, 'provider-above-router')) continue
3334
+ context.report({ node, message: PROVIDER_ABOVE_ROUTER_MESSAGE })
3335
+ }
3336
+ },
3337
+ }
3338
+ },
3339
+ }
3340
+
3341
+ // ── Rule 26 — duplicate-notifications-mount ─────────────────────────────────────────────────────
3342
+
3343
+ const DUPLICATE_NOTIFICATIONS_MOUNT_MESSAGE =
3344
+ '<BasaltNotifications /> mounted alongside <BasaltOverlays notifications> — the two double-mount ' +
3345
+ "Mantine's <Notifications>, so a single notify() renders twice. BasaltOverlays already composes " +
3346
+ 'the notifications layer; drop the standalone mount, or pass `notifications={false}` to ' +
3347
+ 'BasaltOverlays if the standalone one is the mount you want. ' +
3348
+ '(basalt/duplicate-notifications-mount)'
3349
+
3350
+ /**
3351
+ * The ancestor chain from `node` up to its `Program`, OUTERMOST first — captured during the visit,
3352
+ * because `node.parent` is only walkable then, and compared at `Program:exit`. Unlike
3353
+ * {@link ancestorTagBindings} it keeps the NODES, which is what makes a nearest-common-ancestor
3354
+ * question answerable at all.
3355
+ */
3356
+ function ancestorChain(node) {
3357
+ const chain = []
3358
+ let current = node
3359
+ for (let depth = 0; current !== null && current !== undefined; depth++) {
3360
+ if (depth > ANCESTRY_MAX_DEPTH) break
3361
+ chain.push(current)
3362
+ if (current.type === 'Program') break
3363
+ current = current.parent
3364
+ }
3365
+ return chain.toReversed()
3366
+ }
3367
+
3368
+ /**
3369
+ * True when two nodes sit in DIFFERENT branches of ONE conditional — `a ? <X/> : <Y/>`, or the two
3370
+ * operands of one `&&`/`||`. Exactly one of the pair ever renders, so a double-mount rule firing on
3371
+ * them would be reporting a mount that cannot happen.
3372
+ *
3373
+ * Deliberately the NEAREST common ancestor only, and deliberately expression-level: two separate
3374
+ * `cond && <X/>` expressions under one parent are NOT exclusive (both conditions can hold), and an
3375
+ * `if`/`return` split across statements is a control-flow question this does not pretend to answer.
3376
+ * The runtime guard in `src/commands/overlays-mount.tsx` is the lane that counts real mounts.
3377
+ */
3378
+ function mutuallyExclusive(chainA, chainB) {
3379
+ let i = 0
3380
+ while (i < chainA.length && i < chainB.length && chainA[i] === chainB[i]) i++
3381
+ const ancestor = chainA[i - 1]
3382
+ const branchA = chainA[i]
3383
+ const branchB = chainB[i]
3384
+ if (ancestor === undefined || branchA === undefined || branchB === undefined) return false
3385
+ const [first, second] =
3386
+ ancestor.type === 'ConditionalExpression'
3387
+ ? [ancestor.consequent, ancestor.alternate]
3388
+ : ancestor.type === 'LogicalExpression'
3389
+ ? [ancestor.left, ancestor.right]
3390
+ : [undefined, undefined]
3391
+ if (first === undefined || second === undefined) return false
3392
+ return (branchA === first && branchB === second) || (branchA === second && branchB === first)
3393
+ }
3394
+
3395
+ /**
3396
+ * `<BasaltOverlays notifications>` and `<BasaltNotifications />` in ONE file — F5, the second
3397
+ * `basalt-mantine.md` law with no guard, and the static half of a warning that already exists at
3398
+ * runtime (`src/commands/overlays-mount.tsx` warns in dev when both mount).
3399
+ *
3400
+ * A pair split across the two branches of one conditional is EXEMPT: `a ? <BasaltNotifications/> :
3401
+ * <BasaltOverlays/>` is a file with two mounts written and one mount rendered, and reporting it
3402
+ * taught a consumer to reach for a waiver over code that was already right.
3403
+ *
3404
+ * `notifications` DEFAULTS to true on `BasaltOverlays`, so the absence of the attribute counts as
3405
+ * enabled and only an explicit `notifications={false}` clears it — the same polarity the component
3406
+ * itself reads. Both tags are provenance-gated through {@link collectBasaltImportMap}.
3407
+ *
3408
+ * Same-FILE only, and deliberately so: the runtime warning is the lane that sees a cross-file
3409
+ * double mount, because it counts real mounts rather than JSX. This is the cheap half that catches
3410
+ * it before the app runs, which is the whole reason a static twin is worth having.
3411
+ */
3412
+ // Ships: warn (grace → 1.30.0)
3413
+ const duplicateNotificationsMount = {
3414
+ meta: {
3415
+ type: 'suggestion',
3416
+ docs: {
3417
+ description: 'Warn when BasaltOverlays and a standalone BasaltNotifications both mount.',
3418
+ },
3419
+ schema: [],
3420
+ },
3421
+ create(context) {
3422
+ if (isTestFile(context)) return {}
3423
+ const ownTree = isBasaltOwnSource(getFilename(context))
3424
+ const basalt = new Map()
3425
+ const mounts = []
3426
+
3427
+ return {
3428
+ ImportDeclaration(node) {
3429
+ collectBasaltImportMap(node, basalt, ownTree)
3430
+ },
3431
+ JSXOpeningElement(node) {
3432
+ const root = jsxRootName(node.name)
3433
+ if (root === undefined || !basalt.has(root)) return
3434
+ // `notifications` DEFAULTS to true, so only an explicit `={false}` clears the layer.
3435
+ const disabled = (node.attributes ?? []).some(
3436
+ (attr) =>
3437
+ attr.type === 'JSXAttribute' &&
3438
+ attr.name?.name === 'notifications' &&
3439
+ unwrapExpressionContainer(attr.value)?.value === false,
3440
+ )
3441
+ mounts.push({ node, root, disabled, chain: ancestorChain(node) })
3442
+ },
3443
+ 'Program:exit'() {
3444
+ const overlays = mounts.filter(
3445
+ ({ root, disabled }) => basalt.get(root) === 'BasaltOverlays' && !disabled,
3446
+ )
3447
+ if (overlays.length === 0) return
3448
+ for (const { node, root, chain } of mounts) {
3449
+ if (basalt.get(root) !== 'BasaltNotifications') continue
3450
+ // Every enabled overlays mount on the other branch of one conditional → nothing to report.
3451
+ if (overlays.every((overlay) => mutuallyExclusive(overlay.chain, chain))) continue
3452
+ if (hasThemeAllow(context, node, 'duplicate-notifications-mount')) continue
3453
+ context.report({ node, message: DUPLICATE_NOTIFICATIONS_MOUNT_MESSAGE })
3454
+ }
3455
+ },
3456
+ }
3457
+ },
3458
+ }
3459
+
3460
+ // ── Rule 27 — query-dual-import ─────────────────────────────────────────────────────────────────
3461
+
3462
+ /** `@tanstack/react-query` and its subpaths — never `@tanstack/react-query-devtools`. */
3463
+ const TANSTACK_QUERY_SOURCE = /^@tanstack\/react-query(?:\/|$)/
3464
+
3465
+ /** The `basalt-ui/query` subpath, exactly — the seam the hooks are supposed to arrive through. */
3466
+ const BASALT_QUERY_SOURCE = 'basalt-ui/query'
3467
+
3468
+ /** The root barrel — where `QueryState` and the dashboard composites live. */
3469
+ const BASALT_ROOT_SOURCE = 'basalt-ui'
3470
+
3471
+ const QUERY_DUAL_IMPORT_MESSAGE =
3472
+ 'Dual query import — this file imports from @tanstack/react-query AND from basalt-ui/query. ' +
3473
+ 'Take the hooks, the client factory and `unwrap` from basalt-ui/query only: two entry points to ' +
3474
+ 'one library is how a second QueryClient, a second devtools bundle and a duplicated ' +
3475
+ '`toErrorMessage` reach an app (agent/rules/basalt-batteries.md). ' +
3476
+ '(basalt/query-dual-import)'
3477
+
3478
+ const QUERY_ROOT_IMPORT_MESSAGE =
3479
+ 'Raw @tanstack/react-query import in a basalt file — the hooks come through basalt-ui/query, ' +
3480
+ 'which re-exports them beside `createBasaltQueryClient`, `unwrap`, `toErrorMessage` and the lazy ' +
3481
+ 'devtools (agent/rules/basalt-batteries.md). Import from basalt-ui/query instead. ' +
3482
+ '(basalt/query-dual-import)'
3483
+
3484
+ /**
3485
+ * The `basalt-batteries.md` law "import query hooks from `basalt-ui/query`, never dual-import
3486
+ * `@tanstack/react-query`" — F5, and one of the two `./query` surfaces that shipped advisory.
3487
+ *
3488
+ * TWO forms, one id, because they are one law read at two strengths. The DUAL form (both
3489
+ * specifiers present) is the law verbatim. The softer form — a raw `@tanstack/react-query` import
3490
+ * in a file that also imports the basalt root barrel — is a heuristic: it says "this file is
3491
+ * already a basalt file, so the query seam is right there", and it is the form that actually
3492
+ * catches the drift, since a file that has never heard of `basalt-ui/query` cannot dual-import.
3493
+ * The softer form is why the whole rule lands at `warn` rather than `error`.
3494
+ *
3495
+ * Gated on the LITERAL specifier, never on `ownTree` relative resolution: basalt's own
3496
+ * `src/query/**` and `src/dashboard/query-state.tsx` import `@tanstack/react-query` because they
3497
+ * ARE the seam, and they reach their siblings relatively. A file that writes `basalt-ui/query` is
3498
+ * by definition a consumer of the published surface.
3499
+ */
3500
+ // Ships: warn (grace → 1.30.0)
3501
+ const queryDualImport = {
3502
+ meta: {
3503
+ type: 'suggestion',
3504
+ docs: { description: 'Warn on a raw @tanstack/react-query import beside a basalt import.' },
3505
+ schema: [],
3506
+ },
3507
+ create(context) {
3508
+ if (isTestFile(context)) return {}
3509
+ const raw = []
3510
+ let basaltQuery = false
3511
+ let basaltRoot = false
3512
+
3513
+ return {
3514
+ ImportDeclaration(node) {
3515
+ const source = node.source?.value
3516
+ if (typeof source !== 'string') return
3517
+ if (source === BASALT_QUERY_SOURCE) basaltQuery = true
3518
+ else if (source === BASALT_ROOT_SOURCE) basaltRoot = true
3519
+ // `import type { UseQueryResult } from '@tanstack/react-query'` is erased at compile time
3520
+ // — there is no runtime import to route through the seam, and typing against the library's
3521
+ // own result type is exactly what `QueryStateLike` documents as legitimate. Same skip
3522
+ // `agent-no-raw-usechat` and `ai-sdk-major` take.
3523
+ else if (TANSTACK_QUERY_SOURCE.test(source) && node.importKind !== 'type') raw.push(node)
3524
+ },
3525
+ 'Program:exit'() {
3526
+ if (!basaltQuery && !basaltRoot) return
3527
+ const message = basaltQuery ? QUERY_DUAL_IMPORT_MESSAGE : QUERY_ROOT_IMPORT_MESSAGE
3528
+ for (const node of raw) {
3529
+ if (hasThemeAllow(context, node, 'query-dual-import')) continue
3530
+ context.report({ node, message })
3531
+ }
3532
+ },
3533
+ }
3534
+ },
3535
+ }
3536
+
3537
+ // ── Rule 28 — query-fn-unwrap ───────────────────────────────────────────────────────────────────
3538
+
3539
+ const QUERY_FN_UNWRAP_MESSAGE =
3540
+ 'queryFn fetches without unwrap() — `unwrap` throws on the error branch AND on a null data with ' +
3541
+ 'no error (a 204, a silent transport failure), and both are caller bugs that have to surface as ' +
3542
+ 'a rejected query rather than as `data: null` rendered through the success branch ' +
3543
+ '(agent/rules/basalt-batteries.md). Wrap the call: `queryFn: () => unwrap(client.thing.get())`. ' +
3544
+ '(basalt/query-fn-unwrap)'
3545
+
3546
+ /** A bare `fetch(` / `unwrap(` call, as written — the two halves of this rule's text heuristic. */
3547
+ const FETCH_CALL = /\bfetch\s*\(/
3548
+ const UNWRAP_CALL = /\bunwrap\s*\(/
3549
+
3550
+ /**
3551
+ * A `queryFn` that fetches and never `unwrap`s it — F5, the second unguarded `./query` law.
3552
+ *
3553
+ * **The heuristic is deliberately one sentence long, and it is a TEXT test inside a SYNTACTIC
3554
+ * range**: the rule takes the source span of the `queryFn` property's own function value and fires
3555
+ * only when `fetch(` appears in it and `unwrap(` does not. Nothing resolves, nothing follows a
3556
+ * helper out of the span.
3557
+ *
3558
+ * What that buys and what it costs, both stated so neither is a surprise. It sees the two shapes
3559
+ * the doctrine is actually written against — `queryFn: () => fetch(url)` and
3560
+ * `queryFn: async () => (await fetch(url)).json()`. It does NOT see a `queryFn: fetchThing`
3561
+ * reference, an Eden/Treaty call (`client.thing.get()` contains no `fetch(`), or an `unwrap`
3562
+ * applied one frame out; and it counts an `unwrap(` anywhere in the span, including one applied to
3563
+ * something else. A stricter version would need the call graph. The narrow version fires on the
3564
+ * form a consumer writes by reflex on day one, which is the one the law exists for.
3565
+ *
3566
+ * Scoped to files importing `basalt-ui/query` literally, for the reason {@link queryDualImport}
3567
+ * gives: without that gate it is an opinion about `fetch`, which basalt does not have.
3568
+ */
3569
+ // Ships: warn (grace → 1.30.0)
3570
+ const queryFnUnwrap = {
3571
+ meta: {
3572
+ type: 'suggestion',
3573
+ docs: { description: 'Warn on a queryFn that fetches without wrapping the call in unwrap().' },
3574
+ schema: [],
3575
+ },
3576
+ create(context) {
3577
+ if (isTestFile(context)) return {}
3578
+ const candidates = []
3579
+ let basaltQuery = false
3580
+
3581
+ return {
3582
+ ImportDeclaration(node) {
3583
+ if (node.source?.value === BASALT_QUERY_SOURCE) basaltQuery = true
3584
+ },
3585
+ Property(node) {
3586
+ if (propertyKeyName(node) !== 'queryFn') return
3587
+ const value = node.value
3588
+ if (value?.type !== 'ArrowFunctionExpression' && value?.type !== 'FunctionExpression') {
3589
+ return
3590
+ }
3591
+ candidates.push({ node, value })
3592
+ },
3593
+ 'Program:exit'() {
3594
+ if (!basaltQuery) return
3595
+ const text = context.sourceCode?.text ?? ''
3596
+ for (const { node, value } of candidates) {
3597
+ const [start, end] = value.range ?? []
3598
+ if (typeof start !== 'number' || typeof end !== 'number') continue
3599
+ const body = text.slice(start, end)
3600
+ if (!FETCH_CALL.test(body) || UNWRAP_CALL.test(body)) continue
3601
+ if (hasThemeAllow(context, node, 'query-fn-unwrap')) continue
3602
+ context.report({ node, message: QUERY_FN_UNWRAP_MESSAGE })
3603
+ }
3604
+ },
3605
+ }
3606
+ },
3607
+ }
3608
+
3609
+ // ── Rule 29 — deprecated-export ─────────────────────────────────────────────────────────────────
3610
+
3611
+ /**
3612
+ * @typedef {{
3613
+ * subpath: string
3614
+ * name: string
3615
+ * prop?: string
3616
+ * replacement: string
3617
+ * removeIn: string
3618
+ * fix?: false
3619
+ * }} DeprecatedExport
3620
+ */
3621
+
3622
+ /**
3623
+ * The deprecation ledger — every export that is `@deprecated` but still SHIPPED, with what to
3624
+ * write instead and the minor it goes away in. B4: majors are banned here, so a rename cannot be
3625
+ * signalled by a version number; the lifecycle is instead "ship the alias, lint the old name, write
3626
+ * the MIGRATING row, remove it a minor later" (`../CLAUDE.md`, "Deprecation lifecycle").
3627
+ *
3628
+ * A row without `prop` is a NAMED IMPORT from `subpath` — reported on the specifier, with an
3629
+ * autofix that rewrites it to `{ <replacement> as <local> }` so no call site has to move in the
3630
+ * same edit. **That autofix is the row's contract**: only a DROP-IN rename belongs here. A
3631
+ * deprecation whose replacement takes a different shape (`createSearchParamStore` →
3632
+ * `createSearchStore` + a `fields` object) would need the fix suppressed before it could be a row,
3633
+ * and until then it lives in `MIGRATING.md` alone. A row WITH `prop` is a JSX ATTRIBUTE on the component `name`, imported from `subpath`
3634
+ * — no autofix, because the three connectivity props collapse into ONE object prop and a
3635
+ * per-attribute rewrite would produce three `connectivity=` attributes on one tag.
3636
+ *
3637
+ * **Adding a row is the whole job.** Deprecate the export in its JSDoc, add the row here with a
3638
+ * `removeIn` at least one minor out, add the `MIGRATING.md` row, and this rule starts nudging.
3639
+ * `oxlint-plugin.test.ts` scans every `src/**\/index.ts(x)` barrel for a JSDoc `@deprecated` and
3640
+ * fails when one has no row here, so the barrel half cannot be forgotten. Removing the export is a
3641
+ * separate commit that deletes the row in the same change.
3642
+ *
3643
+ * @type {readonly DeprecatedExport[]}
3644
+ */
3645
+ export const DEPRECATED_EXPORTS = [
3646
+ {
3647
+ subpath: 'basalt-ui/forms',
3648
+ name: 'field',
3649
+ replacement: 'inputProps',
3650
+ removeIn: '1.29.0',
3651
+ // Not a drop-in: `inputProps` no longer returns `key`, so a rename alone would silently drop
3652
+ // the reconciler key. The nudge stays; the rewrite is `basalt/forms-field-key`'s job.
3653
+ fix: false,
3654
+ },
3655
+ {
3656
+ subpath: 'basalt-ui',
3657
+ name: 'BasaltProvider',
3658
+ prop: 'sseUrl',
3659
+ replacement: 'connectivity={{ sseUrl }}',
3660
+ removeIn: '1.29.0',
3661
+ },
3662
+ {
3663
+ subpath: 'basalt-ui',
3664
+ name: 'BasaltProvider',
3665
+ prop: 'healthUrl',
3666
+ replacement: 'connectivity={{ healthUrl }}',
3667
+ removeIn: '1.29.0',
3668
+ },
3669
+ {
3670
+ subpath: 'basalt-ui',
3671
+ name: 'BasaltProvider',
3672
+ prop: 'healthIntervalMs',
3673
+ replacement: 'connectivity={{ healthIntervalMs }}',
3674
+ removeIn: '1.29.0',
3675
+ },
3676
+ ]
3677
+
3678
+ function deprecatedExportMessage(row) {
3679
+ const what =
3680
+ row.prop === undefined
3681
+ ? `\`${row.name}\` from ${row.subpath}`
3682
+ : `\`${row.prop}\` on <${row.name}>`
3683
+ return (
3684
+ `Deprecated basalt export — ${what} is superseded by \`${row.replacement}\` and is removed in ` +
3685
+ `${row.removeIn}. It still resolves today, so nothing is broken yet; migrate before that minor ` +
3686
+ '(packages/basalt-ui/MIGRATING.md carries the row). ' +
3687
+ '(basalt/deprecated-export)'
3688
+ )
3689
+ }
3690
+
3691
+ /**
3692
+ * A shipped-but-`@deprecated` export still being imported or written — B4, the sunset half of the
3693
+ * lifecycle whose absence the blueprint audit found (§8): the grace mechanism covers a rule getting
3694
+ * STRICTER, and nothing covered an export going away.
3695
+ *
3696
+ * Permanently `warn` ({@link PLUGIN_RULE_ADVISORY}), for the same reason `shadow-basalt-export` is:
3697
+ * a deprecation is a schedule, not a defect. The code compiles and runs; promoting this to `error`
3698
+ * would break a consumer's build for something that still works, which is the one thing the
3699
+ * no-majors doctrine exists to avoid. The `removeIn` minor is where it becomes an error — by the
3700
+ * export ceasing to exist.
3701
+ */
3702
+ // Ships: warn (advisory)
3703
+ const deprecatedExport = {
3704
+ meta: {
3705
+ type: 'suggestion',
3706
+ docs: { description: 'Warn on an import or prop that basalt has deprecated but still ships.' },
3707
+ schema: [],
3708
+ fixable: 'code',
3709
+ },
3710
+ create(context) {
3711
+ if (isTestFile(context)) return {}
3712
+ if (isBasaltOwnSource(getFilename(context))) return {}
3713
+ const importRows = DEPRECATED_EXPORTS.filter((row) => row.prop === undefined)
3714
+ const propRows = DEPRECATED_EXPORTS.filter((row) => row.prop !== undefined)
3715
+ const components = new Map()
3716
+ const candidates = []
3717
+
3718
+ return {
3719
+ ImportDeclaration(node) {
3720
+ const source = node.source?.value
3721
+ if (typeof source !== 'string') return
3722
+ for (const spec of node.specifiers ?? []) {
3723
+ const imported = spec.imported?.name ?? spec.imported?.value
3724
+ const local = spec.local?.name
3725
+ if (typeof imported !== 'string' || typeof local !== 'string') continue
3726
+ for (const row of propRows) {
3727
+ if (source === row.subpath && imported === row.name) components.set(local, row.name)
3728
+ }
3729
+ const row = importRows.find((r) => r.subpath === source && r.name === imported)
3730
+ if (row === undefined) continue
3731
+ if (hasThemeAllow(context, spec, 'deprecated-export')) continue
3732
+ context.report({
3733
+ node: spec,
3734
+ message: deprecatedExportMessage(row),
3735
+ ...(row.fix === false
3736
+ ? {}
3737
+ : { fix: (fixer) => fixer.replaceText(spec, `${row.replacement} as ${local}`) }),
3738
+ })
3739
+ }
3740
+ },
3741
+ JSXAttribute(node) {
3742
+ const name = node.name?.name
3743
+ if (typeof name !== 'string') return
3744
+ const owner = node.parent
3745
+ if (owner === null || owner === undefined || owner.type !== 'JSXOpeningElement') return
3746
+ const root = jsxRootName(owner.name)
3747
+ if (root === undefined) return
3748
+ candidates.push({ node, root, name })
3749
+ },
3750
+ 'Program:exit'() {
3751
+ for (const { node, root, name } of candidates) {
3752
+ const component = components.get(root)
3753
+ if (component === undefined) continue
3754
+ const row = propRows.find((r) => r.name === component && r.prop === name)
3755
+ if (row === undefined) continue
3756
+ if (hasThemeAllow(context, node, 'deprecated-export')) continue
3757
+ context.report({ node, message: deprecatedExportMessage(row) })
3758
+ }
3759
+ },
3760
+ }
3761
+ },
3762
+ }
3763
+
3764
+ // ── Rule 30 — forms-field-key ───────────────────────────────────────────────────────────────────
3765
+
3766
+ /** The subpath the forms idiom is imported from — the only provenance this rule accepts. */
3767
+ const FORMS_IMPORT_SOURCE = 'basalt-ui/forms'
3768
+
3769
+ /** The three names this rule resolves through the local→imported map. */
3770
+ const FORMS_SPREAD_NAMES = new Set(['inputProps', 'field'])
3771
+
3772
+ const FORMS_FIELD_KEY_MESSAGES = {
3773
+ missing:
3774
+ 'A spread `inputProps(…)` with no sibling `key`. The field loses its reconciler key, so an ' +
3775
+ 'uncontrolled input keeps its old text through `form.reset()`, `form.setValues()` and a ' +
3776
+ 'removed list row — nothing throws and nothing type-checks differently. Write ' +
3777
+ '`key={fieldKey(form, path)} {...inputProps(form, path)}`: `inputProps` stopped returning ' +
3778
+ '`key` because a spread `key` is a React 19 warning, and a call site that merely kept the ' +
3779
+ 'spread compiles unchanged and silently stops resetting. ' +
3780
+ '(basalt/forms-field-key)',
3781
+ deprecatedAlias:
3782
+ 'A spread `field(…)`. That is the @deprecated 1.27 alias and it still bundles `key` INTO the ' +
3783
+ 'returned object, so it still logs React 19\'s `A props object containing a "key" prop is ' +
3784
+ 'being spread into JSX` on every render — kept byte-identical on purpose, because a ' +
3785
+ 'deprecation is a schedule and not a silent behaviour change. Migrate to ' +
3786
+ '`key={fieldKey(form, path)} {...inputProps(form, path)}` (packages/basalt-ui/MIGRATING.md ' +
3787
+ 'carries the row); the alias itself is removed in 1.29.0. ' +
3788
+ '(basalt/forms-field-key)',
3789
+ }
3790
+
3791
+ /** The `key` attribute among a JSXOpeningElement's attributes, if it has one written literally. */
3792
+ function hasKeyAttribute(opening) {
3793
+ for (const attr of opening?.attributes ?? []) {
3794
+ if (attr?.type === 'JSXAttribute' && attr.name?.name === 'key') return true
3795
+ }
3796
+ return false
3797
+ }
3798
+
3799
+ /**
3800
+ * A field spread that lost its reconciler key — the guard for the one change in the 1.28.0 forms
3801
+ * minor that a consumer's compiler cannot see.
3802
+ *
3803
+ * `inputProps` used to return `getInputProps(path)` AND `key` in one object, so
3804
+ * `<TextInput {...inputProps(form, 'x')} />` covered both. It no longer returns `key` (a spread
3805
+ * `key` is a React 19 warning), and that exact call site still type-checks, still renders and still
3806
+ * validates — it just stops resetting. There is no compile error, no throw and no console line to
3807
+ * find it by, which is precisely the profile a lint rule exists for.
3808
+ *
3809
+ * TWO forms under this one id, because they are one law read from either end:
3810
+ *
3811
+ * - `{...inputProps(…)}` with no sibling `key` — the post-migration mistake. Autofixed: the `key`
3812
+ * attribute is inserted with the SAME arguments, and `fieldKey` is added to the existing
3813
+ * `basalt-ui/forms` import when it is not already there, so `--fix` produces working code rather
3814
+ * than an unresolved identifier.
3815
+ * - `{...field(…)}` — the pre-migration call site. Reported ALWAYS, key or no key, and deliberately
3816
+ * NOT autofixed: `field` still returns the bundle, so inserting a second `key` would be wrong,
3817
+ * and the honest remedy is the rename `basalt/deprecated-export` already nudges. This arm is a
3818
+ * pointer at the migration, one lane over from that rule's import-level nudge.
3819
+ *
3820
+ * Provenance is the `basalt-ui/forms` specifier (plus a relative import inside basalt's own `src/`,
3821
+ * or the dogfood surface goes silent — the same gate `collectBasaltImports` uses), read through the
3822
+ * local→imported map so `import { inputProps as f }` is covered. `inputProps` is an ordinary enough
3823
+ * name that firing on an unimported one would be an opinion about a consumer's own helper.
3824
+ */
3825
+ // Ships: warn (grace → 1.30.0)
3826
+ const formsFieldKey = {
3827
+ meta: {
3828
+ type: 'suggestion',
3829
+ docs: { description: 'Warn on a spread field object that lost (or still bundles) its key.' },
3830
+ schema: [],
3831
+ fixable: 'code',
3832
+ },
3833
+ create(context) {
3834
+ if (isTestFile(context)) return {}
3835
+ const ownTree = isBasaltOwnSource(getFilename(context))
3836
+ /** local name → the name it was imported as, for the three forms names. */
3837
+ const imported = new Map()
3838
+ /** The forms ImportDeclaration the `fieldKey` specifier is added to, if one exists. */
3839
+ let formsImport
3840
+ const candidates = []
3841
+
3842
+ return {
3843
+ ImportDeclaration(node) {
3844
+ const source = node.source?.value
3845
+ if (typeof source !== 'string') return
3846
+ if (source !== FORMS_IMPORT_SOURCE && !(ownTree && RELATIVE_IMPORT_SOURCE.test(source))) {
3847
+ return
3848
+ }
3849
+ for (const spec of node.specifiers ?? []) {
3850
+ const local = spec.local?.name
3851
+ const name = spec.imported?.name ?? spec.imported?.value
3852
+ if (typeof local !== 'string' || typeof name !== 'string') continue
3853
+ if (name === 'fieldKey' || FORMS_SPREAD_NAMES.has(name)) {
3854
+ imported.set(local, name)
3855
+ if (source === FORMS_IMPORT_SOURCE) formsImport = node
3856
+ }
3857
+ }
3858
+ },
3859
+ JSXSpreadAttribute(node) {
3860
+ const call = node.argument
3861
+ if (call?.type !== 'CallExpression' || call.callee?.type !== 'Identifier') return
3862
+ const opening = node.parent
3863
+ if (opening === null || opening === undefined || opening.type !== 'JSXOpeningElement')
3864
+ return
3865
+ candidates.push({ node, call, local: call.callee.name, opening })
3866
+ },
3867
+ 'Program:exit'() {
3868
+ const text = context.sourceCode?.text ?? ''
3869
+ const hasFieldKey = [...imported.values()].includes('fieldKey')
3870
+ let importFixed = false
3871
+
3872
+ for (const { node, call, local, opening } of candidates) {
3873
+ const name = imported.get(local)
3874
+ if (name === undefined) continue
3875
+ if (hasThemeAllow(context, node, 'forms-field-key')) continue
3876
+
3877
+ if (name === 'field') {
3878
+ context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.deprecatedAlias })
3879
+ continue
3880
+ }
3881
+ if (hasKeyAttribute(opening)) continue
3882
+
3883
+ const args = call.arguments ?? []
3884
+ const first = args[0]?.range
3885
+ const last = args[args.length - 1]?.range
3886
+ // No arguments to copy means no honest `fieldKey(…)` to write — report without a fix
3887
+ // rather than emit one that does not compile.
3888
+ if (first === undefined || last === undefined) {
3889
+ context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.missing })
3890
+ continue
3891
+ }
3892
+ const argsText = text.slice(first[0], last[1])
3893
+ // The import specifier is added by the FIRST fixed occurrence only: three fields in one
3894
+ // file would otherwise emit three overlapping edits to one import, and an overlapping
3895
+ // fix is dropped rather than merged.
3896
+ const needsImport = !hasFieldKey && !importFixed && formsImport !== undefined
3897
+ const lastSpec = needsImport
3898
+ ? formsImport.specifiers?.[(formsImport.specifiers?.length ?? 0) - 1]
3899
+ : undefined
3900
+ if (needsImport && lastSpec !== undefined) importFixed = true
3901
+
3902
+ context.report({
3903
+ node,
3904
+ message: FORMS_FIELD_KEY_MESSAGES.missing,
3905
+ fix: (fixer) => {
3906
+ const fixes = [fixer.insertTextBefore(node, `key={fieldKey(${argsText})} `)]
3907
+ if (lastSpec !== undefined) fixes.push(fixer.insertTextAfter(lastSpec, ', fieldKey'))
3908
+ return fixes
3909
+ },
3910
+ })
3911
+ }
3912
+ },
3913
+ }
3914
+ },
3915
+ }
3916
+
2759
3917
  // ── Grace ledger ────────────────────────────────────────────────────────────────────────────────
2760
3918
 
2761
3919
  /**
@@ -2789,58 +3947,96 @@ const useSearchFromLiteral = {
2789
3947
  export const PLUGIN_RULE_GRACE = {
2790
3948
  'control-outside-home': {
2791
3949
  since: '1.26.0',
2792
- promote: '1.27.0',
3950
+ promote: '1.30.0',
2793
3951
  why:
2794
3952
  'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
2795
3953
  'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
2796
- 'is carried by three exemptions (overlay/settings-row ancestor, @mantine/form, owner ' +
2797
- 'definition) rather than by certainty. Promotion is additionally gated on running the ' +
2798
- 'shipped preset over argo, linewatch, image-share, rb, image-gen and the playground with ≤3 ' +
2799
- 'total waivers (§9 wave 7).',
3954
+ 'is carried by four exemptions (overlay/settings-row ancestor, the overlay FILENAME ' +
3955
+ 'convention below, @mantine/form, owner definition) rather than by certainty. The other five ' +
3956
+ 'wave-6 rules promoted at 1.27.0; this one did not, and the reason is measured rather than ' +
3957
+ 'cautious: the wave-7 run found 9 remaining warns in argo, every one of them a control in a ' +
3958
+ 'modal/form module whose `<Modal>` is rendered by the PARENT — the cross-file case law C1 is ' +
3959
+ 'explicitly advisory about, which no ancestry walk inside one file can ever see. ' +
3960
+ '{@link isOverlayConventionFile} exempts that declared naming convention, in BOTH the kebab ' +
3961
+ '(`edit-session-modal.tsx`) and the PascalCase (`CbbiPanel.tsx`) dialect. Re-dated ' +
3962
+ '2026-08-28 — the argo wave-7 migration has not run; the PascalCase overlay convention (this ' +
3963
+ 'change) is expected to clear most of the 9. 1.30.0 is when the remainder is re-measured. A ' +
3964
+ "file outside the convention that is still an overlay's own body declares it with " +
3965
+ '`theme-allow-file control-outside-home — overlay`.',
2800
3966
  },
2801
- 'control-size-literal': {
2802
- since: '1.26.0',
2803
- promote: '1.27.0',
3967
+ 'bound-control-outside-home': {
3968
+ since: '1.28.0',
3969
+ promote: '1.30.0',
2804
3970
  why:
2805
- 'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). Rejects a prop that was correct one ' +
2806
- 'minor ago — every `size="xs"` written into a bar before the tier existed — so it takes the ' +
2807
- 'grace minor the doctrine exists for. Every wave ships in ONE minor (1.26.0), so the whole ' +
2808
- 'set promotes together in 1.27.0 — one runway, not one per wave.',
3971
+ 'ASIDE-SPEC G5 — a bound control in a section body rendered as a stray pill and nothing saw ' +
3972
+ 'it. `control-outside-home` matches raw Mantine tags only, so the whole basalt half of law ' +
3973
+ 'C1 was unguarded: `<SelectFilter field={…}/>` written into a `Section` body or a bare page ' +
3974
+ 'stack passed every lane. A NEW id rather than a widening (C16 — a level is per-id), landing ' +
3975
+ 'warn because it inherits the sibling rule\'s heuristic: "this control has no home" is a ' +
3976
+ 'claim about layout intent, and the ancestry it reads now spans four subtree homes ' +
3977
+ '(FilterSet / PageAside / PanelRow / the overlay hosts) that only shipped in 1.27.0. 1.30.0 ' +
3978
+ 'is when the incumbent count across argo, linewatch, image-share, rb, image-gen and the ' +
3979
+ 'playground is re-measured.',
2809
3980
  },
2810
- 'in-body-page-title': {
2811
- since: '1.26.0',
2812
- promote: '1.27.0',
3981
+ 'provider-above-router': {
3982
+ since: '1.28.0',
3983
+ promote: '1.30.0',
2813
3984
  why:
2814
- 'the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C8). Its text-level twin is the ' +
2815
- 'guard kind of the SAME id, which carries the same dates in GRACE_PERIOD_KINDS — one law, ' +
2816
- 'two lanes, one promotion. Warn because an in-body h1/h2 is a judgement about which name ' +
2817
- 'the page already has, and the fix is a route/breadcrumb change rather than a prop edit.',
3985
+ 'F5 — one of the three basalt-mantine.md laws that shipped under a `not guarded: —` banner. ' +
3986
+ 'It reads STATIC JSX ancestry inside one file, which is a genuine fraction of the law: the ' +
3987
+ 'usual real shape composes the two providers across modules, where no oxlint rule can see ' +
3988
+ 'the nesting at all. Landing it at `error` would state a certainty the implementation does ' +
3989
+ 'not have. 1.30.0 is when the incumbent count across argo, linewatch, image-share, rb, ' +
3990
+ 'image-gen and the playground is measured — every one of them mounts a router, so the ' +
3991
+ 'false-positive question has a real sample.',
2818
3992
  },
2819
- 'responsive-twin': {
2820
- since: '1.26.0',
2821
- promote: '1.27.0',
3993
+ 'duplicate-notifications-mount': {
3994
+ since: '1.28.0',
3995
+ promote: '1.30.0',
2822
3996
  why:
2823
- 'the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C9 — linewatch shipped three ' +
2824
- 'doubled controls). Warn for one minor because the fix is a real edit (delete one mount, ' +
2825
- 'move the swap into the control), not a prop removal, and the pair test is a heuristic over ' +
2826
- 'sibling structure.',
3997
+ 'F5 — the static half of a double-mount the runtime already warns about in dev ' +
3998
+ '(`src/commands/overlays-mount.tsx`). Same-file only, so like its sibling it guards a ' +
3999
+ 'fraction of the law and cannot claim `error`. It also reads a prop DEFAULT (`notifications` ' +
4000
+ 'is true when unwritten), which is the kind of polarity a consumer disagrees with exactly ' +
4001
+ 'once before it is worth knowing about — a minor at `warn` is how that disagreement arrives ' +
4002
+ 'as feedback rather than as a broken build.',
2827
4003
  },
2828
- 'search-literal-link': {
2829
- since: '1.26.0',
2830
- promote: '1.27.0',
4004
+ 'query-dual-import': {
4005
+ since: '1.28.0',
4006
+ promote: '1.30.0',
2831
4007
  why:
2832
- 'the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C10 — argo nav.tsx:132 pinned the ' +
2833
- 'fallback on every click, which is why its reader had zero call sites). Warn for one minor ' +
2834
- 'because the fix needs a store to exist at the call site, which is a wave-2 dependency for ' +
2835
- 'consumers still on the old enum-only pair.',
4008
+ 'F5 — the basalt-batteries.md law with no guard. TWO forms under one id: the dual import ' +
4009
+ '(both `@tanstack/react-query` and `basalt-ui/query` in one file) is the law verbatim, and ' +
4010
+ 'the softer form — a raw `@tanstack/react-query` import in a file that also imports the ' +
4011
+ 'basalt root barrel — is the heuristic that actually catches the drift, since a file that ' +
4012
+ 'never names `basalt-ui/query` cannot dual-import. The soft form is what holds the whole ' +
4013
+ 'rule at `warn`: "this file is already a basalt file" is a claim about intent. 1.30.0 is ' +
4014
+ 'when the consumer count is measured and the soft form is either kept or split off.',
2836
4015
  },
2837
- 'use-search-from-literal': {
2838
- since: '1.26.0',
2839
- promote: '1.27.0',
4016
+ 'forms-field-key': {
4017
+ since: '1.28.0',
4018
+ promote: '1.30.0',
4019
+ why:
4020
+ "the 1.28.0 forms minor took `key` out of `inputProps`' return, and the OLD call site " +
4021
+ "(`<TextInput {...inputProps(form, 'x')} />`) survives that change with no compile error, " +
4022
+ 'no throw and no console line — it just stops resetting. This rule is the only thing that ' +
4023
+ 'reports it, which is an argument for `error`, not `warn`; it lands at `warn` anyway ' +
4024
+ 'because it fires on code every consumer is CURRENTLY shipping, and the grace minor is what ' +
4025
+ 'turns "your build is broken by the upgrade" into "your editor lists the eleven lines". ' +
4026
+ 'The `field(` arm is the same law read from the pre-migration end, and that alias is removed ' +
4027
+ 'in 1.29.0 anyway. 1.30.0 is when argo, linewatch, image-share, rb, image-gen and the ' +
4028
+ 'playground have all been through the autofix and the remainder is measured.',
4029
+ },
4030
+ 'query-fn-unwrap': {
4031
+ since: '1.28.0',
4032
+ promote: '1.30.0',
2840
4033
  why:
2841
- 'the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C10). Same runway as ' +
2842
- 'search-literal-link and for the same reason: the remedy is `createSearchStore` + a prop, ' +
2843
- 'so a consumer needs the wave-2 store before the finding is actionable.',
4034
+ 'F5 — the second unguarded ./query law. Openly a TEXT heuristic inside a syntactic range: ' +
4035
+ '`fetch(` present in the queryFn span and `unwrap(` absent. It cannot see a `queryFn: ' +
4036
+ 'fetchThing` reference, an Eden/Treaty call, or an unwrap one frame out, and it counts an ' +
4037
+ '`unwrap(` applied to anything. That is a deliberate floor rather than a first draft, and a ' +
4038
+ 'rule whose stated reach is this narrow has no business failing a build. 1.30.0 is when the ' +
4039
+ 'measured false-positive load decides whether it promotes or stays advisory.',
2844
4040
  },
2845
4041
  }
2846
4042
 
@@ -2865,6 +4061,16 @@ export const PLUGIN_RULE_ADVISORY = {
2865
4061
  'strong evidence and not proof, and renaming defeats it outright, so this stays a permanent ' +
2866
4062
  'warn rather than a grace entry with no honest promote date.',
2867
4063
  },
4064
+ 'deprecated-export': {
4065
+ since: '1.28.0',
4066
+ why:
4067
+ 'B4, the sunset half of the deprecation lifecycle (`../CLAUDE.md`). A deprecation is a ' +
4068
+ 'SCHEDULE, not a defect: the code compiles, runs and is correct until the `removeIn` minor, ' +
4069
+ 'so promoting this to `error` would fail a consumer build over something that still works — ' +
4070
+ 'the one outcome the no-majors doctrine exists to avoid. There is no version at which that ' +
4071
+ 'stops being true, which is what makes it advisory rather than a grace entry: enforcement ' +
4072
+ 'arrives by the export ceasing to exist, not by this rule changing level.',
4073
+ },
2868
4074
  }
2869
4075
 
2870
4076
  // ── Plugin export ───────────────────────────────────────────────────────────────────────────────
@@ -2883,12 +4089,19 @@ export default {
2883
4089
  'raw-scroll-container': rawScrollContainer,
2884
4090
  'hand-rolled-filter': handRolledFilter,
2885
4091
  'control-outside-home': controlOutsideHome,
4092
+ 'bound-control-outside-home': boundControlOutsideHome,
2886
4093
  'control-size-literal': controlSizeLiteral,
2887
4094
  'page-bar-budget': pageBarBudget,
2888
4095
  'in-body-page-title': inBodyPageTitle,
2889
4096
  'responsive-twin': responsiveTwin,
2890
4097
  'search-literal-link': searchLiteralLink,
2891
4098
  'use-search-from-literal': useSearchFromLiteral,
4099
+ 'provider-above-router': providerAboveRouter,
4100
+ 'duplicate-notifications-mount': duplicateNotificationsMount,
4101
+ 'query-dual-import': queryDualImport,
4102
+ 'query-fn-unwrap': queryFnUnwrap,
4103
+ 'deprecated-export': deprecatedExport,
4104
+ 'forms-field-key': formsFieldKey,
2892
4105
  'visx-boundary': visxBoundary,
2893
4106
  'visx-tooltip': visxTooltip,
2894
4107
  'token-layer-boundary': tokenLayerBoundary,