basalt-ui 1.25.0 → 1.26.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 (462) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +18 -0
  3. package/MIGRATING.md +546 -20
  4. package/README.md +45 -32
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/settings-section.d.ts +6 -4
  134. package/dist/dashboard/settings-section.d.ts.map +1 -1
  135. package/dist/dashboard/settings-section.js +23 -16
  136. package/dist/dashboard/settings-section.module.css +10 -0
  137. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  138. package/dist/dashboard/settings-section.test.d.ts +2 -0
  139. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  140. package/dist/dashboard/stat-card.d.ts +44 -9
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +80 -48
  143. package/dist/dashboard/stat-card.module.css +82 -0
  144. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  145. package/dist/data/data-table.d.ts +37 -15
  146. package/dist/data/data-table.d.ts.map +1 -1
  147. package/dist/data/data-table.js +88 -58
  148. package/dist/guard/grace.test.d.ts +20 -0
  149. package/dist/guard/grace.test.d.ts.map +1 -0
  150. package/dist/guard/index.d.ts +66 -3
  151. package/dist/guard/index.d.ts.map +1 -1
  152. package/dist/guard/index.js +82 -7
  153. package/dist/guard/types.d.ts +3 -3
  154. package/dist/guard/types.d.ts.map +1 -1
  155. package/dist/index.d.ts +12 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +13 -10
  158. package/dist/nav/types.d.ts +78 -2
  159. package/dist/nav/types.d.ts.map +1 -1
  160. package/dist/notifications/bell.d.ts +4 -1
  161. package/dist/notifications/bell.d.ts.map +1 -1
  162. package/dist/notifications/index.d.ts +1 -1
  163. package/dist/router-tanstack/field.d.ts +14 -0
  164. package/dist/router-tanstack/field.d.ts.map +1 -0
  165. package/dist/router-tanstack/field.js +4 -0
  166. package/dist/router-tanstack/index.d.ts +6 -2
  167. package/dist/router-tanstack/index.d.ts.map +1 -1
  168. package/dist/router-tanstack/index.js +4 -0
  169. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  170. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  171. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  172. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  173. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/search-param-store.js +16 -45
  175. package/dist/router-tanstack/search-store.d.ts +81 -0
  176. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  177. package/dist/router-tanstack/search-store.js +187 -0
  178. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  179. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  181. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  182. package/dist/router-tanstack/use-nav.d.ts +0 -2
  183. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  184. package/dist/router-tanstack/use-nav.js +34 -12
  185. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  186. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  187. package/dist/section/index.d.ts +2 -0
  188. package/dist/section/index.d.ts.map +1 -0
  189. package/dist/section/index.js +4 -0
  190. package/dist/section/section.d.ts +24 -0
  191. package/dist/section/section.d.ts.map +1 -0
  192. package/dist/section/section.js +126 -0
  193. package/dist/section/section.module.css +104 -0
  194. package/dist/section/section.module.css.d.ts +11 -0
  195. package/dist/section/section.test.d.ts +2 -0
  196. package/dist/section/section.test.d.ts.map +1 -0
  197. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  198. package/dist/shell/app-breadcrumbs.js +18 -15
  199. package/dist/shell/app-header.module.css +77 -37
  200. package/dist/shell/app-header.module.css.d.ts +2 -1
  201. package/dist/shell/app-mobile-nav.d.ts +9 -2
  202. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  203. package/dist/shell/app-mobile-nav.js +98 -11
  204. package/dist/shell/app-mobile-nav.module.css +8 -0
  205. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  206. package/dist/shell/app-sidebar.d.ts +55 -28
  207. package/dist/shell/app-sidebar.d.ts.map +1 -1
  208. package/dist/shell/app-sidebar.js +116 -67
  209. package/dist/shell/app-sidebar.module.css +303 -20
  210. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  211. package/dist/shell/index.d.ts +55 -21
  212. package/dist/shell/index.d.ts.map +1 -1
  213. package/dist/shell/index.js +40 -18
  214. package/dist/shell/mobile-nav-model.d.ts +13 -2
  215. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  216. package/dist/shell/mobile-nav-model.js +15 -1
  217. package/dist/shell/nav-count-badge.d.ts +1 -1
  218. package/dist/shell/page-bar.d.ts +51 -0
  219. package/dist/shell/page-bar.d.ts.map +1 -0
  220. package/dist/shell/page-bar.js +154 -0
  221. package/dist/shell/page-bar.module.css +178 -0
  222. package/dist/shell/page-bar.module.css.d.ts +17 -0
  223. package/dist/shell/page-bar.test.d.ts +2 -0
  224. package/dist/shell/page-bar.test.d.ts.map +1 -0
  225. package/dist/shell/sidebar-block-model.d.ts +48 -0
  226. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  227. package/dist/shell/sidebar-block-model.js +39 -0
  228. package/dist/shell/sidebar-blocks.d.ts +32 -0
  229. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  230. package/dist/shell/sidebar-blocks.js +169 -0
  231. package/dist/shell/sidebar-search.d.ts +13 -1
  232. package/dist/shell/sidebar-search.d.ts.map +1 -1
  233. package/dist/shell/sidebar-search.js +34 -1
  234. package/dist/shell/sidebar-search.module.css +26 -0
  235. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  236. package/dist/state/fields.d.ts +392 -0
  237. package/dist/state/fields.d.ts.map +1 -0
  238. package/dist/state/fields.js +369 -0
  239. package/dist/state/fields.test.d.ts +2 -0
  240. package/dist/state/fields.test.d.ts.map +1 -0
  241. package/dist/state/persisted.d.ts +48 -0
  242. package/dist/state/persisted.d.ts.map +1 -0
  243. package/dist/state/persisted.js +105 -0
  244. package/dist/state.d.ts +11 -56
  245. package/dist/state.d.ts.map +1 -1
  246. package/dist/state.js +14 -120
  247. package/dist/styles.css +13 -0
  248. package/dist/surfaces.d.ts +106 -24
  249. package/dist/surfaces.d.ts.map +1 -1
  250. package/dist/surfaces.js +155 -19
  251. package/dist/theme/controls.module.css +16 -1
  252. package/dist/theme/ctl-theme.d.ts +32 -0
  253. package/dist/theme/ctl-theme.d.ts.map +1 -0
  254. package/dist/theme/ctl-theme.js +54 -0
  255. package/dist/theme/ctl-theme.test.d.ts +2 -0
  256. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  257. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  258. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  259. package/dist/theme/divider-law.test.d.ts +2 -0
  260. package/dist/theme/divider-law.test.d.ts.map +1 -0
  261. package/dist/theme/icon-slot.d.ts +44 -0
  262. package/dist/theme/icon-slot.d.ts.map +1 -0
  263. package/dist/theme/icon-slot.js +21 -0
  264. package/dist/theme/icon-slot.module.css +47 -0
  265. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  266. package/dist/theme/icon-slot.test.d.ts +2 -0
  267. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  268. package/dist/theme/index.d.ts +1 -0
  269. package/dist/theme/index.d.ts.map +1 -1
  270. package/dist/theme/index.js +82 -5
  271. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  272. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  273. package/dist/theme/segmented-control.module.css +20 -0
  274. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  275. package/dist/tokens/index.d.ts +12 -0
  276. package/dist/tokens/index.d.ts.map +1 -1
  277. package/dist/tokens/index.js +38 -8
  278. package/dist/tokens/palette.d.ts +97 -61
  279. package/dist/tokens/palette.d.ts.map +1 -1
  280. package/dist/tokens/palette.js +76 -38
  281. package/dist/tokens.css +10 -2
  282. package/dist/widget-header/delta-badge.d.ts +13 -0
  283. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  284. package/dist/widget-header/delta-badge.js +29 -0
  285. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  286. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  287. package/dist/widget-header/index.d.ts +3 -0
  288. package/dist/widget-header/index.d.ts.map +1 -0
  289. package/dist/widget-header/index.js +6 -0
  290. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  291. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  292. package/dist/widget-header/widget-header.d.ts +37 -0
  293. package/dist/widget-header/widget-header.d.ts.map +1 -0
  294. package/dist/widget-header/widget-header.js +77 -0
  295. package/dist/widget-header/widget-header.module.css +335 -0
  296. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  297. package/dist/widget-header/widget-header.test.d.ts +2 -0
  298. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  299. package/llms.txt +30 -17
  300. package/package.json +14 -1
  301. package/src/charts/cursor/resolve.test.ts +29 -1
  302. package/src/charts/cursor/resolve.ts +38 -6
  303. package/src/charts/cursor/scope.tsx +6 -3
  304. package/src/charts/cursor/store.test.ts +19 -1
  305. package/src/charts/cursor/store.ts +10 -5
  306. package/src/charts/hooks/useChartCursor.ts +16 -8
  307. package/src/charts/index.ts +2 -2
  308. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  309. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  310. package/src/charts/primitives/ChartCard.tsx +86 -146
  311. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  312. package/src/cli/check-coverage.test.ts +297 -0
  313. package/src/cli/check-theme.test.ts +38 -14
  314. package/src/cli/index.ts +392 -16
  315. package/src/cli/init-sync.test.ts +97 -0
  316. package/src/connectivity/connectivity-indicator.tsx +16 -9
  317. package/src/content/index.ts +16 -3
  318. package/src/content/prose.module.css +7 -28
  319. package/src/content/prose.module.css.test.ts +8 -24
  320. package/src/controls/actions.module.css +62 -0
  321. package/src/controls/actions.module.css.d.ts +13 -0
  322. package/src/controls/actions.test.tsx +252 -0
  323. package/src/controls/actions.tsx +655 -0
  324. package/src/controls/actions.type-guard.test.ts +63 -0
  325. package/src/controls/compare-filter.tsx +64 -0
  326. package/src/controls/control-group.test.tsx +229 -0
  327. package/src/controls/control-group.tsx +63 -0
  328. package/src/controls/controls.module.css +542 -0
  329. package/src/controls/controls.module.css.d.ts +35 -0
  330. package/src/controls/controls.router.test.tsx +637 -0
  331. package/src/controls/enum-filter.tsx +159 -0
  332. package/src/controls/filter-context.tsx +153 -0
  333. package/src/controls/filter-pill.tsx +120 -0
  334. package/src/controls/filter-set.test.tsx +472 -0
  335. package/src/controls/filter-set.tsx +225 -0
  336. package/src/controls/filter-sheet.tsx +285 -0
  337. package/src/controls/glyphs.tsx +81 -0
  338. package/src/controls/index.ts +48 -0
  339. package/src/controls/multi-select-filter.tsx +140 -0
  340. package/src/controls/peer-boundary.test.ts +85 -0
  341. package/src/controls/props-contract.test.tsx +106 -0
  342. package/src/controls/range-filter.tsx +184 -0
  343. package/src/controls/search-filter.tsx +123 -0
  344. package/src/controls/select-filter.tsx +89 -0
  345. package/src/controls/sync-button.module.css +63 -0
  346. package/src/controls/sync-button.module.css.d.ts +11 -0
  347. package/src/controls/sync-button.test.tsx +183 -0
  348. package/src/controls/sync-button.tsx +197 -0
  349. package/src/controls/toggle-filter.tsx +60 -0
  350. package/src/controls/view-tabs.tsx +110 -0
  351. package/src/controls-dates/date-range-picker.tsx +42 -0
  352. package/src/controls-dates/index.ts +8 -0
  353. package/src/dashboard/delta-badge.tsx +4 -74
  354. package/src/dashboard/settings-section.module.css +10 -0
  355. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  356. package/src/dashboard/settings-section.test.tsx +56 -0
  357. package/src/dashboard/settings-section.tsx +33 -30
  358. package/src/dashboard/stat-card.module.css +82 -0
  359. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  360. package/src/dashboard/stat-card.test.tsx +67 -1
  361. package/src/dashboard/stat-card.tsx +209 -68
  362. package/src/data/data-table.test.tsx +96 -11
  363. package/src/data/data-table.tsx +220 -84
  364. package/src/guard/check-source.test.ts +126 -13
  365. package/src/guard/grace.test.ts +149 -0
  366. package/src/guard/index.ts +172 -29
  367. package/src/guard/types.ts +4 -2
  368. package/src/index.ts +46 -5
  369. package/src/nav/types.ts +84 -2
  370. package/src/notifications/bell.tsx +6 -3
  371. package/src/notifications/index.ts +1 -1
  372. package/src/router-tanstack/field.ts +31 -0
  373. package/src/router-tanstack/index.ts +24 -2
  374. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  375. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  376. package/src/router-tanstack/search-param-store.ts +42 -159
  377. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  378. package/src/router-tanstack/search-store.test.ts +418 -0
  379. package/src/router-tanstack/search-store.ts +380 -0
  380. package/src/router-tanstack/use-nav.test.tsx +187 -0
  381. package/src/router-tanstack/use-nav.ts +72 -14
  382. package/src/section/index.ts +1 -0
  383. package/src/section/section.module.css +104 -0
  384. package/src/section/section.module.css.d.ts +11 -0
  385. package/src/section/section.test.tsx +204 -0
  386. package/src/section/section.tsx +220 -0
  387. package/src/shell/app-breadcrumbs.tsx +44 -31
  388. package/src/shell/app-header.module.css +77 -37
  389. package/src/shell/app-header.module.css.d.ts +2 -1
  390. package/src/shell/app-mobile-nav.module.css +8 -0
  391. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  392. package/src/shell/app-mobile-nav.tsx +147 -8
  393. package/src/shell/app-sidebar-account.tsx +2 -2
  394. package/src/shell/app-sidebar.module.css +303 -20
  395. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  396. package/src/shell/app-sidebar.test.tsx +456 -41
  397. package/src/shell/app-sidebar.tsx +276 -140
  398. package/src/shell/index.test.tsx +291 -5
  399. package/src/shell/index.tsx +137 -38
  400. package/src/shell/mobile-nav-model.test.ts +228 -7
  401. package/src/shell/mobile-nav-model.ts +53 -3
  402. package/src/shell/nav-count-badge.tsx +1 -1
  403. package/src/shell/page-bar.module.css +178 -0
  404. package/src/shell/page-bar.module.css.d.ts +17 -0
  405. package/src/shell/page-bar.test.tsx +316 -0
  406. package/src/shell/page-bar.tsx +315 -0
  407. package/src/shell/sidebar-block-model.ts +83 -0
  408. package/src/shell/sidebar-blocks.tsx +301 -0
  409. package/src/shell/sidebar-search.module.css +26 -0
  410. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  411. package/src/shell/sidebar-search.tsx +71 -2
  412. package/src/state/fields.test.tsx +258 -0
  413. package/src/state/fields.ts +892 -0
  414. package/src/state/persisted.ts +211 -0
  415. package/src/state.ts +46 -240
  416. package/src/styles.css +13 -0
  417. package/src/surfaces.ts +203 -36
  418. package/src/theme/controls.module.css +16 -1
  419. package/src/theme/ctl-theme.test.tsx +275 -0
  420. package/src/theme/ctl-theme.tsx +138 -0
  421. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  422. package/src/theme/density-relations.test.ts +106 -51
  423. package/src/theme/divider-law.test.ts +259 -0
  424. package/src/theme/icon-slot.module.css +47 -0
  425. package/src/theme/icon-slot.module.css.d.ts +8 -0
  426. package/src/theme/icon-slot.test.tsx +275 -0
  427. package/src/theme/icon-slot.tsx +59 -0
  428. package/src/theme/index.ts +165 -3
  429. package/src/theme/layout-rhythm-css.test.ts +72 -0
  430. package/src/theme/radius.test.ts +11 -1
  431. package/src/theme/segmented-control.module.css +20 -0
  432. package/src/theme/shadow-surfaces.test.ts +20 -7
  433. package/src/theme/spacing.test.ts +75 -27
  434. package/src/theme/use-basalt-spacing.ts +2 -2
  435. package/src/tokens/density.test.ts +7 -25
  436. package/src/tokens/index.ts +48 -8
  437. package/src/tokens/palette.ts +107 -73
  438. package/src/widget-header/delta-badge.test.tsx +60 -0
  439. package/src/widget-header/delta-badge.tsx +57 -0
  440. package/src/widget-header/index.ts +2 -0
  441. package/src/widget-header/no-mantine.test.ts +33 -0
  442. package/src/widget-header/widget-header.module.css +335 -0
  443. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  444. package/src/widget-header/widget-header.test.tsx +187 -0
  445. package/src/widget-header/widget-header.tsx +168 -0
  446. package/agent/rules/basalt-agent.md +0 -452
  447. package/agent/rules/basalt-app.md +0 -246
  448. package/agent/rules/basalt-commands.md +0 -320
  449. package/agent/rules/basalt-content.md +0 -409
  450. package/agent/rules/basalt-data.md +0 -329
  451. package/agent/rules/basalt-forms.md +0 -186
  452. package/agent/rules/basalt-notifications.md +0 -264
  453. package/agent/rules/basalt-query.md +0 -187
  454. package/agent/rules/basalt-router.md +0 -336
  455. package/dist/content/article-filter-bar.d.ts +0 -40
  456. package/dist/content/article-filter-bar.d.ts.map +0 -1
  457. package/dist/content/article-filter-bar.js +0 -56
  458. package/dist/shell/page-header.d.ts +0 -13
  459. package/dist/shell/page-header.d.ts.map +0 -1
  460. package/dist/shell/page-header.js +0 -23
  461. package/src/content/article-filter-bar.tsx +0 -88
  462. package/src/shell/page-header.tsx +0 -35
