basalt-ui 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +25 -0
  3. package/MIGRATING.md +547 -21
  4. package/README.md +46 -33
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/query-state.d.ts +1 -1
  134. package/dist/dashboard/settings-section.d.ts +6 -4
  135. package/dist/dashboard/settings-section.d.ts.map +1 -1
  136. package/dist/dashboard/settings-section.js +23 -16
  137. package/dist/dashboard/settings-section.module.css +10 -0
  138. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  139. package/dist/dashboard/settings-section.test.d.ts +2 -0
  140. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  141. package/dist/dashboard/stat-card.d.ts +44 -9
  142. package/dist/dashboard/stat-card.d.ts.map +1 -1
  143. package/dist/dashboard/stat-card.js +80 -48
  144. package/dist/dashboard/stat-card.module.css +82 -0
  145. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  146. package/dist/data/data-table.d.ts +102 -20
  147. package/dist/data/data-table.d.ts.map +1 -1
  148. package/dist/data/data-table.js +166 -65
  149. package/dist/data/table.d.ts +1 -1
  150. package/dist/data/table.d.ts.map +1 -1
  151. package/dist/guard/grace.test.d.ts +20 -0
  152. package/dist/guard/grace.test.d.ts.map +1 -0
  153. package/dist/guard/index.d.ts +66 -3
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +82 -7
  156. package/dist/guard/types.d.ts +3 -3
  157. package/dist/guard/types.d.ts.map +1 -1
  158. package/dist/index.d.ts +12 -2
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +13 -10
  161. package/dist/nav/types.d.ts +78 -2
  162. package/dist/nav/types.d.ts.map +1 -1
  163. package/dist/notifications/bell.d.ts +4 -1
  164. package/dist/notifications/bell.d.ts.map +1 -1
  165. package/dist/notifications/index.d.ts +1 -1
  166. package/dist/router-tanstack/field.d.ts +14 -0
  167. package/dist/router-tanstack/field.d.ts.map +1 -0
  168. package/dist/router-tanstack/field.js +4 -0
  169. package/dist/router-tanstack/index.d.ts +6 -2
  170. package/dist/router-tanstack/index.d.ts.map +1 -1
  171. package/dist/router-tanstack/index.js +4 -0
  172. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  173. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  175. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  176. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  177. package/dist/router-tanstack/search-param-store.js +16 -45
  178. package/dist/router-tanstack/search-store.d.ts +81 -0
  179. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.js +187 -0
  181. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  182. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  183. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  184. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  185. package/dist/router-tanstack/use-nav.d.ts +0 -2
  186. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  187. package/dist/router-tanstack/use-nav.js +34 -12
  188. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  189. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  190. package/dist/section/index.d.ts +2 -0
  191. package/dist/section/index.d.ts.map +1 -0
  192. package/dist/section/index.js +4 -0
  193. package/dist/section/section.d.ts +24 -0
  194. package/dist/section/section.d.ts.map +1 -0
  195. package/dist/section/section.js +126 -0
  196. package/dist/section/section.module.css +104 -0
  197. package/dist/section/section.module.css.d.ts +11 -0
  198. package/dist/section/section.test.d.ts +2 -0
  199. package/dist/section/section.test.d.ts.map +1 -0
  200. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  201. package/dist/shell/app-breadcrumbs.js +18 -15
  202. package/dist/shell/app-header.module.css +77 -37
  203. package/dist/shell/app-header.module.css.d.ts +2 -1
  204. package/dist/shell/app-mobile-nav.d.ts +9 -2
  205. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  206. package/dist/shell/app-mobile-nav.js +98 -11
  207. package/dist/shell/app-mobile-nav.module.css +8 -0
  208. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  209. package/dist/shell/app-sidebar.d.ts +55 -28
  210. package/dist/shell/app-sidebar.d.ts.map +1 -1
  211. package/dist/shell/app-sidebar.js +116 -67
  212. package/dist/shell/app-sidebar.module.css +303 -20
  213. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  214. package/dist/shell/index.d.ts +55 -21
  215. package/dist/shell/index.d.ts.map +1 -1
  216. package/dist/shell/index.js +40 -18
  217. package/dist/shell/mobile-nav-model.d.ts +13 -2
  218. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  219. package/dist/shell/mobile-nav-model.js +15 -1
  220. package/dist/shell/nav-count-badge.d.ts +1 -1
  221. package/dist/shell/page-bar.d.ts +51 -0
  222. package/dist/shell/page-bar.d.ts.map +1 -0
  223. package/dist/shell/page-bar.js +154 -0
  224. package/dist/shell/page-bar.module.css +178 -0
  225. package/dist/shell/page-bar.module.css.d.ts +17 -0
  226. package/dist/shell/page-bar.test.d.ts +2 -0
  227. package/dist/shell/page-bar.test.d.ts.map +1 -0
  228. package/dist/shell/sidebar-block-model.d.ts +48 -0
  229. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  230. package/dist/shell/sidebar-block-model.js +39 -0
  231. package/dist/shell/sidebar-blocks.d.ts +32 -0
  232. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  233. package/dist/shell/sidebar-blocks.js +169 -0
  234. package/dist/shell/sidebar-search.d.ts +13 -1
  235. package/dist/shell/sidebar-search.d.ts.map +1 -1
  236. package/dist/shell/sidebar-search.js +34 -1
  237. package/dist/shell/sidebar-search.module.css +26 -0
  238. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  239. package/dist/state/fields.d.ts +392 -0
  240. package/dist/state/fields.d.ts.map +1 -0
  241. package/dist/state/fields.js +369 -0
  242. package/dist/state/fields.test.d.ts +2 -0
  243. package/dist/state/fields.test.d.ts.map +1 -0
  244. package/dist/state/persisted.d.ts +48 -0
  245. package/dist/state/persisted.d.ts.map +1 -0
  246. package/dist/state/persisted.js +105 -0
  247. package/dist/state.d.ts +11 -56
  248. package/dist/state.d.ts.map +1 -1
  249. package/dist/state.js +14 -120
  250. package/dist/styles.css +13 -0
  251. package/dist/surfaces.d.ts +106 -24
  252. package/dist/surfaces.d.ts.map +1 -1
  253. package/dist/surfaces.js +155 -19
  254. package/dist/theme/controls.module.css +16 -1
  255. package/dist/theme/ctl-theme.d.ts +32 -0
  256. package/dist/theme/ctl-theme.d.ts.map +1 -0
  257. package/dist/theme/ctl-theme.js +54 -0
  258. package/dist/theme/ctl-theme.test.d.ts +2 -0
  259. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  260. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  261. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  262. package/dist/theme/divider-law.test.d.ts +2 -0
  263. package/dist/theme/divider-law.test.d.ts.map +1 -0
  264. package/dist/theme/icon-slot.d.ts +44 -0
  265. package/dist/theme/icon-slot.d.ts.map +1 -0
  266. package/dist/theme/icon-slot.js +21 -0
  267. package/dist/theme/icon-slot.module.css +47 -0
  268. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  269. package/dist/theme/icon-slot.test.d.ts +2 -0
  270. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  271. package/dist/theme/index.d.ts +1 -0
  272. package/dist/theme/index.d.ts.map +1 -1
  273. package/dist/theme/index.js +82 -5
  274. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  275. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  276. package/dist/theme/segmented-control.module.css +20 -0
  277. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  278. package/dist/tokens/index.d.ts +12 -0
  279. package/dist/tokens/index.d.ts.map +1 -1
  280. package/dist/tokens/index.js +38 -8
  281. package/dist/tokens/palette.d.ts +97 -61
  282. package/dist/tokens/palette.d.ts.map +1 -1
  283. package/dist/tokens/palette.js +76 -38
  284. package/dist/tokens.css +10 -2
  285. package/dist/widget-header/delta-badge.d.ts +13 -0
  286. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  287. package/dist/widget-header/delta-badge.js +29 -0
  288. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  289. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  290. package/dist/widget-header/index.d.ts +3 -0
  291. package/dist/widget-header/index.d.ts.map +1 -0
  292. package/dist/widget-header/index.js +6 -0
  293. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  294. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  295. package/dist/widget-header/widget-header.d.ts +37 -0
  296. package/dist/widget-header/widget-header.d.ts.map +1 -0
  297. package/dist/widget-header/widget-header.js +77 -0
  298. package/dist/widget-header/widget-header.module.css +335 -0
  299. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  300. package/dist/widget-header/widget-header.test.d.ts +2 -0
  301. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  302. package/llms.txt +30 -17
  303. package/package.json +14 -1
  304. package/src/charts/cursor/resolve.test.ts +29 -1
  305. package/src/charts/cursor/resolve.ts +38 -6
  306. package/src/charts/cursor/scope.tsx +6 -3
  307. package/src/charts/cursor/store.test.ts +19 -1
  308. package/src/charts/cursor/store.ts +10 -5
  309. package/src/charts/hooks/useChartCursor.ts +16 -8
  310. package/src/charts/index.ts +2 -2
  311. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  312. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  313. package/src/charts/primitives/ChartCard.tsx +86 -146
  314. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  315. package/src/cli/check-coverage.test.ts +297 -0
  316. package/src/cli/check-theme.test.ts +38 -14
  317. package/src/cli/index.ts +392 -16
  318. package/src/cli/init-sync.test.ts +97 -0
  319. package/src/connectivity/connectivity-indicator.tsx +16 -9
  320. package/src/content/index.ts +16 -3
  321. package/src/content/prose.module.css +7 -28
  322. package/src/content/prose.module.css.test.ts +8 -24
  323. package/src/controls/actions.module.css +62 -0
  324. package/src/controls/actions.module.css.d.ts +13 -0
  325. package/src/controls/actions.test.tsx +252 -0
  326. package/src/controls/actions.tsx +655 -0
  327. package/src/controls/actions.type-guard.test.ts +63 -0
  328. package/src/controls/compare-filter.tsx +64 -0
  329. package/src/controls/control-group.test.tsx +229 -0
  330. package/src/controls/control-group.tsx +63 -0
  331. package/src/controls/controls.module.css +542 -0
  332. package/src/controls/controls.module.css.d.ts +35 -0
  333. package/src/controls/controls.router.test.tsx +637 -0
  334. package/src/controls/enum-filter.tsx +159 -0
  335. package/src/controls/filter-context.tsx +153 -0
  336. package/src/controls/filter-pill.tsx +120 -0
  337. package/src/controls/filter-set.test.tsx +472 -0
  338. package/src/controls/filter-set.tsx +225 -0
  339. package/src/controls/filter-sheet.tsx +285 -0
  340. package/src/controls/glyphs.tsx +81 -0
  341. package/src/controls/index.ts +48 -0
  342. package/src/controls/multi-select-filter.tsx +140 -0
  343. package/src/controls/peer-boundary.test.ts +85 -0
  344. package/src/controls/props-contract.test.tsx +106 -0
  345. package/src/controls/range-filter.tsx +184 -0
  346. package/src/controls/search-filter.tsx +123 -0
  347. package/src/controls/select-filter.tsx +89 -0
  348. package/src/controls/sync-button.module.css +63 -0
  349. package/src/controls/sync-button.module.css.d.ts +11 -0
  350. package/src/controls/sync-button.test.tsx +183 -0
  351. package/src/controls/sync-button.tsx +197 -0
  352. package/src/controls/toggle-filter.tsx +60 -0
  353. package/src/controls/view-tabs.tsx +110 -0
  354. package/src/controls-dates/date-range-picker.tsx +42 -0
  355. package/src/controls-dates/index.ts +8 -0
  356. package/src/dashboard/delta-badge.tsx +4 -74
  357. package/src/dashboard/query-state.tsx +1 -1
  358. package/src/dashboard/settings-section.module.css +10 -0
  359. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  360. package/src/dashboard/settings-section.test.tsx +56 -0
  361. package/src/dashboard/settings-section.tsx +33 -30
  362. package/src/dashboard/stat-card.module.css +82 -0
  363. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  364. package/src/dashboard/stat-card.test.tsx +67 -1
  365. package/src/dashboard/stat-card.tsx +209 -68
  366. package/src/data/data-table.test.tsx +282 -6
  367. package/src/data/data-table.tsx +429 -94
  368. package/src/data/table.ts +2 -0
  369. package/src/guard/check-source.test.ts +126 -13
  370. package/src/guard/grace.test.ts +149 -0
  371. package/src/guard/index.ts +172 -29
  372. package/src/guard/types.ts +4 -2
  373. package/src/index.ts +46 -5
  374. package/src/nav/types.ts +84 -2
  375. package/src/notifications/bell.tsx +6 -3
  376. package/src/notifications/index.ts +1 -1
  377. package/src/router-tanstack/field.ts +31 -0
  378. package/src/router-tanstack/index.ts +24 -2
  379. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  380. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  381. package/src/router-tanstack/search-param-store.ts +42 -159
  382. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  383. package/src/router-tanstack/search-store.test.ts +418 -0
  384. package/src/router-tanstack/search-store.ts +380 -0
  385. package/src/router-tanstack/use-nav.test.tsx +187 -0
  386. package/src/router-tanstack/use-nav.ts +72 -14
  387. package/src/section/index.ts +1 -0
  388. package/src/section/section.module.css +104 -0
  389. package/src/section/section.module.css.d.ts +11 -0
  390. package/src/section/section.test.tsx +204 -0
  391. package/src/section/section.tsx +220 -0
  392. package/src/shell/app-breadcrumbs.tsx +44 -31
  393. package/src/shell/app-header.module.css +77 -37
  394. package/src/shell/app-header.module.css.d.ts +2 -1
  395. package/src/shell/app-mobile-nav.module.css +8 -0
  396. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  397. package/src/shell/app-mobile-nav.tsx +147 -8
  398. package/src/shell/app-sidebar-account.tsx +2 -2
  399. package/src/shell/app-sidebar.module.css +303 -20
  400. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  401. package/src/shell/app-sidebar.test.tsx +456 -41
  402. package/src/shell/app-sidebar.tsx +276 -140
  403. package/src/shell/index.test.tsx +291 -5
  404. package/src/shell/index.tsx +137 -38
  405. package/src/shell/mobile-nav-model.test.ts +228 -7
  406. package/src/shell/mobile-nav-model.ts +53 -3
  407. package/src/shell/nav-count-badge.tsx +1 -1
  408. package/src/shell/page-bar.module.css +178 -0
  409. package/src/shell/page-bar.module.css.d.ts +17 -0
  410. package/src/shell/page-bar.test.tsx +316 -0
  411. package/src/shell/page-bar.tsx +315 -0
  412. package/src/shell/sidebar-block-model.ts +83 -0
  413. package/src/shell/sidebar-blocks.tsx +301 -0
  414. package/src/shell/sidebar-search.module.css +26 -0
  415. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  416. package/src/shell/sidebar-search.tsx +71 -2
  417. package/src/state/fields.test.tsx +258 -0
  418. package/src/state/fields.ts +892 -0
  419. package/src/state/persisted.ts +211 -0
  420. package/src/state.ts +46 -240
  421. package/src/styles.css +13 -0
  422. package/src/surfaces.ts +203 -36
  423. package/src/theme/controls.module.css +16 -1
  424. package/src/theme/ctl-theme.test.tsx +275 -0
  425. package/src/theme/ctl-theme.tsx +138 -0
  426. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  427. package/src/theme/density-relations.test.ts +106 -51
  428. package/src/theme/divider-law.test.ts +259 -0
  429. package/src/theme/icon-slot.module.css +47 -0
  430. package/src/theme/icon-slot.module.css.d.ts +8 -0
  431. package/src/theme/icon-slot.test.tsx +275 -0
  432. package/src/theme/icon-slot.tsx +59 -0
  433. package/src/theme/index.ts +165 -3
  434. package/src/theme/layout-rhythm-css.test.ts +72 -0
  435. package/src/theme/radius.test.ts +11 -1
  436. package/src/theme/segmented-control.module.css +20 -0
  437. package/src/theme/shadow-surfaces.test.ts +20 -7
  438. package/src/theme/spacing.test.ts +75 -27
  439. package/src/theme/use-basalt-spacing.ts +2 -2
  440. package/src/tokens/density.test.ts +7 -25
  441. package/src/tokens/index.ts +48 -8
  442. package/src/tokens/palette.ts +107 -73
  443. package/src/widget-header/delta-badge.test.tsx +60 -0
  444. package/src/widget-header/delta-badge.tsx +57 -0
  445. package/src/widget-header/index.ts +2 -0
  446. package/src/widget-header/no-mantine.test.ts +33 -0
  447. package/src/widget-header/widget-header.module.css +335 -0
  448. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  449. package/src/widget-header/widget-header.test.tsx +187 -0
  450. package/src/widget-header/widget-header.tsx +168 -0
  451. package/agent/rules/basalt-agent.md +0 -452
  452. package/agent/rules/basalt-app.md +0 -246
  453. package/agent/rules/basalt-commands.md +0 -320
  454. package/agent/rules/basalt-content.md +0 -409
  455. package/agent/rules/basalt-data.md +0 -269
  456. package/agent/rules/basalt-forms.md +0 -186
  457. package/agent/rules/basalt-notifications.md +0 -264
  458. package/agent/rules/basalt-query.md +0 -187
  459. package/agent/rules/basalt-router.md +0 -336
  460. package/dist/content/article-filter-bar.d.ts +0 -40
  461. package/dist/content/article-filter-bar.d.ts.map +0 -1
  462. package/dist/content/article-filter-bar.js +0 -56
  463. package/dist/shell/page-header.d.ts +0 -13
  464. package/dist/shell/page-header.d.ts.map +0 -1
  465. package/dist/shell/page-header.js +0 -23
  466. package/src/content/article-filter-bar.tsx +0 -88
  467. package/src/shell/page-header.tsx +0 -35
