basalt-ui 1.30.2 → 1.32.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 (357) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/MIGRATING.md +256 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +5 -5
  6. package/configs/oxlint-plugin.js +371 -4
  7. package/configs/oxlint-plugin.test.ts +327 -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 +47 -82
  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 +138 -145
  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 +68 -111
  47. package/dist/charts/kinds/band-chrome.d.ts +31 -0
  48. package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
  49. package/dist/charts/kinds/band-chrome.js +71 -0
  50. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  51. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  52. package/dist/charts/kinds/donut-layout.js +52 -0
  53. package/dist/charts/layout/auto-margin.d.ts +7 -2
  54. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  55. package/dist/charts/layout/auto-margin.js +12 -3
  56. package/dist/charts/layout/end-labels.d.ts +64 -0
  57. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  58. package/dist/charts/layout/end-labels.js +76 -0
  59. package/dist/charts/primitives/Axes.d.ts +32 -4
  60. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  61. package/dist/charts/primitives/Axes.js +62 -16
  62. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  63. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  64. package/dist/charts/primitives/CartesianChart.js +264 -145
  65. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  66. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartCard.js +56 -21
  68. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  69. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartFrame.js +116 -34
  71. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  72. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  73. package/dist/charts/primitives/ChartLegend.js +86 -73
  74. package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
  75. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  76. package/dist/charts/primitives/ChartTooltip.js +20 -4
  77. package/dist/charts/primitives/Crosshair.js +2 -2
  78. package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
  79. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  80. package/dist/charts/primitives/HoverOverlay.js +54 -27
  81. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  82. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  83. package/dist/charts/primitives/chart-card-context.d.ts +33 -0
  84. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  85. package/dist/charts/primitives/chart-card-context.js +18 -0
  86. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  87. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  88. package/dist/charts/primitives/chart-frame-layout.js +75 -34
  89. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  90. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  91. package/dist/charts/primitives/chart-layout.js +175 -0
  92. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  93. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  94. package/dist/charts/primitives/chart-tier.js +42 -13
  95. package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
  96. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
  97. package/dist/charts/primitives/legend-disclosure.js +153 -0
  98. package/dist/charts/series.d.ts +4 -1
  99. package/dist/charts/series.d.ts.map +1 -1
  100. package/dist/charts/utils/ticks.d.ts +27 -8
  101. package/dist/charts/utils/ticks.d.ts.map +1 -1
  102. package/dist/charts/utils/ticks.js +35 -11
  103. package/dist/common/errors.d.ts +11 -1
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +16 -2
  106. package/dist/common/hit-floor.d.ts +10 -0
  107. package/dist/common/hit-floor.d.ts.map +1 -0
  108. package/dist/common/hit-floor.js +6 -0
  109. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  110. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  111. package/dist/common/use-measured-widths.d.ts +37 -0
  112. package/dist/common/use-measured-widths.d.ts.map +1 -0
  113. package/dist/common/use-measured-widths.js +51 -0
  114. package/dist/common/use-media-query.d.ts +6 -0
  115. package/dist/common/use-media-query.d.ts.map +1 -0
  116. package/dist/common/use-media-query.js +40 -0
  117. package/dist/common/validate.d.ts +9 -0
  118. package/dist/common/validate.d.ts.map +1 -1
  119. package/dist/common/validate.js +9 -4
  120. package/dist/content/article-layout.d.ts +4 -2
  121. package/dist/content/article-layout.d.ts.map +1 -1
  122. package/dist/content/article-layout.js +43 -3
  123. package/dist/content/article-layout.module.css +45 -4
  124. package/dist/content/article-layout.module.css.d.ts +3 -0
  125. package/dist/content/code-block.d.ts.map +1 -1
  126. package/dist/content/code-block.js +40 -2
  127. package/dist/content/code-block.module.css +32 -2
  128. package/dist/content/prose.module.css +13 -8
  129. package/dist/controls/actions.d.ts +29 -6
  130. package/dist/controls/actions.d.ts.map +1 -1
  131. package/dist/controls/actions.js +158 -27
  132. package/dist/controls/actions.module.css +14 -1
  133. package/dist/controls/actions.module.css.d.ts +1 -0
  134. package/dist/controls/controls.module.css +13 -31
  135. package/dist/controls/enum-filter.js +2 -2
  136. package/dist/controls/filter-context.d.ts +10 -0
  137. package/dist/controls/filter-context.d.ts.map +1 -1
  138. package/dist/controls/filter-context.js +2 -0
  139. package/dist/controls/filter-pill.d.ts.map +1 -1
  140. package/dist/controls/filter-pill.js +1 -0
  141. package/dist/controls/index.d.ts +1 -1
  142. package/dist/controls/index.d.ts.map +1 -1
  143. package/dist/controls/index.js +0 -2
  144. package/dist/controls/multi-select-filter.js +2 -2
  145. package/dist/controls/sync-button.module.css +2 -1
  146. package/dist/dashboard/section.d.ts.map +1 -1
  147. package/dist/dashboard/section.js +1 -0
  148. package/dist/dashboard/section.module.css +1 -26
  149. package/dist/dashboard/stat-card.d.ts +1 -1
  150. package/dist/dashboard/stat-card.d.ts.map +1 -1
  151. package/dist/dashboard/stat-card.js +4 -1
  152. package/dist/dashboard/stat-card.module.css +6 -9
  153. package/dist/dashboard/stat-group.module.css +33 -66
  154. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  155. package/dist/dashboard/widget-grid.module.css +11 -38
  156. package/dist/dashboard/widget-header.d.ts +14 -4
  157. package/dist/dashboard/widget-header.d.ts.map +1 -1
  158. package/dist/dashboard/widget-header.js +23 -6
  159. package/dist/dashboard/widget-header.module.css +145 -133
  160. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  161. package/dist/data/data-table-state.d.ts +83 -0
  162. package/dist/data/data-table-state.d.ts.map +1 -0
  163. package/dist/data/data-table-state.js +139 -0
  164. package/dist/data/data-table.d.ts +56 -2
  165. package/dist/data/data-table.d.ts.map +1 -1
  166. package/dist/data/data-table.js +436 -336
  167. package/dist/data/data-table.module.css +94 -0
  168. package/dist/data/data-table.module.css.d.ts +11 -0
  169. package/dist/data/virtual-list.d.ts.map +1 -1
  170. package/dist/data/virtual-list.js +10 -0
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +3 -3
  173. package/dist/forms/form-layout.module.css +20 -5
  174. package/dist/forms/form-layout.module.css.d.ts +1 -0
  175. package/dist/guard/index.d.ts +8 -2
  176. package/dist/guard/index.d.ts.map +1 -1
  177. package/dist/guard/index.js +68 -2
  178. package/dist/guard/types.d.ts +3 -3
  179. package/dist/guard/types.d.ts.map +1 -1
  180. package/dist/index.d.ts +1 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +3 -1
  183. package/dist/notifications/center.d.ts.map +1 -1
  184. package/dist/provider/index.d.ts +20 -1
  185. package/dist/provider/index.d.ts.map +1 -1
  186. package/dist/provider/index.js +30 -8
  187. package/dist/shell/app-brand.d.ts.map +1 -1
  188. package/dist/shell/app-brand.js +1 -0
  189. package/dist/shell/app-brand.module.css +2 -2
  190. package/dist/shell/app-header.module.css +18 -8
  191. package/dist/shell/app-main.module.css +54 -3
  192. package/dist/shell/app-main.module.css.d.ts +1 -0
  193. package/dist/shell/app-mobile-nav.module.css +16 -4
  194. package/dist/shell/app-sidebar.module.css +4 -6
  195. package/dist/shell/aside-docking.d.ts +31 -0
  196. package/dist/shell/aside-docking.d.ts.map +1 -0
  197. package/dist/shell/aside-docking.js +19 -0
  198. package/dist/shell/index.d.ts +1 -0
  199. package/dist/shell/index.d.ts.map +1 -1
  200. package/dist/shell/index.js +56 -16
  201. package/dist/shell/mobile-nav-model.d.ts +1 -1
  202. package/dist/shell/page-aside.d.ts +22 -3
  203. package/dist/shell/page-aside.d.ts.map +1 -1
  204. package/dist/shell/page-aside.js +27 -13
  205. package/dist/shell/page-bar.d.ts.map +1 -1
  206. package/dist/shell/page-bar.js +10 -5
  207. package/dist/shell/page-bar.module.css +10 -34
  208. package/dist/shell/use-breakpoint.d.ts +3 -4
  209. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  210. package/dist/shell/use-breakpoint.js +3 -25
  211. package/dist/shell/use-size-class.d.ts +6 -0
  212. package/dist/shell/use-size-class.d.ts.map +1 -0
  213. package/dist/shell/use-size-class.js +19 -0
  214. package/dist/state/persisted-or-local.d.ts +4 -1
  215. package/dist/state/persisted-or-local.d.ts.map +1 -1
  216. package/dist/state/persisted-or-local.js +4 -3
  217. package/dist/state/persisted.d.ts +10 -0
  218. package/dist/state/persisted.d.ts.map +1 -1
  219. package/dist/state/persisted.js +44 -14
  220. package/dist/styles.css +41 -0
  221. package/dist/surfaces.d.ts +4 -4
  222. package/dist/surfaces.d.ts.map +1 -1
  223. package/dist/surfaces.js +11 -3
  224. package/dist/theme/controls.module.css +7 -0
  225. package/dist/theme/ctl-theme.d.ts +1 -0
  226. package/dist/theme/ctl-theme.d.ts.map +1 -1
  227. package/dist/theme/ctl-theme.js +12 -4
  228. package/dist/theme/floating.module.css +37 -0
  229. package/dist/theme/floating.module.css.d.ts +3 -0
  230. package/dist/theme/index.d.ts +2 -2
  231. package/dist/theme/index.d.ts.map +1 -1
  232. package/dist/theme/index.js +34 -5
  233. package/dist/theme/nav-link.module.css +7 -0
  234. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  235. package/dist/tokens/index.d.ts +7 -16
  236. package/dist/tokens/index.d.ts.map +1 -1
  237. package/dist/tokens/index.js +23 -28
  238. package/dist/tokens/palette.d.ts +5 -27
  239. package/dist/tokens/palette.d.ts.map +1 -1
  240. package/dist/tokens/palette.js +8 -30
  241. package/dist/tokens/size-classes.d.ts +40 -0
  242. package/dist/tokens/size-classes.d.ts.map +1 -0
  243. package/dist/tokens/size-classes.js +29 -0
  244. package/dist/tokens.css +8 -4
  245. package/llms.txt +3 -3
  246. package/package.json +18 -17
  247. package/src/agent-chat/composer.tsx +74 -47
  248. package/src/agent-chat/thread-workspace.tsx +8 -11
  249. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  250. package/src/charts/cursor/touch-pin.ts +138 -0
  251. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  252. package/src/charts/hooks/use-hysteresis.ts +26 -0
  253. package/src/charts/hooks/useBandPlot.ts +90 -27
  254. package/src/charts/hooks/useChartCursor.ts +211 -26
  255. package/src/charts/index.ts +0 -8
  256. package/src/charts/kinds/BandStrip.tsx +25 -61
  257. package/src/charts/kinds/Donut.tsx +153 -62
  258. package/src/charts/kinds/DualPanel.tsx +21 -26
  259. package/src/charts/kinds/Heatmap.tsx +61 -22
  260. package/src/charts/kinds/MirroredBars.tsx +23 -65
  261. package/src/charts/kinds/band-chrome.tsx +91 -0
  262. package/src/charts/kinds/donut-layout.ts +82 -0
  263. package/src/charts/layout/auto-margin.ts +24 -5
  264. package/src/charts/layout/end-labels.ts +135 -0
  265. package/src/charts/primitives/Axes.tsx +112 -21
  266. package/src/charts/primitives/CartesianChart.tsx +282 -111
  267. package/src/charts/primitives/ChartCard.tsx +119 -48
  268. package/src/charts/primitives/ChartFrame.tsx +204 -60
  269. package/src/charts/primitives/ChartLegend.tsx +115 -78
  270. package/src/charts/primitives/ChartTooltip.tsx +24 -3
  271. package/src/charts/primitives/Crosshair.tsx +2 -2
  272. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  273. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  274. package/src/charts/primitives/HoverOverlay.tsx +62 -20
  275. package/src/charts/primitives/chart-card-context.ts +46 -0
  276. package/src/charts/primitives/chart-frame-layout.ts +207 -182
  277. package/src/charts/primitives/chart-layout.ts +318 -0
  278. package/src/charts/primitives/chart-tier.tsx +66 -21
  279. package/src/charts/primitives/legend-disclosure.tsx +177 -0
  280. package/src/charts/series.ts +4 -1
  281. package/src/charts/utils/ticks.ts +95 -19
  282. package/src/common/errors.ts +25 -1
  283. package/src/common/hit-floor.ts +9 -0
  284. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  285. package/src/common/use-measured-widths.ts +88 -0
  286. package/src/common/use-media-query.ts +56 -0
  287. package/src/common/validate.ts +17 -4
  288. package/src/content/article-layout.module.css +45 -4
  289. package/src/content/article-layout.module.css.d.ts +3 -0
  290. package/src/content/article-layout.tsx +60 -5
  291. package/src/content/code-block.module.css +32 -2
  292. package/src/content/code-block.tsx +47 -3
  293. package/src/content/prose.module.css +13 -8
  294. package/src/controls/actions.module.css +14 -1
  295. package/src/controls/actions.module.css.d.ts +1 -0
  296. package/src/controls/actions.tsx +223 -75
  297. package/src/controls/controls.module.css +13 -31
  298. package/src/controls/enum-filter.tsx +2 -2
  299. package/src/controls/filter-context.tsx +11 -0
  300. package/src/controls/filter-pill.tsx +1 -0
  301. package/src/controls/index.ts +0 -1
  302. package/src/controls/multi-select-filter.tsx +2 -2
  303. package/src/controls/sync-button.module.css +2 -1
  304. package/src/dashboard/section.module.css +1 -26
  305. package/src/dashboard/section.tsx +1 -0
  306. package/src/dashboard/stat-card.module.css +6 -9
  307. package/src/dashboard/stat-card.tsx +6 -3
  308. package/src/dashboard/stat-group.module.css +33 -66
  309. package/src/dashboard/widget-grid.module.css +11 -38
  310. package/src/dashboard/widget-grid.tsx +7 -6
  311. package/src/dashboard/widget-header.module.css +145 -133
  312. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  313. package/src/dashboard/widget-header.tsx +45 -11
  314. package/src/data/data-table-state.ts +218 -0
  315. package/src/data/data-table.module.css +94 -0
  316. package/src/data/data-table.module.css.d.ts +11 -0
  317. package/src/data/data-table.tsx +547 -299
  318. package/src/data/virtual-list.tsx +31 -0
  319. package/src/forms/form-layout.module.css +20 -5
  320. package/src/forms/form-layout.module.css.d.ts +1 -0
  321. package/src/forms/form-layout.tsx +12 -10
  322. package/src/guard/index.ts +188 -3
  323. package/src/guard/types.ts +3 -2
  324. package/src/index.ts +2 -0
  325. package/src/notifications/center.tsx +3 -0
  326. package/src/provider/index.tsx +60 -7
  327. package/src/shell/app-brand.module.css +2 -2
  328. package/src/shell/app-brand.tsx +1 -0
  329. package/src/shell/app-header.module.css +18 -8
  330. package/src/shell/app-main.module.css +54 -3
  331. package/src/shell/app-main.module.css.d.ts +1 -0
  332. package/src/shell/app-mobile-nav.module.css +16 -4
  333. package/src/shell/app-sidebar.module.css +4 -6
  334. package/src/shell/aside-docking.ts +54 -0
  335. package/src/shell/index.tsx +178 -129
  336. package/src/shell/mobile-nav-model.ts +1 -1
  337. package/src/shell/page-aside.tsx +70 -21
  338. package/src/shell/page-bar.module.css +10 -34
  339. package/src/shell/page-bar.tsx +10 -5
  340. package/src/shell/use-breakpoint.ts +9 -45
  341. package/src/shell/use-size-class.ts +31 -0
  342. package/src/state/persisted-or-local.ts +10 -4
  343. package/src/state/persisted.ts +76 -17
  344. package/src/styles.css +41 -0
  345. package/src/surfaces.ts +9 -1
  346. package/src/theme/controls.module.css +7 -0
  347. package/src/theme/ctl-theme.tsx +15 -3
  348. package/src/theme/floating.module.css +37 -0
  349. package/src/theme/floating.module.css.d.ts +3 -0
  350. package/src/theme/index.ts +46 -8
  351. package/src/theme/nav-link.module.css +7 -0
  352. package/src/theme-lab/derive-controls.tsx +2 -0
  353. package/src/tokens/index.ts +20 -30
  354. package/src/tokens/palette.ts +6 -28
  355. package/src/tokens/size-classes.ts +46 -0
  356. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  357. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -177,12 +177,7 @@ const SPACE_ANCHORS_BASE = {
177
177
  * rowLineHeight * text.md`) — so a `size="ctl"` button sitting beside any of them read 2px short
178
178
  * and the whole band looked unresolved. 32 lands the entire chrome tier on ONE number, which is
179
179
  * also the smallest square that still holds a 16px icon with a 8px surround. */
180
- controlHeightCtl: 32,
181
- /** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
182
- * `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
183
- * `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
184
- * table. Floored at 30px in {@link deriveSpacing} (C15's "floor 30 at density −3"). */
185
- touchControlHeight: 36
180
+ controlHeightCtl: 32
186
181
  };
187
182
  const SPACE_SCALE_BASE = {
188
183
  xs: 11,
@@ -421,9 +416,8 @@ const SPACE_STEP_BASE = {
421
416
  *
422
417
  * 44, down from 48. The band holds ONE `ctl`-tier row (32 since this pass), so 48 left 8px of
423
418
  * dead air above and below it — the "too much spacing at the top bar" the header reads as. 44
424
- * keeps 6px of breathing room a side, still clears `touchControlHeight` (36) below `sm` and
425
- * `controlHeight` (42) for a `size="md"` control, and is the Apple HIG 44pt figure the rest of
426
- * this table already floors its touch targets at. */
419
+ * keeps 6px of breathing room a side, still clears `controlHeight` (42) for a `size="md"`
420
+ * control, and is the 44px touch floor (WCAG 2.5.5). */
427
421
  appShellHeaderHeight: 44,
428
422
  /** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
429
423
  * permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
@@ -498,12 +492,9 @@ const SPACE_STEP_BASE = {
498
492
  mobileNavTabInsetY: 2,
499
493
  /** Active-pill inset around the slot icon, horizontal. */
500
494
  mobileNavTabInsetX: 12,
501
- /** Menu/sheet row height. Floored at 40px in `deriveSpacing` — a deliberate design choice, not
502
- * a WCAG floor: it's the row height shared with the desktop sidebar rows, above the WCAG 2.5.8
503
- * AA minimum (24px) and below the 2.5.5 AAA figure (44px, the size `mobileNavBarHeight`/
504
- * `sheetRowHeight` still hold to) — the shortest box that still reads as ONE row vocabulary with
505
- * the sidebar instead of a bespoke touch size. */
506
- mobileNavRowHeight: 40,
495
+ /** Menu/sheet row height — the WCAG 2.5.5 44px target, floored at 44 in `deriveSpacing` (the
496
+ * pointer tier's `--vx-hit` is the same number under `(pointer: coarse)`). */
497
+ mobileNavRowHeight: 44,
507
498
  /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
508
499
  * `sidebarSettingsMenuWidth` directly above. */
509
500
  mobileNavMenuWidth: 232,
@@ -643,19 +634,7 @@ const SPACE_FIXED = {
643
634
  * to 1 at density's floor level -3 and 3 at its ceiling level +3 — the widest swing `deriveSpacing`'s
644
635
  * `[-3, 3]` range can produce) for no compensating benefit, since the two radii it sits between
645
636
  * never move independently of each other. */
646
- segmentedTrackInset: 2,
647
- /**
648
- * The WCAG 2.5.5/2.5.8 touch-target floor (44px) for a hit area that has no `ctl`-tier home to
649
- * size it — a raw CSS-module rule bumping a control's hit area under `@media (pointer: coarse)`
650
- * (C5 consolidation: argo carried three such rules with a hand-picked 44/46px literal apiece).
651
- * Density-exempt for the same reason `touchControlHeight`'s FLOOR is: a target this small must
652
- * never shrink further, whatever the density knob says elsewhere. Unlike the rest of this
653
- * object, this ONE key IS emitted as a `--vx-*` var (`tokens/index.ts`'s `space-touch-target`) —
654
- * the others stay JS-only because nothing outside the framework's own components reads them, but
655
- * this floor's whole purpose is being read by a CONSUMER's own `.module.css`, which cannot
656
- * import a JS ref at all.
657
- */
658
- spaceTouchTarget: 44
637
+ segmentedTrackInset: 2
659
638
  };
660
639
  function scaleSpace(value, level, multiplier) {
661
640
  if (level === 0) return value;
@@ -687,11 +666,10 @@ function deriveSpacing(level) {
687
666
  anchors.controlHeightTag = Math.max(18, anchors.controlHeightTag);
688
667
  anchors.controlHeightWidget = Math.max(22, anchors.controlHeightWidget);
689
668
  anchors.controlHeightCtl = Math.max(28, anchors.controlHeightCtl);
690
- anchors.touchControlHeight = Math.max(30, anchors.touchControlHeight);
691
669
  const mappedStep = mapSpaceGroup(SPACE_STEP_BASE, level, multiplier);
692
670
  mappedStep.sidebarSearchTriggerHeight = Math.max(24, mappedStep.sidebarSearchTriggerHeight);
693
671
  mappedStep.mobileNavBarHeight = Math.max(49, mappedStep.mobileNavBarHeight);
694
- mappedStep.mobileNavRowHeight = Math.max(40, mappedStep.mobileNavRowHeight);
672
+ mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight);
695
673
  mappedStep.sheetRowHeight = Math.max(44, mappedStep.sheetRowHeight);
696
674
  const step = {
697
675
  ...mappedStep,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The two responsive tables (`docs/DESIGN-CORE.md` § Layout, elevation, shapes) — pure data, Mantine-free. Each
3
+ * value is the class's minimum width in px.
4
+ *
5
+ * A `@media`/`@container` condition cannot read a custom property, so CSS writes the literal and
6
+ * `breakpoint-literals.test.ts` pins every literal to these tables.
7
+ */
8
+ /** Viewport classes — shell chrome only (bottom bar / rail / sidebar, aside docking). */
9
+ export declare const SIZE_CLASSES: {
10
+ readonly compact: 0;
11
+ readonly medium: 840;
12
+ readonly expanded: 1200;
13
+ };
14
+ /** A component's own inline size — card chrome, chart tier, forms. No CSS reads it yet. */
15
+ export declare const CONTAINER_CLASSES: {
16
+ readonly micro: 0;
17
+ readonly compact: 240;
18
+ readonly regular: 480;
19
+ readonly wide: 800;
20
+ };
21
+ export type ContainerClass = keyof typeof CONTAINER_CLASSES;
22
+ /** The class a MEASURED inline size falls in (`px` is a min-width boundary, so 240 is `compact`). */
23
+ export declare function resolveContainerClass(px: number): ContainerClass;
24
+ /**
25
+ * `theme.breakpoints`, derived — a consumer cannot retune it. Mantine needs all five keys:
26
+ * `base` (no key) is compact, `xs`/`sm` open the medium class, `md`/`lg`/`xl` the expanded one, so
27
+ * every `visibleFrom`/`hiddenFrom` in the package stays "desktop chrome" from `sm` up.
28
+ */
29
+ export declare const SIZE_CLASS_BREAKPOINTS: {
30
+ readonly xs: `${number}em`;
31
+ readonly sm: `${number}em`;
32
+ readonly md: `${number}em`;
33
+ readonly lg: `${number}em`;
34
+ readonly xl: `${number}em`;
35
+ };
36
+ /** A px width as the `em` length media queries use (16px root). */
37
+ export declare function toEm(px: number): string;
38
+ /** The `max-width` complement of a class boundary — 0.1px under it, Mantine's own convention. */
39
+ export declare function sizeClassMaxEm(minPx: number): string;
40
+ //# sourceMappingURL=size-classes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"size-classes.d.ts","sourceRoot":"","sources":["../../src/tokens/size-classes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,yFAAyF;AACzF,eAAO,MAAM,YAAY;;;;CAAuD,CAAA;AAEhF,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB;;;;;CAA+D,CAAA;AAE7F,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,iBAAiB,CAAA;AAE3D,qGAAqG;AACrG,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,CAKhE;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;;CAMzB,CAAA;AAEV,mEAAmE;AACnE,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEvC;AAED,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,29 @@
1
+ const SIZE_CLASSES = { compact: 0, medium: 840, expanded: 1200 };
2
+ const CONTAINER_CLASSES = { micro: 0, compact: 240, regular: 480, wide: 800 };
3
+ function resolveContainerClass(px) {
4
+ if (px >= CONTAINER_CLASSES.wide) return "wide";
5
+ if (px >= CONTAINER_CLASSES.regular) return "regular";
6
+ if (px >= CONTAINER_CLASSES.compact) return "compact";
7
+ return "micro";
8
+ }
9
+ const SIZE_CLASS_BREAKPOINTS = {
10
+ xs: `${SIZE_CLASSES.medium / 16}em`,
11
+ sm: `${SIZE_CLASSES.medium / 16}em`,
12
+ md: `${SIZE_CLASSES.expanded / 16}em`,
13
+ lg: `${SIZE_CLASSES.expanded / 16}em`,
14
+ xl: `${SIZE_CLASSES.expanded / 16}em`
15
+ };
16
+ function toEm(px) {
17
+ return `${Number((px / 16).toFixed(5))}em`;
18
+ }
19
+ function sizeClassMaxEm(minPx) {
20
+ return toEm(minPx - 0.1);
21
+ }
22
+ export {
23
+ CONTAINER_CLASSES,
24
+ SIZE_CLASSES,
25
+ SIZE_CLASS_BREAKPOINTS,
26
+ resolveContainerClass,
27
+ sizeClassMaxEm,
28
+ toEm
29
+ };
package/dist/tokens.css CHANGED
@@ -5,7 +5,7 @@
5
5
  --vx-radius-fine: 4px;
6
6
  --vx-radius-floating: 8px;
7
7
  --vx-radius-pill: 9999px;
8
- --vx-space-touch-target: 44px;
8
+ --vx-hit: 24px;
9
9
  --vx-space-row-inset-x: 10px;
10
10
  --vx-space-row-inset-y: 6px;
11
11
  --vx-space-row-line-height: 1.35;
@@ -14,7 +14,6 @@
14
14
  --vx-space-control-height-tag: 1.25rem;
15
15
  --vx-space-control-height-widget: 1.5rem;
16
16
  --vx-space-control-height-ctl: 2rem;
17
- --vx-space-touch-control-height: 2.25rem;
18
17
  --vx-space-stack-xs: 4px;
19
18
  --vx-space-stack-sm: 8px;
20
19
  --vx-space-stack-md: 12px;
@@ -103,7 +102,7 @@
103
102
  --vx-space-mobile-nav-icon-size: 24px;
104
103
  --vx-space-mobile-nav-tab-inset-y: 2px;
105
104
  --vx-space-mobile-nav-tab-inset-x: 12px;
106
- --vx-space-mobile-nav-row-height: 40px;
105
+ --vx-space-mobile-nav-row-height: 44px;
107
106
  --vx-space-agent-rail-inset-x: 10px;
108
107
  --vx-space-agent-part-gap-top: 6px;
109
108
  --vx-space-agent-code-inset: 8px;
@@ -190,12 +189,17 @@
190
189
  --vx-badRef: var(--vx-bad-ref);
191
190
  --vx-warnRef: var(--vx-warn-ref);
192
191
  }
193
- @media (max-width: 47.99375em) {
192
+ @media (max-width: 52.49375em) {
194
193
  :root {
195
194
  --vx-space-card-inset-x: 11px;
196
195
  --vx-space-section-gap: 8px;
197
196
  }
198
197
  }
198
+ @media (pointer: coarse) {
199
+ :root {
200
+ --vx-hit: 44px;
201
+ }
202
+ }
199
203
  :root,
200
204
  html[data-mantine-color-scheme='dark'] {
201
205
  --vx-status-excellent: #43bf4d;
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.30.2
1
+ # basalt-ui v1.32.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,11 +19,11 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.30.2
22
+ Version: 1.32.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
26
- Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useBreakpoint, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
26
+ Description: BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, PageBar, PageAside, useSizeClass (useBreakpoint on notice), NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, WidgetHeader, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone), ConnectivityProvider/useConnectivity/ConnectivityIndicator (auto-mounted, C1: absorbed from the dropped ./connectivity), createBasaltQueryClient/unwrap/BasaltQueryDevtools (C1: absorbed from the dropped ./query), BasaltDevDock (C5: lazy Router/Query devtools + theme lab in one dock)
27
27
  Layer: mantine-coupled
28
28
  Rule: basalt-mantine
29
29
  OptionalPeers: react-markdown@^10.1.0, remark-gfm@^4.0.1, rehype-sanitize@^6.0.0, remend@1.3.0, shiki@^4.3.0, beautiful-mermaid@^1.1.0, @tanstack/react-router-devtools@^1.166.13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.30.2",
3
+ "version": "1.32.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -162,19 +162,20 @@
162
162
  "clean": "rm -rf dist"
163
163
  },
164
164
  "devDependencies": {
165
- "@arethetypeswrong/cli": "0.18.3",
166
- "@fontsource-variable/hubot-sans": "5.2.8",
167
- "@fontsource-variable/jetbrains-mono": "5.2.8",
168
- "@fontsource-variable/nunito-sans": "5.2.7",
165
+ "@arethetypeswrong/cli": "0.18.5",
166
+ "@fontsource-variable/hubot-sans": "5.3.0",
167
+ "@fontsource-variable/jetbrains-mono": "5.3.0",
168
+ "@fontsource-variable/nunito-sans": "5.3.0",
169
169
  "@mantine/dates": "9.3.0",
170
- "@shikijs/langs": "^4.3.1",
171
- "@shikijs/themes": "^4.3.1",
170
+ "@shikijs/langs": "^4.4.3",
171
+ "@shikijs/themes": "^4.4.3",
172
172
  "@tanstack/react-hotkeys": "0.10.0",
173
- "@tanstack/react-router-devtools": "^1.166.13",
174
- "@types/bun": "^1.3.14",
175
- "@types/hast": "^3.0.4",
176
- "@types/react": "^19.2.17",
177
- "@types/react-dom": "^19.2.3",
173
+ "@tanstack/react-router-devtools": "^1.167.2",
174
+ "@testing-library/react": "16.3.3",
175
+ "@types/bun": "^1.4.2",
176
+ "@types/hast": "^3.0.5",
177
+ "@types/react": "^19.3.0",
178
+ "@types/react-dom": "^19.3.0",
178
179
  "@visx/axis": "4.0.0",
179
180
  "@visx/curve": "4.0.0",
180
181
  "@visx/event": "4.0.0",
@@ -184,19 +185,19 @@
184
185
  "@visx/scale": "4.0.0",
185
186
  "@visx/shape": "4.0.0",
186
187
  "@visx/threshold": "4.0.0",
187
- "ai": "^7.0.15",
188
+ "ai": "^7.0.107",
188
189
  "beautiful-mermaid": "^1.1.3",
189
190
  "motion": "12.42.0",
190
- "publint": "0.3.21",
191
+ "publint": "0.3.24",
191
192
  "react-markdown": "^10.1.0",
192
193
  "rehype-sanitize": "6.0.0",
193
194
  "remark-gfm": "^4.0.1",
194
- "remend": "1.3.0",
195
- "shiki": "^4.3.1",
195
+ "remend": "1.3.1",
196
+ "shiki": "^4.4.3",
196
197
  "tsup": "^8.5.1",
197
198
  "typescript": "^6.0.3",
198
199
  "use-stick-to-bottom": "^1.1.6",
199
- "vite": "^8.0.16",
200
+ "vite": "^8.3.0",
200
201
  "vite-plugin-pwa": "^1.3.0"
201
202
  },
202
203
  "peerDependencies": {
@@ -36,13 +36,14 @@
36
36
  * onSubmit={({ text, attachments }) => send(text, attachments)}
37
37
  * />
38
38
  */
39
- import { ActionIcon, Group, Kbd, Stack, Text, Textarea } from '@mantine/core'
39
+ import { ActionIcon, Box, Group, Kbd, Stack, Text, Textarea } from '@mantine/core'
40
40
  import type { ClipboardEvent, ComponentProps, JSX, KeyboardEvent, ReactNode, Ref } from 'react'
41
41
  import { useImperativeHandle, useLayoutEffect, useRef, useSyncExternalStore } from 'react'
42
42
  import { cx } from '../common/props'
43
43
  import type { BasaltProps, SlotStylesProps } from '../common/props'
44
44
  import { readPersistedValue } from '../state'
45
45
  import { VX } from '../tokens'
46
+ import { useKeyboardInsetRef } from './use-keyboard-inset'
46
47
 
47
48
  /** A minimal, dependency-free send-arrow glyph (icons are passed in as ReactNode elsewhere; this
48
49
  * one is inline since Composer has no icon prop in its contract). */
@@ -202,6 +203,14 @@ export type ComposerSubmit = {
202
203
  /** Stable empty list — keeps the submit payload's `attachments` referentially constant. */
203
204
  const NO_ATTACHMENTS: readonly ComposerAttachment[] = []
204
205
 
206
+ /**
207
+ * The gap between the textarea/actions row and the hint row, inside the inner `Stack` the render
208
+ * below owns exclusively — the keyboard-inset spacer is deliberately NOT one of that Stack's flex
209
+ * children (see the render's own comment), so this value never has to be cancelled back out of
210
+ * anything the spacer contributes.
211
+ */
212
+ const ROOT_GAP = 6
213
+
205
214
  /**
206
215
  * Imperative escape hatch for slot content that must write into the composer from OUTSIDE the
207
216
  * render tree — a voice recorder streaming transcript chunks, a suggestion chip inserting at the
@@ -471,6 +480,13 @@ export function Composer({
471
480
 
472
481
  useImperativeHandle(ref, () => ({ insertText, setValue, focus: focusTextarea }))
473
482
 
483
+ // Pins the composer above a coarse-pointer on-screen keyboard — see use-keyboard-inset.ts. The
484
+ // published custom property defaults to 0px via CSS, so a Composer with no `visualViewport`
485
+ // support (SSR, an unsupporting engine) pays for exactly one no-op custom property on a trailing
486
+ // spacer (see the render below) rather than an inline `paddingBottom` that could ever beat a
487
+ // consumer's own `className`/`classNames.root` padding.
488
+ const keyboardInsetRef = useKeyboardInsetRef()
489
+
474
490
  const submit = (): void => {
475
491
  if (inputDisabled || !hasPayload) return
476
492
  const submittedText = trimmed
@@ -572,51 +588,62 @@ export function Composer({
572
588
  const showActions = showStop || showSend
573
589
 
574
590
  return (
575
- <Stack
576
- gap={6}
577
- className={cx(classNames?.root, className)}
578
- {...(style !== undefined && { style })}
579
- >
580
- <Group gap="xs" align="flex-end" wrap="nowrap">
581
- {leftSection}
582
- <Textarea {...textareaProps} ref={textareaRef} />
583
- {rightSection}
584
- {showActions && (
585
- <Group gap="xs" wrap="nowrap" className={cx(classNames?.actions)}>
586
- {showStop ? (
587
- <ActionIcon
588
- size={42}
589
- radius="md"
590
- variant="filled"
591
- color="red"
592
- onClick={onStop}
593
- aria-label="Stop generating"
594
- >
595
- <StopGlyph />
596
- </ActionIcon>
597
- ) : null}
598
- {showSend ? (
599
- <ActionIcon
600
- size={42}
601
- radius="md"
602
- variant="filled"
603
- onClick={submit}
604
- disabled={inputDisabled || !hasPayload}
605
- aria-label="Send message"
606
- // Send action (docs/DESIGN-SPEC.md §5): the one accent-filled control. It needs no
607
- // color override — `filled` resolves through the theme to `--vx-accent-fill` /
608
- // `--vx-on-accent`. (It used to hand-wire those two vars inline, which is why this
609
- // was the ONLY filled control that stayed legible while the rest of the chrome went
610
- // through Mantine's scheme-blind autoContrast. The theme owns it now, and hover
611
- // works again — an inline style can't express a `:hover` state.)
612
- >
613
- <SendGlyph />
614
- </ActionIcon>
615
- ) : null}
616
- </Group>
617
- )}
618
- </Group>
619
- {hint === undefined ? <DefaultHint /> : hint}
620
- </Stack>
591
+ <Box ref={keyboardInsetRef} className={cx(classNames?.root, className)} style={style}>
592
+ <Stack gap={ROOT_GAP}>
593
+ <Group gap="xs" align="flex-end" wrap="nowrap">
594
+ {leftSection}
595
+ <Textarea {...textareaProps} ref={textareaRef} />
596
+ {rightSection}
597
+ {showActions && (
598
+ <Group gap="xs" wrap="nowrap" className={cx(classNames?.actions)}>
599
+ {showStop ? (
600
+ <ActionIcon
601
+ size={42}
602
+ radius="md"
603
+ variant="filled"
604
+ color="red"
605
+ onClick={onStop}
606
+ aria-label="Stop generating"
607
+ >
608
+ <StopGlyph />
609
+ </ActionIcon>
610
+ ) : null}
611
+ {showSend ? (
612
+ <ActionIcon
613
+ size={42}
614
+ radius="md"
615
+ variant="filled"
616
+ onClick={submit}
617
+ disabled={inputDisabled || !hasPayload}
618
+ aria-label="Send message"
619
+ // Send action (docs/DESIGN-SPEC.md §5): the one accent-filled control. It needs
620
+ // no color override — `filled` resolves through the theme to `--vx-accent-fill`
621
+ // / `--vx-on-accent`. (It used to hand-wire those two vars inline, which is why
622
+ // this was the ONLY filled control that stayed legible while the rest of the
623
+ // chrome went through Mantine's scheme-blind autoContrast. The theme owns it
624
+ // now, and hover works again — an inline style can't express a `:hover` state.)
625
+ >
626
+ <SendGlyph />
627
+ </ActionIcon>
628
+ ) : null}
629
+ </Group>
630
+ )}
631
+ </Group>
632
+ {hint === undefined ? <DefaultHint /> : hint}
633
+ </Stack>
634
+ {/* Pins the composer above a coarse-pointer on-screen keyboard by growing THIS spacer, not
635
+ by padding the root — so a consumer's own `className`/`classNames.root` padding-bottom is
636
+ never overridden (see use-keyboard-inset.ts). It is a SIBLING of the gapped Stack above,
637
+ not one of its flex children, precisely so it never has to cancel that Stack's `gap`
638
+ back out: a consumer's own `style` (which lands on this outer, non-flex root, not on the
639
+ inner Stack) can carry an unrelated `gap` without ever touching what this spacer
640
+ contributes. A closed keyboard (the var's `0px` fallback) then really is 0 extra height,
641
+ regardless of anything the caller's `style`/`className` does to the root. (This used to
642
+ be the Stack's own trailing child, cancelled via a negative `margin-top` reading
643
+ `--stack-gap` back — exact against Stack's OWN `gap` prop, but blind to a `gap` a
644
+ consumer added on top through `style`, which changes the rendered gap without moving that
645
+ var.) */}
646
+ <Box aria-hidden style={{ height: 'var(--vx-keyboard-inset, 0px)' }} />
647
+ </Box>
621
648
  )
622
649
  }
@@ -4,7 +4,7 @@
4
4
  * Composes `ThreadFeed` + a new-thread `Composer` (left/main pane) with `ThreadDetailPanel`
5
5
  * (right pane) over `useAgentThreadRuns`, so a consumer wires one component instead of hand-
6
6
  * assembling the pieces. Responsive: wide viewports show both panes side by side; narrow
7
- * viewports (`<= 768px`) show exactly one — the feed when no thread is open, a full-screen
7
+ * viewports (the `compact` size class, < 840px) show exactly one — the feed when no thread is open, a full-screen
8
8
  * detail panel once one is.
9
9
  *
10
10
  * `useThreads` is a prop, not called internally, because `createThreadsStore` must be invoked
@@ -32,14 +32,14 @@
32
32
  * )
33
33
  * }
34
34
  */
35
- import { Box, Divider, Flex, Skeleton, Stack, Text, useMantineTheme } from '@mantine/core'
36
- import { useMediaQuery } from '@mantine/hooks'
35
+ import { Box, Divider, Flex, Skeleton, Stack, Text } from '@mantine/core'
37
36
  import type { JSX, ReactNode } from 'react'
38
37
  import type { AgentPart, AgentTransport, OutcomeResolver, ThreadsStore } from '../agent'
39
38
  import { useAgentThreadRuns } from '../agent'
40
39
  import { cx } from '../common/props'
41
40
  import type { BasaltProps, SlotStylesProps } from '../common/props'
42
41
  import { assertRequiredProps } from '../common/validate'
42
+ import { useSizeClass } from '../shell/use-size-class'
43
43
  import { VX } from '../tokens'
44
44
  import { Composer } from './composer'
45
45
  import { ThreadDetailPanel } from './thread-detail-panel'
@@ -53,7 +53,7 @@ const FULL_HEIGHT_STYLE = { height: '100%' } as const
53
53
 
54
54
  /** The four boxes `ThreadWorkspace` paints: `root` is the outermost pane split, `feed` is the
55
55
  * thread-list pane, `composer` is the new-thread composer pinned under the feed, `detail` is the
56
- * right-hand (or, below 768px, full-screen) open-thread pane. */
56
+ * right-hand (or, in the `compact` size class, full-screen) open-thread pane. */
57
57
  export type ThreadWorkspaceSlot = 'root' | 'feed' | 'composer' | 'detail'
58
58
 
59
59
  export type ThreadWorkspaceProps = BasaltProps &
@@ -146,7 +146,7 @@ function FeedErrorState(): JSX.Element {
146
146
  /**
147
147
  * A two-pane master-detail thread workspace: a scrollable feed of threads with an anchored
148
148
  * new-thread composer, and a detail panel for the open thread's live transcript. Collapses to a
149
- * single pane below 768px.
149
+ * single pane in the `compact` size class (< 840px).
150
150
  *
151
151
  * @example
152
152
  * <ThreadWorkspace useThreads={useThreads} transport={transport} resolveOutcome={heuristicOutcome} />
@@ -165,12 +165,9 @@ export function ThreadWorkspace({
165
165
  assertRequiredProps('ThreadWorkspace', { useThreads }, ['useThreads'])
166
166
  const store = useThreads()
167
167
  const runs = useAgentThreadRuns({ transport, store, resolveOutcome })
168
- // Matches `shell/page-aside.tsx`'s own `theme.breakpoints.sm` desktop check — read off the theme,
169
- // not hardcoded, so a consumer that retunes `breakpoints.sm` moves the aside and this workspace
170
- // together. `max-width` is the min-width complement: `sm − 0.00625em` (the same 0.1px offset
171
- // Mantine's own CSS breakpoints use), so the two checks never straddle the same pixel.
172
- const theme = useMantineTheme()
173
- const narrow = useMediaQuery(`(max-width: calc(${theme.breakpoints.sm} - 0.00625em))`)
168
+ // Narrow = the compact size class — the same viewport axis as the shell, and SSR/first-frame
169
+ // correct (the provider's `sizeClassHint` is the server snapshot, no post-mount flip).
170
+ const narrow = useSizeClass() === 'compact'
174
171
 
175
172
  const threads = store.threads
176
173
  const activeThread = threads.find((thread) => thread.id === store.activeId) ?? null
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `useKeyboardInsetRef` — publishes the live on-screen-keyboard inset as `--vx-keyboard-inset` on
3
+ * a ref'd root element, so `Composer` can stay pinned above a coarse-pointer on-screen keyboard
4
+ * without the consumer hand-rolling a `dvh` clamp (`docs/MATURATION-LEDGER.md`, the wave 10
5
+ * agent-chat finding). Internal — not exported from the package.
6
+ *
7
+ * Reads `window.visualViewport`, guarded exactly like `useSizeClass`'s `matchMedia` guard
8
+ * (`shell/use-size-class.ts`): absent in SSR and in an engine with no `visualViewport` support
9
+ * (older Safari, and happy-dom/jsdom in tests), where the property is simply never published and
10
+ * CSS's `var(--vx-keyboard-inset, 0px)` fallback carries the zero.
11
+ *
12
+ * The inset is `document.documentElement.clientHeight - visualViewport.height -
13
+ * visualViewport.offsetTop`, floored at 0 — the slice of the LAYOUT viewport's bottom edge the
14
+ * visual viewport no longer covers. `clientHeight` (not `window.innerHeight`) is the layout
15
+ * viewport's height on every engine that also implements `visualViewport`. Both `resize` (fires
16
+ * when the on-screen keyboard opens/closes and shrinks `visualViewport.height`) and `scroll`
17
+ * (fires when the visual viewport's origin moves with no size change — iOS Safari scrolling the
18
+ * page to keep a focused field in view) are subscribed, because either can move the inset on its
19
+ * own.
20
+ *
21
+ * The state-node + layout-effect shape mirrors `shell/page-bar.tsx`'s `useMeasuredHeightVar`: a
22
+ * callback ref stored in state (so the effect re-runs once the node actually mounts) rather than a
23
+ * plain `RefObject` (which is not itself a dependency).
24
+ *
25
+ * Returns the callback ref only. It intentionally does NOT track the live inset (open/closed) —
26
+ * that stays an imperative, no-re-render style write, which is the entire point of publishing it
27
+ * outside React state.
28
+ */
29
+ import { useCallback, useState } from 'react'
30
+ import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
31
+
32
+ const KEYBOARD_INSET_VAR = '--vx-keyboard-inset'
33
+
34
+ export function useKeyboardInsetRef(): (el: HTMLElement | null) => void {
35
+ const [node, setNode] = useState<HTMLElement | null>(null)
36
+
37
+ const attach = useCallback((el: HTMLElement | null) => {
38
+ setNode(el)
39
+ }, [])
40
+
41
+ useIsomorphicLayoutEffect(() => {
42
+ if (node === null) return
43
+ if (typeof window === 'undefined') return
44
+ const viewport = window.visualViewport
45
+ // `undefined`, not the typed `null`, is what an engine with no support actually returns
46
+ // (confirmed: happy-dom — this package's test DOM — has no `visualViewport` property at all).
47
+ if (viewport === null || viewport === undefined) return
48
+
49
+ const publish = (): void => {
50
+ const inset = Math.max(
51
+ 0,
52
+ document.documentElement.clientHeight - viewport.height - viewport.offsetTop,
53
+ )
54
+ node.style.setProperty(KEYBOARD_INSET_VAR, `${inset}px`)
55
+ }
56
+
57
+ publish()
58
+ viewport.addEventListener('resize', publish)
59
+ viewport.addEventListener('scroll', publish)
60
+ return () => {
61
+ viewport.removeEventListener('resize', publish)
62
+ viewport.removeEventListener('scroll', publish)
63
+ node.style.removeProperty(KEYBOARD_INSET_VAR)
64
+ }
65
+ }, [node])
66
+
67
+ return attach
68
+ }