@@ -12,3 +12,23 @@
12
12
  color: var(--vx-ink);
13
13
  font-weight: 600;
14
14
  }
15
+
16
+ /*
17
+ * Numeric segment labels read mono `VX.text.xs` (`docs/CONTROLS-SPEC.md` §3 last paragraph, C7) —
18
+ * a consumer opts in with a bare `data-numeric` attribute on the `SegmentedControl` itself (an
19
+ * unrecognized native prop, forwarded onto the root `Box` via Mantine's own `...others` spread,
20
+ * `SegmentedControl.mjs`), retiring the per-consumer `theme-allow` + inline `styles={{ label: {
21
+ * fontFamily: 'var(--basalt-font-mono)', fontSize: 11.5 } }}` hack (`DashboardDateFilter.tsx`).
22
+ * The MONO FACE itself is now a package-wide law in `styles.css` (`[data-numeric]` in `@layer
23
+ * basalt`, so a plain `<Text data-numeric>` works too — the module-scoped copy is why it used to
24
+ * look broken outside a SegmentedControl); what stays here is the component's own `font-size`, plus
25
+ * the face restated so this module is still correct read on its own.
26
+ * `[data-numeric]` is a bare attribute selector, not a CSS-module class, so it is intentionally left
27
+ * unscoped — same ancestor-attribute pattern as `controls.module.css`'s
28
+ * `html[data-mantine-color-scheme] .inputWrapper[data-variant='default']`. `.label` here is the
29
+ * SAME scoped class `data-active` above targets, so both rules can co-apply (active + numeric).
30
+ */
31
+ [data-numeric] .label {
32
+ font-family: var(--basalt-font-mono);
33
+ font-size: var(--vx-text-xs);
34
+ }
@@ -353,19 +353,32 @@ export const SHADOW_SURFACES: readonly ShadowSurfaceEntry[] = [
353
353
  "on the root part; Mantine's own Notification.css declares `border-radius: var(--" +
354
354
  'notification-radius)` on that same root element the styles.root boxShadow targets).',
355
355
  },
