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
@@ -302,6 +302,23 @@ const SPACE_ANCHORS_BASE = {
302
302
  * Input tracks it at every density level instead of only coinciding at level 0 (the defect this
303
303
  * anchor fixes — Mantine's own `--button-height-md`/`--ai-size` are static, density-blind). */
304
304
  controlHeight: CONTROL_HEIGHT_BASE,
305
+ /** The `size="icon"` count-tag height (`docs/CONTROLS-SPEC.md` §5) — inline chip / table-cell
306
+ * count badge. Floored at 18px in {@link deriveSpacing} (same "negative level can't collapse a
307
+ * real control below a legible/tappable minimum" reasoning as `sidebarSearchTriggerHeight`'s WCAG
308
+ * floor, one notch tighter since a count tag is a badge, not a primary interactive target). */
309
+ controlHeightTag: 20,
310
+ /** The `size="icon"` ActionIcon height (`docs/CONTROLS-SPEC.md` §5) — `WidgetHeader
311
+ * tier="widget"` actions. Floored at 22px in {@link deriveSpacing}. */
312
+ controlHeightWidget: 24,
313
+ /** The `size="ctl"` Mantine tier's resolved height (`docs/CONTROLS-SPEC.md` §5, C5) —
314
+ * `PageBar`/`Section`/table toolbar/sidebar-block controls. Floored at 28px in
315
+ * {@link deriveSpacing}. */
316
+ controlHeightCtl: 30,
317
+ /** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
318
+ * `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
319
+ * `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
320
+ * table. Floored at 30px in {@link deriveSpacing} (C15's "floor 30 at density −3"). */
321
+ touchControlHeight: 36,
305
322
  } as const
306
323
 
307
324
  /**
@@ -355,14 +372,13 @@ const SPACE_STEP_BASE = {
355
372
  timelineBullet: 22,
356
373
 
357
374
  // ── Shared across ≥2 sites (structural — must render at the same value) ──────────────────────
358
- // `stickyHeaderClearance`/`stickyHeaderClearanceMobile` USED to live here as ONE independent base
359
- // literal (84) — they don't any more. Both are DERIVED, post-`step`, from their OWN AppShell
360
- // header (`appShellHeaderHeight` / `appShellHeaderMobileHeight` below) instead (see
361
- // {@link deriveSpacing}'s doc for why): an independent literal could drift from the header it's
362
- // supposed to clear, which is exactly what happened (84 < 96, under-clearing the mobile header at
363
- // every level before this fix) — and a SINGLE derived value still over-cleared the desktop header
364
- // by 60px, since the desktop header is less than half the mobile one's height. `SpaceValues.step`
365
- // still carries both keys — see that type's own doc — they're just not part of this BASE table.
375
+ // `stickyHeaderClearance` USED to live here as an independent base literal (84) — it doesn't any
376
+ // more. It is DERIVED, post-`step`, from the AppShell header it exists to clear
377
+ // (`appShellHeaderHeight` below) instead (see {@link deriveSpacing}'s doc for why): an independent
378
+ // literal could drift from that header, which is exactly what happened. `SpaceValues.step` still
379
+ // carries the key — see that type's own doc — it is just not part of this BASE table. The
380
+ // `stickyHeaderClearanceMobile` sibling went with the two-row mobile header in 1.26.0: the header
381
+ // is one 48px row at every viewport now, so one clearance covers both.
366
382
  /** NavLink leftSection icon-to-label gap — shared by `theme/nav-link.module.css` (every render
367
383
  * path) and `shell/app-sidebar.module.css`'s own `.link` rule (belt-and-suspenders on the same
368
384
  * DOM), and reused verbatim for `.footerBtn`/`.accountRow`'s icon gap (their own doc comments call
@@ -572,36 +588,28 @@ const SPACE_STEP_BASE = {
572
588
  /** Settings-menu `<Menu width={…}>` (`app-sidebar.tsx`). */
573
589
  sidebarSettingsMenuWidth: 200,
574
590
 
575
- // ── shell/app-header.module.css ───────────────────────────────────────────────────────────────
576
- /** Mobile two-row header's page-actions row height. */
577
- appHeaderMobileActionsHeight: 52,
578
-
579
591
  // ── shell/index.tsx (AppShell dimensions) ──────────────────────────────────────────────
580
592
  /** AppShell desktop header bar height — sized to hold one row of `size="md"` controls, so it
581
593
  * tracks `controlHeight`. Its level-0 coincidence with `appShellNavbarRailWidth` below is a
582
594
  * coincidence, not a law: this is a horizontal bar's HEIGHT (governed by control height), that
583
595
  * is a vertical rail's WIDTH (governed by icon footprint). They stay separate entries. */
596
+ /** AppShell header bar height — ONE value at every viewport since 1.26.0 (law C14,
597
+ * `docs/CONTROLS-SPEC.md` §2.1). The pre-1.26.0 `appShellHeaderMobileHeight` (97) existed only
598
+ * because the mobile header wrapped to a second, always-reserved page-actions row; `PageBar`
599
+ * folds that row's overflow into a kebab and moves its filters/tabs into the page flow, so the
600
+ * bar is 48px on a phone too and there is no sum to keep in step any more. */
584
601
  appShellHeaderHeight: 48,
585
- /** AppShell mobile (two-row) header height. Not one concept but a sum: row 1 + `app-header.
586
- * module.css`'s wrap row-gap (`--mantine-spacing-sm`) + `appHeaderMobileActionsHeight`. Two of
587
- * those three addends already track density, so the container must too — held fixed, row 1's
588
- * budget collapses 52px -> 12px across the level range.
602
+ /** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
603
+ * permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
604
+ * `.root` at full width, genuinely one concept.
589
605
  *
590
- * KEEP THIS EQUAL TO THE SUM. At level 0: 32 (row 1) + 13 (`SPACE_SCALE.sm`) + 52
591
- * (`appHeaderMobileActionsHeight`) = 97. It was 96 while `SPACE_SCALE.sm` was 12; the
592
- * component-roominess retune raised that addend, and leaving the total at 96 silently took the
593
- * pixel out of row 1 instead — which is what ate the last of its WCAG 2.5.8 target-size margin at
594
- * density -3 (`theme/density-relations.test.ts` measures row 1 as the REMAINDER of this sum, so
595
- * the total is the only place the budget can come from). Raise this whenever `SPACE_SCALE.sm` or
596
- * `appHeaderMobileActionsHeight` moves. Better still, make it unnecessary: computing this the way
597
- * `stickyHeaderClearance*` already is — post-`step`, from its own addends rather than as an
598
- * independent literal — would make the drift impossible instead of merely re-tuned. That is the
599
- * same refactor, and the same reasoning, this PR applied to the clearances; it is worth doing to
600
- * this one too, and it stays a follow-up only because it moves every non-zero level's value. */
601
- appShellHeaderMobileHeight: 97,
602
- /** AppShell navbar width, expanded — ONE entry for both the `base` (mobile drawer) and the
603
- * expanded `sm` value: the same `.root` at full width, genuinely one concept. */
604
- appShellNavbarWidth: 216,
606
+ * 256, up from 216. At 216 the column was 196px of usable text after `.root`'s 10px inset a side,
607
+ * and the sidebar carries more than nav rows: a block row is `label · meta`, an account row is
608
+ * `avatar · name · plan`, and a `Recents` entry is a sentence. Every one of them was ellipsizing
609
+ * in the second word. 256 is also the width the shell's own dropdowns already assume
610
+ * (`sidebarAccountMenuWidth` 220 + the row inset), so the menu no longer overhangs the column it
611
+ * belongs to. */
612
+ appShellNavbarWidth: 256,
605
613
  /** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above
606
614
  * despite sharing 48 at level 0 (see that entry's note). */
607
615
  appShellNavbarRailWidth: 48,
@@ -685,6 +693,33 @@ const SPACE_STEP_BASE = {
685
693
  virtualRowInsetY: 8,
686
694
  /** `data/virtual-list.tsx`'s skeleton row horizontal inset. */
687
695
  virtualRowInsetX: 12,
696
+
697
+ // ── Controls — homes/tiers (`docs/CONTROLS-SPEC.md` §5) ──────────────────────────────────────
698
+ /** `PageBar` row height (row 1 or row 2, each measured independently — the bar's own overall
699
+ * height is the sum published as `--basalt-page-bar-h`, computed by `PageBar` itself, not a
700
+ * static token). */
701
+ pageBarRowHeight: 40,
702
+ /** `WidgetHeader tier="section"` row height. */
703
+ sectionHeaderHeight: 36,
704
+ /** `WidgetHeader tier="widget"` row height. */
705
+ widgetHeaderHeight: 28,
706
+ /** Sidebar block list-item row height. */
707
+ sidebarBlockRowHeight: 32,
708
+ /** Gap between adjacent controls inside a home (filter pills, action buttons). */
709
+ controlGap: 6,
710
+ /** Mobile bottom-sheet row height (the `Filters (n)` drawer, a block's More-sheet row) — Apple
711
+ * HIG 44pt / WCAG 2.5.5 AAA, same floor family as `mobileNavRowHeight`. */
712
+ sheetRowHeight: 44,
713
+ /** `BasaltDataTable`'s header row. Its own entry rather than a reuse of `sectionHeaderHeight`
714
+ * (which is 36 too at level 0): one is a heading's box, this is a table's `th`, and a density
715
+ * pass that wants denser table rows must not be forced to shrink every section header with them.
716
+ * Mantine sizes both `th` and `td` from ONE `verticalSpacing` prop, so a 36/40 pair is
717
+ * unreachable through it — both are applied in `theme/index.ts`'s `Table.extend` as JS numbers,
718
+ * which is why (like `progressBarSize`) neither is emitted as a `--vx-*` var. */
719
+ tableHeaderHeight: 36,
720
+ /** `BasaltDataTable`'s body row — one step taller than the header, so the mono uppercase `th`
721
+ * reads as a label band over roomier data rows rather than as the first row of the data. */
722
+ tableRowHeight: 40,
688
723
  } as const
689
724
 
690
725
  /**
@@ -723,17 +758,14 @@ export const SPACE_FIXED = {
723
758
  export type SpaceValues = {
724
759
  anchors: { [K in keyof typeof SPACE_ANCHORS_BASE]: number }
725
760
  scale: { [K in keyof typeof SPACE_SCALE_BASE]: number }
726
- /** `SPACE_STEP_BASE`'s one-offs PLUS `stickyHeaderClearance`/`stickyHeaderClearanceMobile`, NEITHER
727
- * of which is in that base table — both are computed after the rest of `step`, one from
728
- * `appShellHeaderHeight` (desktop) and one from `appShellHeaderMobileHeight` (mobile), see
761
+ /** `SPACE_STEP_BASE`'s one-offs PLUS `stickyHeaderClearance`, which is NOT in that base table —
762
+ * it is computed after the rest of `step`, from `appShellHeaderHeight`, see
729
763
  * {@link deriveSpacing}'s doc. Kept on this same `step` object (not split into a third group)
730
- * because every consumer reads them exactly like any other one-off — only their DERIVATION
731
- * differs. Two keys, not one, because the clearance is RESPONSIVE (Decision 3): a consumer picks
732
- * whichever one matches the breakpoint it's rendering at — see `docs/CONTENT-SPEC.md` §5 and
733
- * `content/prose.module.css`/`content/article-layout.module.css` for the CSS-side split. */
764
+ * because every consumer reads it exactly like any other one-off — only its DERIVATION differs.
765
+ * ONE key, not the pre-1.26.0 responsive pair: the AppShell header is a single 48px row at every
766
+ * viewport now (law C14, `docs/CONTROLS-SPEC.md` §2.1), so there is no second header to clear. */
734
767
  step: { [K in keyof typeof SPACE_STEP_BASE]: number } & {
735
768
  stickyHeaderClearance: number
736
- stickyHeaderClearanceMobile: number
737
769
  }
738
770
  /** NavLink row line-height (`theme/index.ts`'s row-geometry styles) — additive, not
739
771
  * multiplicative; see {@link deriveSpacing}'s doc for why. */
@@ -805,8 +837,8 @@ const ROW_LINE_HEIGHT_STEP = 1 / 30
805
837
  * from a single integer `level` (`docs/DESIGN-SPEC.md` §4-adjacent) — the spacing analog of
806
838
  * {@link deriveRadius}. `level: 0` reproduces the shipped identity exactly (locked by
807
839
  * `theme/spacing.test.ts` and `tokens/density.test.ts`) for every value EXCEPT
808
- * `step.stickyHeaderClearance`/`step.stickyHeaderClearanceMobile` — see the third bullet below for
809
- * why those are a deliberate exception.
840
+ * `step.stickyHeaderClearance` — see the third bullet below for why that one is a deliberate
841
+ * exception.
810
842
  *
811
843
  * The law: a MULTIPLIER, `1 + 0.1 * level`, applied to every `SPACE_ANCHORS_BASE`/
812
844
  * `SPACE_SCALE_BASE`/`SPACE_STEP_BASE` constant, then rounded (see {@link scaleSpace}) — a
@@ -831,30 +863,24 @@ const ROW_LINE_HEIGHT_STEP = 1 / 30
831
863
  * (and, transitively, the collapsed rail's `ActionIcon` variant, which reads this same resolved
832
864
  * value) below the accessible floor (22px at level -3) while also crowding out its fixed-size icon
833
865
  * and `Kbd` chip.
834
- * - `step.stickyHeaderClearance` (desktop) and `step.stickyHeaderClearanceMobile` (mobile) are not
835
- * scaled off their own base AT ALL — `SPACE_STEP_BASE` has no such key any more (see that table's
836
- * doc). Both are computed AFTER the rest of `step`, RESPONSIVELY, one per AppShell breakpoint:
837
- * `stickyHeaderClearance = step.appShellHeaderHeight + anchors.stackMd` (desktop, `>= sm`) and
838
- * `stickyHeaderClearanceMobile = step.appShellHeaderMobileHeight + anchors.stackMd` (mobile,
839
- * `< sm` — the SAME `sm`/48em breakpoint the AppShell header itself switches on, `shell/index.tsx`).
840
- * A SINGLE clearance value (this law's own prior shape) can only be tuned against ONE header, and
841
- * whichever one it under-shoots stays broken: the original independent literal (84) was tuned only
842
- * against the desktop header (48) and under-cleared the mobile header (96) by 12px at level 0
843
- * already, before density entered the picture; deriving ONE value off the taller mobile header
844
- * fixed that under-clear but then OVER-cleared the desktop header by 60px (108 vs. the 48px header
845
- * needs) — dead space above every anchored heading and a TOC rail shoved down on the common
846
- * (desktop) path. Splitting into two responsive values fixes both directions at once: each clears
847
- * ONLY its own header, with the SAME `anchors.stackMd` breathing room (not a bare `+ 0`) so a
848
- * scrolled-to heading doesn't sit flush against the header's bottom edge — density-tracking, like
849
- * the header height it's added to, so the margin itself never freezes at one notch. Level 0:
850
- * desktop `48 + 12 = 60` (a deliberate BREAK from the pre-split 84/108 — see `theme/spacing.test.ts`
851
- * for why `stickyHeaderClearance` alone is exempt from the byte-identity gate every other spacing
852
- * token is held to), mobile `96 + 12 = 108` (unchanged from the pre-split single-value law).
853
- * **Consumer coupling (accepted, not hidden):** both values assume `BasaltShell`'s own AppShell
854
- * header heights — a `./content` consumer NOT rendering inside `BasaltShell` (e.g. `Prose`/
855
- * `ArticleLayout` standalone) gets clearance numbers tuned for a header it may not have, and must
856
- * override `--vx-space-sticky-header-clearance`/`--vx-space-sticky-header-clearance-mobile`
857
- * itself. Documented on the `./content` surface too (`docs/CONTENT-SPEC.md` §5), not only here.
866
+ * - `step.stickyHeaderClearance` is not scaled off its own base AT ALL — `SPACE_STEP_BASE` has no
867
+ * such key any more (see that table's doc). It is computed AFTER the rest of `step`, as
868
+ * `step.appShellHeaderHeight + anchors.stackMd`. An independent literal can only be tuned against
869
+ * ONE header height and then drifts from it: the original 84 was tuned against a 48px desktop
870
+ * header and under-cleared the pre-1.26.0 96px mobile one by 12px at level 0, before density
871
+ * entered the picture at all. Deriving it removes that whole failure mode — and since 1.26.0 the
872
+ * AppShell header is ONE 48px row at every viewport (law C14), so the `stickyHeaderClearanceMobile`
873
+ * half of the old responsive pair is deleted rather than re-derived. The `+ anchors.stackMd`
874
+ * breathing room (not a bare `+ 0`) keeps a scrolled-to heading off the header's bottom edge, and
875
+ * tracks density like the header height it is added to, so the margin never freezes at one notch.
876
+ * Level 0: `48 + 12 = 60` (a deliberate BREAK from the pre-derivation 84 — see
877
+ * `theme/spacing.test.ts` for why `stickyHeaderClearance` alone is exempt from the byte-identity
878
+ * gate every other spacing token is held to).
879
+ * **Consumer coupling (accepted, not hidden):** the value assumes `BasaltShell`'s own AppShell
880
+ * header height — a `./content` consumer NOT rendering inside `BasaltShell` (e.g. `Prose`/
881
+ * `ArticleLayout` standalone) gets a clearance number tuned for a header it may not have, and must
882
+ * override `--vx-space-sticky-header-clearance` itself. Documented on the `./content` surface too
883
+ * (`docs/CONTENT-SPEC.md` §5), not only here.
858
884
  *
859
885
  * `rowLineHeight` is the ONE exception to the multiplier entirely: `1.35 + ROW_LINE_HEIGHT_STEP *
860
886
  * level`, ADDITIVE, its own gentler, INDEPENDENT coefficient (see {@link ROW_LINE_HEIGHT_STEP}'s own
@@ -939,6 +965,13 @@ export function deriveSpacing(level: number): SpaceValues {
939
965
  anchors.stackMd = stackUnit * 3
940
966
  anchors.stackLg = stackUnit * 4
941
967
  anchors.stackXl = stackUnit * 6
968
+ // Control-tier floors (`docs/CONTROLS-SPEC.md` §5, C15) — same "negative level can't collapse a
969
+ // real control below its accessible/legible minimum" guardrail as `sidebarSearchTriggerHeight`'s
970
+ // WCAG 2.5.8 floor and the mobile-nav Apple HIG floors below.
971
+ anchors.controlHeightTag = Math.max(18, anchors.controlHeightTag)
972
+ anchors.controlHeightWidget = Math.max(22, anchors.controlHeightWidget)
973
+ anchors.controlHeightCtl = Math.max(28, anchors.controlHeightCtl)
974
+ anchors.touchControlHeight = Math.max(30, anchors.touchControlHeight)
942
975
 
943
976
  const mappedStep = mapSpaceGroup(SPACE_STEP_BASE, level, multiplier)
944
977
  // WCAG 2.5.8 minimum interactive-target size — see this function's doc for why the trigger's
@@ -949,14 +982,15 @@ export function deriveSpacing(level: number): SpaceValues {
949
982
  // sidebar search trigger's 24px floor directly above.
950
983
  mappedStep.mobileNavBarHeight = Math.max(48, mappedStep.mobileNavBarHeight)
951
984
  mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight)
985
+ // Same floor family, same reason — `sheetRowHeight`'s own doc names it "same floor family as
986
+ // `mobileNavRowHeight`", but nothing enforced that below level 0 until now.
987
+ mappedStep.sheetRowHeight = Math.max(44, mappedStep.sheetRowHeight)
952
988
  const step: SpaceValues['step'] = {
953
989
  ...mappedStep,
954
- // RESPONSIVE, one per AppShell breakpoint — each DERIVED from its own header, plus
955
- // density-tracking breathing room — see this function's doc (third bullet, Decision 3) for the
956
- // full rationale, and `theme/spacing.test.ts` for why `stickyHeaderClearance` alone is exempt
957
- // from the level-0 byte-identity gate.
990
+ // DERIVED from the header it clears, plus density-tracking breathing room — see this function's
991
+ // doc (third bullet) for the full rationale, and `theme/spacing.test.ts` for why
992
+ // `stickyHeaderClearance` alone is exempt from the level-0 byte-identity gate.
958
993
  stickyHeaderClearance: mappedStep.appShellHeaderHeight + anchors.stackMd,
959
- stickyHeaderClearanceMobile: mappedStep.appShellHeaderMobileHeight + anchors.stackMd,
960
994
  }
961
995
 
962
996
  return {
@@ -989,9 +1023,9 @@ export const SPACE_SCALE = DEFAULT_SPACE_VALUES.scale
989
1023
 
990
1024
  /** Density-tracking one-offs at the shipped identity — see `SPACE_STEP_BASE`'s doc. `deriveSpacing(0)`'s
991
1025
  * `step`, UNCHANGED from the shipped identity (locked by `theme/spacing.test.ts`) EXCEPT
992
- * `stickyHeaderClearance` (originally 84, then a single derived 108, now the desktop half of the
993
- * Decision-3 responsive split at 60) and its new `stickyHeaderClearanceMobile` sibling (108) — the
994
- * deliberate level-0 breaks in the density work; see `deriveSpacing`'s doc (third bullet) for why. */
1026
+ * `stickyHeaderClearance` (originally 84, then a single derived 108, now 60 — derived from the one
1027
+ * 48px AppShell header) — the deliberate level-0 break in the density work; see `deriveSpacing`'s
1028
+ * doc (third bullet) for why. */
995
1029
  export const SPACE_STEP = DEFAULT_SPACE_VALUES.step
996
1030
 
997
1031
  /** NavLink row line-height at the shipped identity (`1.35`) — `deriveSpacing(0)`'s `rowLineHeight`,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `DeltaBadge` — sign-driven tone/glyph, `withGlyph`, custom `format`, and `period` (docs/
3
+ * DESIGN-SPEC.md §5). Mantine-free, so no `MantineProvider` wrapper is needed.
4
+ *
5
+ * CSS-module class hashes are unavailable under `bun test` (see `app-mobile-nav.test.tsx`'s doc) —
6
+ * every query below goes through text content, never `classes.*`.
7
+ */
8
+ import { render, screen } from '@testing-library/react'
9
+ import { describe, expect, test } from 'bun:test'
10
+ import { DeltaBadge } from './delta-badge'
11
+
12
+ describe('sign drives tone and glyph', () => {
13
+ test('positive renders the up glyph and the formatted magnitude', () => {
14
+ render(<DeltaBadge value={12.4} />)
15
+ expect(screen.getByText('▲')).toBeDefined()
16
+ expect(screen.getByText('12.4%')).toBeDefined()
17
+ })
18
+
19
+ test('negative renders the down glyph and the formatted magnitude', () => {
20
+ render(<DeltaBadge value={-3.1} />)
21
+ expect(screen.getByText('▼')).toBeDefined()
22
+ expect(screen.getByText('3.1%')).toBeDefined()
23
+ })
24
+
25
+ test('zero renders the formatted magnitude with no glyph', () => {
26
+ render(<DeltaBadge value={0} />)
27
+ expect(screen.getByText('0.0%')).toBeDefined()
28
+ expect(screen.queryByText('▲')).toBeNull()
29
+ expect(screen.queryByText('▼')).toBeNull()
30
+ })
31
+ })
32
+
33
+ test('withGlyph={false} suppresses the glyph even for a signed value', () => {
34
+ render(<DeltaBadge value={12.4} withGlyph={false} />)
35
+ expect(screen.getByText('12.4%')).toBeDefined()
36
+ expect(screen.queryByText('▲')).toBeNull()
37
+ })
38
+
39
+ test('a zero value never shows a glyph, even with withGlyph={true}', () => {
40
+ render(<DeltaBadge value={0} withGlyph={true} />)
41
+ expect(screen.queryByText('▲')).toBeNull()
42
+ expect(screen.queryByText('▼')).toBeNull()
43
+ })
44
+
45
+ test('a custom format overrides the default percentage label', () => {
46
+ render(<DeltaBadge value={182} format={(v) => `${Math.abs(v)}ms`} withGlyph={false} />)
47
+ expect(screen.getByText('182ms')).toBeDefined()
48
+ expect(screen.queryByText('182.0%')).toBeNull()
49
+ })
50
+
51
+ test('period renders after the value, in the same badge', () => {
52
+ render(<DeltaBadge value={4.2} period="MoM" />)
53
+ expect(screen.getByText('4.2%')).toBeDefined()
54
+ expect(screen.getByText('MoM')).toBeDefined()
55
+ })
56
+
57
+ test('omitting period renders no extra text', () => {
58
+ render(<DeltaBadge value={4.2} />)
59
+ expect(screen.queryByText('MoM')).toBeNull()
60
+ })
@@ -0,0 +1,57 @@
1
+ /**
2
+ * DeltaBadge — the KPI trend chip (docs/DESIGN-SPEC.md §5): mono 11.5px weight 600, status-color
3
+ * text on a status-13% tint, control radius, 2px/7px padding, optional ▲/▼ glyph at 9px. Sign
4
+ * alone drives the tone — positive reads as success, negative as danger, zero as a neutral faint
5
+ * tint with no glyph. The label never carries a leading "+"; pass `format` to control the
6
+ * magnitude string (defaults to a one-decimal percentage of the absolute value).
7
+ *
8
+ * Mantine-free (plain `<span>` + this folder's CSS module, docs/CONTROLS-SPEC.md §2.2) so
9
+ * `WidgetHeader` can compose it from inside `charts/` without tripping `basalt/token-layer-
10
+ * boundary` or `check-dist-layering.mjs`.
11
+ *
12
+ * @example
13
+ * import { DeltaBadge } from 'basalt-ui'
14
+ *
15
+ * <DeltaBadge value={12.4} /> // ▲ 12.4%, success tint
16
+ * <DeltaBadge value={-3.1} /> // ▼ 3.1%, danger tint
17
+ * <DeltaBadge value={0} /> // 0.0%, neutral faint tint, no glyph
18
+ * <DeltaBadge value={182} format={(v) => `${Math.abs(v)}ms`} withGlyph={false} />
19
+ */
20
+ import { alpha, VX } from '../tokens'
21
+ import classes from './widget-header.module.css'
22
+
23
+ export type DeltaBadgeProps = {
24
+ /** Signed delta driving tone (positive/negative/zero) and, by default, the label magnitude. */
25
+ value: number
26
+ /** Formats the value into the label text. Defaults to `${Math.abs(value).toFixed(1)}%`. */
27
+ format?: (value: number) => string
28
+ /** Render the ▲/▼ direction glyph. Defaults to `true`; a zero value never shows a glyph. */
29
+ withGlyph?: boolean
30
+ /** Optional comparison timeframe rendered directly after the value in a dimmer shade of the same
31
+ * tone (e.g. `MoM`, `WoW`, `YTD`) — states what the delta is measured against without a hover. */
32
+ period?: string | undefined
33
+ }
34
+
35
+ const defaultFormat = (value: number): string => `${Math.abs(value).toFixed(1)}%`
36
+
37
+ export function DeltaBadge({
38
+ value,
39
+ format = defaultFormat,
40
+ withGlyph = true,
41
+ period,
42
+ }: DeltaBadgeProps) {
43
+ const tone = value > 0 ? VX.status.good : value < 0 ? VX.status.bad : VX.faint
44
+ const glyph = value > 0 ? '▲' : value < 0 ? '▼' : undefined
45
+ const showGlyph = withGlyph && glyph !== undefined
46
+
47
+ return (
48
+ <span
49
+ className={classes.deltaBadge}
50
+ style={{ backgroundColor: alpha(tone, 0.13), color: tone }}
51
+ >
52
+ {showGlyph && <span className={classes.deltaGlyph}>{glyph}</span>}
53
+ {format(value)}
54
+ {period !== undefined && <span className={classes.deltaPeriod}>{period}</span>}
55
+ </span>
56
+ )
57
+ }
@@ -0,0 +1,2 @@
1
+ export { WidgetHeader, type WidgetHeaderProps } from './widget-header'
2
+ export { DeltaBadge, type DeltaBadgeProps } from './delta-badge'
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `src/widget-header/**` must stay Mantine-free (docs/CONTROLS-SPEC.md §2.2) — so `ChartCard`
3
+ * (inside the `charts/` boundary) can compose `WidgetHeader`/`DeltaBadge` without tripping
4
+ * `basalt/token-layer-boundary` or `check-dist-layering.mjs`. Source-level regex check, same idiom
5
+ * as `styles.floor.test.ts`'s direct read of shipped text — independent of oxlint, which only runs
6
+ * over changed files locally and could be skipped.
7
+ *
8
+ * The pattern matches an actual import/re-export specifier, not any mention of "@mantine" in
9
+ * prose — this module's own doc comments say "Mantine-free" and name `@mantine/*` on purpose.
10
+ */
11
+ import { readdirSync, readFileSync } from 'node:fs'
12
+ import { dirname, join } from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+ import { describe, expect, test } from 'bun:test'
15
+
16
+ const WIDGET_HEADER_DIR = dirname(fileURLToPath(import.meta.url))
17
+ const MANTINE_IMPORT = /(?:from|import)\s*\(?\s*['"]@mantine\//
18
+
19
+ const sourceFiles = readdirSync(WIDGET_HEADER_DIR).filter(
20
+ (name) =>
21
+ (name.endsWith('.ts') || name.endsWith('.tsx')) &&
22
+ !name.endsWith('.test.ts') &&
23
+ !name.endsWith('.test.tsx'),
24
+ )
25
+
26
+ describe('src/widget-header — Mantine-free', () => {
27
+ for (const name of sourceFiles) {
28
+ test(`${name} imports no '@mantine/*' module`, () => {
29
+ const text = readFileSync(join(WIDGET_HEADER_DIR, name), 'utf8')
30
+ expect(text).not.toMatch(MANTINE_IMPORT)
31
+ })
32
+ }
33
+ })