@@ -1,322 +1,160 @@
1
1
  ---
2
2
  source: basalt-ui
3
- description: Color, spacing, radius, and type discipline — route every value through the basalt-ui token system. Enforced by `basalt-ui check-theme`.
3
+ description: Color, spacing, radius and type discipline — route every value through the basalt-ui token system. Enforced by `basalt-ui check-theme` plus the oxlint type-scale rules.
4
4
  paths:
5
5
  - 'src/**'
6
6
  - 'apps/**/src/**'
7
7
  - 'packages/**/src/**'
8
8
  ---
9
9
 
10
- # Basalt Tokens — Color, Spacing, Radius, Type
10
+ <!-- basalt:coverage -->
11
+ <!-- GENERATED from src/surfaces.ts — `basalt-ui check-coverage --write`. Do not hand-edit. -->
12
+ <!-- backed by: guard kinds — css-raw-surface, inline-font-size, off-identity-accent, off-system-surface-var, raw-color-fn, raw-font-family, raw-hex, raw-radius, raw-surface, surface-shadow-override, theme-allow-unscoped · oxlint rules — basalt/no-raw-font-size, basalt/raw-size-literal, basalt/token-layer-boundary -->
13
+ <!-- not guarded: — -->
14
+ <!-- /basalt:coverage -->
11
15
 