356
+ // ── widget-header ────────────────────────────────────────────────────────────────────────────
357
+ {
358
+ file: 'widget-header/widget-header.module.css',
359
+ site: '.infoBubble',
360
+ roundedBy:
361
+ 'border-radius: var(--vx-radius-card) (co-declared in the same .infoBubble rule as the ' +
362
+ 'box-shadow, so the ring follows this box’s own shape). This is the info-tooltip bubble that ' +
363
+ 'used to live in charts/primitives/ChartCard.tsx as `bubbleStyle`; it moved down into ' +
364
+ 'WidgetHeader when ChartCard started composing it, so every tier gets the same ' +
365
+ 'hover/focus/click affordance. Nothing clips it: its host `.info` span is `position: ' +
366
+ 'relative` with no overflow, and ChartCard deliberately keeps its header outside the chart ' +
367
+ 'body’s clip box for exactly this reason.',
368
+ },
356
369
  // ── charts/primitives ────────────────────────────────────────────────────────────────────────
357
370
  {
358
371
  file: 'charts/primitives/ChartCard.tsx',
359
372
  site: 'default',
360
373
  roundedBy:
361
- 'Two shadowed boxes in this file, each co-declaring borderRadius: VX.radiusCard with its own ' +
362
- 'boxShadow (both fall back to the file-level default label). (1) cardStyle — the outer card box ' +
363
- 'co-declares borderRadius + boxShadow + backgroundColor, so the ring is bound to THIS radius and ' +
374
+ 'One shadowed box in this file: cardStyle — the outer card box co-declares borderRadius: ' +
375
+ 'VX.radiusCard + boxShadow + backgroundColor, so the ring is bound to THIS radius and ' +
364
376
  'matches. It carries NO overflow: the chart body clips itself on a separate inner box ' +
365
- '(bodyClipStyle, identical bottom-corner radius, no shadow) so the header — and its info-tooltip ' +
366
- 'bubble — can escape the clip; the ring is safe on the outer box either way (an element’s own ' +
367
- 'overflow never clips its own shadow). (2) bubbleStyle — the info-tooltip bubble, a floating panel ' +
368
- 'surface whose ring likewise follows its co-declared card radius.',
377
+ '(bodyClipStyle, identical bottom-corner radius, no shadow) so the header — and the info ' +
378
+ 'bubble WidgetHeader opens under it — can escape the clip; the ring is safe on the outer box ' +
379
+ 'either way (an element’s own overflow never clips its own shadow). The second shadowed box ' +
380
+ 'this entry used to cover, `bubbleStyle`, now lives in widget-header.module.css’s ' +
381
+ '.infoBubble (registered above).',
369
382
  },
