basalt-ui 1.25.0 → 1.27.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 (476) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +26 -0
  3. package/MIGRATING.md +815 -23
  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 +160 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +118 -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 +1465 -36
  17. package/configs/oxlint-plugin.test.ts +1190 -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 +39 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +93 -0
  75. package/dist/controls/filter-context.d.ts +60 -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 +20 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +46 -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/number-filter.d.ts +26 -0
  99. package/dist/controls/number-filter.d.ts.map +1 -0
  100. package/dist/controls/number-filter.js +143 -0
  101. package/dist/controls/peer-boundary.test.d.ts +2 -0
  102. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  103. package/dist/controls/props-contract.test.d.ts +2 -0
  104. package/dist/controls/props-contract.test.d.ts.map +1 -0
  105. package/dist/controls/range-filter.d.ts +43 -0
  106. package/dist/controls/range-filter.d.ts.map +1 -0
  107. package/dist/controls/range-filter.js +135 -0
  108. package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
  109. package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
  110. package/dist/controls/search-filter.d.ts +10 -0
  111. package/dist/controls/search-filter.d.ts.map +1 -0
  112. package/dist/controls/search-filter.js +81 -0
  113. package/dist/controls/select-filter.d.ts +74 -0
  114. package/dist/controls/select-filter.d.ts.map +1 -0
  115. package/dist/controls/select-filter.js +10 -0
  116. package/dist/controls/sync-button.d.ts +38 -0
  117. package/dist/controls/sync-button.d.ts.map +1 -0
  118. package/dist/controls/sync-button.js +119 -0
  119. package/dist/controls/sync-button.module.css +63 -0
  120. package/dist/controls/sync-button.module.css.d.ts +11 -0
  121. package/dist/controls/sync-button.test.d.ts +2 -0
  122. package/dist/controls/sync-button.test.d.ts.map +1 -0
  123. package/dist/controls/toggle-filter.d.ts +9 -0
  124. package/dist/controls/toggle-filter.d.ts.map +1 -0
  125. package/dist/controls/toggle-filter.js +42 -0
  126. package/dist/controls/view-tabs.d.ts +23 -0
  127. package/dist/controls/view-tabs.d.ts.map +1 -0
  128. package/dist/controls/view-tabs.js +59 -0
  129. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  130. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  131. package/dist/controls-dates/date-range-picker.js +18 -0
  132. package/dist/controls-dates/index.d.ts +9 -0
  133. package/dist/controls-dates/index.d.ts.map +1 -0
  134. package/dist/controls-dates/index.js +4 -0
  135. package/dist/dashboard/delta-badge.d.ts +6 -12
  136. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  137. package/dist/dashboard/delta-badge.js +1 -50
  138. package/dist/dashboard/index.d.ts +1 -1
  139. package/dist/dashboard/index.d.ts.map +1 -1
  140. package/dist/dashboard/index.js +3 -1
  141. package/dist/dashboard/settings-section.d.ts +6 -4
  142. package/dist/dashboard/settings-section.d.ts.map +1 -1
  143. package/dist/dashboard/settings-section.js +23 -16
  144. package/dist/dashboard/settings-section.module.css +10 -0
  145. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  146. package/dist/dashboard/settings-section.test.d.ts +2 -0
  147. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  148. package/dist/dashboard/stat-card.d.ts +95 -9
  149. package/dist/dashboard/stat-card.d.ts.map +1 -1
  150. package/dist/dashboard/stat-card.js +108 -49
  151. package/dist/dashboard/stat-card.module.css +153 -0
  152. package/dist/dashboard/stat-card.module.css.d.ts +15 -0
  153. package/dist/data/data-table.d.ts +37 -15
  154. package/dist/data/data-table.d.ts.map +1 -1
  155. package/dist/data/data-table.js +96 -58
  156. package/dist/guard/grace.test.d.ts +20 -0
  157. package/dist/guard/grace.test.d.ts.map +1 -0
  158. package/dist/guard/index.d.ts +66 -3
  159. package/dist/guard/index.d.ts.map +1 -1
  160. package/dist/guard/index.js +78 -7
  161. package/dist/guard/types.d.ts +3 -3
  162. package/dist/guard/types.d.ts.map +1 -1
  163. package/dist/index.d.ts +13 -3
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js +13 -10
  166. package/dist/nav/types.d.ts +78 -2
  167. package/dist/nav/types.d.ts.map +1 -1
  168. package/dist/notifications/bell.d.ts +4 -1
  169. package/dist/notifications/bell.d.ts.map +1 -1
  170. package/dist/notifications/index.d.ts +1 -1
  171. package/dist/router-tanstack/field.d.ts +14 -0
  172. package/dist/router-tanstack/field.d.ts.map +1 -0
  173. package/dist/router-tanstack/field.js +4 -0
  174. package/dist/router-tanstack/index.d.ts +6 -2
  175. package/dist/router-tanstack/index.d.ts.map +1 -1
  176. package/dist/router-tanstack/index.js +4 -0
  177. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  178. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  179. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  180. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  181. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  182. package/dist/router-tanstack/search-param-store.js +16 -45
  183. package/dist/router-tanstack/search-store.d.ts +84 -0
  184. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  185. package/dist/router-tanstack/search-store.js +271 -0
  186. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  187. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  188. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  189. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  190. package/dist/router-tanstack/use-nav.d.ts +0 -2
  191. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  192. package/dist/router-tanstack/use-nav.js +34 -12
  193. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  194. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  195. package/dist/section/index.d.ts +2 -0
  196. package/dist/section/index.d.ts.map +1 -0
  197. package/dist/section/index.js +4 -0
  198. package/dist/section/section.d.ts +24 -0
  199. package/dist/section/section.d.ts.map +1 -0
  200. package/dist/section/section.js +126 -0
  201. package/dist/section/section.module.css +104 -0
  202. package/dist/section/section.module.css.d.ts +11 -0
  203. package/dist/section/section.test.d.ts +2 -0
  204. package/dist/section/section.test.d.ts.map +1 -0
  205. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  206. package/dist/shell/app-breadcrumbs.js +18 -15
  207. package/dist/shell/app-header.module.css +77 -37
  208. package/dist/shell/app-header.module.css.d.ts +2 -1
  209. package/dist/shell/app-mobile-nav.d.ts +9 -2
  210. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  211. package/dist/shell/app-mobile-nav.js +98 -11
  212. package/dist/shell/app-mobile-nav.module.css +8 -0
  213. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  214. package/dist/shell/app-sidebar.d.ts +55 -28
  215. package/dist/shell/app-sidebar.d.ts.map +1 -1
  216. package/dist/shell/app-sidebar.js +116 -67
  217. package/dist/shell/app-sidebar.module.css +303 -20
  218. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  219. package/dist/shell/index.d.ts +55 -21
  220. package/dist/shell/index.d.ts.map +1 -1
  221. package/dist/shell/index.js +40 -18
  222. package/dist/shell/mobile-nav-model.d.ts +13 -2
  223. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  224. package/dist/shell/mobile-nav-model.js +15 -1
  225. package/dist/shell/nav-count-badge.d.ts +1 -1
  226. package/dist/shell/page-bar.d.ts +51 -0
  227. package/dist/shell/page-bar.d.ts.map +1 -0
  228. package/dist/shell/page-bar.js +154 -0
  229. package/dist/shell/page-bar.module.css +178 -0
  230. package/dist/shell/page-bar.module.css.d.ts +17 -0
  231. package/dist/shell/page-bar.test.d.ts +2 -0
  232. package/dist/shell/page-bar.test.d.ts.map +1 -0
  233. package/dist/shell/sidebar-block-model.d.ts +48 -0
  234. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  235. package/dist/shell/sidebar-block-model.js +39 -0
  236. package/dist/shell/sidebar-blocks.d.ts +32 -0
  237. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  238. package/dist/shell/sidebar-blocks.js +169 -0
  239. package/dist/shell/sidebar-search.d.ts +13 -1
  240. package/dist/shell/sidebar-search.d.ts.map +1 -1
  241. package/dist/shell/sidebar-search.js +34 -1
  242. package/dist/shell/sidebar-search.module.css +26 -0
  243. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  244. package/dist/state/fields.d.ts +565 -0
  245. package/dist/state/fields.d.ts.map +1 -0
  246. package/dist/state/fields.js +405 -0
  247. package/dist/state/fields.test.d.ts +2 -0
  248. package/dist/state/fields.test.d.ts.map +1 -0
  249. package/dist/state/persisted.d.ts +60 -0
  250. package/dist/state/persisted.d.ts.map +1 -0
  251. package/dist/state/persisted.js +123 -0
  252. package/dist/state/persisted.test.d.ts +2 -0
  253. package/dist/state/persisted.test.d.ts.map +1 -0
  254. package/dist/state.d.ts +11 -56
  255. package/dist/state.d.ts.map +1 -1
  256. package/dist/state.js +14 -120
  257. package/dist/styles.css +13 -0
  258. package/dist/surfaces.d.ts +106 -24
  259. package/dist/surfaces.d.ts.map +1 -1
  260. package/dist/surfaces.js +156 -19
  261. package/dist/theme/controls.module.css +16 -1
  262. package/dist/theme/ctl-theme.d.ts +32 -0
  263. package/dist/theme/ctl-theme.d.ts.map +1 -0
  264. package/dist/theme/ctl-theme.js +54 -0
  265. package/dist/theme/ctl-theme.test.d.ts +2 -0
  266. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  267. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  268. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  269. package/dist/theme/divider-law.test.d.ts +2 -0
  270. package/dist/theme/divider-law.test.d.ts.map +1 -0
  271. package/dist/theme/icon-slot.d.ts +44 -0
  272. package/dist/theme/icon-slot.d.ts.map +1 -0
  273. package/dist/theme/icon-slot.js +21 -0
  274. package/dist/theme/icon-slot.module.css +47 -0
  275. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  276. package/dist/theme/icon-slot.test.d.ts +2 -0
  277. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  278. package/dist/theme/index.d.ts +1 -0
  279. package/dist/theme/index.d.ts.map +1 -1
  280. package/dist/theme/index.js +82 -5
  281. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  282. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  283. package/dist/theme/segmented-control.module.css +20 -0
  284. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  285. package/dist/tokens/index.d.ts +12 -0
  286. package/dist/tokens/index.d.ts.map +1 -1
  287. package/dist/tokens/index.js +38 -8
  288. package/dist/tokens/palette.d.ts +97 -61
  289. package/dist/tokens/palette.d.ts.map +1 -1
  290. package/dist/tokens/palette.js +76 -38
  291. package/dist/tokens.css +10 -2
  292. package/dist/widget-header/delta-badge.d.ts +13 -0
  293. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  294. package/dist/widget-header/delta-badge.js +29 -0
  295. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  296. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  297. package/dist/widget-header/index.d.ts +3 -0
  298. package/dist/widget-header/index.d.ts.map +1 -0
  299. package/dist/widget-header/index.js +6 -0
  300. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  301. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  302. package/dist/widget-header/widget-header.d.ts +67 -0
  303. package/dist/widget-header/widget-header.d.ts.map +1 -0
  304. package/dist/widget-header/widget-header.js +89 -0
  305. package/dist/widget-header/widget-header.module.css +350 -0
  306. package/dist/widget-header/widget-header.module.css.d.ts +25 -0
  307. package/dist/widget-header/widget-header.test.d.ts +2 -0
  308. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  309. package/llms.txt +30 -17
  310. package/package.json +14 -1
  311. package/src/charts/cursor/resolve.test.ts +29 -1
  312. package/src/charts/cursor/resolve.ts +38 -6
  313. package/src/charts/cursor/scope.tsx +6 -3
  314. package/src/charts/cursor/store.test.ts +19 -1
  315. package/src/charts/cursor/store.ts +10 -5
  316. package/src/charts/hooks/useChartCursor.ts +16 -8
  317. package/src/charts/index.ts +2 -2
  318. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  319. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  320. package/src/charts/primitives/ChartCard.tsx +86 -146
  321. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  322. package/src/cli/check-coverage.test.ts +297 -0
  323. package/src/cli/check-theme.test.ts +38 -14
  324. package/src/cli/index.ts +392 -16
  325. package/src/cli/init-sync.test.ts +97 -0
  326. package/src/connectivity/connectivity-indicator.tsx +16 -9
  327. package/src/content/index.ts +16 -3
  328. package/src/content/prose.module.css +7 -28
  329. package/src/content/prose.module.css.test.ts +8 -24
  330. package/src/controls/actions.module.css +62 -0
  331. package/src/controls/actions.module.css.d.ts +13 -0
  332. package/src/controls/actions.test.tsx +252 -0
  333. package/src/controls/actions.tsx +655 -0
  334. package/src/controls/actions.type-guard.test.ts +63 -0
  335. package/src/controls/compare-filter.tsx +64 -0
  336. package/src/controls/control-group.test.tsx +229 -0
  337. package/src/controls/control-group.tsx +63 -0
  338. package/src/controls/controls.module.css +542 -0
  339. package/src/controls/controls.module.css.d.ts +35 -0
  340. package/src/controls/controls.router.test.tsx +1053 -0
  341. package/src/controls/enum-filter.tsx +170 -0
  342. package/src/controls/filter-context.tsx +158 -0
  343. package/src/controls/filter-pill.tsx +120 -0
  344. package/src/controls/filter-set.test.tsx +472 -0
  345. package/src/controls/filter-set.tsx +225 -0
  346. package/src/controls/filter-sheet.tsx +285 -0
  347. package/src/controls/glyphs.tsx +81 -0
  348. package/src/controls/index.ts +49 -0
  349. package/src/controls/multi-select-filter.tsx +140 -0
  350. package/src/controls/number-filter.tsx +291 -0
  351. package/src/controls/peer-boundary.test.ts +85 -0
  352. package/src/controls/props-contract.test.tsx +137 -0
  353. package/src/controls/range-filter.tsx +237 -0
  354. package/src/controls/range-filter.type-guard.test.tsx +122 -0
  355. package/src/controls/search-filter.tsx +123 -0
  356. package/src/controls/select-filter.tsx +89 -0
  357. package/src/controls/sync-button.module.css +63 -0
  358. package/src/controls/sync-button.module.css.d.ts +11 -0
  359. package/src/controls/sync-button.test.tsx +183 -0
  360. package/src/controls/sync-button.tsx +197 -0
  361. package/src/controls/toggle-filter.tsx +60 -0
  362. package/src/controls/view-tabs.tsx +110 -0
  363. package/src/controls-dates/date-range-picker.tsx +42 -0
  364. package/src/controls-dates/index.ts +8 -0
  365. package/src/dashboard/delta-badge.tsx +4 -74
  366. package/src/dashboard/index.ts +6 -1
  367. package/src/dashboard/settings-section.module.css +10 -0
  368. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  369. package/src/dashboard/settings-section.test.tsx +56 -0
  370. package/src/dashboard/settings-section.tsx +33 -30
  371. package/src/dashboard/stat-card.module.css +153 -0
  372. package/src/dashboard/stat-card.module.css.d.ts +15 -0
  373. package/src/dashboard/stat-card.test.tsx +298 -1
  374. package/src/dashboard/stat-card.tsx +289 -67
  375. package/src/data/data-table.test.tsx +96 -11
  376. package/src/data/data-table.tsx +228 -84
  377. package/src/guard/check-source.test.ts +159 -13
  378. package/src/guard/grace.test.ts +158 -0
  379. package/src/guard/index.ts +185 -29
  380. package/src/guard/types.ts +4 -2
  381. package/src/index.ts +51 -5
  382. package/src/nav/types.ts +84 -2
  383. package/src/notifications/bell.tsx +6 -3
  384. package/src/notifications/index.ts +1 -1
  385. package/src/router-tanstack/field.ts +35 -0
  386. package/src/router-tanstack/index.ts +28 -2
  387. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  388. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  389. package/src/router-tanstack/search-param-store.ts +42 -159
  390. package/src/router-tanstack/search-store.router.test.tsx +882 -0
  391. package/src/router-tanstack/search-store.test.ts +578 -0
  392. package/src/router-tanstack/search-store.ts +546 -0
  393. package/src/router-tanstack/use-nav.test.tsx +187 -0
  394. package/src/router-tanstack/use-nav.ts +72 -14
  395. package/src/section/index.ts +1 -0
  396. package/src/section/section.module.css +104 -0
  397. package/src/section/section.module.css.d.ts +11 -0
  398. package/src/section/section.test.tsx +204 -0
  399. package/src/section/section.tsx +220 -0
  400. package/src/shell/app-breadcrumbs.tsx +44 -31
  401. package/src/shell/app-header.module.css +77 -37
  402. package/src/shell/app-header.module.css.d.ts +2 -1
  403. package/src/shell/app-mobile-nav.module.css +8 -0
  404. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  405. package/src/shell/app-mobile-nav.tsx +147 -8
  406. package/src/shell/app-sidebar-account.tsx +2 -2
  407. package/src/shell/app-sidebar.module.css +303 -20
  408. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  409. package/src/shell/app-sidebar.test.tsx +456 -41
  410. package/src/shell/app-sidebar.tsx +276 -140
  411. package/src/shell/index.test.tsx +291 -5
  412. package/src/shell/index.tsx +137 -38
  413. package/src/shell/mobile-nav-model.test.ts +228 -7
  414. package/src/shell/mobile-nav-model.ts +53 -3
  415. package/src/shell/nav-count-badge.tsx +1 -1
  416. package/src/shell/page-bar.module.css +178 -0
  417. package/src/shell/page-bar.module.css.d.ts +17 -0
  418. package/src/shell/page-bar.test.tsx +316 -0
  419. package/src/shell/page-bar.tsx +315 -0
  420. package/src/shell/sidebar-block-model.ts +83 -0
  421. package/src/shell/sidebar-blocks.tsx +301 -0
  422. package/src/shell/sidebar-search.module.css +26 -0
  423. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  424. package/src/shell/sidebar-search.tsx +71 -2
  425. package/src/state/fields.test.tsx +512 -0
  426. package/src/state/fields.ts +1150 -0
  427. package/src/state/persisted.test.ts +91 -0
  428. package/src/state/persisted.ts +261 -0
  429. package/src/state.ts +52 -240
  430. package/src/styles.css +13 -0
  431. package/src/surfaces.ts +204 -36
  432. package/src/theme/controls.module.css +16 -1
  433. package/src/theme/ctl-theme.test.tsx +275 -0
  434. package/src/theme/ctl-theme.tsx +138 -0
  435. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  436. package/src/theme/density-relations.test.ts +106 -51
  437. package/src/theme/divider-law.test.ts +259 -0
  438. package/src/theme/icon-slot.module.css +47 -0
  439. package/src/theme/icon-slot.module.css.d.ts +8 -0
  440. package/src/theme/icon-slot.test.tsx +275 -0
  441. package/src/theme/icon-slot.tsx +59 -0
  442. package/src/theme/index.ts +165 -3
  443. package/src/theme/layout-rhythm-css.test.ts +72 -0
  444. package/src/theme/radius.test.ts +11 -1
  445. package/src/theme/segmented-control.module.css +20 -0
  446. package/src/theme/shadow-surfaces.test.ts +20 -7
  447. package/src/theme/spacing.test.ts +75 -27
  448. package/src/theme/use-basalt-spacing.ts +2 -2
  449. package/src/tokens/density.test.ts +7 -25
  450. package/src/tokens/index.ts +48 -8
  451. package/src/tokens/palette.ts +107 -73
  452. package/src/widget-header/delta-badge.test.tsx +60 -0
  453. package/src/widget-header/delta-badge.tsx +57 -0
  454. package/src/widget-header/index.ts +2 -0
  455. package/src/widget-header/no-mantine.test.ts +33 -0
  456. package/src/widget-header/widget-header.module.css +350 -0
  457. package/src/widget-header/widget-header.module.css.d.ts +25 -0
  458. package/src/widget-header/widget-header.test.tsx +187 -0
  459. package/src/widget-header/widget-header.tsx +213 -0
  460. package/agent/rules/basalt-agent.md +0 -452
  461. package/agent/rules/basalt-app.md +0 -246
  462. package/agent/rules/basalt-commands.md +0 -320
  463. package/agent/rules/basalt-content.md +0 -409
  464. package/agent/rules/basalt-data.md +0 -329
  465. package/agent/rules/basalt-forms.md +0 -186
  466. package/agent/rules/basalt-notifications.md +0 -264
  467. package/agent/rules/basalt-query.md +0 -187
  468. package/agent/rules/basalt-router.md +0 -336
  469. package/dist/content/article-filter-bar.d.ts +0 -40
  470. package/dist/content/article-filter-bar.d.ts.map +0 -1
  471. package/dist/content/article-filter-bar.js +0 -56
  472. package/dist/shell/page-header.d.ts +0 -13
  473. package/dist/shell/page-header.d.ts.map +0 -1
  474. package/dist/shell/page-header.js +0 -23
  475. package/src/content/article-filter-bar.tsx +0 -88
  476. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,275 @@