12
- basalt-ui ships one color/type/spacing/radius identity shared by the Mantine chrome and the visx
13
- charts. The **identity is modern zinc** (long form: <https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, which does not ship in the
14
- package) —
15
- **cool-neutral zinc surfaces**
16
- (Tailwind zinc family) on both light and dark, low-contrast panel lift on a slightly darker page,
17
- depth via a whisper shadow + 1px ring (`shadow-card` on panels, `shadow-raised` on interactive
18
- controls, `shadow-overlay` on floating surfaces — see basalt-mantine.md) rather than a hairline
19
- border, carrying
20
- **one saturated sky-blue accent**, split by ROLE: as INK (links, active-nav icon, chart
21
- lines, focus ring) it is `#0077bd` light / `#8ec5ff` dark; as a FILLED SURFACE it is
22
- `#0077bd` in BOTH schemes with a WHITE label (`--vx-accent-fill`, never `--vx-accent`). Neutrals do ~90% of the
23
- surface (60/30/10, pushed toward 90/10); the accent only points — primary CTA, focus, links, small
24
- status pops — never floods. (Blueprint/Basalt zinc-charcoal are the historical hue-tuning
25
- ancestors; DESIGN-SPEC supersedes both — see its "Doctrine inversions" section.) This
26
- rule is the operational checklist; it is enforced mechanically by **`basalt-ui check-theme`**
27
- (wire it into `lint`: `oxlint . && basalt-ui check-theme`; `init` seeds that as `lint:basalt`). A
28
- violation fails the build. Escape hatch: a scoped `theme-allow <rule-id> — <reason>` comment
29
- (diff-visible, deliberate) — see the contract below.
16
+ # Basalt Tokens — color, spacing, radius, type
30
17
 