370
383
  {
371
384
  file: 'charts/primitives/ChartTooltip.tsx',
@@ -20,21 +20,19 @@
20
20
  * `rowInsetY`, the 4px stack rhythm, and the sidebar SIZES (`sidebarAvatarSize`,
21
21
  * `sidebarSearchTriggerHeight`, the two Menu widths) were held back on purpose — see that commit.
22
22
  * - `SPACE_STEP.stickyHeaderClearance` moved from 84 (the original literal) through an
23
- * intermediate single derived value of 108, to its FINAL shape — a responsive PAIR (Decision 3):
24
- * `stickyHeaderClearance` (desktop, `>= sm`) now 60, plus a new `stickyHeaderClearanceMobile`
25
- * (mobile, `< sm`) at 108. Both are DERIVED from their own AppShell header (`appShellHeaderHeight`/
26
- * `appShellHeaderMobileHeight`) instead of one independent literal, which fixed two bugs at once:
27
- * the original 84 under-cleared the mobile header (96px at the time, 97 since the retune above) at
28
- * level 0 (before density entered the
29
- * picture at all), and a single derived value tuned against the mobile header over-cleared the 48px
30
- * desktop header by 60px on the common (desktop) path. See `deriveSpacing`'s doc in
31
- * `tokens/palette.ts` for the full rationale. Every value NOT named above is still byte-identical to
32
- * the pre-tokenization number it replaced.
23
+ * intermediate single derived value of 108, then a responsive PAIR (Decision 3), to its FINAL shape:
24
+ * ONE value, 60, DERIVED as `appShellHeaderHeight + anchors.stackMd`. Deriving it fixed the original
25
+ * 84's drift from the header it was supposed to clear; the `stickyHeaderClearanceMobile` half of the
26
+ * pair was deleted in 1.26.0 along with the two-row mobile header it existed for — the AppShell
27
+ * header is one 48px row at every viewport now (law C14, `docs/CONTROLS-SPEC.md` §2.1), so
28
+ * `appShellHeaderMobileHeight` and `appHeaderMobileActionsHeight` went with it. See `deriveSpacing`'s
29
+ * doc in `tokens/palette.ts` for the full rationale. Every value NOT named above is still
30
+ * byte-identical to the pre-tokenization number it replaced.
33
31
  */
34
32
  import { DEFAULT_THEME, mergeMantineTheme } from '@mantine/core'
35
33
  import type { MantineTheme } from '@mantine/core'
36
34
  import { describe, expect, test } from 'bun:test'
37
- import { buildPaletteCss } from '../tokens'
35
+ import { buildPaletteCss, pxRem } from '../tokens'
38
36
  import { SPACE, SPACE_FIXED, SPACE_SCALE, SPACE_STEP } from '../tokens/palette'
39
37
  import { baseTheme } from './index'
40
38
 
@@ -58,6 +56,13 @@ describe('SPACE anchors match the shipped identity', () => {
58
56
  expect(SPACE.inputHeight).toBe(42)
59
57
  expect(SPACE.controlHeight).toBe(42)
60
58
  })
59
+
60
+ test('the controls tier anchors are 20/24/30/36 (docs/CONTROLS-SPEC.md §5)', () => {
61
+ expect(SPACE.controlHeightTag).toBe(20)
62
+ expect(SPACE.controlHeightWidget).toBe(24)
63
+ expect(SPACE.controlHeightCtl).toBe(30)
64
+ expect(SPACE.touchControlHeight).toBe(36)
65
+ })
61
66
  })
62
67
 
