basalt-ui 1.30.2 → 1.31.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 (334) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +31 -0
  3. package/MIGRATING.md +172 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +1 -1
  6. package/configs/oxlint-plugin.js +254 -4
  7. package/configs/oxlint-plugin.test.ts +145 -2
  8. package/configs/oxlint.json +2 -1
  9. package/dist/agent-chat/composer.d.ts.map +1 -1
  10. package/dist/agent-chat/composer.js +41 -43
  11. package/dist/agent-chat/thread-workspace.d.ts +2 -2
  12. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  13. package/dist/agent-chat/thread-workspace.js +3 -4
  14. package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
  15. package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
  16. package/dist/agent-chat/use-keyboard-inset.js +34 -0
  17. package/dist/charts/cursor/touch-pin.d.ts +45 -0
  18. package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
  19. package/dist/charts/cursor/touch-pin.js +79 -0
  20. package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
  21. package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
  22. package/dist/charts/hooks/use-discrete-cursor.js +198 -0
  23. package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
  24. package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
  25. package/dist/charts/hooks/use-hysteresis.js +12 -0
  26. package/dist/charts/hooks/useBandPlot.d.ts +7 -0
  27. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  28. package/dist/charts/hooks/useBandPlot.js +62 -18
  29. package/dist/charts/hooks/useChartCursor.d.ts +40 -11
  30. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  31. package/dist/charts/hooks/useChartCursor.js +102 -11
  32. package/dist/charts/index.d.ts +0 -1
  33. package/dist/charts/index.d.ts.map +1 -1
  34. package/dist/charts/index.js +0 -10
  35. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  36. package/dist/charts/kinds/BandStrip.js +54 -41
  37. package/dist/charts/kinds/Donut.d.ts +9 -3
  38. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  39. package/dist/charts/kinds/Donut.js +110 -48
  40. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  41. package/dist/charts/kinds/DualPanel.js +145 -131
  42. package/dist/charts/kinds/Heatmap.d.ts +7 -0
  43. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  44. package/dist/charts/kinds/Heatmap.js +37 -16
  45. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  46. package/dist/charts/kinds/MirroredBars.js +83 -68
  47. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  48. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  49. package/dist/charts/kinds/donut-layout.js +52 -0
  50. package/dist/charts/layout/auto-margin.d.ts +7 -2
  51. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  52. package/dist/charts/layout/auto-margin.js +12 -3
  53. package/dist/charts/layout/end-labels.d.ts +64 -0
  54. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  55. package/dist/charts/layout/end-labels.js +76 -0
  56. package/dist/charts/primitives/Axes.d.ts +32 -4
  57. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  58. package/dist/charts/primitives/Axes.js +62 -16
  59. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  60. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  61. package/dist/charts/primitives/CartesianChart.js +271 -125
  62. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  63. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  64. package/dist/charts/primitives/ChartCard.js +74 -21
  65. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  66. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartFrame.js +133 -34
  68. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  69. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartLegend.js +212 -71
  71. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  72. package/dist/charts/primitives/ChartTooltip.js +4 -3
  73. package/dist/charts/primitives/Crosshair.js +2 -2
  74. package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
  75. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  76. package/dist/charts/primitives/HoverOverlay.js +37 -26
  77. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  78. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  79. package/dist/charts/primitives/chart-card-context.d.ts +29 -0
  80. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  81. package/dist/charts/primitives/chart-card-context.js +13 -0
  82. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  83. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  84. package/dist/charts/primitives/chart-frame-layout.js +76 -29
  85. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  86. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  87. package/dist/charts/primitives/chart-layout.js +175 -0
  88. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  89. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  90. package/dist/charts/primitives/chart-tier.js +42 -13
  91. package/dist/charts/series.d.ts +4 -1
  92. package/dist/charts/series.d.ts.map +1 -1
  93. package/dist/charts/utils/ticks.d.ts +27 -8
  94. package/dist/charts/utils/ticks.d.ts.map +1 -1
  95. package/dist/charts/utils/ticks.js +35 -11
  96. package/dist/common/errors.d.ts +11 -1
  97. package/dist/common/errors.d.ts.map +1 -1
  98. package/dist/common/errors.js +16 -2
  99. package/dist/common/hit-floor.d.ts +10 -0
  100. package/dist/common/hit-floor.d.ts.map +1 -0
  101. package/dist/common/hit-floor.js +6 -0
  102. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  103. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  104. package/dist/common/use-measured-widths.d.ts +37 -0
  105. package/dist/common/use-measured-widths.d.ts.map +1 -0
  106. package/dist/common/use-measured-widths.js +51 -0
  107. package/dist/common/use-media-query.d.ts +6 -0
  108. package/dist/common/use-media-query.d.ts.map +1 -0
  109. package/dist/common/use-media-query.js +40 -0
  110. package/dist/common/validate.d.ts +9 -0
  111. package/dist/common/validate.d.ts.map +1 -1
  112. package/dist/common/validate.js +9 -4
  113. package/dist/content/article-layout.d.ts +4 -2
  114. package/dist/content/article-layout.d.ts.map +1 -1
  115. package/dist/content/article-layout.js +43 -3
  116. package/dist/content/article-layout.module.css +45 -4
  117. package/dist/content/article-layout.module.css.d.ts +3 -0
  118. package/dist/content/code-block.d.ts.map +1 -1
  119. package/dist/content/code-block.js +40 -2
  120. package/dist/content/code-block.module.css +32 -2
  121. package/dist/content/prose.module.css +13 -8
  122. package/dist/controls/actions.d.ts +29 -6
  123. package/dist/controls/actions.d.ts.map +1 -1
  124. package/dist/controls/actions.js +158 -27
  125. package/dist/controls/actions.module.css +14 -1
  126. package/dist/controls/actions.module.css.d.ts +1 -0
  127. package/dist/controls/controls.module.css +8 -31
  128. package/dist/controls/enum-filter.js +2 -2
  129. package/dist/controls/filter-context.d.ts +10 -0
  130. package/dist/controls/filter-context.d.ts.map +1 -1
  131. package/dist/controls/filter-context.js +2 -0
  132. package/dist/controls/filter-pill.d.ts.map +1 -1
  133. package/dist/controls/filter-pill.js +1 -0
  134. package/dist/controls/index.d.ts +1 -1
  135. package/dist/controls/index.d.ts.map +1 -1
  136. package/dist/controls/index.js +0 -2
  137. package/dist/controls/multi-select-filter.js +2 -2
  138. package/dist/controls/sync-button.module.css +1 -1
  139. package/dist/dashboard/section.d.ts.map +1 -1
  140. package/dist/dashboard/section.js +1 -0
  141. package/dist/dashboard/section.module.css +1 -26
  142. package/dist/dashboard/stat-card.d.ts +1 -1
  143. package/dist/dashboard/stat-card.d.ts.map +1 -1
  144. package/dist/dashboard/stat-card.js +4 -1
  145. package/dist/dashboard/stat-card.module.css +6 -9
  146. package/dist/dashboard/stat-group.module.css +16 -4
  147. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  148. package/dist/dashboard/widget-header.d.ts +14 -4
  149. package/dist/dashboard/widget-header.d.ts.map +1 -1
  150. package/dist/dashboard/widget-header.js +23 -6
  151. package/dist/dashboard/widget-header.module.css +145 -133
  152. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  153. package/dist/data/data-table.d.ts +23 -0
  154. package/dist/data/data-table.d.ts.map +1 -1
  155. package/dist/data/data-table.js +339 -141
  156. package/dist/data/data-table.module.css +94 -0
  157. package/dist/data/data-table.module.css.d.ts +11 -0
  158. package/dist/data/virtual-list.d.ts.map +1 -1
  159. package/dist/data/virtual-list.js +10 -0
  160. package/dist/forms/form-layout.d.ts.map +1 -1
  161. package/dist/forms/form-layout.js +3 -3
  162. package/dist/forms/form-layout.module.css +30 -5
  163. package/dist/forms/form-layout.module.css.d.ts +1 -0
  164. package/dist/guard/index.d.ts +8 -2
  165. package/dist/guard/index.d.ts.map +1 -1
  166. package/dist/guard/index.js +67 -2
  167. package/dist/guard/types.d.ts +3 -3
  168. package/dist/guard/types.d.ts.map +1 -1
  169. package/dist/index.d.ts +1 -1
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +3 -1
  172. package/dist/provider/index.d.ts +20 -1
  173. package/dist/provider/index.d.ts.map +1 -1
  174. package/dist/provider/index.js +22 -2
  175. package/dist/shell/app-brand.d.ts.map +1 -1
  176. package/dist/shell/app-brand.js +1 -0
  177. package/dist/shell/app-brand.module.css +2 -2
  178. package/dist/shell/app-header.module.css +18 -8
  179. package/dist/shell/app-main.module.css +54 -3
  180. package/dist/shell/app-main.module.css.d.ts +1 -0
  181. package/dist/shell/app-mobile-nav.module.css +16 -4
  182. package/dist/shell/app-sidebar.module.css +4 -6
  183. package/dist/shell/index.d.ts +12 -0
  184. package/dist/shell/index.d.ts.map +1 -1
  185. package/dist/shell/index.js +57 -16
  186. package/dist/shell/mobile-nav-model.d.ts +1 -1
  187. package/dist/shell/page-aside.d.ts +22 -3
  188. package/dist/shell/page-aside.d.ts.map +1 -1
  189. package/dist/shell/page-aside.js +27 -13
  190. package/dist/shell/page-bar.d.ts.map +1 -1
  191. package/dist/shell/page-bar.js +10 -5
  192. package/dist/shell/page-bar.module.css +10 -34
  193. package/dist/shell/use-breakpoint.d.ts +3 -4
  194. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  195. package/dist/shell/use-breakpoint.js +3 -25
  196. package/dist/shell/use-size-class.d.ts +6 -0
  197. package/dist/shell/use-size-class.d.ts.map +1 -0
  198. package/dist/shell/use-size-class.js +19 -0
  199. package/dist/state/persisted-or-local.d.ts +4 -1
  200. package/dist/state/persisted-or-local.d.ts.map +1 -1
  201. package/dist/state/persisted-or-local.js +4 -3
  202. package/dist/state/persisted.d.ts +10 -0
  203. package/dist/state/persisted.d.ts.map +1 -1
  204. package/dist/state/persisted.js +44 -14
  205. package/dist/styles.css +41 -0
  206. package/dist/surfaces.d.ts +4 -4
  207. package/dist/surfaces.d.ts.map +1 -1
  208. package/dist/surfaces.js +11 -3
  209. package/dist/theme/controls.module.css +7 -0
  210. package/dist/theme/ctl-theme.d.ts +1 -0
  211. package/dist/theme/ctl-theme.d.ts.map +1 -1
  212. package/dist/theme/ctl-theme.js +12 -4
  213. package/dist/theme/floating.module.css +36 -0
  214. package/dist/theme/floating.module.css.d.ts +3 -0
  215. package/dist/theme/index.d.ts +2 -2
  216. package/dist/theme/index.d.ts.map +1 -1
  217. package/dist/theme/index.js +34 -5
  218. package/dist/theme/nav-link.module.css +7 -0
  219. package/dist/tokens/index.d.ts +7 -16
  220. package/dist/tokens/index.d.ts.map +1 -1
  221. package/dist/tokens/index.js +23 -28
  222. package/dist/tokens/palette.d.ts +5 -27
  223. package/dist/tokens/palette.d.ts.map +1 -1
  224. package/dist/tokens/palette.js +8 -30
  225. package/dist/tokens/size-classes.d.ts +40 -0
  226. package/dist/tokens/size-classes.d.ts.map +1 -0
  227. package/dist/tokens/size-classes.js +29 -0
  228. package/dist/tokens.css +8 -4
  229. package/llms.txt +3 -3
  230. package/package.json +17 -17
  231. package/src/agent-chat/composer.tsx +74 -47
  232. package/src/agent-chat/thread-workspace.tsx +8 -11
  233. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  234. package/src/charts/cursor/touch-pin.ts +138 -0
  235. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  236. package/src/charts/hooks/use-hysteresis.ts +26 -0
  237. package/src/charts/hooks/useBandPlot.ts +90 -27
  238. package/src/charts/hooks/useChartCursor.ts +211 -26
  239. package/src/charts/index.ts +0 -8
  240. package/src/charts/kinds/BandStrip.tsx +11 -1
  241. package/src/charts/kinds/Donut.tsx +153 -62
  242. package/src/charts/kinds/DualPanel.tsx +19 -4
  243. package/src/charts/kinds/Heatmap.tsx +61 -22
  244. package/src/charts/kinds/MirroredBars.tsx +15 -3
  245. package/src/charts/kinds/donut-layout.ts +82 -0
  246. package/src/charts/layout/auto-margin.ts +24 -5
  247. package/src/charts/layout/end-labels.ts +135 -0
  248. package/src/charts/primitives/Axes.tsx +112 -21
  249. package/src/charts/primitives/CartesianChart.tsx +282 -68
  250. package/src/charts/primitives/ChartCard.tsx +141 -48
  251. package/src/charts/primitives/ChartFrame.tsx +243 -60
  252. package/src/charts/primitives/ChartLegend.tsx +257 -73
  253. package/src/charts/primitives/ChartTooltip.tsx +8 -3
  254. package/src/charts/primitives/Crosshair.tsx +2 -2
  255. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  256. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  257. package/src/charts/primitives/HoverOverlay.tsx +15 -4
  258. package/src/charts/primitives/chart-card-context.ts +37 -0
  259. package/src/charts/primitives/chart-frame-layout.ts +214 -177
  260. package/src/charts/primitives/chart-layout.ts +318 -0
  261. package/src/charts/primitives/chart-tier.tsx +66 -21
  262. package/src/charts/series.ts +4 -1
  263. package/src/charts/utils/ticks.ts +95 -19
  264. package/src/common/errors.ts +25 -1
  265. package/src/common/hit-floor.ts +9 -0
  266. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  267. package/src/common/use-measured-widths.ts +88 -0
  268. package/src/common/use-media-query.ts +56 -0
  269. package/src/common/validate.ts +17 -4
  270. package/src/content/article-layout.module.css +45 -4
  271. package/src/content/article-layout.module.css.d.ts +3 -0
  272. package/src/content/article-layout.tsx +60 -5
  273. package/src/content/code-block.module.css +32 -2
  274. package/src/content/code-block.tsx +47 -3
  275. package/src/content/prose.module.css +13 -8
  276. package/src/controls/actions.module.css +14 -1
  277. package/src/controls/actions.module.css.d.ts +1 -0
  278. package/src/controls/actions.tsx +223 -75
  279. package/src/controls/controls.module.css +8 -31
  280. package/src/controls/enum-filter.tsx +2 -2
  281. package/src/controls/filter-context.tsx +11 -0
  282. package/src/controls/filter-pill.tsx +1 -0
  283. package/src/controls/index.ts +0 -1
  284. package/src/controls/multi-select-filter.tsx +2 -2
  285. package/src/controls/sync-button.module.css +1 -1
  286. package/src/dashboard/section.module.css +1 -26
  287. package/src/dashboard/section.tsx +1 -0
  288. package/src/dashboard/stat-card.module.css +6 -9
  289. package/src/dashboard/stat-card.tsx +6 -3
  290. package/src/dashboard/stat-group.module.css +16 -4
  291. package/src/dashboard/widget-grid.tsx +7 -6
  292. package/src/dashboard/widget-header.module.css +145 -133
  293. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  294. package/src/dashboard/widget-header.tsx +45 -11
  295. package/src/data/data-table.module.css +94 -0
  296. package/src/data/data-table.module.css.d.ts +11 -0
  297. package/src/data/data-table.tsx +503 -164
  298. package/src/data/virtual-list.tsx +31 -0
  299. package/src/forms/form-layout.module.css +30 -5
  300. package/src/forms/form-layout.module.css.d.ts +1 -0
  301. package/src/forms/form-layout.tsx +12 -10
  302. package/src/guard/index.ts +183 -3
  303. package/src/guard/types.ts +3 -2
  304. package/src/index.ts +2 -0
  305. package/src/provider/index.tsx +53 -1
  306. package/src/shell/app-brand.module.css +2 -2
  307. package/src/shell/app-brand.tsx +1 -0
  308. package/src/shell/app-header.module.css +18 -8
  309. package/src/shell/app-main.module.css +54 -3
  310. package/src/shell/app-main.module.css.d.ts +1 -0
  311. package/src/shell/app-mobile-nav.module.css +16 -4
  312. package/src/shell/app-sidebar.module.css +4 -6
  313. package/src/shell/index.tsx +200 -129
  314. package/src/shell/mobile-nav-model.ts +1 -1
  315. package/src/shell/page-aside.tsx +70 -21
  316. package/src/shell/page-bar.module.css +10 -34
  317. package/src/shell/page-bar.tsx +10 -5
  318. package/src/shell/use-breakpoint.ts +9 -45
  319. package/src/shell/use-size-class.ts +31 -0
  320. package/src/state/persisted-or-local.ts +10 -4
  321. package/src/state/persisted.ts +76 -17
  322. package/src/styles.css +41 -0
  323. package/src/surfaces.ts +9 -1
  324. package/src/theme/controls.module.css +7 -0
  325. package/src/theme/ctl-theme.tsx +15 -3
  326. package/src/theme/floating.module.css +36 -0
  327. package/src/theme/floating.module.css.d.ts +3 -0
  328. package/src/theme/index.ts +46 -8
  329. package/src/theme/nav-link.module.css +7 -0
  330. package/src/tokens/index.ts +20 -30
  331. package/src/tokens/palette.ts +6 -28
  332. package/src/tokens/size-classes.ts +46 -0
  333. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  334. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -143,6 +143,7 @@ export const KNOWN_RULE_IDS = new Set([
143
143
  'visx-tooltip',
144
144
  'token-layer-boundary',
145
145
  'no-import-meta-env',
146
+ 'raw-breakpoint',
146
147
  // These three honour `basalt-agent-allow`, never `theme-allow` — but they are real ids, so an
147
148
  // annotation naming one must parse as a (useless) scoped annotation rather than as prose.
148
149
  'agent-resume-guard',
@@ -175,6 +176,7 @@ export const KNOWN_RULE_IDS = new Set([
175
176
  'hidden-inline-style',
176
177
  'in-body-page-title',
177
178
  'raw-selection-control',
179
+ 'raw-media-query',
178
180
  // Retired guard kinds — no longer in GUARD_RULES, but a `theme-allow` naming one must still
179
181
  // parse as a real (dead) waiver rather than an unknown-id typo. See RETIRED_RULE_IDS above.
180
182
  ...RETIRED_RULE_IDS,
@@ -2706,7 +2708,7 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
2706
2708
  * `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
2707
2709
  * exemption — a file DEFINING a basalt control cannot be told to use one.
2708
2710
  */
2709
- // Ships: warn (grace → 1.31.0)
2711
+ // Ships: warn (grace → 1.32.0)
2710
2712
  const controlOutsideHome = {
2711
2713
  meta: {
2712
2714
  type: 'suggestion',
@@ -3645,8 +3647,19 @@ const queryFnUnwrap = {
3645
3647
  * @type {readonly DeprecatedExport[]}
3646
3648
  */
3647
3649
  export const DEPRECATED_EXPORTS = [
3648
- // Empty since 1.29.0: every 1.28.x deprecation was removed on schedule. The next
3649
- // deprecation adds a row here in the same commit as its @deprecated JSDoc and MIGRATING entry.
3650
+ // The chart phone-tier API (`resolveChartTier`/`chartTierMetrics`/`useChartTier`/
3651
+ // `useChartTierMetrics`/`ChartTierMetrics`/`phoneChartWidth`) is NOT here: MIGRATING.md's own
3652
+ // Unreleased section states it a hard removal with no grace window (zero consumers), and these
3653
+ // exports are no longer reachable from any public barrel in `src/charts`/`src/tokens` — a row
3654
+ // here would nudge a rewrite to an import that no longer resolves, i.e. "it still resolves today"
3655
+ // would be false.
3656
+ {
3657
+ subpath: 'basalt-ui',
3658
+ name: 'useBreakpoint',
3659
+ replacement: 'useSizeClass (compact/medium/expanded) or @container for a container concern',
3660
+ removeIn: '1.31.0',
3661
+ fix: false,
3662
+ },
3650
3663
  ]
3651
3664
 
3652
3665
  function deprecatedExportMessage(row) {
@@ -3935,6 +3948,227 @@ const noImportMetaEnv = {
3935
3948
  },
3936
3949
  }
3937
3950
 
3951
+ // ── Rule 32 — raw-breakpoint ─────────────────────────────────────────────────────────────────────
3952
+
3953
+ /** The Mantine responsive-style-prop shorthand keys — `{ base, xs, sm, md, lg, xl }`, never a mix. */
3954
+ const RESPONSIVE_OBJECT_KEYS = new Set(['base', 'xs', 'sm', 'md', 'lg', 'xl'])
3955
+
3956
+ /**
3957
+ * Is `node` a Mantine responsive-style-prop object literal — `{ base: 1, sm: 2 }`? Every property
3958
+ * key has to be one of {@link RESPONSIVE_OBJECT_KEYS}, and there has to be at least one: a random
3959
+ * object that merely happens to have a `sm` key among other, unrelated ones is not this shape, and
3960
+ * an empty object is not a breakpoint decision at all.
3961
+ */
3962
+ function isResponsiveObjectValue(node) {
3963
+ if (node === null || node === undefined || node.type !== 'ObjectExpression') return false
3964
+ if (node.properties.length === 0) return false
3965
+ return node.properties.every((prop) => {
3966
+ if (prop.type !== 'Property') return false
3967
+ const name = propertyKeyName(prop)
3968
+ return typeof name === 'string' && RESPONSIVE_OBJECT_KEYS.has(name)
3969
+ })
3970
+ }
3971
+
3972
+ /**
3973
+ * Does this prop value CONTAIN a responsive object — itself, nested one level down in a property
3974
+ * (`footer={{ height: { base: 60, sm: 0 } }}`, Mantine's own AppShell idiom), or behind a same-file
3975
+ * `const` binding at either place (`const h = { base: 60, sm: 0 }`, then `footer={{ height: h }}`)?
3976
+ * `consts` is the same-file name→object map, so an Identifier resolves identically wherever it
3977
+ * appears — the top-level attribute value and a nested property value alike. Matching only a nested
3978
+ * `ObjectExpression` left the Identifier-in-a-property shape trivially bypassable.
3979
+ */
3980
+ function containsResponsiveObjectValue(node, consts, seen = new Set()) {
3981
+ const resolved =
3982
+ node !== null && node !== undefined && node.type === 'Identifier' ? consts.get(node.name) : node
3983
+ if (resolved === null || resolved === undefined || resolved.type !== 'ObjectExpression')
3984
+ return false
3985
+ if (seen.has(resolved)) return false
3986
+ seen.add(resolved)
3987
+ if (isResponsiveObjectValue(resolved)) return true
3988
+ return resolved.properties.some(
3989
+ (prop) => prop.type === 'Property' && containsResponsiveObjectValue(prop.value, consts, seen),
3990
+ )
3991
+ }
3992
+
3993
+ /**
3994
+ * The shell components that are `visibleFrom`/`hiddenFrom`'s legitimate home — the size-class
3995
+ * axis's one JSX escape hatch, alongside overlays. Matched by DECLARATION name, the same
3996
+ * owner-by-declaration test `hand-rolled-shell`'s
3997
+ * `notesOwnerDefinition` already uses for "this file IS the framework piece, not a consumer of it"
3998
+ * — a file defining one of these is exempt for the whole file, because deciding which JSX line
3999
+ * inside a shell's own render tree is "the shell deciding its own chrome" vs. "a stray breakpoint"
4000
+ * is not a distinction the shell's own implementation can make about itself. The OTHER legitimate
4001
+ * home is a control implementing its own C9 swap (`ViewTabs`, `ActionGroup`, …) — that half reuses
4002
+ * {@link createControlOwnerProbe} (`controlOwner` below) rather than a second name list.
4003
+ */
4004
+ const SHELL_HOME_NAMES = new Set(['BasaltShell', 'AppSidebar', 'MobileNav', 'AppBrand'])
4005
+
4006
+ /**
4007
+ * The two files exempted by NAME rather than by declaration: `useMediaQuery`'s own implementation
4008
+ * (`common/use-media-query.ts` — the one sanctioned `window.matchMedia` read every other file,
4009
+ * including `useSizeClass`, now funnels through) and `ChartTooltip` (the Mantine-free chart layer's
4010
+ * own pointer-tier read).
4011
+ */
4012
+ const RAW_BREAKPOINT_EXEMPT_FILE = /(?:^|[\\/])(?:use-media-query\.ts|ChartTooltip\.tsx)$/
4013
+
4014
+ const RAW_BREAKPOINT_RESPONSIVE_PROP_MESSAGE =
4015
+ 'Responsive-object style prop on a Mantine component — the framework owns the three responsive ' +
4016
+ 'axes (size class for shell chrome, container class for a component’s own width, pointer tier), ' +
4017
+ 'never a per-component `{ base, sm, … }`. Reach for a container query for this component’s own ' +
4018
+ 'width, or useSizeClass() for shell-only chrome. (basalt/raw-breakpoint)'
4019
+
4020
+ const RAW_BREAKPOINT_VISIBLE_HIDDEN_MESSAGE =
4021
+ "visibleFrom/hiddenFrom outside a shell home or a control's own C9 swap — this pair is the " +
4022
+ "size-class axis's one JSX escape hatch. Anywhere else it is a hand-rolled viewport breakpoint " +
4023
+ "the framework cannot see or move. Use a container query for a component's own width instead. " +
4024
+ '(basalt/raw-breakpoint)'
4025
+
4026
+ const RAW_BREAKPOINT_HOOK_MESSAGE =
4027
+ "useMediaQuery/useMatches/useViewportSize — a raw viewport read outside the framework's one seam " +
4028
+ '(useSizeClass(), shell-only). A component deciding its own layout from a viewport read instead of ' +
4029
+ 'a container query is exactly the law this rule exists to catch. (basalt/raw-breakpoint)'
4030
+
4031
+ const RAW_BREAKPOINT_GLOBAL_MESSAGE =
4032
+ 'window.matchMedia/window.innerWidth read directly — the same viewport read useSizeClass() already ' +
4033
+ "owns, shell-only. Route a component's own layout decision through a container query instead. " +
4034
+ '(basalt/raw-breakpoint)'
4035
+
4036
+ /** The three viewport hooks the law names, whatever module they are imported from. */
4037
+ const RAW_BREAKPOINT_HOOK_NAMES = new Set(['useMediaQuery', 'useMatches', 'useViewportSize'])
4038
+
4039
+ /**
4040
+ * Four independent shapes, one law: a consumer (or basalt itself, outside the shell homes)
4041
+ * reaching past the framework's two sanctioned seams — `useSizeClass()` for shell-only viewport
4042
+ * chrome, a container query for everything else — to read or react to a raw
4043
+ * viewport breakpoint. `visibleFrom`/`hiddenFrom` and the responsive-object shape are reported once
4044
+ * the whole file has been walked ({@link SHELL_HOME_NAMES} may be declared after its first JSX use,
4045
+ * the same ordering hazard `hand-rolled-shell` defers a `Program:exit` report to avoid); the import
4046
+ * and the two global reads carry no such ordering hazard and report immediately. A file that
4047
+ * declares a shell home (or a control's own C9 swap) is exempt for all four shapes uniformly — the
4048
+ * shell's own responsive chrome is the sanctioned use, whatever shape it takes.
4049
+ */
4050
+ // Ships: warn (grace → 1.32.0)
4051
+ const rawBreakpoint = {
4052
+ meta: {
4053
+ type: 'suggestion',
4054
+ docs: {
4055
+ description:
4056
+ 'Disallow a hand-rolled viewport breakpoint — a responsive-object Mantine prop, ' +
4057
+ 'visibleFrom/hiddenFrom outside a shell home, useMediaQuery/useMatches/useViewportSize, or ' +
4058
+ 'a raw window.matchMedia/innerWidth read. All four shapes are exempt inside a file that ' +
4059
+ 'declares a shell-home component (or a control implementing its own C9 swap).',
4060
+ },
4061
+ schema: [],
4062
+ },
4063
+ create(context) {
4064
+ if (isTestFile(context)) return {}
4065
+ if (RAW_BREAKPOINT_EXEMPT_FILE.test(getFilename(context))) return {}
4066
+
4067
+ const mantineImports = new Map()
4068
+ // A control implementing its OWN C9 responsive swap (`ViewTabs`, `ActionGroup`'s file, …) is
4069
+ // the other legitimate visibleFrom/hiddenFrom home, alongside the three named shell pieces —
4070
+ // reuse `responsive-twin`'s existing owner probe rather than hand-listing every control file
4071
+ // here too (`view-tabs.tsx`/`actions.tsx` were both false positives before this reuse).
4072
+ const controlOwner = createControlOwnerProbe()
4073
+ let definesShellHome = false
4074
+ const visibleHiddenCandidates = []
4075
+ const responsiveObjectCandidates = []
4076
+ // Same-file `const NAME = { base, sm }` bindings, resolved when a JSX attribute or a nested
4077
+ // property passes the Identifier — no scope manager here, so this is a flat name→object map.
4078
+ // A name declared more than once in the file is ambiguous (its two bindings cannot be told
4079
+ // apart), so it is dropped from the map entirely rather than guessed at last-wins.
4080
+ const responsiveObjectConsts = new Map()
4081
+ const seenDeclaratorNames = new Set()
4082
+
4083
+ const noteShellHomeOwner = (name) => {
4084
+ if (typeof name === 'string' && SHELL_HOME_NAMES.has(name)) definesShellHome = true
4085
+ }
4086
+
4087
+ return {
4088
+ ImportDeclaration(node) {
4089
+ controlOwner.noteImport(node)
4090
+ collectMantineImports(node, mantineImports)
4091
+ if (node.importKind === 'type') return
4092
+ // `useMatches` collides with `@tanstack/react-router`'s own hook of the same name (basalt's
4093
+ // own `src/router-tanstack/index.ts` imports it) — scope the whole set to a `@mantine/*`
4094
+ // source rather than name alone, the same provenance gate every other Mantine-name rule in
4095
+ // this file already uses (`collectMantineImports`).
4096
+ const source = node.source?.value
4097
+ if (typeof source !== 'string' || !source.startsWith('@mantine/')) return
4098
+ for (const specifier of node.specifiers ?? []) {
4099
+ if (specifier.type !== 'ImportSpecifier') continue
4100
+ if (specifier.importKind === 'type') continue
4101
+ const imported = specifier.imported
4102
+ const importedName = imported.type === 'Identifier' ? imported.name : imported.value
4103
+ if (typeof importedName !== 'string' || !RAW_BREAKPOINT_HOOK_NAMES.has(importedName))
4104
+ continue
4105
+ if (hasThemeAllow(context, specifier, 'raw-breakpoint')) continue
4106
+ context.report({ node: specifier, message: RAW_BREAKPOINT_HOOK_MESSAGE })
4107
+ }
4108
+ },
4109
+ FunctionDeclaration(node) {
4110
+ noteShellHomeOwner(node.id?.name)
4111
+ controlOwner.visitors.FunctionDeclaration(node)
4112
+ },
4113
+ VariableDeclarator(node) {
4114
+ noteShellHomeOwner(node.id?.name)
4115
+ controlOwner.visitors.VariableDeclarator(node)
4116
+ if (node.id?.type !== 'Identifier') return
4117
+ const name = node.id.name
4118
+ if (seenDeclaratorNames.has(name)) {
4119
+ // A redeclared name — drop whatever the first declaration put here, so the ambiguous
4120
+ // binding is never used to flag (or clear) a JSX value it may not even be.
4121
+ responsiveObjectConsts.delete(name)
4122
+ return
4123
+ }
4124
+ seenDeclaratorNames.add(name)
4125
+ if (node.init?.type === 'ObjectExpression') responsiveObjectConsts.set(name, node.init)
4126
+ },
4127
+ JSXAttribute(node) {
4128
+ const name = node.name?.name
4129
+ if (typeof name !== 'string') return
4130
+ if (name === 'visibleFrom' || name === 'hiddenFrom') {
4131
+ visibleHiddenCandidates.push(node)
4132
+ return
4133
+ }
4134
+ // Resolved at `Program:exit` so a `const` declared after its first JSX use still resolves.
4135
+ responsiveObjectCandidates.push(node)
4136
+ },
4137
+ CallExpression(node) {
4138
+ if (node.callee?.type !== 'Identifier' || node.callee.name !== 'matchMedia') return
4139
+ if (hasThemeAllow(context, node, 'raw-breakpoint')) return
4140
+ context.report({ node, message: RAW_BREAKPOINT_GLOBAL_MESSAGE })
4141
+ },
4142
+ MemberExpression(node) {
4143
+ if (node.object?.type !== 'Identifier' || node.object.name !== 'window') return
4144
+ if (node.computed) return
4145
+ const property = node.property?.name
4146
+ if (property !== 'matchMedia' && property !== 'innerWidth') return
4147
+ if (hasThemeAllow(context, node, 'raw-breakpoint')) return
4148
+ context.report({ node, message: RAW_BREAKPOINT_GLOBAL_MESSAGE })
4149
+ },
4150
+ 'Program:exit'() {
4151
+ // A shell home (or a control implementing its own C9 swap) IS the framework piece — the
4152
+ // shell's own responsive chrome is the sanctioned use, so nothing here reports there.
4153
+ if (definesShellHome || controlOwner.isOwner()) return
4154
+ for (const node of responsiveObjectCandidates) {
4155
+ const value = unwrapExpressionContainer(node.value)
4156
+ if (!containsResponsiveObjectValue(value, responsiveObjectConsts)) continue
4157
+ const owner = node.parent
4158
+ if (owner === null || owner === undefined || owner.type !== 'JSXOpeningElement') continue
4159
+ if (resolveMantineTag(owner.name, mantineImports) === undefined) continue
4160
+ if (hasThemeAllow(context, node, 'raw-breakpoint')) continue
4161
+ context.report({ node, message: RAW_BREAKPOINT_RESPONSIVE_PROP_MESSAGE })
4162
+ }
4163
+ for (const node of visibleHiddenCandidates) {
4164
+ if (hasThemeAllow(context, node, 'raw-breakpoint')) continue
4165
+ context.report({ node, message: RAW_BREAKPOINT_VISIBLE_HIDDEN_MESSAGE })
4166
+ }
4167
+ },
4168
+ }
4169
+ },
4170
+ }
4171
+
3938
4172
  // ── Grace ledger ────────────────────────────────────────────────────────────────────────────────
3939
4173
 
3940
4174
  /**
@@ -3968,8 +4202,9 @@ const noImportMetaEnv = {
3968
4202
  export const PLUGIN_RULE_GRACE = {
3969
4203
  'control-outside-home': {
3970
4204
  since: '1.26.0',
3971
- promote: '1.31.0',
4205
+ promote: '1.32.0',
3972
4206
  why:
4207
+ 'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
3973
4208
  'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
3974
4209
  'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
3975
4210
  'is carried by four exemptions (overlay/settings-row ancestor, the overlay FILENAME ' +
@@ -3995,6 +4230,20 @@ export const PLUGIN_RULE_GRACE = {
3995
4230
  "still an overlay's own body declares it with " +
3996
4231
  '`theme-allow-file control-outside-home — overlay`.',
3997
4232
  },
4233
+ 'raw-breakpoint': {
4234
+ since: '1.31.0',
4235
+ promote: '1.32.0',
4236
+ why:
4237
+ 'new in the wave-11 responsive/touch guards. Catches four ' +
4238
+ 'independent shapes at once — a responsive-object Mantine prop, visibleFrom/hiddenFrom outside ' +
4239
+ 'the three shell homes, the three raw viewport hooks, and window.matchMedia/innerWidth — none ' +
4240
+ 'of which basalt policed before this wave, so every consumer on an earlier minor has a green ' +
4241
+ "build with all four shapes already in it. basalt-ui/content's own article-card.tsx (a " +
4242
+ "`SimpleGrid cols={{ base: 1, sm: 2, lg }}`) is one incumbent measured in basalt's own tree at " +
4243
+ 'ship time — a real number, not zero, so this ships warn rather than error from the start. ' +
4244
+ '1.32.0 is when that incumbent and any consumer-side ones are expected to have moved to a ' +
4245
+ 'container query or a theme-allow with a stated reason.',
4246
+ },
3998
4247
  }
3999
4248
 
4000
4249
  /**
@@ -4064,5 +4313,6 @@ export default {
4064
4313
  'agent-no-raw-usechat': agentNoRawUseChat,
4065
4314
  'ai-sdk-major': aiSdkMajor,
4066
4315
  'no-import-meta-env': noImportMetaEnv,
4316
+ 'raw-breakpoint': rawBreakpoint,
4067
4317
  },
4068
4318
  }
@@ -70,6 +70,7 @@ beforeEach(() => {
70
70
  'basalt/agent-resume-guard': 'error',
71
71
  'basalt/agent-no-raw-usechat': 'error',
72
72
  'basalt/ai-sdk-major': 'error',
73
+ 'basalt/raw-breakpoint': 'error',
73
74
  },
74
75
  }),
75
76
  )
@@ -2735,10 +2736,11 @@ describe('basalt/responsive-twin', () => {
2735
2736
  })
2736
2737
 
2737
2738
  it('does NOT flag two halves on DIFFERENT breakpoints', () => {
2738
- const { code, rules } = run(
2739
+ // `code` is not asserted here: raw visibleFrom/hiddenFrom outside a shell home is its own,
2740
+ // unrelated finding (basalt/raw-breakpoint) — this test is only about responsive-twin's silence.
2741
+ const { rules } = run(
2739
2742
  `export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="md" data={[]} />\n </div>\n)\n`,
2740
2743
  )
2741
- expect(code).toBe(0)
2742
2744
  expect(rules).not.toContain('responsive-twin')
2743
2745
  })
2744
2746
 
@@ -3400,6 +3402,147 @@ describe('basalt/forms-field-key', () => {
3400
3402
  })
3401
3403
  })
3402
3404
 
3405
+ // ── raw-breakpoint ────────────────────────────────────────────────────────────
3406
+
3407
+ describe('basalt/raw-breakpoint', () => {
3408
+ it('flags a responsive-object prop on an imported Mantine component', () => {
3409
+ const { rules } = run(
3410
+ `import { SimpleGrid } from '@mantine/core'\n` +
3411
+ `export const C = () => <SimpleGrid cols={{ base: 1, sm: 2 }} />\n`,
3412
+ )
3413
+ expect(rules).toContain('raw-breakpoint')
3414
+ })
3415
+
3416
+ it('flags a responsive object nested one level down (Mantine AppShell idiom)', () => {
3417
+ const { rules } = run(
3418
+ `import { AppShell } from '@mantine/core'\n` +
3419
+ `export const C = () => <AppShell footer={{ height: { base: 60, sm: 0 } }}>hi</AppShell>\n`,
3420
+ )
3421
+ expect(rules).toContain('raw-breakpoint')
3422
+ })
3423
+
3424
+ it('flags a responsive object behind a same-file const binding', () => {
3425
+ const { rules } = run(
3426
+ `import { SimpleGrid } from '@mantine/core'\n` +
3427
+ `const responsiveCols = { base: 1, sm: 2 }\n` +
3428
+ `export const C = () => <SimpleGrid cols={responsiveCols} />\n`,
3429
+ )
3430
+ expect(rules).toContain('raw-breakpoint')
3431
+ })
3432
+
3433
+ // The nested-property half: a property VALUE that is itself an Identifier must resolve through
3434
+ // the same-file const map, exactly as the attribute's own top-level value already does.
3435
+ it('flags a responsive object behind a const used as a nested property value', () => {
3436
+ const { rules } = run(
3437
+ `import { AppShell } from '@mantine/core'\n` +
3438
+ `const h = { base: 60, sm: 0 }\n` +
3439
+ `export const C = () => <AppShell footer={{ height: h }}>hi</AppShell>\n`,
3440
+ )
3441
+ expect(rules).toContain('raw-breakpoint')
3442
+ })
3443
+
3444
+ // A name declared twice in the file has no single binding without a scope manager, so it is
3445
+ // dropped rather than resolved last-declaration-wins — the outer (non-responsive) `cols` the JSX
3446
+ // actually references must not be flagged via the inner function's responsive one.
3447
+ it('does NOT flag a name redeclared in the file — an ambiguous binding is skipped, not guessed', () => {
3448
+ const { rules } = run(
3449
+ `import { SimpleGrid } from '@mantine/core'\n` +
3450
+ `const cols = { fixed: 2 }\n` +
3451
+ `function Other() {\n const cols = { base: 1, sm: 2 }\n return null\n}\n` +
3452
+ `export const C = () => <SimpleGrid cols={cols} />\n`,
3453
+ )
3454
+ expect(rules).not.toContain('raw-breakpoint')
3455
+ })
3456
+
3457
+ it('does NOT flag a fixed cols on the same component', () => {
3458
+ const { rules } = run(
3459
+ `import { SimpleGrid } from '@mantine/core'\n` +
3460
+ `export const C = () => <SimpleGrid cols={2} />\n`,
3461
+ )
3462
+ expect(rules).not.toContain('raw-breakpoint')
3463
+ })
3464
+
3465
+ it('does NOT flag an object prop on a component never imported from @mantine/*', () => {
3466
+ const { rules } = run(`export const C = () => <WidgetGrid cols={{ base: 1, sm: 2 }} />\n`)
3467
+ expect(rules).not.toContain('raw-breakpoint')
3468
+ })
3469
+
3470
+ it('flags visibleFrom/hiddenFrom outside a shell home', () => {
3471
+ const { rules } = run(
3472
+ `import { Box } from '@mantine/core'\n` +
3473
+ `export const C = () => <Box visibleFrom="sm">desktop only</Box>\n`,
3474
+ )
3475
+ expect(rules).toContain('raw-breakpoint')
3476
+ })
3477
+
3478
+ it('does NOT flag visibleFrom/hiddenFrom inside a file that DEFINES BasaltShell', () => {
3479
+ const { rules } = run(
3480
+ `import { Box } from '@mantine/core'\n` +
3481
+ `export function BasaltShell() {\n return <Box visibleFrom="sm">desktop only</Box>\n}\n`,
3482
+ )
3483
+ expect(rules).not.toContain('raw-breakpoint')
3484
+ })
3485
+
3486
+ // The exemption is uniform across all four shapes: a file that IS the shell may also carry a
3487
+ // responsive-object prop, not only visibleFrom/hiddenFrom.
3488
+ it('does NOT flag a responsive-object prop inside a file that DEFINES BasaltShell', () => {
3489
+ const { rules } = run(
3490
+ `import { SimpleGrid } from '@mantine/core'\n` +
3491
+ `export function BasaltShell() {\n return null\n}\n` +
3492
+ `export const C = () => <SimpleGrid cols={{ base: 1, sm: 2 }} />\n`,
3493
+ )
3494
+ expect(rules).not.toContain('raw-breakpoint')
3495
+ })
3496
+
3497
+ it('does NOT flag visibleFrom/hiddenFrom inside a file that DEFINES a C9 control (ViewTabs) and imports no basalt-ui', () => {
3498
+ const { rules } = run(
3499
+ `import { Box } from '@mantine/core'\n` +
3500
+ `export function ViewTabs() {\n return <Box visibleFrom="sm">desktop only</Box>\n}\n`,
3501
+ )
3502
+ expect(rules).not.toContain('raw-breakpoint')
3503
+ })
3504
+
3505
+ it('flags an import of useMediaQuery/useMatches/useViewportSize from @mantine/*', () => {
3506
+ expect(run(`import { useMediaQuery } from '@mantine/hooks'\n`).rules).toContain(
3507
+ 'raw-breakpoint',
3508
+ )
3509
+ expect(run(`import { useMatches } from '@mantine/core'\n`).rules).toContain('raw-breakpoint')
3510
+ expect(run(`import { useViewportSize } from '@mantine/hooks'\n`).rules).toContain(
3511
+ 'raw-breakpoint',
3512
+ )
3513
+ })
3514
+
3515
+ it('does NOT flag useMatches imported from a non-Mantine router (name collision)', () => {
3516
+ // @tanstack/react-router (and react-router) export their own unrelated `useMatches` —
3517
+ // basalt's own `src/router-tanstack/index.ts` imports it; scoped to `@mantine/*` sources only.
3518
+ expect(run(`import { useMatches } from '@tanstack/react-router'\n`).rules).not.toContain(
3519
+ 'raw-breakpoint',
3520
+ )
3521
+ expect(run(`import { useViewportSize } from 'some-lib'\n`).rules).not.toContain(
3522
+ 'raw-breakpoint',
3523
+ )
3524
+ })
3525
+
3526
+ it('flags window.matchMedia and window.innerWidth', () => {
3527
+ expect(run(`const mq = window.matchMedia('(max-width: 600px)')\n`).rules).toContain(
3528
+ 'raw-breakpoint',
3529
+ )
3530
+ expect(run(`const w = window.innerWidth\n`).rules).toContain('raw-breakpoint')
3531
+ })
3532
+
3533
+ it('flags a bare matchMedia() call', () => {
3534
+ expect(run(`const mq = matchMedia('(max-width: 600px)')\n`).rules).toContain('raw-breakpoint')
3535
+ })
3536
+
3537
+ it('honors a scoped theme-allow on the responsive-object prop', () => {
3538
+ const { rules } = run(
3539
+ `import { SimpleGrid } from '@mantine/core'\n` +
3540
+ `export const C = () => (\n // theme-allow raw-breakpoint — migrating in a follow-up\n <SimpleGrid cols={{ base: 1, sm: 2 }} />\n)\n`,
3541
+ )
3542
+ expect(rules).not.toContain('raw-breakpoint')
3543
+ })
3544
+ })
3545
+
3403
3546
  // ── the deprecation ledger (B4) ──────────────────────────────────────────────
3404
3547
 
3405
3548
  describe('DEPRECATED_EXPORTS', () => {
@@ -49,7 +49,8 @@
49
49
  "basalt/visx-tooltip": "error",
50
50
  "basalt/agent-resume-guard": "error",
51
51
  "basalt/agent-no-raw-usechat": "error",
52
- "basalt/ai-sdk-major": "error"
52
+ "basalt/ai-sdk-major": "error",
53
+ "basalt/raw-breakpoint": "warn"
53
54
  },
54
55
  "overrides": [
55
56
  {
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/agent-chat/composer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/agent-chat/composer.tsx"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { ActionIcon, Group, Kbd, Stack, Text, Textarea } from "@mantine/core";
2
+ import { ActionIcon, Box, Group, Kbd, Stack, Text, Textarea } from "@mantine/core";
3
3
  import { useImperativeHandle, useLayoutEffect, useRef, useSyncExternalStore } from "react";
4
4
  import { cx } from "../common/props.js";
5
5
  import { readPersistedValue } from "../state.js";
6
6
  import { VX } from "../tokens/index.js";
7
+ import { useKeyboardInsetRef } from "./use-keyboard-inset.js";
7
8
  function SendGlyph() {
8
9
  return /* @__PURE__ */ jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx(
9
10
  "path",
@@ -90,6 +91,7 @@ const noopSubscribe = (_cb) => () => {
90
91
  };
91
92
  const emptyDraft = () => "";
92
93
  const NO_ATTACHMENTS = [];
94
+ const ROOT_GAP = 6;
93
95
  function liveSelection(el) {
94
96
  if (document.activeElement !== el) return null;
95
97
  const { selectionStart, selectionEnd } = el;
@@ -170,6 +172,7 @@ function Composer({
170
172
  textareaRef.current?.focus();
171
173
  };
172
174
  useImperativeHandle(ref, () => ({ insertText, setValue, focus: focusTextarea }));
175
+ const keyboardInsetRef = useKeyboardInsetRef();
173
176
  const submit = () => {
174
177
  if (inputDisabled || !hasPayload) return;
175
178
  const submittedText = trimmed;
@@ -221,48 +224,43 @@ function Composer({
221
224
  if (onPaste !== void 0) textareaProps.onPaste = onPaste;
222
225
  if (classNames?.input !== void 0) textareaProps.className = cx(classNames.input);
223
226
  const showActions = showStop || showSend;
224
- return /* @__PURE__ */ jsxs(
225
- Stack,
226
- {
227
- gap: 6,
228
- className: cx(classNames?.root, className),
229
- ...style !== void 0 && { style },
230
- children: [
231
- /* @__PURE__ */ jsxs(Group, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
232
- leftSection,
233
- /* @__PURE__ */ jsx(Textarea, { ...textareaProps, ref: textareaRef }),
234
- rightSection,
235
- showActions && /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", className: cx(classNames?.actions), children: [
236
- showStop ? /* @__PURE__ */ jsx(
237
- ActionIcon,
238
- {
239
- size: 42,
240
- radius: "md",
241
- variant: "filled",
242
- color: "red",
243
- onClick: onStop,
244
- "aria-label": "Stop generating",
245
- children: /* @__PURE__ */ jsx(StopGlyph, {})
246
- }
247
- ) : null,
248
- showSend ? /* @__PURE__ */ jsx(
249
- ActionIcon,
250
- {
251
- size: 42,
252
- radius: "md",
253
- variant: "filled",
254
- onClick: submit,
255
- disabled: inputDisabled || !hasPayload,
256
- "aria-label": "Send message",
257
- children: /* @__PURE__ */ jsx(SendGlyph, {})
258
- }
259
- ) : null
260
- ] })
261
- ] }),
262
- hint === void 0 ? /* @__PURE__ */ jsx(DefaultHint, {}) : hint
263
- ]
264
- }
265
- );
227
+ return /* @__PURE__ */ jsxs(Box, { ref: keyboardInsetRef, className: cx(classNames?.root, className), style, children: [
228
+ /* @__PURE__ */ jsxs(Stack, { gap: ROOT_GAP, children: [
229
+ /* @__PURE__ */ jsxs(Group, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
230
+ leftSection,
231
+ /* @__PURE__ */ jsx(Textarea, { ...textareaProps, ref: textareaRef }),
232
+ rightSection,
233
+ showActions && /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", className: cx(classNames?.actions), children: [
234
+ showStop ? /* @__PURE__ */ jsx(
235
+ ActionIcon,
236
+ {
237
+ size: 42,
238
+ radius: "md",
239
+ variant: "filled",
240
+ color: "red",
241
+ onClick: onStop,
242
+ "aria-label": "Stop generating",
243
+ children: /* @__PURE__ */ jsx(StopGlyph, {})
244
+ }
245
+ ) : null,
246
+ showSend ? /* @__PURE__ */ jsx(
247
+ ActionIcon,
248
+ {
249
+ size: 42,
250
+ radius: "md",
251
+ variant: "filled",
252
+ onClick: submit,
253
+ disabled: inputDisabled || !hasPayload,
254
+ "aria-label": "Send message",
255
+ children: /* @__PURE__ */ jsx(SendGlyph, {})
256
+ }
257
+ ) : null
258
+ ] })
259
+ ] }),
260
+ hint === void 0 ? /* @__PURE__ */ jsx(DefaultHint, {}) : hint
261
+ ] }),
262
+ /* @__PURE__ */ jsx(Box, { "aria-hidden": true, style: { height: "var(--vx-keyboard-inset, 0px)" } })
263
+ ] });
266
264
  }