31
- `check-theme` reads its config from your `package.json` `"basalt"` key
32
- (`{ roots?, exempt?, include?, profile?, severity?, spacingSteps?, forbiddenAccents? }`); set `roots`
33
- to your source dirs and `exempt` to the files that legitimately define palette values.
18
+ ## The identity (stated once, here)
34
19
 
35
- **`roots` is not optional in practice.** Omitted it falls back to `src` relative to the cwd — fine
36
- for a single-app repo, zero files for anything under `apps/*/src`, and `check-theme` then exits 1
37
- with `0 files scanned`. Since 1.20.0 `init` writes a real `roots` from the detected layout and
38
- `doctor`'s `guard-scan` check fails when the configured roots resolve to nothing; before that a repo
39
- could look fully wired while the gate scanned nothing.
20
+ basalt-ui ships ONE identity shared by the Mantine chrome and the visx charts: **cool-neutral zinc
21
+ surfaces on both schemes**, a page slightly darker than its panels, depth from a whisper shadow with
22
+ a 1px ring baked in rather than a hairline border, and **one saturated sky-blue accent** that only
23
+ points. Neutrals carry ~90% of the surface.
40
24
 
41
- The scan reaches slightly beyond `roots`: each root's PARENT contributes its `index.html` and its
42
- `public/` tree (the Vite layout basalt's own preset assumes), because that is where a `theme-color`
43
- meta and a webmanifest's `background_color` actually live. `.json` is never blanket-scanned — name a
44
- design-surface JSON in `basalt.include`, the only route to one.
25
+ The accent is **split by ROLE**, and this is the one thing an author gets wrong:
45
26
 
46
- ## `theme-allow` — the 1.21.0 contract
27
+ - **As ink** — links, active-nav icon, chart lines, focus ring — it is read against the page, so it
28
+ inverts across schemes. `VX.accent`.
29
+ - **As a filled surface** — a control carrying a white label — it cannot invert: it is squeezed
30
+ between the label's contrast and the page's, which leaves one narrow luminance band. `VX.accentFill`,
31
+ labelled with `VX.onAccent`. Every Mantine family's fill sits in that same band, so a filled
32
+ control always reads white on either page. **Never fill with `VX.accent`.**
47
33
 
48
- The full grammar, in both engines:
49
-
50
- ```text
51
- theme-allow → this node/line, EVERY rule (reports theme-allow-unscoped)
52
- theme-allow <id>[, <id>…] [— <why>] → this node/line, those rules (unscoped without a why)
53
- theme-allow-file <id>[, <id>…] — <why> → the WHOLE FILE, those rules; a bare one waives NOTHING
54
- "basalt:theme-allow[-file]": "<id>… — <why>" → the same two, for JSON / .webmanifest
55
- ```
56
-
57
- An id is a guard kind (`raw-surface`, `inline-spacing`, …) or an oxlint plugin rule
58
- (`hand-rolled-plot`, `raw-scroll-container`, …), with or without the `basalt/` prefix.
59
-
60
- ```tsx
61
- // theme-allow raw-surface — third-party widget needs a literal corner
62
- <Widget style={{ borderRadius: 3 }} />
63
- ```
64
-
65
- - **An annotation must START its comment** — after `//`, `/*`, `<!--`, a block-comment gutter `*`,
66
- or nothing but whitespace. Before 1.21.0 any comment that merely _mentioned_ the token parsed as
67
- the bare blanket form and switched every rule off on the line below: linewatch documented its own
68
- waivers in a docblock and thereby disarmed the file. Prose about the escape hatch no longer waives
69
- anything; everything a consumer actually writes still qualifies.
70
- - **File scope is spelled, not inferred.** `theme-allow-file <id>… — <why>`, anywhere in the file.
71
- Plain `theme-allow` is the node/line waiver in both engines. At 1.20.0 the two halves of the
72
- contract intersected at exactly one legal shape and that shape was whole-file — naming a rule and
73
- giving a reason was read as a file declaration, which is why per-node scoping did not actually
74
- ship. **This is the consumer break in 1.21.0**: move each existing file declaration one word.
75
- - **A bare `theme-allow` still waives everything on its placement** but reports
76
- `theme-allow-unscoped` (`warn` for one minor, then `error`). A bare `theme-allow-file` waives
77
- nothing at all — whole-file blanket immunity off one unnamed comment is the thing this contract
78
- exists to price.
79
- - **Spell the id right — a typo waives nothing.** A word in the id slot that names no rule is
80
- recorded as unknown and suppresses nothing; the annotation covers exactly the ids it got right.
81
- Because the id slot is read strictly, a prose reason has to be introduced by a separator:
82
- `theme-allow: <why>` or `theme-allow <id> — <why>`, never `theme-allow <why>`.
83
- - **JSON and `.webmanifest` get a member key**, since they have been scanned since 1.20.0 and cannot
84
- hold a comment — their findings were unwaivable and the printed remedy prescribed something
85
- impossible. `"basalt:theme-allow-file": "raw-hex — a PWA manifest theme_color must be a literal"`
86
- carries the same grammar. For a manifest the first remedy is `basaltAppPlugin`, not a waiver: a
87
- hand-copied hex drifts from the palette.
88
- - **Three placements work and both engines agree on all three**: the reported line, a comment-ONLY
89
- line directly above it (the only form JSX can express — the reported line is usually a multi-line
90
- opening tag or a `{expr}` child), and in CSS a trailing annotation reaching back over the
91
- declaration it terminates. The third is what survives the shipped `oxfmt` reflowing a long
92
- `background-color` so the hex lands ABOVE the comment.
93
- - **A comment-only annotation reaches the first CODE line below it**, walking through the rest of
94
- its own comment block. A wrapped reason, or a docblock's `*/`, used to absorb the waiver so the
95
- natural shape silently waived nothing — argo hit that three times in one upgrade. A blank line
96
- still ends the block; that is how you say "this comment is not about the next statement".
97
- - **`basalt/hand-rolled-plot` grants whole-file immunity only through `theme-allow-file`.** Every
98
- assembly node is otherwise waived on its own.
99
- - **Eight shapes are asserted NOT to waive, in both halves.** They are pinned as unsupported rather
100
- than merely left out, so "unsupported" and "silently broken" stop reading the same. Don't write
101
- them:
102
-
103
- | Shape | Why it does not waive |
104
- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
105
- | a blank line between the annotation and the code — after `//`, after `{/* */}`, or after a `{/*` whose closer sits on its own line | a blank line is how you say "this comment is not about the next statement" |
106
- | the token mid-sentence in a line comment, a docblock gutter, or a JSX expression comment | prose that MENTIONS the token is not an annotation — the reason it must START its comment |
107
- | the token inside a string literal | same |
108
- | above a multi-line OPENING tag, when the finding sits on a later attribute line | a waiver reaches the first line below its comment, not an arbitrary line further down — put it beside the attribute |
109
-
110
- The whole grid — four axes, comment style × token position × where the closer falls × what
111
- follows — is pinned row for row in `src/guard/check-source.test.ts` (37 supported + these 8) and
112
- `configs/oxlint-plugin.test.ts` (32 + 8; five of the guard's rows are CSS/HTML/JSON dialects
113
- oxlint never sees). Zero disagreements between the two, and no waiver tally moved in any of the
114
- seven consumer repos.
115
-
116
- ### Audit them: `basalt-ui check-theme --audit-allows`
117
-
118
- Reports instead of scanning. For every annotation and every `basalt.exemptRules` entry it re-runs
119
- the guard with that one occurrence neutralized, so the verdict comes from the scan rather than from
120
- parsing the waiver's text:
121
-
122
- ```text
123
- theme-allow annotations (3):
124
- src/shell/x.tsx:42 suppresses raw-surface@43
125
- src/theme/y.ts:18 SUPPRESSES NOTHING — dead, delete it
126
- src/charts/z.tsx:7 suppresses hand-rolled-plot@8 (oxlint)
127
-
128
- basalt.exemptRules entries (1):
129
- raw-hex: "site.webmanifest" suppresses findings in public/site.webmanifest — <the recorded reason>
130
-
131
- 3 live, 1 dead, 0 unjudgeable, 0 unaccountable (reported as theme-allow-unscoped by a normal run).
132
- Scope: the N file(s) check-theme scans — everything under basalt.roots (…), plus each root's
133
- sibling index.html and public/ tree, plus anything named in basalt.include (none). …
134
- ```
135
-
136
- **Two sections, not one list** — annotations first, then `basalt.exemptRules`, each with its own
137
- count. A plugin-rule annotation is judged too (since 1.22.0, by re-running oxlint over one
138
- neutralized sibling file), so `unjudgeable` now means only "names no rule at all" or "oxlint could
139
- not be run here" — never "not a check-theme kind". The scope line is part of the report: `0 dead`
140
- is not `0 dead anywhere`.
141
-
142
- It closes with a `live / dead / unjudgeable / unaccountable` tally and **exits 1 on a dead
143
- waiver**, so it wires into CI. A waiver that stopped covering anything is not harmless: it silently
144
- covers the next real finding on that line. Basalt's own source had one; it is deleted.
145
-
146
- ### New in 1.20.0 — five guard kinds, all `warn` for one minor
147
-
148
- | Kind | Fires on |
149
- | ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
150
- | `theme-allow-unscoped` | a `theme-allow` with no rule id, no reason, or an id that names no rule |
151
- | `surface-shadow-override` | a `boxShadow` built FROM tokens that REPLACES `--vx-shadow-card` instead of composing with it |
152
- | `css-raw-surface` | the kebab dialect of the surface kinds in CSS (`border-radius: 6px`); sub-scale corners and circle/pill values pass |
153
- | `inline-font-size` | `style={{ fontSize: 11 }}` — the `check-theme` half of `basalt/no-raw-font-size`, for a CI that runs only the guard |
154
- | `hidden-inline-style` | a multi-line opening tag, or a style object hoisted to a const — `raw-html-layout`'s formatting-independent half |
155
-
156
- Two new oxlint rules ship at `warn` alongside them: **`basalt/shadow-basalt-export`** (below) and
157
- **`basalt/hand-rolled-shell`** (`AppShell` parts or `Burger` in a file that does not render
158
- `BasaltShell`). **`basalt/raw-size-literal` is now `error`** — CSS-length strings on
159
- `size`/`fz`/`fontSize`. Every kind keeps its own boolean, `basalt.severity` and `exemptRules`.
160
-
161
- ## The guard sees palette, not vocabulary
162
-
163
- Round 4 swept seven consumer repos: every gate green, and ~15 independently re-rolled copies of
164
- components basalt already ships (`StatCard` in 4 of 4 apps, `EmptyState`, a 266-line hand-rolled
165
- `AppShell`). A fork written by a token-fluent author uses exactly the right tokens, so no palette
166
- guard can ever see it. **`basalt/shadow-basalt-export` is a cheap detector, not a safety net** — the
167
- rule says so in its own message: it is a **tripwire, not coverage**, and silence is not evidence that
168
- nothing is forked.
169
-
170
- - **Exact name match only** — the one limit that is structural. Round 5 confirmed the miss rate:
171
- linewatch's forks are named `Cell` and `Box`, rb's is `Stat`. Rename the fork and the rule goes
172
- quiet, which is what a fork's author naturally does.
173
- - **Scope: basalt consumers only, and component-shaped declarations only.** Since 1.22.0 the rule
174
- gates on `isBasaltScopedFile` like every other rule in the file, and needs a function, an arrow, a
175
- `memo`/`forwardRef` wrapper or a class extending one. A plain data class sharing a name with a
176
- shipped export is a collision, not a fork — it fired on a `SlugTracker` class in a React-free
177
- package that cannot import basalt-ui.
178
- - **Scope: everything importable.** Since 1.21.0 it reads all nine published barrels, not just
179
- `dist/index.d.ts` — so the `basalt-ui/charts` layer, which is where forks actually live, is finally
180
- covered. Names a barrel merely re-exports from a third party are skipped: `./charts` passes
181
- `Bar` / `Line` / `Pie` straight through from `@visx/shape`, and a local `Bar` is not a fork of
182
- anything basalt wrote.
183
-
184
- So the renamed majority goes unreported. **Detection does not substitute for expressiveness**: a
185
- composite that cannot express a common case gets routed around by compliant-looking code, and a
186
- name-collision rule only catches the authors who did not bother to rename. The habit it stands in
187
- for is the actual control: check whether basalt already ships the thing before building it.
188
-
189
- ## Filled surfaces — the fill band
190
-
191
- A filled control is a SURFACE, not ink, and it does not invert across schemes. It is squeezed from
192
- both sides at once: a **white label** needs ≥4.5:1 against the fill, and the control needs ≥3:1
193
- against the page behind it — on BOTH pages. That leaves one narrow luminance band, so:
194
-
195
- - **Every family's fill sits in that band** (`FILL` / `ACCENT` in the palette): the hue varies, the
196
- luminance does not. Every filled surface therefore reads WHITE, on either page, at ~4.9:1.
197
- - **Never fill with the ink accent.** `--vx-accent` is light on dark by design (it is read against
198
- the page) — filling with it puts a white label on a light blue button. Fill with
199
- `--vx-accent-fill` / `--vx-fill-{family}`; label with `--vx-on-accent` / `--vx-on-{family}`.
200
- - **Never decide a foreground yourself**, and do not trust Mantine's `autoContrast`: it resolves the
201
- color once in JS, scheme-blindly, using a brightness heuristic that does not track WCAG contrast.
202
- The theme emits `--vx-on-*` per scheme instead, and every filled control is bound to it.
203
- - Hover is **derived** from the fill in CSS, so retuning a fill carries its hover along.
204
-
205
- Retuning a fill OUTSIDE the band breaks its label. `theme/contrast.test.ts` fails the build on it.
34
+ **The palette is DERIVED, not authored** (`tokens/derive.ts` from one seed + five bounded knobs), and
35
+ a consumer retunes it through `createBasaltTheme(overrides?, { derive, fonts, radius, density })`.
36
+ That is why no hex, no shade index and no pixel constant belongs in a rule, a doc or a component:
37
+ every one of them is an output of a config the consumer may move. Read the values off `VX.*` and the
38
+ theme; read the numbers, when you genuinely need them, off `tokens/palette.ts`.
206
39
 
207
40
  ## Color — never a raw literal
208
41
 
209
- - **No raw `#hex` / `rgb()` / `rgba()` / `hsl()`** in scanned source. Route every color through the
210
- palette: `VX.*` tokens (from `basalt-ui/tokens` or `basalt-ui/charts` — they are CSS vars, so they
211
- work in components AND non-component files), or the Mantine theme (`color`/`c`/`bg` props, `theme.colors`).
212
- Opacity via `alpha(token, a)` — never `rgba()` — so the hue keeps resolving per color scheme.
213
- - **No off-identity Mantine accents.** `color`/`c`/`bg`/`backgroundColor` must not be
214
- `teal`/`violet`/`grape`/`indigo`/`pink`. `createBasaltTheme` reskins _every_ Mantine accent to the
215
- Basalt identity, so those names still render on-palette — but the guard rejects them because they
216
- signal off-identity intent. Allowed accents: **`blue`** (the one earned identity hue — the
217
- saturated sky accent), **`gray`** (neutral), and the status hues **`red`/`green`/`orange`/`yellow`**
218
- (muted/forest, never raw Material/AntD). Categorical/series color goes through series tokens
219
- (`defineSeries` → `seriesTokens`/`groupTokens`), never a Mantine accent prop. Success-button flips
220
- and positive deltas use `color="green"` (forest green), not `teal` (vivid turquoise).
221
- - **"Ink earns its color"** — default to neutral. A hue is justified only for trend, signal/status,
222
- or genuine multi-series separation. A count badge is a signal → it may carry blue; a nav active-state
223
- is UI chrome → it stays neutral (`NavCountBadge` already encodes this). Active nav renders a
224
- **neutral surface fill** + plain text + a weight bump, never the accent — baked into the theme's
225
- NavLink `--nl-*` defaults across every render path (including a router `<Link>` hosted through
226
- `SidebarItem.Anchor`).
227
- - **Accent restraint, mechanically.** The accent lands on exactly the single primary CTA per view
228
- (`variant="filled"`), focus rings, links, and small status pops — nowhere else. Routine/secondary
229
- buttons use `variant="default"` (neutral); don't reach for a colored `variant="light"` (it reads
230
- washed-out on the warm light canvas). Neutrals carry borders, large fills, and routine icons.
42
+ - **No raw `#hex` / `rgb()` / `rgba()` / `hsl()` in scanned source.** Route color through `VX.*`
43
+ (from `basalt-ui/tokens` or `basalt-ui/charts` — they are CSS vars, so they work in components AND
44
+ in non-component files) or through the Mantine theme (`color` / `c` / `bg`, `theme.colors`).
45
+ - **Opacity is `alpha(token, a)`** (`color-mix`), never `rgba()` — so the hue keeps resolving per
46
+ scheme.
47
+ - **No off-identity Mantine accents.** `teal`/`violet`/`grape`/`indigo`/`pink` still render
48
+ on-palette (the theme reskins every family), and are rejected anyway because they signal
49
+ off-identity intent. Allowed: **`blue`** (the one earned hue), **`gray`**, and the status names
50
+ `red`/`green`/`orange`/`yellow`. A positive delta is `color="green"`, never `teal`.
51
+ - **Never pin a shade index** — `c="yellow.7"`, `bg="blue.4"`, `var(--mantine-color-red-6)` each name
52
+ one fixed swatch that is identical in both schemes, so the shade that is legible on dark is the one
53
+ that fails on light. Use `VX.status.*` for a verdict, or the bare hue name and let the theme pick.
54
+ - **"Ink earns its color."** Default to neutral. A hue is justified by trend, signal/status, or
55
+ genuine multi-series separation. The accent lands on the single primary CTA per view, focus rings,
56
+ links and small status pops — never on borders, large fills, routine icons or secondary buttons.
57
+ - **Categorical/series color is consumer data**, declared once through `defineSeries` →
58
+ `seriesTokens`/`groupTokens` and wired into `buildPaletteCss`, never a Mantine accent prop and
59
+ never an inline pair. `groupTokens(GROUP, MAP)` assumes the same map reaches
60
+ `BasaltProvider`'s `paletteOptions.groups` under the key `` `${GROUP}-` `` — **with the trailing
61
+ dash**, which is a CSS-var prefix and not a group name. Derive both from one `GROUP` constant;
62
+ a mismatch emits refs to variables the stylesheet never declares, with no tsc error and no
63
+ runtime throw — just unstyled charts.
231
64
 