63
68
  describe('SPACE_SCALE matches the shipped identity — independent of SPACE even where it coincides', () => {
@@ -118,7 +123,17 @@ describe('every re-pointed component styles/defaultProps spacing value matches i
118
123
  })
119
124
 
120
125
  test('SegmentedControl root padding = 2 (a density-exempt SPACE_FIXED literal, not a var)', () => {
121
- expect(baseTheme.components?.['SegmentedControl']?.styles?.['root']?.['padding']).toBe('2px')
126
+ // `styles` is function-form (size='ctl' gates a label override — see `theme/index.ts`), not a
127
+ // flat object, so it has to be called rather than indexed directly.
128
+ const stylesFn = baseTheme.components?.['SegmentedControl']?.styles as
129
+ | ((
130
+ theme: unknown,
131
+ props: { size?: string },
132
+ ctx: unknown,
133
+ ) => Record<string, Record<string, unknown>>)
134
+ | undefined
135
+ const resolved = stylesFn?.({}, {}, {})
136
+ expect(resolved?.['root']?.['padding']).toBe('2px')
122
137
  })
123
138
 
124
139
  test('Timeline defaultProps stays bulletSize: 22, lineWidth: 1', () => {
@@ -240,6 +255,19 @@ describe('--vx-space-* is emitted from the SAME constants', () => {
240
255
  expect(css).toContain('--vx-space-control-height: 2.625rem;')
241
256
  expect(css).not.toContain('--vx-space-control-height: 42px;')
242
257
  })
258
+
259
+ test('emits the controls-tier heights in REM, and the control gap in px (docs/CONTROLS-SPEC.md §5)', () => {
260
+ expect(css).toContain('--vx-space-control-height-tag: 1.25rem;')
261
+ expect(css).toContain('--vx-space-control-height-widget: 1.5rem;')
262
+ expect(css).toContain('--vx-space-control-height-ctl: 1.875rem;')
263
+ expect(css).toContain('--vx-space-touch-control-height: 2.25rem;')
264
+ expect(css).toContain('--vx-space-page-bar-row-height: 2.5rem;')
265
+ expect(css).toContain('--vx-space-section-header-height: 2.25rem;')
266
+ expect(css).toContain('--vx-space-widget-header-height: 1.75rem;')
267
+ expect(css).toContain('--vx-space-sidebar-block-row-height: 2rem;')
268
+ expect(css).toContain('--vx-space-control-gap: 6px;')
269
+ expect(css).toContain('--vx-space-sheet-row-height: 2.75rem;')
270
+ })
243
271
  })
244
272
 
245
273
  /**
@@ -253,16 +281,22 @@ describe('--vx-space-* is emitted from the SAME constants', () => {
253
281
  * in `tokens/index.ts`).
254
282
  */
255
283
  const SPACE_STEP_SWEEP: ReadonlyArray<
256
- readonly [key: keyof typeof SPACE_STEP, value: number, cssVar: string | null]
284
+ readonly [
285
+ key: keyof typeof SPACE_STEP,
286
+ value: number,
287
+ cssVar: string | null,
288
+ /** Defaults to `'px'`. The controls-tier row/sheet heights emit REM (`pxRem`), same
289
+ * reconstruction as `space-input-height`/`space-control-height` — see `spaceDecls`'s doc in
290
+ * `tokens/index.ts`. */
291
+ unit?: 'px' | 'rem',
292
+ ]
257
293
  > = [
258
294
  // DERIVED, not an independent literal — see `deriveSpacing`'s doc (`tokens/palette.ts`, third
259
- // bullet, Decision 3) for why 60/108 (not 84) are the level-0 values: the ONE responsive PAIR
260
- // deliberately exempt from the "every SPACE_STEP number is locked at level 0" invariant
261
- // this file otherwise enforces (locked here, not skipped, precisely so a future regression back to
262
- // 84, or back to a single shared value, shows up as a failing assertion rather than a silent
263
- // revert).
295
+ // bullet) for why 60 (not 84) is the level-0 value: the ONE key deliberately exempt from the
296
+ // "every SPACE_STEP number is locked at level 0" invariant this file otherwise enforces (locked
297
+ // here, not skipped, precisely so a future regression back to 84 shows up as a failing assertion
298
+ // rather than a silent revert).
264
299
  ['stickyHeaderClearance', 60, 'space-sticky-header-clearance'],
265
- ['stickyHeaderClearanceMobile', 109, 'space-sticky-header-clearance-mobile'],
266
300
  ['navIconGap', 10, 'space-nav-icon-gap'],
267
301
  ['sidebarRegionGap', 10, 'space-sidebar-region-gap'],
268
302
  ['proseQuoteInsetY', 2, 'space-prose-quote-inset-y'],
@@ -343,13 +377,11 @@ const SPACE_STEP_SWEEP: ReadonlyArray<
343
377
  // Mantine's `<Menu width={…}>` also takes a number, not a `var()` string).
344
378
  ['sidebarAccountMenuWidth', 220, null],
345
379
  ['sidebarSettingsMenuWidth', 200, null],
346
- ['appHeaderMobileActionsHeight', 52, 'space-app-header-mobile-actions-height'],
347
- // JS-number-only (`shell/index.tsx` reads all four via `useBasaltSpacing()` — Mantine's AppShell
380
+ // JS-number-only (`shell/index.tsx` reads all three via `useBasaltSpacing()` — Mantine's AppShell
348
381
  // `header`/`navbar` props take numbers, not `var()` strings) — see `spaceDecls`'s doc in
349
382
  // `tokens/index.ts` for why a `--vx-space-app-shell-*` declaration would have zero consumers.
350
383
  ['appShellHeaderHeight', 48, null],
351
- ['appShellHeaderMobileHeight', 97, null],
352
- ['appShellNavbarWidth', 216, null],
384
+ ['appShellNavbarWidth', 256, null],
353
385
  ['appShellNavbarRailWidth', 48, null],
354
386
  ['mobileNavTabGap', 3, 'space-mobile-nav-tab-gap'],
355
387
  // JS-number-only, and for the same two reasons the six above are: `shell/index.tsx` hands
@@ -384,6 +416,19 @@ const SPACE_STEP_SWEEP: ReadonlyArray<
384
416
  ['chartDotR', 5, null],
385
417
  ['progressBarSize', 6, null],
386
418
  ['timelineBullet', 22, null],
419
+ // Controls tier row/gap heights (`docs/CONTROLS-SPEC.md` §5) — emitted in REM for the heights
420
+ // (`pageBarRowHeight`/`sectionHeaderHeight`/`widgetHeaderHeight`/`sidebarBlockRowHeight`/
421
+ // `sheetRowHeight`, same `pxRem` reconstruction as `space-input-height`), px for the gap.
422
+ ['pageBarRowHeight', 40, 'space-page-bar-row-height', 'rem'],
423
+ ['sectionHeaderHeight', 36, 'space-section-header-height', 'rem'],
424
+ ['widgetHeaderHeight', 28, 'space-widget-header-height', 'rem'],
425
+ ['sidebarBlockRowHeight', 32, 'space-sidebar-block-row-height', 'rem'],
426
+ ['controlGap', 6, 'space-control-gap'],
427
+ ['sheetRowHeight', 44, 'space-sheet-row-height', 'rem'],
428
+ // Applied as JS numbers in `Table.extend`'s `styles.th`/`styles.td` `height`, so — like
429
+ // `progressBarSize` — neither has a `--vx-space-*` declaration to assert.
430
+ ['tableHeaderHeight', 36, null],
431
+ ['tableRowHeight', 40, null],
387
432
  ]
388
433
 
389
434
  describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identity', () => {
@@ -395,9 +440,10 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
395
440
 
396
441
  test('every constant with a cssVar is emitted as its own --vx-space-* declaration, unchanged', () => {
397
442
  const css = buildPaletteCss()
398
- for (const [, value, cssVar] of SPACE_STEP_SWEEP) {
443
+ for (const [, value, cssVar, unit = 'px'] of SPACE_STEP_SWEEP) {
399
444
  if (cssVar === null) continue
400
- expect(css).toContain(`--vx-${cssVar}: ${value}px;`)
445
+ const expected = unit === 'rem' ? pxRem(value) : `${value}px`
446
+ expect(css).toContain(`--vx-${cssVar}: ${expected};`)
401
447
  }
402
448
  })
403
449
 
@@ -410,7 +456,6 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
410
456
  'sidebarAccountMenuWidth',
411
457
  'sidebarSettingsMenuWidth',
412
458
  'appShellHeaderHeight',
413
- 'appShellHeaderMobileHeight',
414
459
  'appShellNavbarWidth',
415
460
  'appShellNavbarRailWidth',
416
461
  'mobileNavBarHeight',
@@ -424,11 +469,12 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
424
469
  'chartDotR',
425
470
  'progressBarSize',
426
471
  'timelineBullet',
472
+ 'tableHeaderHeight',
473
+ 'tableRowHeight',
427
474
  ])