267
265
  export {
268
266
  Composer
@@ -3,7 +3,7 @@ import type { AgentPart, AgentTransport, OutcomeResolver, ThreadsStore } from '.
3
3
  import type { BasaltProps, SlotStylesProps } from '../common/props.js';
4
4
  /** The four boxes `ThreadWorkspace` paints: `root` is the outermost pane split, `feed` is the
5
5
  * thread-list pane, `composer` is the new-thread composer pinned under the feed, `detail` is the
6
- * right-hand (or, below 768px, full-screen) open-thread pane. */
6
+ * right-hand (or, in the `compact` size class, full-screen) open-thread pane. */
7
7
  export type ThreadWorkspaceSlot = 'root' | 'feed' | 'composer' | 'detail';
8
8
  export type ThreadWorkspaceProps = BasaltProps & SlotStylesProps<ThreadWorkspaceSlot> & {
9
9
  /**
@@ -35,7 +35,7 @@ export type ThreadWorkspaceProps = BasaltProps & SlotStylesProps<ThreadWorkspace
35
35
  /**
36
36
  * A two-pane master-detail thread workspace: a scrollable feed of threads with an anchored
37
37
  * new-thread composer, and a detail panel for the open thread's live transcript. Collapses to a
38
- * single pane below 768px.
38
+ * single pane in the `compact` size class (< 840px).
39
39
  *
40
40
  * @example
41
41
  * <ThreadWorkspace useThreads={useThreads} transport={transport} resolveOutcome={heuristicOutcome} />
@@ -1 +1 @@
1
- {"version":3,"file":"thread-workspace.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-workspace.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGxF,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAanE;;iEAEiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEzE,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAC5C,eAAe,CAAC,mBAAmB,CAAC,GAAG;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,YAAY,CAAA;IACvC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACrD,mEAAmE;IACnE,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;IACnD,qEAAqE;IACrE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;CAChC,CAAA;AA4DH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,SAAS,EACT,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,UAAU,EACV,SAAS,EACT,KAAK,EACL,UAAU,GACX,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CAsHpC"}
1
+ {"version":3,"file":"thread-workspace.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-workspace.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGxF,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAcnE;;iFAEiF;AACjF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEzE,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAC5C,eAAe,CAAC,mBAAmB,CAAC,GAAG;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,YAAY,CAAA;IACvC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACrD,mEAAmE;IACnE,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;IACnD,qEAAqE;IACrE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;CAChC,CAAA;AA4DH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,SAAS,EACT,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,UAAU,EACV,SAAS,EACT,KAAK,EACL,UAAU,GACX,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CAmHpC"}