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
package/src/styles.css CHANGED
@@ -210,6 +210,47 @@
210
210
  background-color: var(--basalt-scrollbar-thumb-hover);
211
211
  }
212
212
 
213
+ /* Bottom-anchored toasts clear the shell's bottom bar + safe area (docs/CONTROLS-SPEC.md §2). */
214
+ .mantine-Notifications-root[data-position^='bottom'] {
215
+ bottom: calc(
216
+ var(--app-shell-footer-offset, 0px) + env(safe-area-inset-bottom, 0px) +
217
+ var(--mantine-spacing-md)
218
+ );
219
+ }
220
+
221
+ /* Pointer tier: an invisible hit-area expansion to at least `--vx-hit` (44px coarse, 24px fine),
222
+ no layout change. `::after`, not `::before` — Mantine's Button owns `::before` for its loading
223
+ overlay. `overflow: visible` is load-bearing: Button/ActionIcon roots clip to their box, which
224
+ would clip the overlay; the clip only serves their `[data-loading]` overlay, so it stays then.
225
+ Adopters: `data-basalt-hit` on the control, or `data-basalt-hit-scope` on a home whose
226
+ `button`/`a` descendants (consumer-supplied) adopt it.
227
+ DECISION: the axis is `pointer: coarse` (the PRIMARY input, via `--vx-hit`), never
228
+ `any-pointer` — a touch laptop's mouse-first session must not inflate every hit area.
229
+ OVERLAP CAP: a host with neighbours sets `--vx-hit-gap` (its real gap between controls); the
230
+ expansion is then limited to that gap in total (half per side), so adjacent hit boxes never
231
+ overlap and a later sibling never steals a tap. Unset = uncapped (an isolated control). The
232
+ `+ 2px` is the control's own 1px border (Mantine buttons carry one, transparent or not): `100%`
233
+ is the PADDING box, the gap is measured between border boxes.
234
+ `position: relative` is the ::after's containing block; an adopter that must be
235
+ absolute/sticky sets its own `position` in an unlayered rule, which outranks this layer. */
236
+ :is([data-basalt-hit], [data-basalt-hit-scope] :is(button, a)) {
237
+ position: relative;
238
+ }
239
+
240
+ :is([data-basalt-hit], [data-basalt-hit-scope] :is(button, a)):not([data-loading]) {
241
+ overflow: visible;
242
+ }
243
+
244
+ :is([data-basalt-hit], [data-basalt-hit-scope] :is(button, a))::after {
245
+ content: '';
246
+ position: absolute;
247
+ top: 50%;
248
+ left: 50%;
249
+ translate: -50% -50%;
250
+ width: min(max(100%, var(--vx-hit)), calc(100% + 2px + var(--vx-hit-gap, 100vmax)));
251
+ height: min(max(100%, var(--vx-hit)), calc(100% + 2px + var(--vx-hit-gap, 100vmax)));
252
+ }
253
+
213
254
  /* Print: hide the BasaltShell chrome so only page content prints.
214
255
  Scoped to AppShell's own class hooks, NOT to bare nav/header/footer. The element form matched
215
256
  any landmark on the page — a consumer with a real <nav> and no BasaltShell silently printed a
package/src/surfaces.ts CHANGED
@@ -66,6 +66,7 @@ export const PLUGIN_RULE_ID_LIST = [
66
66
  'agent-no-raw-usechat',
67
67
  'ai-sdk-major',
68
68
  'no-import-meta-env',
69
+ 'raw-breakpoint',
69
70
  ] as const
70
71
 
71
72
  /** One registered oxlint plugin rule id — the literal union of {@link PLUGIN_RULE_ID_LIST}. */
@@ -262,12 +263,19 @@ export const SURFACES = {
262
263
  'inline-display',
263
264
  'raw-html-layout',
264
265
  'hidden-inline-style',
266
+ // The CSS-text twin of `raw-breakpoint` (below) — a width @media, an undeclared @container
267
+ // name/literal, or a non-coarse pointer query, the shape a JSX-only AST rule cannot see.
268
+ 'raw-media-query',
265
269
  ],
266
270
  // The chrome half of the plugin: the shell, the card idiom, the page title and the scroll
267
271
  // doctrine. `hand-rolled-shell` sits here rather than on a shell-shaped surface because
268
272
  // `BasaltShell` IS the root barrel's promise (docs/CONTROLS-SPEC.md §6 wave 6).
