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
@@ -131,8 +131,59 @@
131
131
  * `padding-right`; the block padding keeps coming from the shorthand. The bottom inset already
132
132
  * rides `margin-bottom` above (`env(safe-area-inset-bottom)`); the PWA's top inset needs no rule here
133
133
  * (the status bar uses the opaque `default` style, per argo's own seed comment). */
134
- @media (display-mode: standalone) {
135
- .main {
136
- padding-right: max(var(--app-shell-padding), env(safe-area-inset-right));
134
+ :global(:root[data-basalt-host]:not([data-basalt-host='web'])) .main {
135
+ padding-right: max(var(--app-shell-padding), env(safe-area-inset-right));
136
+ }
137
+
138
+ /*
139
+ * PUBLIC SHELL BOTTOM INSET — `--app-shell-footer-offset` (stable name, do not rename): the visible
140
+ * bottom-bar height WITHOUT the safe-area inset, and 0 wherever there is no bar (from `sm` up).
141
+ * Anything bottom-anchored (toasts, a chat composer) sits at
142
+ * `calc(var(--app-shell-footer-offset, 0px) + env(safe-area-inset-bottom, 0px))`.
143
+ *
144
+ * Mantine emits it on `:root` for a fixed-mode AppShell (its `cssVariablesSelector`), so a
145
+ * body-level portal reads it; the landscape override below is on `:root` for the same reason.
146
+ */
147
+
148
+ /*
149
+ * An OPEN aside that does not dock: the region reserved only its rail, so it widens itself over
150
+ * main (`--app-shell-aside-width` is set on this element by `shell/index.tsx`) instead of shrinking
151
+ * it, lifted on the card depth token.
152
+ */
153
+ .asideOverlay {
154
+ box-shadow: var(--vx-shadow-card);
155
+ }
156
+
157
+ /*
158
+ * Landscape phone: the header and the bottom bar take their compact row, SCALED to the viewport's
159
+ * own height budget rather than a flat step — a flat 40/45 barely shrinks (44→40, 56→45, ≈9-20%)
160
+ * against the height itself halving (812→375), so the chrome's SHARE of the screen nearly doubles
161
+ * (r2 N1: 16.7% portrait → 32.3% landscape even with the flat numbers). `clamp(floor, Ndvh,
162
+ * ceiling)` keeps continuity at the 480px breakpoint edge (Ndvh ≈ ceiling there) and keeps
163
+ * shrinking below it, down to a floor: 36px for the header (room for the brand/burger row), and
164
+ * `--vx-space-mobile-nav-row-height + 1px` (its own seam) for the bar — the row token, not a flat
165
+ * 45px, because `deriveSpacing`'s density knob only FLOORS that token at 44 and is free to scale it
166
+ * taller at density > 0; a hardcoded 45px floor would then clamp the footer BELOW its own row's
167
+ * real height at any positive density, clipping it. The heights are JS numbers in `AppShell` props,
168
+ * so the override is on `:root` where Mantine writes them; `:root:root` outranks its later-rendered
169
+ * `<style>` at equal source order. Both rules are gated to the same width — the header and the bar
170
+ * both exist ONLY below `sm` in this landscape-compact form; a wide landscape phone/tablet has no
171
+ * bar (and keeps `--app-shell-footer-offset` 0) and reads its header at the ordinary, un-clamped
172
+ * height, not this mobile row. Labels hidden: `app-mobile-nav.module.css`.
173
+ */
174
+ @media (max-height: 480px) and (orientation: landscape) and (max-width: 52.49375em) {
175
+ :global(:root:root) {
176
+ --app-shell-header-height: clamp(36px, 9dvh, 40px);
177
+ --app-shell-header-offset: clamp(36px, 9dvh, 40px);
178
+ --app-shell-footer-height: clamp(
179
+ calc(var(--vx-space-mobile-nav-row-height) + 1px),
180
+ 9.4dvh,
181
+ 56px
182
+ );
183
+ --app-shell-footer-offset: clamp(
184
+ calc(var(--vx-space-mobile-nav-row-height) + 1px),
185
+ 9.4dvh,
186
+ 56px
187
+ );
137
188
  }
138
189
  }
@@ -7,5 +7,6 @@ declare const classes: {
7
7
  readonly shell: string
8
8
  readonly band: string
9
9
  readonly main: string
10
+ readonly asideOverlay: string
10
11
  }
11
12
  export default classes
@@ -195,7 +195,7 @@
195
195
  * The popover's SECTION RHYTHM — what replaced the single `Menu.Divider`, and it is SPACE, not a
196
196
  * line.
197
197
  *
198
- * The Drawer's bulk was never its row height (40px is a WCAG 2.5.5 floor pinned by
198
+ * The Drawer's bulk was never its row height (44px is the WCAG 2.5.5 AAA-enhanced floor pinned by
199
199
  * `theme/density-relations.test.ts`); it was the stack rhythm around the rows — a 12px section
200
200
  * gap, 18px of Drawer padding and a header row. A popover with one hairline through the middle and
201
201
  * no rhythm at all is the other extreme, so each group opens its own band instead.
@@ -218,8 +218,8 @@
218
218
  * `max-height` above answers, and the cheaper half. MEASURED per heading at 8/8: 8px margin + 8px
219
219
  * padding-top + 2px padding-bottom + a 17px line = a 35px box, and there are FIVE of them in the
220
220
  * shipped More surface — 175px, 28% of the whole dropdown spent on five words. 6/4 takes each
221
- * banded heading to 29px and hands 24px back to the rows. It does NOT touch the row height (40px
222
- * is the WCAG 2.5.5 floor named at the top of this comment) and it does not add the line the
221
+ * banded heading to 29px and hands 24px back to the rows. It does NOT touch the row height (44px
222
+ * is the WCAG 2.5.5 AAA-enhanced floor named at the top of this comment) and it does not add the line the
223
223
  * paragraph above rules out: this is the same space that argument asked to carry the boundary,
224
224
  * spent tighter.
225
225
  */
@@ -331,7 +331,7 @@
331
331
  * longer fights that — it used to force the dense desktop row's padding back OPEN with
332
332
  * `!important`, which is exactly how a "44px touch row" and a "29px sidebar row" ended up as two
333
333
  * unrelated geometries for the same list. What is left here is only what the theme does not supply:
334
- * the touch-target `min-height` floor (`--vx-space-mobile-nav-row-height`, 40px — WCAG 2.5.5 AA).
334
+ * the touch-target `min-height` floor (`--vx-space-mobile-nav-row-height`, 44px — WCAG 2.5.5 AAA-enhanced).
335
335
  */
336
336
  .row {
337
337
  display: flex;
@@ -403,3 +403,15 @@
403
403
  transition: none;
404
404
  }
405
405
  }
406
+
407
+ /* Landscape phone: the bar's compact row (`app-main.module.css` sets its height) is icons only —
408
+ * the label is visually hidden, not removed, so each tab keeps its accessible name. */
409
+ @media (max-height: 480px) and (orientation: landscape) {
410
+ .label {
411
+ position: absolute;
412
+ width: 1px;
413
+ height: 1px;
414
+ overflow: hidden;
415
+ clip-path: inset(50%);
416
+ }
417
+ }
@@ -30,10 +30,8 @@
30
30
  * notch on the side the sidebar touches. `max()` is a FLOOR — the inset only grows the padding
31
31
  * past its normal value, never shrinks it. Right inset is `app-main.module.css`'s to draw; the
32
32
  * PWA's top inset needs no rule here (the status bar uses the opaque `default` style). */
33
- @media (display-mode: standalone) {
34
- .root {
35
- padding-left: max(var(--vx-space-row-inset-x), env(safe-area-inset-left));
36
- }
33
+ :global(:root[data-basalt-host]:not([data-basalt-host='web'])) .root {
34
+ padding-left: max(var(--vx-space-row-inset-x), env(safe-area-inset-left));
37
35
  }
38
36
 
39
37
  .link {
@@ -634,12 +632,12 @@
634
632
  }
635
633
 
636
634
  /*
637
- * Icon-rail (collapsed) — desktop only. Gated behind min-width: sm (48em) so the sidebar doesn't
635
+ * Icon-rail (collapsed) — desktop only. Gated behind min-width: sm (52.5em) so the sidebar doesn't
638
636
  * exist at all below that breakpoint (see `MobileNav`) regardless of the persisted collapse flag.
639
637
  * Hide the section labels and nav labels; center the remaining icons. The brand's own rail form
640
638
  * lives in `app-brand.module.css` now, keyed off the same `data-collapsed` attribute.
641
639
  */
642
- @media (min-width: 48em) {
640
+ @media (min-width: 52.5em) {
643
641
  .root[data-collapsed] .searchSlot {
644
642
  padding-block: var(--vx-space-sidebar-region-gap);
645
643
  display: flex;
@@ -15,14 +15,17 @@
15
15
  * `item.onClick`, `item.badge`/`item.count` and supplies `item.Anchor` — its router `<Link>`,
16
16
  * which basalt HOSTS rather than delegating rendering to. The breadcrumb is derived from the
17
17
  * active item across `sections`, not from a router hook. Collapse is persisted via basalt's own
18
- * `createPersistedState` (`../state`) keyed by `storageKey` — see `collapseStore`.
18
+ * `createPersistedStore` (`../state/persisted`, `createPersistedState` plus an `isSet` flag) keyed
19
+ * by `storageKey` — see `collapseStore`. While nothing is persisted the size class picks the
20
+ * default (rail in `medium`); any stored value, `false` included, wins.
19
21
  */
20
- import { AppShell, Box } from '@mantine/core'
21
- import { Fragment, useCallback, useEffect, useMemo } from 'react'
22
- import type { MouseEvent, ReactNode } from 'react'
22
+ import { AppShell, Box, rem } from '@mantine/core'
23
+ import { Fragment, useCallback, useContext, useEffect, useMemo } from 'react'
24
+ import type { CSSProperties, MouseEvent, ReactNode } from 'react'
23
25
  import { registerSidebarToggle } from '../commands/shell-bridge'
24
26
  import { cx } from '../common/props'
25
27
  import type { BasaltProps } from '../common/props'
28
+ import { useMediaQuery } from '../common/use-media-query'
26
29
  import { assertRequiredProps } from '../common/validate'
27
30
  import { AppSidebar } from './app-sidebar'
28
31
  import { AppBrand } from './app-brand'
@@ -30,7 +33,7 @@ import { MobileNav, accountRowCount } from './app-mobile-nav'
30
33
  import { blockRowCount, projectMobileNav } from './mobile-nav-model'
31
34
  import { AppBreadcrumbs } from './app-breadcrumbs'
32
35
  import { PageBarBandOutlet, PageBarOutlet, PageBarProvider, usePageKebabClaimed } from './page-bar'
33
- import { AsideOutlet, AsideProvider, useAsideRegion } from './page-aside'
36
+ import { AsideDocksContext, AsideOutlet, AsideProvider, useAsideRegion } from './page-aside'
34
37
  import { OverflowMenu, globalActionAsBarAction, globalActionMobile } from '../controls/actions'
35
38
  import type { GlobalAction } from '../controls/actions'
36
39
  import type { AccountMenuItem, BasaltAccountProps } from './account-types'
@@ -43,7 +46,10 @@ import type {
43
46
  SidebarSection,
44
47
  } from './nav-types'
45
48
  import { CtlSlot, useBasaltSpacing } from '../theme'
46
- import { createPersistedState } from '../state'
49
+ import { createPersistedStore } from '../state/persisted'
50
+ import { toEm } from '../tokens/size-classes'
51
+ import { SizeClassHintContext, useSizeClass } from './use-size-class'
52
+ import type { SizeClass } from './use-size-class'
47
53
  import brandClasses from './app-brand.module.css'
48
54
  import headerClasses from './app-header.module.css'
49
55
  import mainClasses from './app-main.module.css'
@@ -80,6 +86,7 @@ export { AppBreadcrumbs, type AppBreadcrumbsProps } from './app-breadcrumbs'
80
86
  export { PageBar, type PageBarProps, type PageBarSlot } from './page-bar'
81
87
  export { PageAside, type PageAsideProps, type PageAsideSlot } from './page-aside'
82
88
  export { useBreakpoint, type BreakpointName } from './use-breakpoint'
89
+ export { useSizeClass, type SizeClass } from './use-size-class'
83
90
  export { PageTitle, type PageTitleProps, type PageTitleSlot } from './page-title'
84
91
 
85
92
  /**
@@ -268,7 +275,9 @@ function findActiveWithParent(
268
275
  const COLLAPSE_VERSION = 1
269
276
 
270
277
  /**
271
- * One `createPersistedState` store per `storageKey`, memoized at module scope.
278
+ * One `createPersistedStore` store per `storageKey`, memoized at module scope. Its third tuple
279
+ * element, `isSet`, is what lets an unset key take the size-class default while an explicit
280
+ * write of `false` still pins the navbar open.
272
281
  *
273
282
  * Through 1.20.0 the shell persisted collapse with `@mantine/hooks`' `useLocalStorage` while
274
283
  * `createPersistedState` was the documented house API — and this component's own docstring told
@@ -276,21 +285,43 @@ const COLLAPSE_VERSION = 1
276
285
  * `localStorage` call was COMPLIANCE with the shipped component rather than drift. A framework
277
286
  * cannot ship a persistence rule its own shell breaks; this is the shell coming into line.
278
287
  *
279
- * The memo is required, not an optimization: `createPersistedState` is a per-key module FACTORY and
288
+ * The memo is required, not an optimization: `createPersistedStore` is a per-key module FACTORY and
280
289
  * `storageKey` is a runtime prop, so calling it during render would allocate a fresh store (and a
281
290
  * fresh `useSyncExternalStore` subscription) on every commit. Swapping keys mid-life stays safe —
282
- * every store's hook calls exactly one `useSyncExternalStore`, so the hook count never moves.
291
+ * every store's hook calls the same two `useSyncExternalStore`s (value, `isSet`), so the hook count
292
+ * never moves.
283
293
  */
284
- const collapseStores = new Map<string, () => readonly [boolean, (next: boolean) => void]>()
294
+ type CollapseStore = () => readonly [boolean, (next: boolean) => void, boolean]
285
295
 
286
- function collapseStore(key: string): () => readonly [boolean, (next: boolean) => void] {
296
+ const collapseStores = new Map<string, CollapseStore>()
297
+
298
+ function collapseStore(key: string): CollapseStore {
287
299
  const cached = collapseStores.get(key)
288
300
  if (cached) return cached
289
- const store = createPersistedState<boolean>({ key, version: COLLAPSE_VERSION, initial: false })
301
+ const store = createPersistedStore<boolean>({ key, version: COLLAPSE_VERSION, initial: false })
290
302
  collapseStores.set(key, store)
291
303
  return store
292
304
  }
293
305
 
306
+ /**
307
+ * Main keeps at least this much width, or an open aside overlays it instead of docking. Not
308
+ * derived from a token: it is the readable floor for a page body, independent of density.
309
+ */
310
+ const MAIN_MIN_WIDTH = 720
311
+
312
+ /**
313
+ * The room-to-dock `min-width` query's SSR/no-`matchMedia` fallback — an `'expanded'`
314
+ * `sizeClassHint` means the server already believes there is room, matching `useSizeClass`'s own
315
+ * hint-seeded guard one level up (`use-size-class.ts`). Extracted as its own pure function (rather
316
+ * than the inline `sizeClassHint === 'expanded'` it replaces) purely so `index.test.tsx` can pin it
317
+ * without needing to render `BasaltShell` through a real SSR pass — `AppShell`'s + `PageAside`'s own
318
+ * effects/refs/portals make that path unobservable through rendered markup, and the previous
319
+ * hardcoded `false` fallback here shipped with no test at all for exactly that reason.
320
+ */
321
+ export function roomToDockServerFallback(sizeClassHint: SizeClass | undefined): boolean {
322
+ return sizeClassHint === 'expanded'
323
+ }
324
+
294
325
  /**
295
326
  * The shell's two page-level regions are providers, and both wrap the frame rather than living
296
327
  * inside it: `PageBarProvider` owns the header portal and the single-kebab claim, `AsideProvider`
@@ -355,11 +386,48 @@ function ShellFrame({
355
386
  // Mantine's own scale, which is where the 13px came from.
356
387
  const inset = { base: step.appShellInsetMobile, sm: step.appShellInset }
357
388
  const aside = useAsideRegion()
358
- const [storedCollapsed, setStoredCollapsed] = collapseStore(storageKey)()
389
+ const sizeClass = useSizeClass()
390
+ const [storedCollapsed, setStoredCollapsed, collapseIsSet] = collapseStore(storageKey)()
359
391
  // Controlled/uncontrolled seam (item 19): an explicit `collapsed` prop overrides the internal
360
- // localStorage-persisted state entirely — the consumer becomes the source of truth.
392
+ // localStorage-persisted state entirely — the consumer becomes the source of truth. Otherwise the
393
+ // size class picks the default (rail in `medium`) ONLY while the user has never toggled; an
394
+ // explicit write — `false` included — always wins.
361
395
  const isCollapseControlled = collapsedProp !== undefined
362
- const collapsed = isCollapseControlled ? collapsedProp : storedCollapsed
396
+ const uncontrolledCollapsed = collapseIsSet ? storedCollapsed : sizeClass === 'medium'
397
+ const collapsed = isCollapseControlled ? collapsedProp : uncontrolledCollapsed
398
+ // Aside docking (`docs/DESIGN-CORE.md` § Layout, elevation, shapes): an open aside pushes main only in
399
+ // `expanded` while main keeps `MAIN_MIN_WIDTH` beside this navbar; otherwise the region reserves
400
+ // just its rail and an open panel overlays main. Provided through `AsideDocksContext` in the SAME
401
+ // render, so `PageAside` defaults its fold from the right value on its first pass.
402
+ const navbarWidth = collapsed ? step.appShellNavbarRailWidth : step.appShellNavbarWidth
403
+ // The SSR/first-paint fallback mirrors `useSizeClass`'s own hint-seeded guard: an `expanded` hint
404
+ // from `<BasaltProvider sizeClassHint>` means the server already believes there is room to dock, so
405
+ // the first client paint agrees instead of defaulting to "cannot dock" and folding a default-open
406
+ // aside for one commit.
407
+ const sizeClassHint = useContext(SizeClassHintContext)
408
+ const roomToDock = useMediaQuery(
409
+ `(min-width: ${toEm(navbarWidth + step.appShellAsideWidth + MAIN_MIN_WIDTH)})`,
410
+ roomToDockServerFallback(sizeClassHint),
411
+ )
412
+ const docks = sizeClass === 'expanded' && roomToDock
413
+ const asideOpen = aside.claimed && !aside.folded
414
+ const asideOverlay = asideOpen && !docks
415
+ const asideReserved = asideOpen && docks ? step.appShellAsideWidth : step.appShellAsideRailWidth
416
+ const asideWidth = aside.claimed ? asideReserved : 0
417
+ // Keyboard escape hatch for the overlay form only — the docked form never blocks content, so
418
+ // there is nothing to escape out of. `requestClose` is a stable callback from `AsideProvider`
419
+ // (destructured, not read as `aside.requestClose`, so the effect deps name it directly rather
420
+ // than the whole `aside` object — a fresh literal every render); `PageAside` folds itself in
421
+ // response (see its own `closeSignal` effect).
422
+ const { requestClose } = aside
423
+ useEffect(() => {
424
+ if (!asideOverlay) return
425
+ const onKeyDown = (event: KeyboardEvent): void => {
426
+ if (event.key === 'Escape') requestClose()
427
+ }
428
+ window.addEventListener('keydown', onKeyDown)
429
+ return () => window.removeEventListener('keydown', onKeyDown)
430
+ }, [asideOverlay, requestClose])
363
431
  const toggleCollapse = useCallback(() => {
364
432
  const next = !collapsed
365
433
  if (!isCollapseControlled) setStoredCollapsed(next)
@@ -392,126 +460,129 @@ function ShellFrame({
392
460
  )
393
461
 
394
462
  return (
395
- <AppShell
396
- h="100dvh"
397
- // Mantine's DEFAULT layout, not `alt`: the header spans the full viewport width on top and
398
- // the navbar/aside start underneath it at `top: var(--app-shell-header-offset)`. `alt` did
399
- // the opposite — a full-height navbar with the header inset beside it — which put the top
400
- // bar to the RIGHT of the sidebar instead of above it.
401
- header={{
402
- // ONE height at every width (law C14): nothing reserves a second mobile row any more.
403
- height: step.appShellHeaderHeight,
404
- }}
405
- navbar={{
406
- width: {
407
- base: step.appShellNavbarWidth,
408
- sm: collapsed ? step.appShellNavbarRailWidth : step.appShellNavbarWidth,
409
- },
410
- breakpoint: 'sm',
411
- // No mobile sidebar drawer, ever — the bottom bar is the entire mobile nav.
412
- collapsed: { mobile: true },
413
- }}
414
- // The aside region (`docs/ASIDE-SPEC.md` §0). It is DECLARED here on every route and costs
415
- // nothing until a page mounts a `PageAside` to claim it: unclaimed it is zero-wide and
416
- // `collapsed.desktop`, so `--app-shell-aside-offset` stays 0 and the main column is
417
- // full-width (law C14 — an empty home renders nothing). There is no `BasaltShellProps`
418
- // prop for it on purpose; the ROUTE decides, the same way it decides its page bar.
419
- aside={{
420
- width: aside.claimed
421
- ? aside.folded
422
- ? step.appShellAsideRailWidth
423
- : step.appShellAsideWidth
424
- : 0,
425
- breakpoint: 'sm',
426
- // Below `sm` there is no region at all — `PageAside` renders its content in the page
427
- // flow instead, one node, no responsive twin (law C9).
428
- collapsed: { desktop: !aside.claimed, mobile: true },
429
- }}
430
- // A plain number, NOT a `calc(... + env(safe-area-inset-bottom))` string: Mantine's own
431
- // `.footer` rule already adds the inset to both the height and the padding (§2.7).
432
- footer={{ height: { base: step.mobileNavBarHeight, sm: 0 } }}
433
- padding={inset}
434
- // `mainClasses.shell` is not decoration — see its rule for why the root has to establish a
435
- // block formatting context now that Main is offset with margins.
436
- className={cx(mainClasses.shell, className)}
437
- {...(style !== undefined && { style })}
438
- >
439
- {/* Region seams (docs/DESIGN-SPEC.md §5, §8 #12): Mantine's `[data-with-border]` painted in
440
- * `--vx-divider` by the theme's `AppShell.extend({ vars })`. No shell module draws a region
441
- * edge; never opt a section back out of its border here. */}
442
- <AppShell.Header px={inset}>
443
- <div className={headerClasses.bar}>
444
- {/* The header's LEADING ZONE — exactly `--app-shell-navbar-offset` wide, so its trailing
445
- * edge lands on the sidebar|main seam and the breadcrumb after it starts on Main's own
446
- * content edge. The brand lives here rather than in the sidebar because with a full-width
447
- * header a sidebar brand row painted as a SECOND 48px band under the header seam; see
448
- * `app-brand.tsx`. `data-collapsed` is the rail's one signal — the zone narrows with the
449
- * navbar and shows the toggle alone, exactly as the sidebar's own row did. */}
450
- <div className={brandClasses.zone} data-collapsed={collapsed || undefined}>
451
- <AppBrand brand={brand} collapsed={collapsed} onToggleCollapse={toggleCollapse} />
452
- </div>
453
- <div className={headerClasses.lead}>
454
- <AppBreadcrumbs {...activeCrumb} />
463
+ <AsideDocksContext.Provider value={docks}>
464
+ <AppShell
465
+ h="100dvh"
466
+ // Mantine's DEFAULT layout, not `alt`: the header spans the full viewport width on top and
467
+ // the navbar/aside start underneath it at `top: var(--app-shell-header-offset)`. `alt` did
468
+ // the opposite — a full-height navbar with the header inset beside it — which put the top
469
+ // bar to the RIGHT of the sidebar instead of above it.
470
+ header={{
471
+ // ONE height at every width (law C14): nothing reserves a second mobile row any more.
472
+ height: step.appShellHeaderHeight,
473
+ }}
474
+ navbar={{
475
+ width: { base: step.appShellNavbarWidth, sm: navbarWidth },
476
+ breakpoint: 'sm',
477
+ // No mobile sidebar drawer, ever — the bottom bar is the entire mobile nav.
478
+ collapsed: { mobile: true },
479
+ }}
480
+ // The aside region (`docs/ASIDE-SPEC.md` §0). It is DECLARED here on every route and costs
481
+ // nothing until a page mounts a `PageAside` to claim it: unclaimed it is zero-wide and
482
+ // `collapsed.desktop`, so `--app-shell-aside-offset` stays 0 and the main column is
483
+ // full-width (law C14 — an empty home renders nothing). There is no `BasaltShellProps`
484
+ // prop for it on purpose; the ROUTE decides, the same way it decides its page bar.
485
+ aside={{
486
+ // An overlay reserves the rail only — `<AppShell.Aside>` below widens itself over main.
487
+ width: asideWidth,
488
+ breakpoint: 'sm',
489
+ // Below `sm` there is no region at all — `PageAside` renders its content in the page
490
+ // flow instead, one node, no responsive twin (law C9).
491
+ collapsed: { desktop: !aside.claimed, mobile: true },
492
+ }}
493
+ // A plain number, NOT a `calc(... + env(safe-area-inset-bottom))` string: Mantine's own
494
+ // `.footer` rule already adds the inset to both the height and the padding (§2.7).
495
+ footer={{ height: { base: step.mobileNavBarHeight, sm: 0 } }}
496
+ padding={inset}
497
+ // `mainClasses.shell` is not decoration — see its rule for why the root has to establish a
498
+ // block formatting context now that Main is offset with margins.
499
+ className={cx(mainClasses.shell, className)}
500
+ {...(style !== undefined && { style })}
501
+ >
502
+ {/* Region seams (docs/DESIGN-SPEC.md §5, §8 #12): Mantine's `[data-with-border]` painted in
503
+ * `--vx-divider` by the theme's `AppShell.extend({ vars })`. No shell module draws a region
504
+ * edge; never opt a section back out of its border here. */}
505
+ <AppShell.Header px={inset}>
506
+ <div className={headerClasses.bar}>
507
+ {/* The header's LEADING ZONE — exactly `--app-shell-navbar-offset` wide, so its trailing
508
+ * edge lands on the sidebar|main seam and the breadcrumb after it starts on Main's own
509
+ * content edge. The brand lives here rather than in the sidebar because with a full-width
510
+ * header a sidebar brand row painted as a SECOND 48px band under the header seam; see
511
+ * `app-brand.tsx`. `data-collapsed` is the rail's one signal — the zone narrows with the
512
+ * navbar and shows the toggle alone, exactly as the sidebar's own row did. */}
513
+ <div className={brandClasses.zone} data-collapsed={collapsed || undefined}>
514
+ <AppBrand brand={brand} collapsed={collapsed} onToggleCollapse={toggleCollapse} />
515
+ </div>
516
+ <div className={headerClasses.lead}>
517
+ <AppBreadcrumbs {...activeCrumb} />
518
+ </div>
519
+ <PageBarOutlet className={headerClasses.pageBar} />
520
+ <HeaderGlobalActions actions={globalActions ?? []} />
455
521
  </div>
456
- <PageBarOutlet className={headerClasses.pageBar} />
457
- <HeaderGlobalActions actions={globalActions ?? []} />
458
- </div>
459
- </AppShell.Header>
522
+ </AppShell.Header>
460
523
 
461
- <AppShell.Navbar p={0}>
462
- <AppSidebar
463
- brand={brand}
464
- sections={sections}
465
- collapsed={collapsed}
466
- onToggleCollapse={toggleCollapse}
467
- {...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
468
- {...(settingsMenuItems !== undefined && { settingsMenuItems })}
469
- {...(settingsMenu !== undefined && { settingsMenu })}
470
- {...(account !== undefined && { account })}
471
- {...(search !== undefined && { search })}
472
- />
473
- </AppShell.Navbar>
524
+ <AppShell.Navbar p={0}>
525
+ <AppSidebar
526
+ brand={brand}
527
+ sections={sections}
528
+ collapsed={collapsed}
529
+ onToggleCollapse={toggleCollapse}
530
+ {...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
531
+ {...(settingsMenuItems !== undefined && { settingsMenuItems })}
532
+ {...(settingsMenu !== undefined && { settingsMenu })}
533
+ {...(account !== undefined && { account })}
534
+ {...(search !== undefined && { search })}
535
+ />
536
+ </AppShell.Navbar>
474
537
 
475
- {/* The PAGE-BAR BAND — a shell-owned outlet between the header and the scrollport that
476
- * `PageBar` row 2 portals into, the same mechanism row 1 uses one region up. It is written
477
- * BEFORE Main because the two are the AppShell root's only in-flow children (every other
478
- * region is `position: fixed`), so DOM order is column order: band, then whatever height is
479
- * left. Empty, it is a zero-height, seam-less box (law C14). */}
480
- <PageBarBandOutlet className={mainClasses.band} />
538
+ {/* The PAGE-BAR BAND — a shell-owned outlet between the header and the scrollport that
539
+ * `PageBar` row 2 portals into, the same mechanism row 1 uses one region up. It is written
540
+ * BEFORE Main because the two are the AppShell root's only in-flow children (every other
541
+ * region is `position: fixed`), so DOM order is column order: band, then whatever height is
542
+ * left. Empty, it is a zero-height, seam-less box (law C14). */}
543
+ <PageBarBandOutlet className={mainClasses.band} />
481
544
 
482
- {/* MAIN IS THE SCROLLPORT (`app-main.module.css`), not the document: the scrollbar belongs
483
- * on the content's own right edge, inside the aside, and every sticky offset in the package
484
- * resolves against this box rather than the window. `data-basalt-scrollport` is the public
485
- * handle — `scrollParentOf` (`../common`) and `MobileNav`'s scroll-to-top both resolve it —
486
- * and `data-scroll-restoration-id` is the attribute TanStack Router's scroll restoration
487
- * reads (`@tanstack/router-core`'s `scrollRestorationIdAttribute`), so a router-driven app
488
- * restores THIS element's scrollTop instead of the window's, which no longer moves. */}
489
- <AppShell.Main
490
- className={mainClasses.main}
491
- data-basalt-scrollport
492
- data-scroll-restoration-id="basalt-main"
493
- >
494
- {children}
495
- </AppShell.Main>
545
+ {/* MAIN IS THE SCROLLPORT (`app-main.module.css`), not the document: the scrollbar belongs
546
+ * on the content's own right edge, inside the aside, and every sticky offset in the package
547
+ * resolves against this box rather than the window. `data-basalt-scrollport` is the public
548
+ * handle — `scrollParentOf` (`../common`) and `MobileNav`'s scroll-to-top both resolve it —
549
+ * and `data-scroll-restoration-id` is the attribute TanStack Router's scroll restoration
550
+ * reads (`@tanstack/router-core`'s `scrollRestorationIdAttribute`), so a router-driven app
551
+ * restores THIS element's scrollTop instead of the window's, which no longer moves. */}
552
+ <AppShell.Main
553
+ className={mainClasses.main}
554
+ data-basalt-scrollport
555
+ data-scroll-restoration-id="basalt-main"
556
+ >
557
+ {children}
558
+ </AppShell.Main>
496
559
 
497
- {/* The outlet is bare; the region's leading seam is the AppShell's own. An unclaimed region
498
- * is zero-wide but NOT display-none — Mantine's collapsed aside keeps its border-box, so a
499
- * seam would still paint as a 1px ghost at the viewport's right edge (measured on
500
- * `/dashboard`). The border follows the claim, not the section (C14). */}
501
- <AppShell.Aside p={0} withBorder={aside.claimed}>
502
- <AsideOutlet className={asideClasses.outlet} />
503
- </AppShell.Aside>
560
+ {/* The outlet is bare; the region's leading seam is the AppShell's own. An unclaimed region
561
+ * is zero-wide but NOT display-none — Mantine's collapsed aside keeps its border-box, so a
562
+ * seam would still paint as a 1px ghost at the viewport's right edge (measured on
563
+ * `/dashboard`). The border follows the claim, not the section (C14). */}
564
+ <AppShell.Aside
565
+ p={0}
566
+ withBorder={aside.claimed}
567
+ {...(asideOverlay && {
568
+ className: mainClasses.asideOverlay,
569
+ style: { '--app-shell-aside-width': rem(step.appShellAsideWidth) } as CSSProperties,
570
+ })}
571
+ >
572
+ <AsideOutlet className={asideClasses.outlet} />
573
+ </AppShell.Aside>
504
574
 
505
- <AppShell.Footer hiddenFrom="sm" p={0}>
506
- <MobileNav
507
- model={model}
508
- config={mobileNav}
509
- {...(account !== undefined && { account })}
510
- {...(settingsMenuItems !== undefined && { settingsMenuItems })}
511
- {...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
512
- />
513
- </AppShell.Footer>
514
- </AppShell>
575
+ <AppShell.Footer hiddenFrom="sm" p={0}>
576
+ <MobileNav
577
+ model={model}
578
+ config={mobileNav}
579
+ {...(account !== undefined && { account })}
580
+ {...(settingsMenuItems !== undefined && { settingsMenuItems })}
581
+ {...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
582
+ />
583
+ </AppShell.Footer>
584
+ </AppShell>
585
+ </AsideDocksContext.Provider>
515
586
  )
516
587
  }
517
588
 
@@ -19,7 +19,7 @@
19
19
  * padding), so the 560px cap uses 73% of it; on the smallest supported viewport (320x568) the
20
20
  * headroom is ~496px and 72dvh is 409px — the cap is binding on every supported phone, and the row
21
21
  * count never is. The old arithmetic here ("6 x 44px + 8px padding against 415px of headroom") was
22
- * stale in almost every number: rows are 40px (`--vx-space-mobile-nav-row-height`), the dropdown's
22
+ * stale in almost every number: rows are 44px (`--vx-space-mobile-nav-row-height`), the dropdown's
23
23
  * own padding is `2 * --vx-space-stack-xs` = 8px, and the headroom figures above are the measured
24
24
  * ones.
25
25
  *