232
65
  ## Surfaces — one collapsed token set
233
66
 
234
- The surface token set is **`bg` · `panel` · `elevated` · `subtle` · `border`**:
67
+ `VX.surface` is exactly: `bg` · `panel` · `panelHover` · `elevated` · `subtle` · `overlay` ·
68
+ `field` · `border` · `hairline`. Plus `VX.divider` for layout separators, which is NOT a surface.
235
69
 
236
- - `--vx-surface-bg` (`VX.surface.bg`) — the page background.
237
- - `--vx-surface-panel` (`VX.surface.panel`) — the card/`Paper` background.
238
- - `--vx-surface-elevated` (`VX.surface.elevated`) — a lifted surface (chart areas, tooltips).
239
- - `--vx-surface-subtle` (`VX.surface.subtle`) — a faint hover/striped/track surface: a step
240
- **below white on light** (`#f2f2f1`), **above panel on dark** (`#323239`). Used for Table
241
- hover/striped rows, `Code`, the `SegmentedControl` track, and Tabs/Accordion/Menu hover.
242
- - `--vx-surface-border` (`VX.surface.border`) — the single hairline border token.
243
-
244
- The theme **collapses Mantine's raw ramp steps onto these tokens** (`cssVariablesResolver` plus
245
- theme `styles`): light/dark border ramp steps → `--vx-surface-border`, light hover steps →
246
- `--vx-surface-subtle`, card bg → `--vx-surface-panel`, `--app-shell-border-color` →
247
- `--vx-surface-border`. So **every** Mantine component (AppShell, Table, Input, Divider, Tabs,
248
- Popover, Accordion, cards) renders one border shade, one card background, and one radius
249
- (`--vx-radius-card`). Never inline a surface color or reach for a raw ramp-step var
250
- (`var(--mantine-color-gray-N)`) — the `off-system-surface-var` guard rejects it.
70
+ The theme **collapses Mantine's raw ramp steps onto those tokens** (`cssVariablesResolver` + theme
71
+ `styles`), so every Mantine component — AppShell, Table, Input, Divider, Tabs, Popover, Accordion,
72
+ Card — renders one border shade, one card background and one radius. Never inline a surface color
73
+ and never reach for a raw ramp-step var (`var(--mantine-color-gray-N)`).
251
74
 