269
273
  pluginRules: [
270
274
  'hand-rolled-shell',
275
+ // The size-class axis's one JSX escape hatch (docs/CONTROLS-SPEC.md §6) is reserved
276
+ // for BasaltShell/AppSidebar/MobileNav/AppBrand (`SHELL_HOME_NAMES`, configs/oxlint-plugin.js)
277
+ // — the same shell promise `hand-rolled-shell` polices, one axis over.
278
+ 'raw-breakpoint',
271
279
  'card-inset',
272
280
  'in-body-page-title',
273
281
  'raw-scroll-container',
@@ -291,7 +299,7 @@ export const SURFACES = {
291
299
  "no second cssVariablesResolver — don't hand-build createTheme or re-add the resolver basalt already installs",
292
300
  ],
293
301
  description:
294
- '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)',
302
+ '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)',
295
303
  optionalPeers: [
296
304
  'react-markdown',
297
305
  'remark-gfm',
@@ -61,6 +61,13 @@ html[data-mantine-color-scheme] .inputWrapper[data-variant='default']:not([data-
61
61
  box-shadow: var(--field-depth);
62
62
  }
63
63
 
64
+ /* An input cannot host the `[data-basalt-hit]` ::after overlay, so its touch floor is real height. */
65
+ @media (pointer: coarse) {
66
+ .input {
67
+ min-height: var(--vx-hit);
68
+ }
69
+ }
70
+
64
71
  .input[data-variant='default'] {
65
72
  --field-depth: var(--vx-shadow-raised);
66
73
  }
@@ -48,6 +48,12 @@ import {
48
48
  import type { MantineSize, MantineThemeOverride } from '@mantine/core'
49
49
  import type { ReactNode } from 'react'
50
50
 
51
+ // Mantine's typed prop bags reject `data-*` literals; a spread carries the hit-area attribute in.
52
+ // Lives here (not `./index`, which imports THIS module) rather than the reverse, so `./index`'s own
53
+ // USE of it (alongside `CTL_THEME`/`CtlSlot`) closes no new import cycle. `./index` does not
54
+ // re-export it — it stays internal, reachable only through that import (see `./index`'s own doc).
55
+ export const HIT_ATTR: Record<string, true> = { 'data-basalt-hit': true }
56
+
51
57
  /** The `MantineThemeOverride` a `<CtlSlot>` provides — see this module's doc for the merge/
52
58
  * precedence contract. Exported so a consumer composing its own slot (a bespoke home not covered
53
59
  * by `PageBar`/`Section`/the sidebar blocks) can reach the SAME tier without re-declaring it. */
@@ -63,7 +69,9 @@ export const CTL_THEME: MantineThemeOverride = {
63
69
  NativeSelect: NativeSelect.extend({ defaultProps: { size: 'ctl' } }),
64
70
  // The theme keys are the component names Mantine's own `useProps` looks up — `RadioGroup`, not
65
71
  // `Radio.Group` (`RadioGroup.mjs`: `useProps("RadioGroup", …)`).
66
- Radio: Radio.extend({ defaultProps: { size: 'ctl' } }),
72
+ Radio: Radio.extend({
73
+ defaultProps: { size: 'ctl', attributes: { label: HIT_ATTR } },
74
+ }),
67
75
  // Mantine types `RadioGroup.size` as a CLOSED `MantineSize`, while `Radio`, `Checkbox`,
68
76
  // `Checkbox.Group`, `Switch` and `Switch.Group` all type theirs OPEN
69
77
  // (`MantineSize | (string & {})`) — verified in the installed 9.3.0 `.d.ts` files, so this is an
@@ -71,9 +79,13 @@ export const CTL_THEME: MantineThemeOverride = {
71
79
  // runtime `getSize` interpolates the string identically for all six. The cast restores what
72
80
  // every sibling type already allows; it widens nothing.
73
81
  RadioGroup: Radio.Group.extend({ defaultProps: { size: 'ctl' as MantineSize } }),
74
- Checkbox: Checkbox.extend({ defaultProps: { size: 'ctl' } }),
82
+ Checkbox: Checkbox.extend({
83
+ defaultProps: { size: 'ctl', attributes: { label: HIT_ATTR } },
84
+ }),
75
85
  CheckboxGroup: Checkbox.Group.extend({ defaultProps: { size: 'ctl' } }),
76
- Switch: Switch.extend({ defaultProps: { size: 'ctl' } }),
86
+ Switch: Switch.extend({
87
+ defaultProps: { size: 'ctl', attributes: { track: HIT_ATTR } },
88
+ }),
77
89
  SwitchGroup: Switch.Group.extend({ defaultProps: { size: 'ctl' } }),
78
90
  },
79
91
  }
@@ -33,3 +33,39 @@
33
33
  .menuItem[data-menu-active]:not(:disabled):not([data-disabled]) {
34
34
  background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent);
35
35
  }
36
+
37
+ /*
38
+ * Modal as a bottom sheet at the `compact` size class (docs/DESIGN-CORE.md § Layout, elevation,
39
+ * shapes): anchored
40
+ * to the bottom edge, full width, radius on the top corners only, safe-area bottom padding. The
41
+ * literal is `sizeClassMaxEm(SIZE_CLASSES.medium)`, pinned by `breakpoint-literals.test.ts`.
42
+ * `.sheetBody` is the Modal body; it reaches the trailing button row `overlays.confirm`
43
+ * renders (Mantine `Group`), stretching the buttons to full width at the `--vx-hit` floor.
44
+ */
45
+ @media (max-width: 52.49375em) {
46
+ .sheetInner {
47
+ align-items: flex-end;
48
+ padding: 0;
49
+ }
50
+
51
+ .sheetContent {
52
+ flex: 0 0 100%;
53
+ max-width: 100%;
54
+ /* theme-allow css-raw-surface — top corners only, the shape of a sheet. `!important`: the
55
+ Modal content is a Paper whose theme `styles.root` writes `border-radius` inline. */
56
+ border-radius: var(--vx-radius-card) var(--vx-radius-card) 0 0 !important;
57
+ }
58
+
59
+ .sheetBody {
60
+ padding-bottom: calc(var(--mantine-spacing-md) + env(safe-area-inset-bottom, 0px));
61
+ }
62
+
63
+ .sheetBody > :global(.mantine-Group-root):last-child {
64
+ flex-wrap: nowrap;
65
+ }
66
+
67
+ .sheetBody > :global(.mantine-Group-root):last-child > button {
68
+ flex: 1;
69
+ min-height: var(--vx-hit);
70
+ }
71
+ }
@@ -5,5 +5,8 @@
5
5
  declare const classes: {
6
6
  readonly closeButton: string
7
7
  readonly menuItem: string
8
+ readonly sheetBody: string
9
+ readonly sheetContent: string
10
+ readonly sheetInner: string
8
11
  }
9
12
  export default classes
@@ -43,6 +43,7 @@ import {
43
43
  Notification,
44
44
  NumberInput,
45
45
  Pagination,
46
+ PaginationControl,
46
47
  Paper,
47
48
  PasswordInput,
48
49
  PillsInput,
@@ -81,12 +82,14 @@ import type { PaletteData, RadiusValues, SpaceValues } from '../tokens/palette'
81
82
  import { DEFAULT_DERIVE_CONFIG, isDefaultDeriveConfig, resolveDeriveConfig } from '../tokens/derive'
82
83
  import type { DeriveConfig } from '../tokens/derive'
83
84
  import { pxRem, VX } from '../tokens'
85
+ import { SIZE_CLASS_BREAKPOINTS } from '../tokens/size-classes'
84
86
  import type { BasaltFontsConfig } from '../tokens'
85
87
  import controlsClasses from './controls.module.css'
86
88
  import floatingClasses from './floating.module.css'
87
89
  import navLinkClasses from './nav-link.module.css'
88
90
  import segmentedControlClasses from './segmented-control.module.css'
89
91
  import timelineClasses from './timeline.module.css'
92
+ import { HIT_ATTR } from './ctl-theme'
90
93
 
91
94
  /**
92
95
  * The `fonts` option's resolved shape — defined in `basalt-ui/tokens` (`BasaltFontsConfig`,
@@ -101,7 +104,9 @@ export type { BasaltFontsConfig } from '../tokens'
101
104
  export { useBasaltSpacing } from './use-basalt-spacing'
102
105
 
103
106
  // The `size="ctl"` slot theme (`docs/CONTROLS-SPEC.md` §5) — see `./ctl-theme`'s own doc for the
104
- // merge/precedence contract.
107
+ // merge/precedence contract. `HIT_ATTR` is deliberately NOT in this list: it has no consumer of its
108
+ // own (consolidation doctrine — adopt or delete), and stays reachable only through the internal
109
+ // `./ctl-theme` import above, for this file's own use below.
105
110
  export { CTL_THEME, CtlSlot, type CtlSlotProps } from './ctl-theme'
106
111
 
107
112
  // `ThemeToggle` moved into this directory (C1 consolidation, merging the former `theme-toggle/`
@@ -182,6 +187,12 @@ function pinShades(
182
187
  return out as unknown as MantineColorsTuple
183
188
  }
184
189
 
190
+ // Mantine's typed prop bags reject `data-*` literals; a spread carries the hit-area attribute in.
191
+ // Defined in `./ctl-theme` (imported above) rather than here: `CTL_THEME` there is a top-level
192
+ // `const` built eagerly at module load, so a `HIT_ATTR` defined in THIS file and imported back from
193
+ // `ctl-theme.tsx` would close an import cycle this file's own line 107 doesn't already have — the
194
+ // existing direction (this file depends on `ctl-theme.tsx`, not the reverse) is what keeps it safe.
195
+
185
196
  /** The shipped, config-independent palette data — `baseTheme` / `cssVariablesResolver` below are
186
197
  * built from this once, at module load. */
187
198
  const DEFAULT_PALETTE_DATA = buildPaletteData(DEFAULT_DERIVE_CONFIG)
@@ -422,6 +433,7 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
422
433
  const { radius = DEFAULT_RADIUS_VALUES, spacing = DEFAULT_SPACE_VALUES } = options
423
434
  const { ACCENT, FILL } = data
424
435
  return createTheme({
436
+ breakpoints: SIZE_CLASS_BREAKPOINTS,
425
437
  primaryColor: 'blue',
426
438
  // ONE shade in both schemes — a filled control is a SURFACE, and the accent surface is the same
427
439
  // hex either way (see `ACCENT.accentFill`). The scheme-inverting half of the accent lives in
@@ -670,6 +682,8 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
670
682
  classNames: { wrapper: controlsClasses.inputWrapper, input: controlsClasses.input },
671
683
  }),
672
684
  TextInput: TextInput.extend({ defaultProps: { size: 'md' } }),
685
+ // The stepper chevrons sit ~15px apart, so neither can own a 44px overlay; the coarse
686
+ // `.input` min-height floor (theme/controls.module.css) is the touch floor.
673
687
  NumberInput: NumberInput.extend({ defaultProps: { size: 'md' } }),
674
688
  PasswordInput: PasswordInput.extend({ defaultProps: { size: 'md' } }),
675
689
  Select: Select.extend({ defaultProps: { size: 'md' } }),
@@ -986,10 +1000,16 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
986
1000
  // under it but shows every scrolled row straight through the title once one does (the mobile
987
1001
  // filter sheet, the shell's "More" nav sheet). The header stays sticky; only its own
988
1002
  // background moves back to opaque, matching the content box it sits above rather than the page
989
- // behind it. Title = head font; close button = the ghost idiom (floating.module.css).
1003
+ // behind it. Title = head font; close button = the ghost idiom (floating.module.css). Below the
1004
+ // `medium` size class the Modal is a bottom sheet (floating.module.css `sheet*`).
990
1005
  Modal: Modal.extend({
991
- defaultProps: { radius: radius.floating },
992
- classNames: { close: floatingClasses.closeButton },
1006
+ defaultProps: { radius: radius.floating, closeButtonProps: { ...HIT_ATTR } },
1007
+ classNames: {
1008
+ close: floatingClasses.closeButton,
1009
+ inner: floatingClasses.sheetInner,
1010
+ content: floatingClasses.sheetContent,
1011
+ body: floatingClasses.sheetBody,
1012
+ },
993
1013
  styles: {
994
1014
  content: {
995
1015
  backgroundColor: 'var(--vx-surface-overlay)',
@@ -1067,7 +1087,15 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
1067
1087
  // The v9 thumb indicator is a center dot painted in the TRACK color — over the remapped
1068
1088
  // neutral track it read as an artifact, so it's disabled: clean track, plain thumb, accent
1069
1089
  // fill when checked (`--switch-color` defaults to the primary filled shade = the accent).
1070
- Switch: Switch.extend({ defaultProps: { withThumbIndicator: false } }),
1090
+ // `track` is the Switch's <label>, so the invisible `--vx-hit` overlay (`data-basalt-hit`)
1091
+ // still toggles it. Checkbox/Radio boxes are bare <input>s under a plain <div> — an overlay
1092
+ // there would swallow clicks, so only their `label` (a real <label>) carries it.
1093
+ Switch: Switch.extend({
1094
+ defaultProps: {
1095
+ withThumbIndicator: false,
1096
+ attributes: { track: HIT_ATTR },
1097
+ },
1098
+ }),
1071
1099
  // The tab-list bottom rule (and the inactive-tab hover underline) is a layout separator, not
1072
1100
  // control chrome — it reads the divider token instead of the heavy gray-3/dark-4 line
1073
1101
  // (controls.module.css; custom properties are stripped from `styles` objects). The active tab
@@ -1091,6 +1119,7 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
1091
1119
  // `useStyles`), so re-pointing that var here wins on every render path. Without this, a dark-mode
1092
1120
  // Checkbox/Radio paints a white checkmark on the light accent fill — invisible.
1093
1121
  Checkbox: Checkbox.extend({
1122
+ defaultProps: { attributes: { label: HIT_ATTR } },
1094
1123
  vars: (theme, props) => ({
1095
1124
  root: {
1096
1125
  // `iconColor` is an explicit call-site override, and `outline` paints the mark in the
@@ -1101,6 +1130,7 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
1101
1130
  }),
1102
1131
  }),
1103
1132
  Radio: Radio.extend({
1133
+ defaultProps: { attributes: { label: HIT_ATTR } },
1104
1134
  vars: (theme, props) => ({
1105
1135
  root: {
1106
1136
  '--radio-icon-color':
@@ -1113,6 +1143,12 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
1113
1143
  root: { '--pagination-active-color': onColorFor(theme, props) },
1114
1144
  }),
1115
1145
  }),
1146
+ // `Pagination`'s own controls (prev/next/page buttons) resolve `PaginationControl`'s
1147
+ // defaultProps independently of the parent — `data-basalt-hit` here is what closes the
1148
+ // gap Switch/Checkbox/Radio already close at their own theme entries.
1149
+ PaginationControl: PaginationControl.extend({
1150
+ defaultProps: { ...HIT_ATTR },
1151
+ }),
1116
1152
  Stepper: Stepper.extend({
1117
1153
  vars: (theme, props) => ({ root: { '--stepper-icon-color': onColorFor(theme, props) } }),
1118
1154
  }),
@@ -1556,7 +1592,7 @@ function hasFontsConfig(fonts: BasaltFontsConfig | undefined): fonts is BasaltFo
1556
1592
  /**
1557
1593
  * Build the Basalt theme, optionally merged with consumer overrides. Overrides win on conflict
1558
1594
  * (Mantine `mergeThemeOverrides` is last-wins), so a consumer can retune any field without
1559
- * forking the base.
1595
+ * forking the base — except `breakpoints`, which is always derived from `SIZE_CLASSES`.
1560
1596
  *
1561
1597
  * Pass `{ derive }` to retune the palette identity instead of the shipped default, `{ fonts }` to
1562
1598
  * retune the three-font system, `{ radius }` to retune the two radius anchors, and/or `{ density }`
@@ -1578,7 +1614,7 @@ function hasFontsConfig(fonts: BasaltFontsConfig | undefined): fonts is BasaltFo
1578
1614
  * — it is only lost if a consumer's own `overrides.other` explicitly sets that same key.
1579
1615
  */
1580
1616
  export function createBasaltTheme(
1581
- overrides?: MantineThemeOverride,
1617
+ overrides?: Omit<MantineThemeOverride, 'breakpoints'>,
1582
1618
  options?: CreateBasaltThemeOptions,
1583
1619
  ): MantineThemeOverride {
1584
1620
  const resolvedConfig = resolveDeriveConfig(options?.derive)
@@ -1610,5 +1646,7 @@ export function createBasaltTheme(
1610
1646
  if (hasFontsConfig(fontsConfig)) {
1611
1647
  theme = mergeThemeOverrides(theme, { other: { basaltFonts: fontsConfig } })
1612
1648
  }
1613
- return overrides ? mergeThemeOverrides(theme, overrides) : theme
1649
+ if (!overrides) return theme
1650
+ // `theme.breakpoints` is derived from `SIZE_CLASSES` — a JS caller's override is dropped.
1651
+ return { ...mergeThemeOverrides(theme, overrides), breakpoints: SIZE_CLASS_BREAKPOINTS }
1614
1652
  }
@@ -21,6 +21,13 @@
21
21
  font-weight: 600;
22
22
  }
23
23
 
24
+ /* Touch floor: rows are adjacent (1px gap), so an overlay would overlap — the row itself is 44. */
25
+ @media (pointer: coarse) {
26
+ .root {
27
+ min-height: var(--vx-hit);
28
+ }
29
+ }
30
+
24
31
  /* Active + hover — one step stronger than the resting active fill, so a pointer over the active row
25
32
  * still reads as feedback rather than as a flat, unresponsive selection. */
26
33
  @media (hover: hover) {
@@ -16,11 +16,12 @@ import {
16
16
  ROW_LINE_HEIGHT,
17
17
  SHADOW,
18
18
  SPACE,
19
- SPACE_FIXED,
20
19
  SPACE_SCALE,
21
20
  SPACE_STEP,
22
21
  } from './palette'
23
22
  import type { PaletteData, RadiusValues, SpaceValues } from './palette'
23
+ import { SIZE_CLASSES, sizeClassMaxEm } from './size-classes'
24
+ import { HIT_COARSE, HIT_FINE } from '../common/hit-floor'
24
25
 
25
26
  // The raw hue families + pair-picker — the building blocks a consumer's series module composes
26
27
  // (`hrv: p(BP.blue)`). The doctrine sends every consumer here, so they are public surface, not
@@ -159,11 +160,9 @@ export const VX = {
159
160
  // Fully-rounded pill affordance (scroll-to-bottom button, chips) — fixed, does not track the
160
161
  // radius knob (see `RADIUS_STEP.pill`'s doc in `tokens/palette.ts`).
161
162
  radiusPill: 'var(--vx-radius-pill)',
162
- // The WCAG 2.5.5/2.5.8 touch-target floor (44px), fixed — does not track the density knob (see
163
- // `SPACE_FIXED.spaceTouchTarget`'s doc in `tokens/palette.ts`). For a hit area with no `ctl`-tier
164
- // home to size it — a consumer's own `@media (pointer: coarse)` CSS-module rule reads the same
165
- // `--vx-space-touch-target` var this resolves to.
166
- spaceTouchTarget: 'var(--vx-space-touch-target)',
163
+ // The pointer-tier hit floor: 44px under `(pointer: coarse)` (WCAG 2.5.5), 24px otherwise
164
+ // (2.5.8). Density-exempt; `[data-basalt-hit]::after` in `styles.css` reads it.
165
+ hit: 'var(--vx-hit)',
167
166
 
168
167
  // Controls tier sizing (`docs/CONTROLS-SPEC.md` §5) — `var()` refs onto the density-tracking
169
168
  // `--vx-space-*` declarations `spaceDecls` emits above (same ref pattern as `radiusCard`/
@@ -173,7 +172,6 @@ export const VX = {
173
172
  controlHeightTag: 'var(--vx-space-control-height-tag)',
174
173
  controlHeightWidget: 'var(--vx-space-control-height-widget)',
175
174
  controlHeightCtl: 'var(--vx-space-control-height-ctl)',
176
- touchControlHeight: 'var(--vx-space-touch-control-height)',
177
175
  pageBarRowHeight: 'var(--vx-space-page-bar-row-height)',
178
176
  sectionHeaderHeight: 'var(--vx-space-section-header-height)',
179
177
  widgetHeaderHeight: 'var(--vx-space-widget-header-height)',
@@ -292,18 +290,10 @@ export const VX = {
292
290
  * legend rolls up).
293
291
  */
294
292
  minPlotHeight: 120,
295
- /**
296
- * The measured CONTAINER width, in px, below which a chart resolves to its phone tier
297
- * (`resolveChartTier`, `docs/CHARTS-SPEC.md` §8): one step smaller legend and tick fonts,
298
- * tightened margin floors, a smaller crosshair dot, a narrower tooltip, and a two-entry legend
299
- * rollup.
300
- *
301
- * Deliberately NOT a media query and NOT a `theme.breakpoints` value. A chart in a two-column
302
- * grid cell on a 1440px desktop is as narrow as one on a phone, and only the element's own
303
- * measured box knows that — the viewport does not. It also keeps the chart layer Mantine-free:
304
- * `theme.breakpoints` lives on the coupled side of the boundary.
305
- */
306
- phoneChartWidth: 480,
293
+ /** Every floating chart overlay portalled to `document.body` — a pinned tooltip, the legend's
294
+ * `All N` disclosure and its backdrop — shares one stacking value, so they always stack the same
295
+ * way relative to page chrome and to each other. */
296
+ zIndexFloating: 9999,
307
297
  } as const
308
298
 
309
299
  /**
@@ -317,13 +307,13 @@ export const VX = {
317
307
  * `styles.floor.test.ts`).
318
308
  *
319
309
  * Only breakpoints that are genuinely a CSS-authored `@media` belong here. A component that can
320
- * measure its own box uses the measurement instead (`VX.phoneChartWidth`), and Mantine chrome uses
310
+ * measure its own box uses the measurement instead (`CONTAINER_CLASSES`), and Mantine chrome uses
321
311
  * `theme.breakpoints`.
322
312
  */
323
313
  export const BREAKPOINTS = {
324
314
  /**
325
315
  * `content/article-layout.module.css` — below this the TOC rail is dropped and the article
326
- * collapses to a single centred column. Well above the `sm` (48em/768px) shell breakpoint on
316
+ * collapses to a single centred column. Well above the `sm` (52.5em/840px) shell breakpoint on
327
317
  * purpose: the rail needs the prose measure PLUS its own width plus the column gap, which runs
328
318
  * out long before the shell goes mobile.
329
319
  */
@@ -484,7 +474,7 @@ function frameworkPrimitives(side: Side, data: PaletteData, legacyAliases: boole
484
474
  * the two lists can never drift apart — mirrors `frameworkDerived`'s own single-source reasoning for
485
475
  * the radii.
486
476
  *
487
- * The ten controls-tier anchors/one-offs (`controlHeightTag`/`Widget`/`Ctl`, `touchControlHeight`,
477
+ * The nine controls-tier anchors/one-offs (`controlHeightTag`/`Widget`/`Ctl`,
488
478
  * `pageBarRowHeight`, `sectionHeaderHeight`, `widgetHeaderHeight`, `sidebarBlockRowHeight`,
489
479
  * `controlGap`, `sheetRowHeight` — `docs/CONTROLS-SPEC.md` §5) are a DELIBERATE exception to the
490
480
  * "has at least one real consumer today" rule below: `theme/ctl-theme.tsx`'s `CTL_THEME` and the
@@ -548,7 +538,6 @@ function spaceDecls(space: SpaceValues): string[] {
548
538
  decl('space-control-height-tag', pxRem(space.anchors.controlHeightTag)),
549
539
  decl('space-control-height-widget', pxRem(space.anchors.controlHeightWidget)),
550
540
  decl('space-control-height-ctl', pxRem(space.anchors.controlHeightCtl)),
551
- decl('space-touch-control-height', pxRem(space.anchors.touchControlHeight)),
552
541
  decl('space-stack-xs', `${space.anchors.stackXs}px`),
553
542
  decl('space-stack-sm', `${space.anchors.stackSm}px`),
554
543
  decl('space-stack-md', `${space.anchors.stackMd}px`),
@@ -774,10 +763,9 @@ function frameworkDerived(data: PaletteData, only: 'core' | 'all', legacyAliases
774
763
  // only in this static block, never in `buildRadiusCss`'s dynamic override (see
775
764
  // `RADIUS_STEP.pill`'s doc).
776
765
  decl('radius-pill', `${RADIUS_STEP.pill}px`),
777
- // The WCAG touch-target floor — fixed, density-EXEMPT, so it lives only in this static block,
778
- // never in `buildDensityCss`'s dynamic override (same law as `radius-pill` above; see
779
- // `SPACE_FIXED.spaceTouchTarget`'s doc).
780
- decl('space-touch-target', `${SPACE_FIXED.spaceTouchTarget}px`),
766
+ // The pointer tier's fine default — density-EXEMPT, so it lives only in this static block
767
+ // (same law as `radius-pill` above). `coarseHitBlock` re-points it to 44px.
768
+ decl('hit', `${HIT_FINE}px`),
781
769
  ...(only === 'core' ? space.filter(isCoreSpaceDecl) : space),
782
770
  // Article-density Prose measure (docs/CONTENT-SPEC.md §5) — theme-independent, like the radii.
783
771
  decl('prose-measure', '72ch'),
@@ -944,7 +932,9 @@ const indent = (block: string): string =>
944
932
  * properties exist, so the number cannot be a `var()`. Held here as a constant rather than inlined
945
933
  * so the emitter and the modules can be grepped as one set.
946
934
  */
947
- const MOBILE_MEDIA = '@media (max-width: 47.99375em)'
935
+ const MOBILE_MEDIA = `@media (max-width: ${sizeClassMaxEm(SIZE_CLASSES.medium)})`
936
+
937
+ const coarseHitBlock = `@media (pointer: coarse) {\n :root {\n --vx-hit: ${HIT_COARSE}px;\n }\n}`
948
938
 
949
939
  /**
950
940
  * The MOBILE INSET LADDER, and this function is its ONE home — framework-wide, not per module.
@@ -1032,8 +1022,8 @@ export function buildPaletteCss(
1032
1022
  // consumer never receives the base pair the block exists to override.
1033
1023
  const rootBlock =
1034
1024
  only === 'core'
1035
- ? `:root {\n${derived}\n}`
1036
- : `:root {\n${derived}\n}\n${mobileLadderBlock(DEFAULT_SPACE_VALUES)}`
1025
+ ? `:root {\n${derived}\n}\n${coarseHitBlock}`
1026
+ : `:root {\n${derived}\n}\n${mobileLadderBlock(DEFAULT_SPACE_VALUES)}\n${coarseHitBlock}`
1037
1027
 
1038
1028
  // The legacy shape — kept verbatim, and only reachable when the caller asked for nothing.
1039
1029
  if (opts.scheme === undefined && opts.defaultScheme === undefined && !opts.mediaFallback) {
@@ -321,11 +321,6 @@ const SPACE_ANCHORS_BASE = {
321
321
  * and the whole band looked unresolved. 32 lands the entire chrome tier on ONE number, which is
322
322
  * also the smallest square that still holds a 16px icon with a 8px surround. */
323
323
  controlHeightCtl: 32,
324
- /** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
325
- * `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
326
- * `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
327
- * table. Floored at 30px in {@link deriveSpacing} (C15's "floor 30 at density −3"). */
328
- touchControlHeight: 36,
329
324
  } as const
330
325
 
331
326
  /**
@@ -618,9 +613,8 @@ const SPACE_STEP_BASE = {
618
613
  *
619
614
  * 44, down from 48. The band holds ONE `ctl`-tier row (32 since this pass), so 48 left 8px of
620
615
  * dead air above and below it — the "too much spacing at the top bar" the header reads as. 44
621
- * keeps 6px of breathing room a side, still clears `touchControlHeight` (36) below `sm` and
622
- * `controlHeight` (42) for a `size="md"` control, and is the Apple HIG 44pt figure the rest of
623
- * this table already floors its touch targets at. */
616
+ * keeps 6px of breathing room a side, still clears `controlHeight` (42) for a `size="md"`
617
+ * control, and is the 44px touch floor (WCAG 2.5.5). */
624
618
  appShellHeaderHeight: 44,
625
619
  /** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
626
620
  * permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
@@ -696,12 +690,9 @@ const SPACE_STEP_BASE = {
696
690
  mobileNavTabInsetY: 2,
697
691
  /** Active-pill inset around the slot icon, horizontal. */
698
692
  mobileNavTabInsetX: 12,
699
- /** Menu/sheet row height. Floored at 40px in `deriveSpacing` — a deliberate design choice, not
700
- * a WCAG floor: it's the row height shared with the desktop sidebar rows, above the WCAG 2.5.8
701
- * AA minimum (24px) and below the 2.5.5 AAA figure (44px, the size `mobileNavBarHeight`/
702
- * `sheetRowHeight` still hold to) — the shortest box that still reads as ONE row vocabulary with
703
- * the sidebar instead of a bespoke touch size. */
704
- mobileNavRowHeight: 40,
693
+ /** Menu/sheet row height — the WCAG 2.5.5 44px target, floored at 44 in `deriveSpacing` (the
694
+ * pointer tier's `--vx-hit` is the same number under `(pointer: coarse)`). */
695
+ mobileNavRowHeight: 44,
705
696
  /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
706
697
  * `sidebarSettingsMenuWidth` directly above. */
707
698
  mobileNavMenuWidth: 232,
@@ -853,18 +844,6 @@ export const SPACE_FIXED = {
853
844
  * `[-3, 3]` range can produce) for no compensating benefit, since the two radii it sits between
854
845
  * never move independently of each other. */
855
846
  segmentedTrackInset: 2,
856
- /**
857
- * The WCAG 2.5.5/2.5.8 touch-target floor (44px) for a hit area that has no `ctl`-tier home to
858
- * size it — a raw CSS-module rule bumping a control's hit area under `@media (pointer: coarse)`
859
- * (C5 consolidation: argo carried three such rules with a hand-picked 44/46px literal apiece).
860
- * Density-exempt for the same reason `touchControlHeight`'s FLOOR is: a target this small must
861
- * never shrink further, whatever the density knob says elsewhere. Unlike the rest of this
862
- * object, this ONE key IS emitted as a `--vx-*` var (`tokens/index.ts`'s `space-touch-target`) —
863
- * the others stay JS-only because nothing outside the framework's own components reads them, but
864
- * this floor's whole purpose is being read by a CONSUMER's own `.module.css`, which cannot
865
- * import a JS ref at all.
866
- */
867
- spaceTouchTarget: 44,
868
847
  } as const
869
848
 
870
849
  /**
@@ -1095,7 +1074,6 @@ export function deriveSpacing(level: number): SpaceValues {
1095
1074
  anchors.controlHeightTag = Math.max(18, anchors.controlHeightTag)
1096
1075
  anchors.controlHeightWidget = Math.max(22, anchors.controlHeightWidget)
1097
1076
  anchors.controlHeightCtl = Math.max(28, anchors.controlHeightCtl)
1098
- anchors.touchControlHeight = Math.max(30, anchors.touchControlHeight)
1099
1077
 
1100
1078
  const mappedStep = mapSpaceGroup(SPACE_STEP_BASE, level, multiplier)
1101
1079
  // WCAG 2.5.8 minimum interactive-target size — see this function's doc for why the trigger's
@@ -1112,7 +1090,7 @@ export function deriveSpacing(level: number): SpaceValues {
1112
1090
  // box) would render at 47px if the floor stopped at 48. +1 buys back exactly the pixel the seam
1113
1091
  // claims, so the tappable bar itself still measures >= 48.
1114
1092
  mappedStep.mobileNavBarHeight = Math.max(49, mappedStep.mobileNavBarHeight)
1115
- mappedStep.mobileNavRowHeight = Math.max(40, mappedStep.mobileNavRowHeight)
1093
+ mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight)
1116
1094
  // Same floor family, same reason — `sheetRowHeight`'s own doc names it "same floor family as
1117
1095
  // `mobileNavRowHeight`", but nothing enforced that below level 0 until now.
1118
1096
  mappedStep.sheetRowHeight = Math.max(44, mappedStep.sheetRowHeight)
@@ -0,0 +1,46 @@
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
+
9
+ /** Viewport classes — shell chrome only (bottom bar / rail / sidebar, aside docking). */
10
+ export const SIZE_CLASSES = { compact: 0, medium: 840, expanded: 1200 } as const
11
+
12
+ /** A component's own inline size — card chrome, chart tier, forms. No CSS reads it yet. */
13
+ export const CONTAINER_CLASSES = { micro: 0, compact: 240, regular: 480, wide: 800 } as const
14
+
15
+ export type ContainerClass = keyof typeof CONTAINER_CLASSES
16
+
17
+ /** The class a MEASURED inline size falls in (`px` is a min-width boundary, so 240 is `compact`). */
18
+ export function resolveContainerClass(px: number): ContainerClass {
19
+ if (px >= CONTAINER_CLASSES.wide) return 'wide'
20
+ if (px >= CONTAINER_CLASSES.regular) return 'regular'
21
+ if (px >= CONTAINER_CLASSES.compact) return 'compact'
22
+ return 'micro'
23
+ }
24
+
25
+ /**
26
+ * `theme.breakpoints`, derived — a consumer cannot retune it. Mantine needs all five keys:
27
+ * `base` (no key) is compact, `xs`/`sm` open the medium class, `md`/`lg`/`xl` the expanded one, so
28
+ * every `visibleFrom`/`hiddenFrom` in the package stays "desktop chrome" from `sm` up.
29
+ */
30
+ export const SIZE_CLASS_BREAKPOINTS = {
31
+ xs: `${SIZE_CLASSES.medium / 16}em`,
32
+ sm: `${SIZE_CLASSES.medium / 16}em`,
33
+ md: `${SIZE_CLASSES.expanded / 16}em`,
34
+ lg: `${SIZE_CLASSES.expanded / 16}em`,
35
+ xl: `${SIZE_CLASSES.expanded / 16}em`,
36
+ } as const
37
+
38
+ /** A px width as the `em` length media queries use (16px root). */
39
+ export function toEm(px: number): string {
40
+ return `${Number((px / 16).toFixed(5))}em`
41
+ }
42
+
43
+ /** The `max-width` complement of a class boundary — 0.1px under it, Mantine's own convention. */
44
+ export function sizeClassMaxEm(minPx: number): string {
45
+ return toEm(minPx - 0.1)
46
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"isomorphic-layout-effect.d.ts","sourceRoot":"","sources":["../../src/shell/isomorphic-layout-effect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,SAAS,EAAmB,MAAM,OAAO,CAAA;AAElD,eAAO,MAAM,yBAAyB,kBAA8D,CAAA"}