428
475
  expect(css).not.toContain('--vx-space-sidebar-account-menu-width')
429
476
  expect(css).not.toContain('--vx-space-sidebar-settings-menu-width')
430
477
  expect(css).not.toContain('--vx-space-app-shell-header-height')
431
- expect(css).not.toContain('--vx-space-app-shell-header-mobile-height')
432
478
  expect(css).not.toContain('--vx-space-app-shell-navbar-width')
433
479
  expect(css).not.toContain('--vx-space-app-shell-navbar-rail-width')
434
480
  expect(css).not.toContain('--vx-space-mobile-nav-bar-height')
@@ -436,6 +482,8 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
436
482
  expect(css).not.toContain('--vx-space-chart-legend-gap')
437
483
  expect(css).not.toContain('--vx-space-progress-bar-size')
438
484
  expect(css).not.toContain('--vx-space-timeline-bullet')
485
+ expect(css).not.toContain('--vx-space-table-header-height')
486
+ expect(css).not.toContain('--vx-space-table-row-height')
439
487
  })
440
488
 
441
489
  test('the sweep table covers every SPACE_STEP key', () => {
@@ -13,8 +13,8 @@
13
13
  *
14
14
  * Three `../shell` components consume it today, two distinct classes of use:
15
15
  * - `BasaltShell` (`../shell/index.tsx`) — its AppShell header/navbar dimensions need the ACTIVE
16
- * density level's numbers (`step.appShellHeaderHeight`/`.appShellHeaderMobileHeight`/
17
- * `.appShellNavbarWidth`/`.appShellNavbarRailWidth`), not the frozen shipped-identity constants,
16
+ * density level's numbers (`step.appShellHeaderHeight`/`.appShellNavbarWidth`/
17
+ * `.appShellNavbarRailWidth`), not the frozen shipped-identity constants,
18
18
  * because those boxes must grow/shrink with the density-tracked controls rendered inside them.
19
19
  * - `AppSidebar` (`../shell/app-sidebar.tsx`) and `SidebarAccount` (`../shell/app-sidebar-
20
20
  * account.tsx`) each read `step.sidebarSettingsMenuWidth`/`.sidebarAccountMenuWidth` for their
@@ -125,8 +125,8 @@ describe('rowLineHeight follows its own additive law, not the multiplier', () =>
125
125
  })
126
126
  })
127
127
 