252
75
  ## Spacing & radius — prefer the scale token
253
76
 
254
- - `baseTheme` owns the scales: `spacing` 11/13/18/20/26 → `xs…xl`, `radius` 2/4/6/16/32 → `xs…xl`.
255
- Both are DERIVED — `deriveSpacing(level)` / `deriveRadius(level)` in `tokens/palette.ts` move them
256
- with `createBasaltTheme`'s `density` / `radius` knobs, so read the numbers as level-0 values, not
257
- as constants. **Use the token**, not the raw number, when a value equals a step: `p="md"` not
258
- `p={18}`, `gap="sm"` not `gap={13}`, `radius="sm"` not `radius={4}`. The guard flags exact
259
- token-equals (`p={18}`, any numeric `radius`).
260
- - **Card radius has one source: `--vx-radius-card` (7px) = `VX.radiusCard`.** Every
261
- card corner — the Mantine chrome (`Card`/`Paper`, `radius="md"`) and the Mantine-free `ChartCard`
262
- (`var(--vx-radius-card)`) — resolves to this single token; cards must never diverge. Don't inline
263
- a `borderRadius` on a surface (the `raw-surface` guard rejects it).
264
- - **Dense by default.** The framework targets compact (Linear/Notion) surfaces — prefer the tighter
265
- spacing step (`sm`/`xs`) for shell, nav, and card padding rather than `md`/`lg` air (see
266
- basalt-mantine.md for the shipped dense defaults).
267
- - **Sub-scale micro-spacing is legitimate and allowed raw** — `gap={2}`, `pl={4}`, `mt={6}` have no
268
- token equivalent (the scale starts at 11). Use them freely for tight clusters; don't invent
269
- micro-tokens or pepper `theme-allow`. One-off layout dims (`h={36}`, `w={64}`) are also fine raw.
270
- **The same permission holds in CSS modules**, where it has to be enforced differently: there is no
271
- prop form to prefer, so `inline-spacing` simply does not fire on a declaration whose every literal
272
- is ≤ 10px (`gap: 2px`, `padding: 4px 8px`). It reads longhands and logical properties too
273
- (`padding-top`, `margin-inline`, `row-gap`, `padding-inline-start`), not just the shorthands.
274
- `rem` resolves against the 16px root, so `0.25rem` and `4px` get the same answer; units that need
275
- layout context (`em`, `%`, `ch`, `vw`) are not micro-spacing claims and flag. A `var()` component
276
- is dropped before that judgement; one non-micro value makes the whole declaration a finding
277
- (`padding: 4px 16px` flags). An inline style OBJECT in TSX (`style={{ padding: 4 }}`) also still
278
- flags: there the Mantine prop exists and should have been used.
279
- - **Icons** size via the icon's own `size` prop (`size={16}`), not spacing tokens — that's not spacing.
77
+ - `baseTheme` owns both scales and both are DERIVED (`deriveSpacing(level)` / `deriveRadius(level)`
78
+ move them with the `density` / `radius` knobs). **Use the token, not the number**: `p="md"`, not
79
+ the integer that happens to equal `md` today; `radius="sm"`, never a numeric radius.
80
+ - **Card radius has one source**, `VX.radiusCard` / `--vx-radius-card`: the Mantine chrome and the
81
+ Mantine-free `ChartCard` resolve to the same token. Cards must never diverge.
82
+ - **Sub-scale micro-spacing is legitimate and allowed raw** — a tight cluster below the scale's
83
+ first step has no token, so use the raw number rather than inventing a micro-token or peppering
84
+ `theme-allow`. The same permission holds in CSS modules: a declaration whose every literal is
85
+ micro does not fire. One non-micro value makes the whole declaration a finding, and an inline
86
+ style OBJECT in TSX still fires, because there the Mantine prop existed.
87
+ - **Dense by default** — the framework targets compact surfaces; reach for the tighter step.
88
+ - **Icons size through the icon's own `size` prop**, which is not spacing.
280
89
 