1
+ /**
2
+ * `CtlSlot`/`CTL_THEME` — proves the `size="ctl"` default reaches a bare `<Button>` dropped into
3
+ * the slot with NO prop at the call site, and that the tier stays scoped to the slot (a sibling
4
+ * `<Button>` outside `<CtlSlot>` never resolves the `-ctl` var).
5
+ *
6
+ * `data-size`/the inline `style` custom-property string are Mantine's own render output — verified
7
+ * directly against a real render (`@testing-library/react`, `tests/setup/dom.ts`'s jsdom harness),
8
+ * not asserted from memory. jsdom does not compute the CSS cascade, so this can only prove the
9
+ * INLINE var REFERENCE Mantine's `varsResolver` emits (`--button-height: var(--button-height-ctl)`)
10
+ * — not the resolved pixel value, which is `cssVariablesResolver`'s and `spaceDecls`'s job
11
+ * (covered by `spacing.test.ts`/`density.test.ts`).
12
+ *
13
+ * Outside `<CtlSlot>`, `<Button>` resolves to Mantine's own native default, NOT basalt's `md` —
14
+ * verified against `theme/index.ts`'s `Button.extend`: unlike Input/TextInput/NumberInput/
15
+ * PasswordInput/Select/Textarea, basalt sets no `defaultProps.size` for Button/ActionIcon at all,
16
+ * so an un-sized Button carries no `data-size` attribute and no `--button-height` inline var
17
+ * (Mantine's own CSS stylesheet default takes over). The test below asserts the accurate,
18
+ * verified claim — the tier does not leak past the slot — rather than a `size="md"` claim Button
19
+ * was never given.
20
+ */
21
+ import { ActionIcon, Button, Checkbox, MantineProvider, Radio, Switch } from '@mantine/core'
22
+ import { act, fireEvent, render, screen } from '@testing-library/react'
23
+ import { describe, expect, test } from 'bun:test'
24
+ import { FilterPill } from '../controls/filter-pill'
25
+ import { StatCard } from '../dashboard/stat-card'
26
+ import { baseTheme } from './index'
27
+ import { CTL_THEME, CtlSlot } from './ctl-theme'
28
+
29
+ describe('CtlSlot', () => {
30
+ test('a bare Button inside CtlSlot resolves the ctl tier with no size prop', () => {
31
+ const { container } = render(
32
+ <MantineProvider theme={baseTheme}>
33
+ <CtlSlot>
34
+ <Button>Save</Button>
35
+ </CtlSlot>
36
+ </MantineProvider>,
37
+ )
38
+ const button = container.querySelector('button')
39
+ expect(button?.getAttribute('data-size')).toBe('ctl')
40
+ expect(button?.style.getPropertyValue('--button-height')).toBe('var(--button-height-ctl)')
41
+ expect(button?.style.getPropertyValue('--button-padding-x')).toBe('var(--button-padding-x-ctl)')
42
+ })
43
+
44
+ test('the wrapper carries the data-basalt-tier marker and stays out of flow', () => {
45
+ const { container } = render(
46
+ <MantineProvider theme={baseTheme}>
47
+ <CtlSlot>
48
+ <Button>Save</Button>
49
+ </CtlSlot>
50
+ </MantineProvider>,
51
+ )
52
+ const wrapper = container.querySelector('[data-basalt-tier]')
53
+ expect(wrapper?.getAttribute('data-basalt-tier')).toBe('ctl')
54
+ expect((wrapper as HTMLElement | null)?.style.display).toBe('contents')
55
+ })
56
+
57
+ test('a Button outside CtlSlot never resolves the ctl tier', () => {
58
+ const { container } = render(
59
+ <MantineProvider theme={baseTheme}>
60
+ <Button>Save</Button>
61
+ </MantineProvider>,
62
+ )
63
+ const button = container.querySelector('button')
64
+ expect(button?.getAttribute('data-size')).not.toBe('ctl')
65
+ expect(button?.style.getPropertyValue('--button-height')).not.toBe('var(--button-height-ctl)')
66
+ })
67
+
68
+ test('CTL_THEME sets defaultProps.size "ctl" for every documented component', () => {
69
+ const names = [
70
+ 'Button',
71
+ 'ActionIcon',
72
+ 'Input',
73
+ 'TextInput',
74
+ 'Select',
75
+ 'MultiSelect',
76
+ 'SegmentedControl',
77
+ 'NativeSelect',
78
+ 'Radio',
79
+ 'RadioGroup',
80
+ 'Checkbox',
81
+ 'CheckboxGroup',
82
+ 'Switch',
83
+ 'SwitchGroup',
84
+ ] as const
85
+ for (const name of names) {
86
+ expect(CTL_THEME.components?.[name]?.defaultProps?.['size']).toBe('ctl')
87
+ }
88
+ })
89
+ })
90
+
91
+ /**
92
+ * Radio/Checkbox/Switch at the tier, and the portal question that had to be settled before the
93
+ * defaults could be trusted: a filter's popover body and the mobile sheet are rendered
94
+ * `withinPortal`, so they are NOT DOM descendants of the `<CtlSlot>` wrapper — but a React portal
95
+ * preserves the React tree, and `MantineThemeProvider` is React context. So the tier DOES reach them,
96
+ * and no second `CtlSlot` inside `Popover.Dropdown`/`Drawer` is needed. These tests are what makes
97
+ * that a checked fact rather than a reasoned one.
98
+ */
99
+ describe('the toggle family at the ctl tier', () => {
100
+ test('a bare Radio inside CtlSlot resolves the ctl indicator box', () => {
101
+ const { container } = render(
102
+ <MantineProvider theme={baseTheme}>
103
+ <CtlSlot>
104
+ <Radio value="a" label="Previous period" />
105
+ </CtlSlot>
106
+ </MantineProvider>,
107
+ )
108
+ const root = container.querySelector('.mantine-Radio-root')
109
+ expect(root?.getAttribute('style')).toContain('--radio-size: var(--radio-size-ctl)')
110
+ expect(root?.getAttribute('style')).toContain('--radio-icon-size: var(--radio-icon-size-ctl)')
111
+ })
112
+
113
+ test('a bare Checkbox inside CtlSlot resolves the ctl indicator box', () => {
114
+ const { container } = render(
115
+ <MantineProvider theme={baseTheme}>
116
+ <CtlSlot>
117
+ <Checkbox value="a" label="Web" />
118
+ </CtlSlot>
119
+ </MantineProvider>,
120
+ )
121
+ expect(container.querySelector('.mantine-Checkbox-root')?.getAttribute('style')).toContain(
122
+ '--checkbox-size: var(--checkbox-size-ctl)',
123
+ )
124
+ })
125
+
126
+ test('a bare Switch inside CtlSlot resolves all five of its ctl vars', () => {
127
+ const { container } = render(
128
+ <MantineProvider theme={baseTheme}>
129
+ <CtlSlot>
130
+ <Switch label="Errors only" />
131
+ </CtlSlot>
132
+ </MantineProvider>,
133
+ )
134
+ const style = container.querySelector('.mantine-Switch-root')?.getAttribute('style') ?? ''
135
+ for (const prefix of [
136
+ 'switch-height',
137
+ 'switch-width',
138
+ 'switch-thumb-size',
139
+ 'switch-label-font-size',
140
+ 'switch-track-label-padding',
141
+ ]) {
142
+ expect(style).toContain(`--${prefix}: var(--${prefix}-ctl)`)
143
+ }
144
+ })
145
+
146
+ test('a Radio outside CtlSlot keeps Mantine’s own default — the tier does not leak', () => {
147
+ const { container } = render(
148
+ <MantineProvider theme={baseTheme}>
149
+ <Radio value="a" label="Previous period" />
150
+ </MantineProvider>,
151
+ )
152
+ expect(
153
+ container.querySelector('.mantine-Radio-root')?.getAttribute('style') ?? '',
154
+ ).not.toContain('var(--radio-size-ctl)')
155
+ })
156
+
157
+ test('a PORTALED popover body inherits the tier — React context crosses a portal', async () => {
158
+ render(
159
+ <MantineProvider theme={baseTheme}>
160
+ <CtlSlot>
161
+ <FilterPill label="No comparison">
162
+ <Radio.Group value="previous">
163
+ <Radio value="previous" label="Previous period" />
164
+ </Radio.Group>
165
+ </FilterPill>
166
+ </CtlSlot>
167
+ </MantineProvider>,
168
+ )
169
+ await act(async () => {
170
+ fireEvent.click(screen.getByRole('button', { name: 'No comparison' }))
171
+ })
172
+ // The dropdown is a portal at document.body — deliberately queried off `document`, not the
173
+ // render container, because being outside the container is the whole point of the test.
174
+ const root = document.querySelector('.mantine-Popover-dropdown .mantine-Radio-root')
175
+ expect(root).not.toBeNull()
176
+ expect(root?.getAttribute('style')).toContain('--radio-size: var(--radio-size-ctl)')
177
+ })
178
+ })
179
+
180
+ /**
181
+ * `tier="widget"` — the 24px ActionIcon step a `WidgetHeader tier="widget"` header row can hold.
182
+ *
183
+ * The bug: `StatCard` wrapped its `actions` slot in a plain `<CtlSlot>`, so a bare `ActionIcon`
184
+ * resolved to the 30px `ctl` tier inside a 28px header row (`--vx-space-widget-header-height`) and
185
+ * grew it to 30. Measured in Chrome on the playground's dashboard at 390px, two KPI cards in the
186
+ * same grid row: the one WITH a kebab put its hero value at y=164, the one without at y=162 — the
187
+ * whole card, and every number in it, 2px out of line because of a slot's tier.
188
+ */
189
+ describe('CtlSlot tier="widget"', () => {
190
+ test('a bare ActionIcon resolves the 24px icon step, not the 30px ctl one', () => {
191
+ const { container } = render(
192
+ <MantineProvider theme={baseTheme}>
193
+ <CtlSlot tier="widget">
194
+ <ActionIcon variant="subtle" aria-label="Card actions">
195
+ <span />
196
+ </ActionIcon>
197
+ </CtlSlot>
198
+ </MantineProvider>,
199
+ )
200
+ const button = container.querySelector('button')
201
+ expect(button?.getAttribute('data-size')).toBe('icon')
202
+ // `--ai-size-icon` reads `--vx-space-control-height-widget` (24px) in `ctlSizeVars()`, so the
203
+ // tier tracks the density knob like every other control height.
204
+ expect(button?.style.getPropertyValue('--ai-size')).toBe('var(--ai-size-icon)')
205
+ })
206
+
207
+ test('the marker is the TIER name, the same one ChartCard writes by hand', () => {
208
+ const { container } = render(
209
+ <MantineProvider theme={baseTheme}>
210
+ <CtlSlot tier="widget">
211
+ <span />
212
+ </CtlSlot>
213
+ </MantineProvider>,
214
+ )
215
+ expect(container.querySelector('[data-basalt-tier]')?.getAttribute('data-basalt-tier')).toBe(
216
+ 'widget',
217
+ )
218
+ })
219
+
220
+ test('the default is still `ctl` — no call site had to change', () => {
221
+ const { container } = render(
222
+ <MantineProvider theme={baseTheme}>
223
+ <CtlSlot>
224
+ <ActionIcon variant="subtle" aria-label="Sync">
225
+ <span />
226
+ </ActionIcon>
227
+ </CtlSlot>
228
+ </MantineProvider>,
229
+ )
230
+ expect(container.querySelector('button')?.getAttribute('data-size')).toBe('ctl')
231
+ expect(container.querySelector('[data-basalt-tier]')?.getAttribute('data-basalt-tier')).toBe(
232
+ 'ctl',
233
+ )
234
+ })
235
+
236
+ test('the widget tier is ActionIcon-only — a Button in it keeps whatever it had', () => {
237
+ // Only `--ai-size-icon` exists at this step (`theme/index.ts`'s `ctlSizeVars`). Defaulting a
238
+ // Button to `size="icon"` would resolve `--button-height: var(--button-height-icon)` to nothing
239
+ // and paint a zero-height button — the exact `-xs`-var failure `ctlSizeVars`' doc records.
240
+ const { container } = render(
241
+ <MantineProvider theme={baseTheme}>
242
+ <CtlSlot tier="widget">
243
+ <Button>Save</Button>
244
+ </CtlSlot>
245
+ </MantineProvider>,
246
+ )
247
+ expect(container.querySelector('button')?.getAttribute('data-size')).not.toBe('icon')
248
+ })
249
+ })
250
+
251
+ /**
252
+ * `StatCard`'s own slot — the regression guard on the call site, not just on the mechanism. A
253
+ * `<CtlSlot>` here (the default tier) is what shipped, and nothing else in the suite would have
254
+ * caught it.
255
+ */
256
+ describe('StatCard mounts its actions slot at the widget tier', () => {
257
+ test('a raw ActionIcon in `actions` lands on the 24px step', () => {
258
+ const { container } = render(
259
+ <MantineProvider theme={baseTheme}>
260
+ <StatCard
261
+ title="Total sales"
262
+ value="$1294.9k"
263
+ actions={
264
+ <ActionIcon variant="subtle" aria-label="Card actions">
265
+ <span />
266
+ </ActionIcon>
267
+ }
268
+ />
269
+ </MantineProvider>,
270
+ )
271
+ const slot = container.querySelector('[data-basalt-tier]')
272
+ expect(slot?.getAttribute('data-basalt-tier')).toBe('widget')
273
+ expect(container.querySelector('button')?.getAttribute('data-size')).toBe('icon')
274
+ })
275
+ })
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The `size="ctl"` slot theme (`docs/CONTROLS-SPEC.md` §5) — every home (`PageBar`, `Section`,
3
+ * table toolbar, sidebar blocks, wave 3/4) wraps its **slot**, never its body, in `<CtlSlot>` so a
4
+ * raw `Button`/`ActionIcon`/`Input`/… dropped into that slot renders at the `ctl` tier with NO prop
5
+ * at the call site — a `size="xs"` typed there is C5 (`basalt/control-size-literal`).
6
+ *
7
+ * `CTL_THEME.components[*].defaultProps.size = 'ctl'` only sets the DEFAULT — an explicit
8
+ * `size="md"` (or anything else) at the call site still wins, same override precedence as every
9
+ * other Mantine `defaultProps`. Mantine's own `mergeMantineTheme` DEEP-merges `components` (not a
10
+ * shallow replace), so nesting `<CtlSlot>` under `BasaltProvider` never drops the base theme's
11
+ * `Button.extend`/`ActionIcon.extend`/… `vars`/`classNames` (the shadow/depth idiom,
12
+ * `docs/DESIGN-SPEC.md` §5) — only `defaultProps.size` is added on top.
13
+ *
14
+ * `Menu` is deliberately absent — verified against the installed `@mantine/core` 9.3.0 source
15
+ * (`Menu.d.ts`, `MenuItem.d.ts`): neither accepts a `size` prop at all, so "Menu/Menu.Item where
16
+ * applicable" (the spec's own hedge) resolves to "not applicable" here.
17
+ *
18
+ * **Radio / Checkbox / Switch and their `.Group`s, and the one consequence worth stating.** These
19
+ * three default to Mantine's `sm` — a 20px indicator beside the tier's 13.5px option label — which
20
+ * is what every filter popover and the mobile sheet rendered. They are here rather than fixed in a
21
+ * CSS module because their geometry is a Mantine SIZE (five vars for a Switch alone), so the tier
22
+ * reaches them the same way it reaches Button and Input: through `-ctl` vars in
23
+ * `cssVariablesResolver`. The `.Group`s are listed too, so the group's `Input.Wrapper` label lands
24
+ * on the tier's font step as well.
25
+ *
26
+ * The consequence: Mantine resolves a Radio's size as `props.size ? props.size : ctx.size`
27
+ * (`Radio.mjs`), and a theme `defaultProps.size` is indistinguishable from an explicit prop to that
28
+ * check — so inside a `<CtlSlot>` a `<Radio.Group size="lg">` no longer sizes its children, they stay
29
+ * `ctl`. That is the correct trade in a home's SLOT (the whole contract there is "everything is the
30
+ * tier"), and it is stated here because it is the kind of thing that otherwise gets rediscovered as
31
+ * a bug. A consumer wanting a `lg` radio group puts it in a BODY, which no `CtlSlot` wraps.
32
+ */
33
+ import {
34
+ ActionIcon,
35
+ Box,
36
+ Button,
37
+ Checkbox,
38
+ Input,
39
+ MantineThemeProvider,
40
+ MultiSelect,
41
+ NativeSelect,
42
+ Radio,
43
+ SegmentedControl,
44
+ Select,
45
+ Switch,
46
+ TextInput,
47
+ } from '@mantine/core'
48
+ import type { MantineSize, MantineThemeOverride } from '@mantine/core'
49
+ import type { ReactNode } from 'react'
50
+
51
+ /** The `MantineThemeOverride` a `<CtlSlot>` provides — see this module's doc for the merge/
52
+ * precedence contract. Exported so a consumer composing its own slot (a bespoke home not covered
53
+ * by `PageBar`/`Section`/the sidebar blocks) can reach the SAME tier without re-declaring it. */
54
+ export const CTL_THEME: MantineThemeOverride = {
55
+ components: {
56
+ Button: Button.extend({ defaultProps: { size: 'ctl' } }),
57
+ ActionIcon: ActionIcon.extend({ defaultProps: { size: 'ctl' } }),
58
+ Input: Input.extend({ defaultProps: { size: 'ctl' } }),
59
+ TextInput: TextInput.extend({ defaultProps: { size: 'ctl' } }),
60
+ Select: Select.extend({ defaultProps: { size: 'ctl' } }),
61
+ MultiSelect: MultiSelect.extend({ defaultProps: { size: 'ctl' } }),
62
+ SegmentedControl: SegmentedControl.extend({ defaultProps: { size: 'ctl' } }),
63
+ NativeSelect: NativeSelect.extend({ defaultProps: { size: 'ctl' } }),
64
+ // The theme keys are the component names Mantine's own `useProps` looks up — `RadioGroup`, not
65
+ // `Radio.Group` (`RadioGroup.mjs`: `useProps("RadioGroup", …)`).
66
+ Radio: Radio.extend({ defaultProps: { size: 'ctl' } }),
67
+ // Mantine types `RadioGroup.size` as a CLOSED `MantineSize`, while `Radio`, `Checkbox`,
68
+ // `Checkbox.Group`, `Switch` and `Switch.Group` all type theirs OPEN
69
+ // (`MantineSize | (string & {})`) — verified in the installed 9.3.0 `.d.ts` files, so this is an
70
+ // upstream inconsistency, not a statement that `RadioGroup` cannot take a custom tier. At
71
+ // runtime `getSize` interpolates the string identically for all six. The cast restores what
72
+ // every sibling type already allows; it widens nothing.
73
+ RadioGroup: Radio.Group.extend({ defaultProps: { size: 'ctl' as MantineSize } }),
74
+ Checkbox: Checkbox.extend({ defaultProps: { size: 'ctl' } }),
75
+ CheckboxGroup: Checkbox.Group.extend({ defaultProps: { size: 'ctl' } }),
76
+ Switch: Switch.extend({ defaultProps: { size: 'ctl' } }),
77
+ SwitchGroup: Switch.Group.extend({ defaultProps: { size: 'ctl' } }),
78
+ },
79
+ }
80
+
81
+ /**
82
+ * The `tier="widget"` slot theme — ActionIcon-ONLY, at the 24px `size="icon"` step
83
+ * (`--ai-size-icon` → `--vx-space-control-height-widget`, `docs/CONTROLS-SPEC.md` §5).
84
+ *
85
+ * It exists because a `WidgetHeader tier="widget"` header row is 28px
86
+ * (`--vx-space-widget-header-height`) and a 30px `ctl` control does not fit inside it. `StatCard`
87
+ * wrapped its `actions` slot in a plain `<CtlSlot>`, so a card WITH a kebab measured a 30px title
88
+ * row and a card WITHOUT one measured 28 — two KPI cards side by side in the same grid row were
89
+ * 2px out of alignment, and every number under them with it. Measured on the playground's dashboard
90
+ * at 390px: `Total sales` at 164 against `Orders` at 162.
91
+ *
92
+ * The tier is a HEIGHT statement, so a slot can only fix it by owning the size — which is exactly
93
+ * what {@link controlHeightWidget}'s own doc already said this step was for ("the `size="icon"`
94
+ * ActionIcon height — `WidgetHeader tier="widget"` actions"). It is ActionIcon-only because the
95
+ * tier has one var (`--ai-size-icon`): a 24px Button or Input has no vars declared and would
96
+ * silently resolve to its initial value, which is why widening this set is a
97
+ * `theme/index.ts`-first change, never a line added here.
98
+ */
99
+ const WIDGET_THEME: MantineThemeOverride = {
100
+ components: {
101
+ ActionIcon: ActionIcon.extend({ defaultProps: { size: 'icon' } }),
102
+ },
103
+ }
104
+
105
+ export type CtlSlotProps = {
106
+ children: ReactNode
107
+ /**
108
+ * Which tier the slot imposes. `ctl` (the default) is the 30px control tier every home's control
109
+ * row uses. `widget` is the 24px ActionIcon step a `WidgetHeader tier="widget"` header row can
110
+ * hold — see {@link WIDGET_THEME} for the misalignment that makes it a separate tier rather than
111
+ * a caller's `size` prop.
112
+ *
113
+ * The values are the TIER names, matching the `data-basalt-tier` attribute the slot emits and the
114
+ * one `ChartCard` already writes by hand (it lives inside the Mantine-free `charts/` boundary and
115
+ * so cannot mount this component). `widget` maps to Mantine's `size="icon"`; the two vocabularies
116
+ * differ and that is upstream's, not basalt's.
117
+ *
118
+ * @default 'ctl'
119
+ */
120
+ tier?: 'ctl' | 'widget'
121
+ }
122
+
123
+ /**
124
+ * Wrap a home's SLOT (never its body) to render every Mantine control inside it at the `ctl` tier.
125
+ * The marker is a Mantine `Box` (not a raw `<div>` — `basalt/raw-html-layout` flags inline
126
+ * layout/surface styling on a bare HTML tag), `display: contents` keeps it out of the slot's own
127
+ * flex/grid layout while leaving the `data-basalt-tier="ctl"` attribute queryable/testable
128
+ * (`docs/CONTROLS-SPEC.md` §5) without the wrapper participating in flow.
129
+ */
130
+ export function CtlSlot({ children, tier = 'ctl' }: CtlSlotProps): ReactNode {
131
+ return (
132
+ <MantineThemeProvider inherit theme={tier === 'widget' ? WIDGET_THEME : CTL_THEME}>
133
+ <Box data-basalt-tier={tier} style={{ display: 'contents' }}>
134
+ {children}
135
+ </Box>
136
+ </MantineThemeProvider>
137
+ )
138
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The ctl-tier coverage gate (`docs/CONTROLS-SPEC.md` §5) — greps every `getSize(size, '<prefix>')`
3
+ * / `getFontSize(size)` call in the INSTALLED `@mantine/core` source for the TWELVE components the
4
+ * tier covers (Button, ActionIcon, Input, SegmentedControl, Combobox, Select, MultiSelect,
5
+ * TextInput, Menu — the nine the spec names — plus Radio, Checkbox and Switch) and asserts
6
+ * `cssVariablesResolver`'s `variables` block declares a `-ctl` var for every distinct prefix found —
7
+ * so a missing var (the `--button-padding-x-ctl` the spec's own text calls out as "every draft
8
+ * omitted") fails the build instead of silently rendering at 0px/undefined.
9
+ *
10
+ * Radio/Checkbox/Switch joined the scan when they joined `CTL_THEME`: they had defaulted to
11
+ * Mantine's `sm`, a 20px indicator beside the tier's 13.5px option label, in every filter popover and
12
+ * in the mobile sheet. Adding them to `defaultProps` without declaring their vars would have been
13
+ * strictly worse than leaving them alone — an undeclared `--radio-size-ctl` resolves to the
14
+ * property's INITIAL value, which is how a Select once shipped with no chevron (see
15
+ * `theme/index.ts`'s `ctlSizeVars` doc). This scan is what makes the two halves inseparable.
16
+ *
17
+ * Reads the REAL installed package (not a hand-typed list of prefixes) so a future `@mantine/core`
18
+ * minor that adds a new `getSize`/`getFontSize` call to one of these nine components fails this
19
+ * test until the matching `-ctl` var is declared — the same "verify against source, not memory"
20
+ * discipline `ctl-theme.tsx`'s own doc comment describes.
21
+ *
22
+ * `getFontSize(size)` is `getSize(size, 'mantine-font-size')` (`core/utils/get-size/get-size.mjs`),
23
+ * so every `-fz`/`-font-size` call site (Button's `--button-fz`, Input's `--input-fz`,
24
+ * SegmentedControl's `--sc-font-size`, Combobox's `--combobox-option-fz`) collapses onto the ONE
25
+ * `mantine-font-size` prefix — verified by proving the prove-it-would-fail step below actually red-
26
+ * lines when `--mantine-font-size-ctl` is removed, not merely when a component-named font var is
27
+ * missing (no such var is ever read).
28
+ *
29
+ * `*Group`/`*GroupSection` subdirectories (`Button.Group`, `ActionIcon.Group`) are excluded — they
30
+ * are separate, undocumented-for-`ctl` components (`section-height`/`section-padding-x`/
31
+ * `section-fz`), out of scope for `docs/CONTROLS-SPEC.md` §5's nine-component list.
32
+ */
33
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
34
+ import { dirname, join } from 'node:path'
35
+ import { DEFAULT_THEME, mergeMantineTheme } from '@mantine/core'
36
+ import type { MantineTheme } from '@mantine/core'
37
+ import { describe, expect, test } from 'bun:test'
38
+ import { CTL_THEME, baseTheme, cssVariablesResolver } from './index'
39
+
40
+ const theme: MantineTheme = mergeMantineTheme(DEFAULT_THEME, baseTheme)
41
+
42
+ const MANTINE_CORE_ESM_COMPONENTS = join(
43
+ dirname(require.resolve('@mantine/core/package.json')),
44
+ 'esm/components',
45
+ )
46
+
47
+ /** Every component `CTL_THEME` sets a `ctl` default on, plus `Input`/`Combobox`/`Menu` which the
48
+ * others resolve through. Keep this list and `CTL_THEME.components` in step — the last test in this
49
+ * file asserts exactly that, so they cannot drift apart silently. */
50
+ const CTL_TIER_COMPONENTS = [
51
+ 'Button',
52
+ 'ActionIcon',
53
+ 'Input',
54
+ 'SegmentedControl',
55
+ 'Combobox',
56
+ 'Select',
57
+ 'MultiSelect',
58
+ 'TextInput',
59
+ 'Menu',
60
+ 'Radio',
61
+ 'Checkbox',
62
+ 'Switch',
63
+ // In `CTL_THEME` since the tier shipped, and never scanned until the drift test below was added —
64
+ // which is the whole argument for that test. It renders a native `<select>` through
65
+ // `Input`/`InputBase`, so it contributes no prefix of its own; being scanned is what proves that
66
+ // rather than assuming it.
67
+ 'NativeSelect',
68
+ ] as const
69
+
70
+ /** Every `.mjs` file (not `.mjs.map`) under a component directory, recursing into subcomponents but
71
+ * skipping `*Group`/`*GroupSection` (see this file's doc). */
72
+ function collectMjsFiles(dir: string): string[] {
73
+ if (!existsSync(dir)) return []
74
+ const out: string[] = []
75
+ for (const entry of readdirSync(dir)) {
76
+ if (/Group/.test(entry)) continue
77
+ const full = join(dir, entry)
78
+ const stat = statSync(full)
79
+ if (stat.isDirectory()) out.push(...collectMjsFiles(full))
80
+ else if (entry.endsWith('.mjs')) out.push(full)
81
+ }
82
+ return out
83
+ }
84
+
85
+ /** Every distinct `getSize`/`getFontSize` prefix the nine components actually read, extracted from
86
+ * the installed source — `getFontSize(...)` always collapses to `mantine-font-size` (see this
87
+ * file's doc). */
88
+ function collectSizePrefixes(): Set<string> {
89
+ const prefixes = new Set<string>()
90
+ for (const component of CTL_TIER_COMPONENTS) {
91
+ for (const file of collectMjsFiles(join(MANTINE_CORE_ESM_COMPONENTS, component))) {
92
+ const src = readFileSync(file, 'utf8')
93
+ for (const m of src.matchAll(/get(?:Size|Spacing)\(\s*[\w.?]+\s*,\s*["']([\w-]+)["']/g)) {
94
+ prefixes.add(m[1]!)
95
+ }
96
+ if (/getFontSize\(/.test(src)) prefixes.add('mantine-font-size')
97
+ }
98
+ }
99
+ return prefixes
100
+ }
101
+
102
+ describe('ctl-tier var coverage', () => {
103
+ const prefixes = collectSizePrefixes()
104
+
105
+ test('the scan finds the prefixes this spec is grounded in (sanity check on the scan itself)', () => {
106
+ expect(prefixes.has('button-height')).toBe(true)
107
+ expect(prefixes.has('button-padding-x')).toBe(true)
108
+ expect(prefixes.has('ai-size')).toBe(true)
109
+ expect(prefixes.has('input-height')).toBe(true)
110
+ expect(prefixes.has('sc-padding')).toBe(true)
111
+ expect(prefixes.has('combobox-option-padding')).toBe(true)
112
+ expect(prefixes.has('combobox-chevron-size')).toBe(true)
113
+ expect(prefixes.has('mantine-font-size')).toBe(true)
114
+ })
115
+
116
+ test('the scan reaches Radio/Checkbox/Switch — the three the popovers and the sheet render', () => {
117
+ expect(prefixes.has('radio-size')).toBe(true)
118
+ expect(prefixes.has('radio-icon-size')).toBe(true)
119
+ expect(prefixes.has('checkbox-size')).toBe(true)
120
+ expect(prefixes.has('switch-height')).toBe(true)
121
+ expect(prefixes.has('switch-width')).toBe(true)
122
+ expect(prefixes.has('switch-thumb-size')).toBe(true)
123
+ expect(prefixes.has('switch-label-font-size')).toBe(true)
124
+ expect(prefixes.has('switch-track-label-padding')).toBe(true)
125
+ })
126
+
127
+ test('there is NO --checkbox-icon-size to declare — Checkbox derives its tick in CSS', () => {
128
+ // Worth pinning: the obvious symmetry with `radio-icon-size` does not exist in 9.3.0, and
129
+ // declaring a var no `getSize` call reads would be dead weight that reads like coverage.
130
+ expect(prefixes.has('checkbox-icon-size')).toBe(false)
131
+ })
132
+
133
+ test('every scanned prefix has a declared -ctl var', () => {
134
+ const declared = cssVariablesResolver(theme).variables
135
+ const missing = [...prefixes].filter((prefix) => !(`--${prefix}-ctl` in declared))
136
+ expect(missing).toEqual([])
137
+ })
138
+
139
+ test('ActionIcon additionally gets an -icon var (size="icon", WidgetHeader tier="widget")', () => {
140
+ const declared = cssVariablesResolver(theme).variables
141
+ expect(declared['--ai-size-icon']).toBeDefined()
142
+ })
143
+
144
+ test('--mantine-line-height-ctl is declared (read by every -ctl Input/Button label via the line-height cascade)', () => {
145
+ const declared = cssVariablesResolver(theme).variables
146
+ expect(declared['--mantine-line-height-ctl']).toBeDefined()
147
+ })
148
+
149
+ test('every component CTL_THEME defaults to `ctl` is in the scanned set', () => {
150
+ // The two halves of the tier — `defaultProps.size = 'ctl'` and the `-ctl` vars — must move
151
+ // together, and this is what makes adding a component to one without the other fail. `*Group`
152
+ // keys resolve to their base component's prefixes (a Group reads `Input.Wrapper`'s vars, and the
153
+ // scan already covers `Input`), so they are stripped before the comparison.
154
+ const scanned = new Set<string>(CTL_TIER_COMPONENTS)
155
+ const themed = Object.keys(CTL_THEME.components ?? {}).map((name) =>
156
+ name.endsWith('Group') ? name.slice(0, -'Group'.length) : name,
157
+ )
158
+ expect(themed.filter((name) => !scanned.has(name))).toEqual([])
159
+ })
160
+
161
+ // Proves the gate actually bites — the exact regression the spec calls out
162
+ // ("--button-padding-x-ctl every draft omitted").
163
+ test('regression proof: a var missing from cssVariablesResolver would fail the coverage assertion above', () => {
164
+ const declared = cssVariablesResolver(theme).variables
165
+ const withoutOne = { ...declared }
166
+ delete withoutOne['--button-padding-x-ctl']
167
+ const missing = [...prefixes].filter((prefix) => !(`--${prefix}-ctl` in withoutOne))
168
+ expect(missing).toEqual(['button-padding-x'])
169
+ })
170
+ })