128
- describe('stickyHeaderClearance/stickyHeaderClearanceMobile derive from their OWN AppShell header, not a shared base (Decision 3 — responsive split)', () => {
129
- test('level -3/0/+3: desktop 43 / 60 / 77 — appShellHeaderHeight + anchors.stackMd at each level', () => {
128
+ describe('stickyHeaderClearance derives from the AppShell header, not an independent base', () => {
129
+ test('level -3/0/+3: 43 / 60 / 77 — appShellHeaderHeight + anchors.stackMd at each level', () => {
130
130
  const low = deriveSpacing(-3)
131
131
  const zero = deriveSpacing(0)
132
132
  const high = deriveSpacing(3)
@@ -139,28 +139,10 @@ describe('stickyHeaderClearance/stickyHeaderClearanceMobile derive from their OW
139
139
  )
140
140
  })
141
141
 
142
- test('level -3/0/+3: mobile 77 / 109 / 141 — appShellHeaderMobileHeight + anchors.stackMd at each level', () => {
143
- const low = deriveSpacing(-3)
144
- const zero = deriveSpacing(0)
145
- const high = deriveSpacing(3)
146
- expect(low.step.stickyHeaderClearanceMobile).toBe(77)
147
- expect(low.step.stickyHeaderClearanceMobile).toBe(
148
- low.step.appShellHeaderMobileHeight + low.anchors.stackMd,
149
- )
150
- expect(zero.step.stickyHeaderClearanceMobile).toBe(109)
151
- expect(high.step.stickyHeaderClearanceMobile).toBe(141)
152
- expect(high.step.stickyHeaderClearanceMobile).toBe(
153
- high.step.appShellHeaderMobileHeight + high.anchors.stackMd,
154
- )
155
- })
156
-
157
- test('every level: each clearance exceeds ONLY the header it exists to clear, exactly by anchors.stackMd', () => {
142
+ test('every level: the clearance exceeds the header it exists to clear, exactly by anchors.stackMd', () => {
158
143
  for (let level = -3; level <= 3; level++) {
159
144
  const { step, anchors } = deriveSpacing(level)
160
145
  expect(step.stickyHeaderClearance - step.appShellHeaderHeight).toBe(anchors.stackMd)
161
- expect(step.stickyHeaderClearanceMobile - step.appShellHeaderMobileHeight).toBe(
162
- anchors.stackMd,
163
- )
164
146
  }
165
147
  })
166
148
  })
@@ -206,10 +188,9 @@ describe('buildDensityCss', () => {
206
188
  // REM, not px — see the input-height describe block below for why.
207
189
  expect(css).toContain('--vx-space-input-height: 3.4375rem;')
208
190
  expect(css).toContain('--vx-space-control-height: 3.4375rem;')
209
- // 77 (desktop) / 141 (mobile), not the anchor/scale/step multiplier result for an 84px base —
210
- // both are DERIVED (own header + anchors.stackMd), see the dedicated describe block above.
191
+ // 77, not the anchor/scale/step multiplier result for an 84px base — it is DERIVED (the header
192
+ // plus anchors.stackMd), see the dedicated describe block above.
211
193
  expect(css).toContain('--vx-space-sticky-header-clearance: 77px;')
212
- expect(css).toContain('--vx-space-sticky-header-clearance-mobile: 141px;')
213
194
  // JS-number-only constants (Timeline's bulletSize, VX chart geometry, Progress's size) have no
214
195
  // CSS var to override — see `spaceDecls`'s doc in `tokens/index.ts`.
215
196
  expect(css).not.toContain('--vx-space-timeline-bullet')
@@ -233,10 +214,11 @@ describe('buildDensityCss', () => {
233
214
  const jsNumberOnly = [
234
215
  'space-timeline-bullet', // Timeline defaultProps.bulletSize
235
216
  'space-progress-bar-size', // Progress defaultProps.size
217
+ 'space-table-header-height', // Table.extend styles.th height
218
+ 'space-table-row-height', // Table.extend styles.td height
236
219
  'space-sidebar-account-menu-width', // Menu width prop
237
220
  'space-sidebar-settings-menu-width', // Menu width prop
238
221
  'space-app-shell-header-height', // AppShell header/navbar props
239
- 'space-app-shell-header-mobile-height',
240
222
  'space-app-shell-navbar-width',
241
223
  'space-app-shell-navbar-rail-width',
242
224
  'space-mobile-nav-bar-height', // AppShell footer.height prop
@@ -159,6 +159,23 @@ export const VX = {
159
159
  // radius knob (see `RADIUS_STEP.pill`'s doc in `tokens/palette.ts`).
160
160
  radiusPill: 'var(--vx-radius-pill)',
161
161
 
162
+ // Controls tier sizing (`docs/CONTROLS-SPEC.md` §5) — `var()` refs onto the density-tracking
163
+ // `--vx-space-*` declarations `spaceDecls` emits above (same ref pattern as `radiusCard`/
164
+ // `radiusCtrl`), so a home/control reads ONE source for its tier height whether it's Mantine
165
+ // chrome (`cssVariablesResolver`/`CTL_THEME`) or a Mantine-free composed layout.
166
+ space: {
167
+ controlHeightTag: 'var(--vx-space-control-height-tag)',
168
+ controlHeightWidget: 'var(--vx-space-control-height-widget)',
169
+ controlHeightCtl: 'var(--vx-space-control-height-ctl)',
170
+ touchControlHeight: 'var(--vx-space-touch-control-height)',
171
+ pageBarRowHeight: 'var(--vx-space-page-bar-row-height)',
172
+ sectionHeaderHeight: 'var(--vx-space-section-header-height)',
173
+ widgetHeaderHeight: 'var(--vx-space-widget-header-height)',
174
+ sidebarBlockRowHeight: 'var(--vx-space-sidebar-block-row-height)',
175
+ controlGap: 'var(--vx-space-control-gap)',
176
+ sheetRowHeight: 'var(--vx-space-sheet-row-height)',
177
+ },
178
+
162
179
  // Secondary-line color (back-compat alias; now theme-aware via --vx-line2)
163
180
  line2Dark: 'var(--vx-line2)',
164
181
 
@@ -418,6 +435,15 @@ function frameworkPrimitives(side: Side, data: PaletteData, legacyAliases: boole
418
435
  * the two lists can never drift apart — mirrors `frameworkDerived`'s own single-source reasoning for
419
436
  * the radii.
420
437
  *
438
+ * The ten controls-tier anchors/one-offs (`controlHeightTag`/`Widget`/`Ctl`, `touchControlHeight`,
439
+ * `pageBarRowHeight`, `sectionHeaderHeight`, `widgetHeaderHeight`, `sidebarBlockRowHeight`,
440
+ * `controlGap`, `sheetRowHeight` — `docs/CONTROLS-SPEC.md` §5) are a DELIBERATE exception to the
441
+ * "has at least one real consumer today" rule below: `theme/ctl-theme.tsx`'s `CTL_THEME` and the
442
+ * `cssVariablesResolver`'s `-ctl`/`-icon` var sets consume the four control-height anchors this
443
+ * commit; `PageBar`/`WidgetHeader`/the sidebar blocks (wave 3/4) consume the six one-offs. Emitted
444
+ * now, ahead of that second wave, rather than left as JS-only — unlike the seventeen genuinely
445
+ * dead-weight keys below, these are forward-declared framework surface, not vestigial.
446
+ *
421
447
  * `SPACE_FIXED` (Timeline `lineWidth`, 1px borders, the ReadingProgress bar height, SegmentedControl's
422
448
  * `segmentedTrackInset`) is deliberately NEVER part of this list — it never moves, so a var would
423
449
  * only invite someone to think it does.
@@ -428,7 +454,7 @@ function frameworkPrimitives(side: Side, data: PaletteData, legacyAliases: boole
428
454
  * resolved `SpaceValues` as a JS NUMBER instead, in one of three ways:
429
455
  * `{timelineBullet,progressBarSize}` are Mantine `defaultProps` (`theme/index.ts`'s Timeline/
430
456
  * Progress); `{sidebarAccountMenuWidth,sidebarSettingsMenuWidth,mobileNavMenuWidth,
431
- * appShellHeaderHeight,appShellHeaderMobileHeight,appShellNavbarWidth,appShellNavbarRailWidth,
457
+ * appShellHeaderHeight,appShellNavbarWidth,appShellNavbarRailWidth,
432
458
  * mobileNavBarHeight}` are numeric Mantine component props (Menu `width`, AppShell `header`/
433
459
  * `navbar`/`footer`); and `{chartLegendGap,chartMarginTop,chartMarginRight,chartMarginRightAxis,
434
460
  * chartMarginBottom,chartMarginLeft,chartDotR}` are visx SVG props via
@@ -465,6 +491,14 @@ function spaceDecls(space: SpaceValues): string[] {
465
491
  // `ActionIcon.extend` `vars`), single-sourced from the SAME `controlHeight` anchor
466
492
  // `space-input-height` reads from (`SPACE_ANCHORS_BASE`'s doc in `tokens/palette.ts`).
467
493
  decl('space-control-height', pxRem(space.anchors.controlHeight)),
494
+ // Controls tier heights (`docs/CONTROLS-SPEC.md` §5) — same rem-plus-`--mantine-scale`
495
+ // reconstruction as `space-input-height`/`space-control-height` above; `theme/ctl-theme.tsx`'s
496
+ // `CTL_THEME` and the `cssVariablesResolver`'s `-ctl`/`-icon` var sets both wrap these in the
497
+ // matching `calc(... * var(--mantine-scale))`.
498
+ decl('space-control-height-tag', pxRem(space.anchors.controlHeightTag)),
499
+ decl('space-control-height-widget', pxRem(space.anchors.controlHeightWidget)),
500
+ decl('space-control-height-ctl', pxRem(space.anchors.controlHeightCtl)),
501
+ decl('space-touch-control-height', pxRem(space.anchors.touchControlHeight)),
468
502
  decl('space-stack-xs', `${space.anchors.stackXs}px`),
469
503
  decl('space-stack-sm', `${space.anchors.stackSm}px`),
470
504
  decl('space-stack-md', `${space.anchors.stackMd}px`),
@@ -472,11 +506,9 @@ function spaceDecls(space: SpaceValues): string[] {
472
506
  decl('space-stack-xl', `${space.anchors.stackXl}px`),
473
507
  // The CSS-module spacing sweep's one-offs (docs/STATUS.md) — same single-source reasoning,
474
508
  // grouped to match `SPACE_STEP_BASE`'s own grouping in tokens/palette.ts.
475
- // Responsive pair (Decision 3) — `stickyHeaderClearance` is the desktop (`>= sm`) value,
476
- // `stickyHeaderClearanceMobile` the mobile (`< sm`) override; a consumer CSS module picks
477
- // whichever matches its own breakpoint (see `content/prose.module.css`).
509
+ // ONE value at every viewport since 1.26.0 — the AppShell header is a single 48px row (law
510
+ // C14), so the `-mobile` override of the old Decision-3 responsive pair is gone.
478
511
  decl('space-sticky-header-clearance', `${space.step.stickyHeaderClearance}px`),
479
- decl('space-sticky-header-clearance-mobile', `${space.step.stickyHeaderClearanceMobile}px`),
480
512
  decl('space-nav-icon-gap', `${space.step.navIconGap}px`),
481
513
  decl('space-sidebar-region-gap', `${space.step.sidebarRegionGap}px`),
482
514
  decl('space-prose-quote-inset-y', `${space.step.proseQuoteInsetY}px`),
@@ -553,7 +585,6 @@ function spaceDecls(space: SpaceValues): string[] {
553
585
  decl('space-sidebar-child-list-indent', `${space.step.sidebarChildListIndent}px`),
554
586
  decl('space-sidebar-child-row-inset-y', `${space.step.sidebarChildRowInsetY}px`),
555
587
  decl('space-sidebar-child-row-indent', `${space.step.sidebarChildRowIndent}px`),
556
- decl('space-app-header-mobile-actions-height', `${space.step.appHeaderMobileActionsHeight}px`),
557
588
  decl('space-mobile-nav-tab-gap', `${space.step.mobileNavTabGap}px`),
558
589
  decl('space-mobile-nav-icon-size', `${space.step.mobileNavIconSize}px`),
559
590
  decl('space-mobile-nav-tab-inset-y', `${space.step.mobileNavTabInsetY}px`),
@@ -573,11 +604,20 @@ function spaceDecls(space: SpaceValues): string[] {
573
604
  decl('space-stat-card-gap', `${space.step.statCardGap}px`),
574
605
  decl('space-virtual-row-inset-y', `${space.step.virtualRowInsetY}px`),
575
606
  decl('space-virtual-row-inset-x', `${space.step.virtualRowInsetX}px`),
607
+ // Controls tier row/gap heights (`docs/CONTROLS-SPEC.md` §5) — the current commit's consumers
608
+ // are `theme/ctl-theme.tsx` (`--vx-space-control-gap`) and the wave-3/4 homes (`PageBar`,
609
+ // `WidgetHeader`, sidebar blocks, the mobile `Filters (n)` sheet) landing next.
610
+ decl('space-page-bar-row-height', pxRem(space.step.pageBarRowHeight)),
611
+ decl('space-section-header-height', pxRem(space.step.sectionHeaderHeight)),
612
+ decl('space-widget-header-height', pxRem(space.step.widgetHeaderHeight)),
613
+ decl('space-sidebar-block-row-height', pxRem(space.step.sidebarBlockRowHeight)),
614
+ decl('space-control-gap', `${space.step.controlGap}px`),
615
+ decl('space-sheet-row-height', pxRem(space.step.sheetRowHeight)),
576
616
  // `chartLegendGap`/`chartMarginTop`/`chartMarginRight`/`chartMarginBottom`/`chartMarginLeft`/
577
617
  // `chartDotR`/`progressBarSize`/`timelineBullet` are DELIBERATELY absent — see this function's
578
618
  // doc for why (JS-number-only consumers, zero `var()` reads). `appShellHeaderHeight`/
579
- // `appShellHeaderMobileHeight`/`appShellNavbarWidth`/`appShellNavbarRailWidth` join that list —
580
- // `shell/index.tsx` reads all four as JS numbers via `useBasaltSpacing()` (Mantine's `AppShell`
619
+ // `appShellNavbarWidth`/`appShellNavbarRailWidth` join that list —
620
+ // `shell/index.tsx` reads all three as JS numbers via `useBasaltSpacing()` (Mantine's `AppShell`
581
621
  // `header`/`navbar` props take numbers, not `var()` strings), so a `--vx-space-app-shell-*` decl
582
622
  // here would have had zero consumers, framework-wide, the same dead-weight shape this function's
583
623
  // doc already calls out for the chart/Progress/Timeline group. `sidebarAccountMenuWidth`/