281
90
  ## Type
282
91
 
283
- - A shipped **three-font system**: `Nunito Sans Variable` (body), `Hubot Sans Variable` (headings,
284
- brand, card titles — always `font-stretch: 88%`), `JetBrains Mono Variable` (all numerals,
285
- micro-labels, kbd/badges, axis ticks). Fonts ship as exact-pinned `@fontsource-variable/*` deps,
286
- `@import`ed in `styles.css`; the `--basalt-font-{sans,head,mono}` vars stay the override seam
287
- (system-font fallback chains preserved). Type is carried by Mantine's `fontSizes` + `headings` +
288
- the named `fontWeights` ladder (`fw="semibold"`, `fw="medium"`, …) and `fontFamilyMonospace` for
289
- numbers. Don't hard-code `fontSize` in px on chrome; use `size`/`fz` tokens.
290
-
291
- ## `groupTokens` ↔ `paletteOptions.groups` — the prefix lockstep
92
+ Three fonts: sans (body), a condensed head font (headings, brand, card titles), mono (every numeral,
93
+ micro-label, kbd, axis tick). They ship as exact-pinned `@fontsource-variable/*` peers imported by
94
+ `basalt-ui/styles.css`; `createBasaltTheme(_, { fonts })` is the ONE override seam, writing the
95
+ `--basalt-font-*` vars. Size and weight come from the theme's `fontSizes` / `headings` / named
96
+ `fontWeights` ladder (`fw="semibold"`) — never a hardcoded `fontSize`, and never a CSS-length string
97
+ on `size`/`fz`.
292
98
 
293
- `groupTokens(GROUP, MAP)` (from `basalt-ui/tokens` or `basalt-ui/charts`) generates `var(--vx-GROUP-key)`
294
- refs assuming the same map is also handed to `BasaltProvider`'s `paletteOptions.groups` under the key
295
- `` `${GROUP}-` `` (**with the trailing dash** — `paletteOptions.groups` is keyed by CSS-var _prefix_,
296
- not by group name). Drift between the two — e.g. calling `groupTokens('activity', MAP)` but wiring
297
- `paletteOptions={{ groups: { activity: MAP } }}` (missing the dash) — emits token refs that point at
298
- CSS variables the palette stylesheet never declares: no tsc error, silent unstyled/transparent charts
299
- at runtime. Derive both from one `GROUP` constant so they can't drift:
99
+ ## `theme-allow` — the escape hatch and its grammar (stated once, here)
300
100
 
301
- ```ts
302
- // theme/series.ts
303
- export const GROUP = 'activity'
304
- export const colors = groupTokens(GROUP, SERIES) // -> var(--vx-activity-key)
305
- export const paletteGroups = { [`${GROUP}-`]: SERIES } // -> { 'activity-': SERIES }
101
+ ```text
102
+ theme-allow → this node/line, EVERY rule (reports theme-allow-unscoped)
103
+ theme-allow <id>[, <id>…] [— <why>] → this node/line, those rules (unscoped without a why)
104
+ theme-allow-file <id>[, <id>…] — <why> → the WHOLE FILE, those rules; a bare one waives NOTHING
105
+ "basalt:theme-allow[-file]": "<id>… — <why>" → the same two, for JSON / .webmanifest
306
106
  ```
307
107
 
108
+ An id is a guard kind (`raw-surface`, `inline-spacing`, …) or an oxlint plugin rule
109
+ (`hand-rolled-plot`, `hand-rolled-filter`, …), with or without the `basalt/` prefix.
110
+
308
111
  ```tsx
309
- // main.tsx
310
- <BasaltProvider paletteOptions={{ groups: paletteGroups }}>{/* app */}</BasaltProvider>
112
+ // theme-allow raw-surface — third-party widget needs a literal corner
113
+ <Widget style={{ borderRadius: 3 }} />
311
114
  ```
312
115
 
313
- ## When the guard fires
314
-
315
- Fix the source, don't silence it — reach for the right token first. Only add `theme-allow` for a
316
- genuine, documented exception (a third-party widget needing a literal), scoped to the rule id, with
317
- a reason: a bare `// theme-allow` passes the check, tells the next reader nothing, and now reports
318
- `theme-allow-unscoped` besides. The palette-definition files are listed in your `exempt` set so they
319
- don't self-trip. A stylesheet emitted by `basalt-ui tokens:css` needs no entry — in a `.css` file
320
- carrying that exact two-line `@generated basalt-ui` header, the guard skips the LINES that are
321
- basalt custom properties, selectors, `}` or self-closing comments. Pasting the marker anywhere else
322
- suppresses nothing, and an ordinary declaration added to such a file is still reported.
116
+ - **An annotation must START its comment** — after `//`, `/*`, `<!--`, a block gutter `*`, or
117
+ nothing but whitespace. Prose that merely MENTIONS the token waives nothing; a file documenting
118
+ its own waivers used to disarm itself.
119
+ - **File scope is spelled, never inferred**: `theme-allow-file <id>… — <why>`, anywhere in the file.
120
+ A bare `theme-allow-file` waives nothing at all.
121
+ - **A typo waives nothing.** A word in the id slot naming no rule is recorded as unknown; the
122
+ annotation still covers the ids it got right. Because the id slot is read strictly, a bare reason
123
+ needs a separator — `theme-allow: <why>` or `theme-allow <id> — <why>`, never `theme-allow <why>`.
124
+ - **Three placements work, and both engines agree on all three**: the reported line, a comment-ONLY
125
+ line directly above it (reaching the first CODE line below, through the rest of its comment
126
+ block), and in CSS a trailing annotation reaching back over the declaration it terminates. A blank
127
+ line ends the reach — that is how you say "this comment is not about the next statement".
128
+ - **JSON and `.webmanifest` take a member key**, since they cannot hold a comment. For a manifest
129
+ the first remedy is `basaltAppPlugin`, not a waiver: a hand-copied hex drifts from the palette.
130
+ - **The three import-boundary rules honour no escape at all** (`visx-boundary`, `visx-tooltip`,
131
+ `token-layer-boundary`) — an architecture boundary a stray comment can switch off is the bypass
132
+ they exist to prevent. The agent-chat guards honour `basalt-agent-allow`, a separate token that
133
+ never waives a `theme-allow` finding, or the reverse.
134
+
135
+ Unsupported shapes are asserted as unsupported in both engines' test grids (`src/guard/
136
+ check-source.test.ts`, `configs/oxlint-plugin.test.ts`), so "unsupported" and "silently broken"
137
+ cannot read the same. **Audit them**: `basalt-ui check-theme --audit-allows` re-runs the scan with each waiver
138
+ neutralized, one at a time, and **exits 1 on a waiver that suppresses nothing** — a dead waiver
139
+ silently covers the next real finding on that line. It judges plugin-rule waivers too, by re-running
140
+ oxlint; where oxlint is unreachable the verdict is "cannot judge", never "dead". Its scope line is
141
+ part of the report: `0 dead` is not `0 dead anywhere`.
142
+
143
+ ## Wiring the guard
144
+
145
+ `check-theme` reads `package.json`'s `"basalt"` key (`roots`, `exempt`, `include`, `profile`,
146
+ `severity`, `exemptRules`, …). **`roots` is not optional in practice** — omitted, it falls back to
147
+ `src` relative to cwd, which is zero files for anything under `apps/*/src`; `init` writes a real one
148
+ and `doctor`'s `guard-scan` fails when it resolves to nothing. The scan also reaches each root's
149
+ PARENT for its `index.html` and `public/` tree, because that is where a `theme-color` meta and a
150
+ webmanifest's `background_color` live. Wire it as `oxlint . && basalt-ui check-theme` — what `init`
151
+ seeds as `lint:basalt`.
152
+
153
+ **When it fires, fix the source** — the right token first. `theme-allow` is for a documented
154
+ exception; a palette-definition file belongs in `exempt`, not behind a comment.
155
+
156
+ **The guard sees palette, not vocabulary.** A fork written by a token-fluent author uses exactly the
157
+ right tokens, so no palette guard can see it — `basalt/shadow-basalt-export` catches an exact name
158
+ collision and nothing else, and renaming the fork is what a fork's author does anyway. It is a
159
+ tripwire, not coverage. The habit is the control: **check whether basalt already ships the thing
160
+ before building it.**