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
@@ -1,219 +1,94 @@
1
1
  ---
2
2
  name: basalt-app
3
- description: Scaffold or refresh a basalt-ui app — guided wrapper around 'basalt-ui init' (first-time scaffold) and 'basalt-ui sync' (drift-managed refresh). The whole agentic layer ships in the npm package — init places the skills, rules, CLAUDE block, and DESIGN.md; sync keeps the managed half fresh. Use when setting up basalt-ui in a new/existing app, wiring the provider/shell/vite preset, or when the doctrine feels missing.
4
- when_to_use: User is adding basalt-ui to a new or existing app, asks how to set it up / scaffold it, wants to wire BasaltProvider / BasaltShell / the vite preset, refresh the shipped rules and DESIGN.md, or run a freshness check in CI. Also when /basalt-design reports no consumer DESIGN.md (the app isn't scaffolded yet).
3
+ description: Scaffold or refresh a basalt-ui app — the procedure around `basalt-ui init` (first-time scaffold) and `basalt-ui sync` (drift-managed refresh), plus the runtime wiring neither of them does for you (provider, shell, vite, CSS order). Use when setting up basalt-ui in a new or existing app, after upgrading it, or when the shipped doctrine seems missing.
4
+ when_to_use: User is adding basalt-ui to a new or existing app, asks how to set it up or scaffold it, wants to wire BasaltProvider / BasaltShell / the vite preset, refresh the shipped rules after an upgrade, or wire a freshness check in CI. Also when /basalt-design reports no consumer DESIGN.md.
5
5
  ---
6
6
 
7
- `/basalt-app` scaffolds a basalt-ui consumer and keeps its shipped doctrine fresh. It wraps the two
8
- CLI subcommands — `basalt-ui init` (first run) and `basalt-ui sync` (refresh) — and explains the
9
- wiring they don't do for you (provider, shell, vite). Bun runtime.
7
+ `/basalt-app` gets a consumer scaffolded and keeps its shipped doctrine fresh. Everything — the
8
+ components, the toolchain presets, the rules, the skills, the templates — ships in the one npm
9
+ package; `init` places it and `sync` refreshes it.
10
10
 
11
- ## One package, one command, one version
12
-
13
- Everything ships in the `basalt-ui` npm package — components, toolchain presets, rules, skills,
14
- templates. Becoming a consumer is exactly two steps:
11
+ ## 1. Install and scaffold
15
12
 
16
13
  ```bash
17
14
  bun add basalt-ui
18
- bunx basalt-ui init # `init` is the ONE command that legitimately predates the install
19
- ./node_modules/.bin/basalt-ui --version # every command after it: the LOCAL bin
15
+ bunx basalt-ui init # the ONE command that legitimately predates the install
16
+ ./node_modules/.bin/basalt-ui --version # every command after it: the LOCAL bin
20
17
  ```
21
18
 
22
- **After `init`, never `bunx`.** `bunx` does not re-resolve a package it has cached, so it can run
23
- a months-old CLI against a freshly-pinned install and report a green gate you did not configure —
24
- which is exactly how one consumer filed a P0 against a 1.20.0 cache while believing it was on
25
- 1.22.0. Use `./node_modules/.bin/basalt-ui` (or a `package.json` script, which `bun run` resolves
26
- from the same place). `basalt-ui --version` / `-v` prints one bare line and exits 0; that is the
27
- check, and it costs nothing to run first.
28
-
29
- There is no plugin and no marketplace. `init`/`sync` place every file with one of two modes, and
30
- which mode a file gets answers a single question — **does Claude read it?**
31
-
32
- | Mode | Files | Lifecycle |
33
- | ----------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
34
- | **managed** | `.claude/rules/basalt-*.md`, `.claude/skills/basalt-*/SKILL.md`, the `CLAUDE.md` block | basalt owns it; `sync` refreshes it; a local edit is skipped and reported (`--force` overwrites); the sync diff is the review gate |
35
- | **seed** | `DESIGN.md`, `.oxlintrc.json`, `.oxfmtrc.json`, `lefthook.yml`, `.github/workflows/check.yml`, optional scaffolds | written once, then yours forever; `sync` recreates it only when missing and never reports it |
36
-
37
- Claude Code cannot load rules or skills from `node_modules` — that platform limit is the only
38
- reason anything is copied. Everything a machine reads is a **reference** instead: the seeded
39
- `.oxlintrc.json` and `lefthook.yml` just `extends` the presets inside
40
- `node_modules/basalt-ui/configs/`, so the toolchain auto-updates with the package while you still
41
- own the files.
42
-
43
- ## First-time scaffold: `bunx basalt-ui init`
44
-
45
- `init` writes the repo-side doctrine and toolchain into a consumer (monorepo globs supported):
46
-
47
- - `.claude/rules/basalt-*.md` — the thirteen shipped rules (`basalt-tokens`, `basalt-charts`,
48
- `basalt-mantine`, `basalt-router`, `basalt-query`, `basalt-state`, `basalt-forms`,
49
- `basalt-notifications`, `basalt-commands`, `basalt-data`, `basalt-agent`, `basalt-content`,
50
- `basalt-app`), managed verbatim.
51
- - `.claude/skills/basalt-{app,charts,design}/SKILL.md` — the three skills (`/basalt-app`,
52
- `/basalt-charts`, `/basalt-design`), managed verbatim, same path the rules take.
53
- - A managed `<!-- basalt:begin --> ... <!-- basalt:end -->` block in `CLAUDE.md`: stack facts, the
54
- **DESIGN.md-is-law** pointer, and the **frontend-design restraint override** (in a dashboard, the
55
- bold move is calm — defer to `/basalt-design`, not generic "make it striking" instincts).
56
- - A **thin** `DESIGN.md` seed (deltas only on top of the `basalt-*` rules): identity confirmation,
57
- the series dictionary, app deviations. The CLAUDE block `@`-imports it, so it auto-loads.
58
- - Toolchain seeds: `.oxlintrc.json` and `lefthook.yml` are `extends` stubs pointing into
59
- `node_modules/basalt-ui/configs/`; `.oxfmtrc.json` is a starting copy (oxfmt has no `extends`);
60
- `.github/workflows/check.yml` is a starting workflow. All four are yours after the first write.
61
- - `.basalt/manifest.json` — a sha256 per managed file + the basalt-ui version that wrote it, so
62
- `sync` can three-way diff and `doctor` can spot a stale install.
63
-
64
- It also patches `package.json`: **`basalt.roots`** inferred from the real layout (workspace packages
65
- depending on basalt or Mantine, else every workspace `src/`, else `src`) and a **`lint:basalt`**
66
- script (`oxlint . && basalt-ui check-theme`). Everything derives from `roots` — the guard's scan, the
67
- seeded CI `oxfmt` globs, the default exemption — so a workspace repo without it scaffolds a guard
68
- that scans zero files while every gate reports green. Correct it if your sources live elsewhere. A
69
- Mantine-free consumer adds `"basalt": { "profile": "tokens-only" }` by hand; `check-theme` never
70
- infers that, because inferring it from a missing `@mantine/core` would silence 17 kinds on a repo
71
- that simply keeps Mantine in another workspace package.
72
-
73
- **`init` on an EXISTING app is a lint-debt event, not a no-op.** Adopting the shipped preset turns
74
- on whole oxlint plugins the repo was never linted against, so previously-clean code lands with real
75
- findings on the first run. `init` names the plugins and every file it kept — plus what keeping it
76
- costs (an existing `.oxlintrc.json` that does not `extends` the preset means the entire basalt lint
77
- half is off, and nothing else says so; `--merge-lint` splices it in). Run `oxlint .` and triage
78
- before the next commit; turn a rule off with a written reason rather than disabling a plugin.
79
-
80
- Then `basalt-ui doctor`. `SKIPPED` is a third outcome beside pass/warn/fail and exits non-zero on
81
- its own — a check that could not run is not a check that passed — and the hard checks now include
82
- `basalt-resolves`, `guard-scan` (would `check-theme` cover more than zero files?) and
83
- `oxlint-preset` (does `.oxlintrc.json` really extend the preset? JSONC is parsed, not rejected).
84
- Expect doctor to go red where it was green on an older basalt; that is the point.
85
-
86
- After `init`, do the runtime wiring (the CLI scaffolds files, not your app's composition):
19
+ `init` writes the managed doctrine (`.claude/rules/basalt-*.md`, `.claude/skills/basalt-*/SKILL.md`,
20
+ the `CLAUDE.md` block), the seeds you then own (`DESIGN.md`, `.oxlintrc.json`, `.oxfmtrc.json`,
21
+ `lefthook.yml`, CI, optional scaffolds), and `.basalt/manifest.json` — a sha256 per managed unit so
22
+ `sync` can three-way diff. It also patches `basalt.roots` and a `lint:basalt` script.
23
+
24
+ Two modes, decided by one question — **does Claude read this file?** Managed files are
25
+ framework-owned and meant to be overwritten (the sync diff is the review gate); seeds are written
26
+ once and yours forever. Claude Code cannot load rules or skills out of `node_modules`, which is the
27
+ only reason anything is copied at all; everything a MACHINE reads is an `extends` reference into
28
+ `node_modules/basalt-ui/configs/`, so the toolchain auto-updates with the package.
29
+
30
+ **`init` on an existing app is a lint-debt event, not a no-op** — read
31
+ `.claude/rules/basalt-batteries.md` § "App bootstrapping" for what it turns on and what keeping your
32
+ own `.oxlintrc.json` costs.
33
+
34
+ ## 2. Wire the runtime (the CLI scaffolds files, not your app's composition)
87
35
 
88
36
  ```tsx
89
- // Provider — wraps MantineProvider, injects the --vx-* palette, bridges the Vx tokens
90
- import { BasaltProvider } from 'basalt-ui'
91
- ;<BasaltProvider>{/* app */}</BasaltProvider>
92
-
93
- // Shell — sidebar / mobile-nav / breadcrumbs / page-header; router-agnostic
94
- import { BasaltShell, NavCountBadge, ThemeToggle } from 'basalt-ui'
95
- // <ThemeToggle /> in globalActions — cycles light/dark/system on click, hover/focus reveals
96
- // a direct-select popover. Animated (motion) sun/moon glyph, never a computer/monitor icon.
97
-
98
- // Theme — start from the base, override deltas only
99
- import { createBasaltTheme } from 'basalt-ui'
100
- const theme = createBasaltTheme({
101
- /* app deltas */
102
- })
37
+ // main.tsx — CSS order is load-bearing: every @mantine/*/styles.layer.css, THEN basalt's
38
+ import '@mantine/core/styles.layer.css'
39
+ import 'basalt-ui/styles.css'
40
+ import { BasaltProvider, createBasaltTheme } from 'basalt-ui'
41
+ import { BasaltOverlays } from 'basalt-ui/commands'
42
+ ;<BasaltProvider theme={createBasaltTheme(/* app deltas only */)} defaultColorScheme="dark">
43
+ <BasaltOverlays>{/* data layer, then the router */}</BasaltOverlays>
44
+ </BasaltProvider>
103
45
  ```
104
46
 
105
47
  ```ts
106
- // vite.config.ts — basaltViteConfig (dedupe react + @mantine/*, strictPort, /api proxy,
107
- // BASALT_LOCAL alias) composed with basaltAppPlugin (PWA head, manifest, icon metadata)
108
- import react from '@vitejs/plugin-react'
109
- import { basaltAppPlugin, basaltViteConfig } from 'basalt-ui/vite'
110
- import { defineConfig } from 'vite'
111
-
48
+ // vite.config.ts — the preset is config-only; the plugin half is spread into your own plugins
112
49
  export default defineConfig({
113
50
  ...basaltViteConfig({ port: 5173, apiTarget: 'http://localhost:3000' }),
114
51
  plugins: [react(), ...basaltAppPlugin({ name: 'MyApp', description: '…' })],
115
52
  })
116
53
  ```
117
54
 
118
- `basaltViteConfig` stays config-only (no `plugins`) by contract; `basaltAppPlugin` is the plugin
119
- half — spread it into your own `plugins` array. See `agent/rules/basalt-app.md` for plugin
120
- ordering (e.g. alongside `@content-collections/vite`) and the full head/PWA/manifest surface.
55
+ Then `BasaltShell` with a `useNav(NAV)` spread (basalt-state.md), `PageBar` per page
56
+ (basalt-controls.md), and `<first basalt.root>/lib/series.ts` as the app's one guard-exempt series
57
+ source (`/basalt-charts`). There is no Tailwind.
58
+
59
+ ## 3. Verify the wiring took
60
+
61
+ ```bash
62
+ ./node_modules/.bin/basalt-ui doctor # read it to the LAST line
63
+ bun run lint:basalt # oxlint . && basalt-ui check-theme
64
+ ```
65
+
66
+ `SKIPPED` is a third outcome beside pass/warn/fail and **exits non-zero on its own** — a check that
67
+ could not run is not a check that passed. Expect doctor to go red where an older basalt was green;
68
+ that is the point. Then triage `oxlint .` before the next commit.
69
+
70
+ ## 4. Refresh after every upgrade
121
71
 
122
- ```js
123
- // Tailwind era is over — there is no Tailwind. Styles come from:
124
- import 'basalt-ui/styles.css' // @layer basalt base styles, iOS input safety net, font stack
72
+ ```bash
73
+ ./node_modules/.bin/basalt-ui sync # --check in CI, --force to overwrite local edits
74
+ ```
75
+
76
+ - Unchanged since basalt wrote it → overwritten. Locally edited → diffed and SKIPPED. Missing →
77
+ recreated. **Retired upstream → deleted**, and named in the output; a rule file basalt stopped
78
+ shipping is doctrine your agent would otherwise keep reading.
79
+ - `sync --check` makes no writes and exits non-zero on any of that — wire it as the freshness gate.
80
+ - Run it anywhere in the repo: it resolves the project the way `check-theme` and `doctor` do,
81
+ announces a relocation, refreshes a PARENT install from a sub-package, and still refuses to
82
+ scaffold a second consumer (that stays `init`'s decision).
83
+ - Seeds are never reconciled or reported. They are yours.
84
+
85
+ ## 5. Not a React app?
86
+
87
+ ```bash
88
+ bunx basalt-ui tokens:css --out src/tokens.css # no install to resolve, so bunx is right here
89
+ bunx basalt-ui fonts:css --out src/fonts.css
125
90
  ```
126
91
 
127
- Lint wires as `oxlint . && basalt-ui check-theme` (seeded as the `lint:basalt` script) — the theme
128
- guard is the teeth behind the token doctrine. Before writing any composite, check whether basalt
129
- already ships it: round 4 found ~15 forked copies of shipped components across seven repos, all
130
- passing every gate, because a fork by a token-fluent author uses exactly the right tokens.
131
- `basalt/shadow-basalt-export` reads all nine published barrels since 1.21.0 (the charts layer
132
- included), but it warns only on an **exact** name collision, in a basalt-scoped file, on a
133
- component-shaped declaration — round 5 found the renamed forks (`Cell`, `Box`, `Stat`) sail straight
134
- past it. It is a tripwire, not coverage; nothing catches the
135
- rest. Ask before you build; do not wait to be told.
136
-
137
- The consumer series file (`<first basalt.root>/lib/series.ts` by default — so `src/lib/series.ts` on
138
- a plain app, `apps/web/src/lib/series.ts` on a monorepo; override via `basalt.seriesModulePath`) is
139
- the single guard-exempt palette source — see `/basalt-charts`.
140
-
141
- ## Refresh: `basalt-ui sync`
142
-
143
- `sync` re-applies shipped doctrine after a basalt-ui upgrade, three-way against
144
- `.basalt/manifest.json`:
145
-
146
- - **Unchanged since last write** → overwrite with the new version.
147
- - **Locally edited** → show a diff and **skip** (preserves your edits) unless `--force`.
148
- - **Missing** → recreate.
149
- - `sync --check` makes no changes and exits non-zero on drift — wire it as a CI freshness gate so a
150
- consumer can't silently fall behind the shipped rules.
151
-
152
- **Run it anywhere in the repo.** `sync`, `check-theme` and `doctor` share one resolver:
153
- `BASALT_CWD` → the cwd → declared workspace packages → a two-level descend → an **ascend** to the
154
- nearest ancestor carrying a basalt project, bounded by the repo root. Both directions announce the
155
- relocation in one sentence. From a sub-package `sync` now refreshes the parent install rather than
156
- refusing — but it still **exits 1 rather than scaffolding a second consumer** where the resolved
157
- project has no `.basalt/manifest.json`. Creating an install stays `basalt-ui init`'s decision.
158
-
159
- Seeds (`DESIGN.md`, the toolchain files, the scaffolds) are never reconciled or drift-reported —
160
- they are yours. The managed files (rules, skills, the CLAUDE block) are framework-owned and meant
161
- to be overwritten; the sync diff is where you review a doctrine change before committing it.
162
-
163
- ## Precedence the scaffold establishes
164
-
165
- After scaffolding, the doctrine resolves in this order (highest wins):
166
-
167
- > consumer `DESIGN.md` (deltas) > shipped `basalt-*` rules > skills
168
-
169
- So an app deviation goes in `DESIGN.md`; the universal law and its enforcement live in the
170
- `basalt-*` rules; and the skills (`/basalt-design`, `/basalt-charts`) are the method that obeys
171
- both.
172
-
173
- ## Checklist
174
-
175
- - Ran `bun add basalt-ui && bunx basalt-ui init` — rules, skills, CLAUDE block, DESIGN.md all
176
- present from the one package. Every command after it runs the local bin, not `bunx`.
177
- - `BasaltProvider` wraps the app; `createBasaltTheme` used for theme deltas; `basalt-ui/styles.css`
178
- imported.
179
- - `basaltViteConfig` adopted; `oxlint . && basalt-ui check-theme` is the lint command.
180
- - `basaltAppPlugin` composed into `plugins` (see the vite.config.ts snippet above); either the six
181
- default icon files (`favicon.ico`, `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`,
182
- `web-app-manifest-192x192.png`, `web-app-manifest-512x512.png`) under `public/`, or an `icons`
183
- array naming the ones you actually have. `doctor` reads the option out of your vite config, so an
184
- app that names one SVG is not told to generate five PNGs it does not want.
185
- - `site.webmanifest` is emitted by `basaltAppPlugin` — no manual manifest file needed unless
186
- `manifest: false` was passed.
187
- - A thin `DESIGN.md` (deltas) + the thirteen `basalt-*` rules + the managed CLAUDE block are present.
188
- - `<first basalt.root>/lib/series.ts` exists as the one guard-exempt series source (for any app metric colors).
189
- - `basalt-ui sync --check` wired in CI (recommended) to catch drift on future upgrades.
190
- - `basalt-ui doctor` run after `init` and read to the last line — a `SKIPPED` check exits non-zero
191
- and is not a pass.
192
-
193
- ## Notes
194
-
195
- - `init`, `sync`, and `check-theme` are all **real**. `sync` reconciles via a sha256 three-way diff
196
- (`--check` is a CI drift gate, `--force` overwrites local edits); `check-theme` is the palette
197
- guard; `doctor` checks resolution, version, guard scan, oxlint preset, spacing-scale drift and
198
- cross-package `ai` major parity. All via the local bin. An unrecognized flag exits 1 naming it
199
- (`doctor --json` used to run doctor and exit 0), and an unknown command says so above the usage
200
- block.
201
- - **Not a React app?** `bunx basalt-ui tokens:css --out src/tokens.css` (no install to resolve, so
202
- `bunx` is right here) emits the `--vx-*` layer with
203
- no package in your dependency tree, `--selector-class dark` for the Tailwind `<html class="dark">`
204
- convention, and `fonts:css` emits the shipped `--basalt-font-*` stacks. Both carry the two-line
205
- `@generated basalt-ui` header the guard skips on — that header verbatim, and then only the LINES
206
- that are basalt custom properties, selectors, `}` or self-closing comments, so the marker pasted
207
- into a `.tsx` suppresses nothing and a declaration added to such a file is still reported — and
208
- both take `--check` as a CI drift gate — which compares everything but the header's provenance
209
- line, so a basalt-ui version bump alone never forces a no-op commit. See <https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md> (not in the package).
210
- - The framework ships **no** icon or notification dep — pass icons as `ReactNode` and wire toasts
211
- yourself (e.g. `ThemeLabControls`' `copyIcon` / `onCopy`).
212
- - The framework DOES ship `motion` (motion.dev, formerly framer-motion) as a bundled dependency —
213
- animated chrome (`ThemeToggle` today) comes for free, no consumer dependency needed. Reach for
214
- the shared `MOTION_DURATION` / `MOTION_SPRING` tokens for any new animated interaction rather
215
- than inventing ad hoc durations/easings or a competing animation library.
216
- - `vite-plugin-pwa` (`^1.3.0`) is an OPTIONAL peer, only needed when `basaltAppPlugin`'s
217
- `serviceWorker` option is truthy — omit it entirely for apps that don't want an installable/
218
- offline-capable app. See `agent/rules/basalt-app.md` for the full head/PWA/manifest/icons
219
- surface `basaltAppPlugin` owns.
92
+ Both carry the two-line `@generated basalt-ui` header the guard skips, and both take `--check` as a
93
+ CI drift gate that ignores the provenance version — so a basalt bump alone never forces a no-op
94
+ commit. Declare `"basalt": { "profile": "tokens-only" }` by hand; `check-theme` never infers it.
@@ -1,254 +1,74 @@
1
1
  ---
2
2
  name: basalt-charts
3
- description: Add or extend a chart in a basalt-ui app — pick a kind vs go bespoke (Rule of Three), register a consumer series with defineSeries/groupTokens (since VX.series is app-side, not shipped), compose CartesianChart (or ChartFrame directly for a non-single-plot shape), and respect the @visx-only-in-charts + Mantine-free boundary. Use when adding a new chart, a new metric/series color, or extracting a repeated chart shape into a kind.
4
- when_to_use: User wants to add a chart, restyle an existing one structurally, introduce a new metric/series and its color, extract a recurring chart shape into a reusable kind, or asks where chart series colors live / how to register them. The how-to companion to /basalt-design (which owns the aesthetic law).
3
+ description: Add or extend a chart in a basalt-ui app — the procedure: pick a shipped kind vs extract one vs stay bespoke (Rule of Three), register the consumer series with defineSeries/groupTokens, compose CartesianChart (or ChartFrame for a non-single-plot shape), then verify against the guards. Use when adding a chart, a new metric/series color, or extracting a repeated chart shape.
4
+ when_to_use: User wants to add a chart, restyle one structurally, introduce a new metric/series and its color, extract a recurring chart shape into a reusable kind, or asks where chart series colors live and how to register them. The how-to companion to /basalt-design, which owns the aesthetic law.
5
5
  ---
6
6
 
7
- `/basalt-charts` is the how-to for the chart system. `/basalt-design` owns the aesthetic law
8
- (palette, restraint, the theme-lab loop) and the DESIGN.md precedence — read it for "what color /
9
- how should this look". This skill answers "how do I wire it" with basalt-ui's `./charts` and
10
- `./tokens` surfaces. `.claude/rules/basalt-charts.md` — which you have — is the chart-primitive
11
- contract everything below assumes (the repo's `docs/CHARTS-SPEC.md` is the long form and is not in
12
- the package:
13
- <https://github.com/jkrumm/basalt-ui/blob/master/docs/CHARTS-SPEC.md>) — composing `CartesianChart` (single-plot) or `ChartFrame` directly
14
- (multi-pane/radial/matrix) is mandatory, mechanically enforced (see "Respect the boundary" below).
7
+ `/basalt-charts` is the HOW. `.claude/rules/basalt-charts.md` is the contract everything below
8
+ assumes, and `/basalt-design` owns the aesthetic law — read both before writing chart code. The
9
+ props themselves are in the shipped types and `llms.txt` at the install directory.
15
10
 
16
- ## The decision: kind vs bespoke (Rule of Three)
11
+ ## 1. Pick the path (Rule of Three)
17
12
 
18
- Before writing chart code, decide which of three paths you are on:
13
+ 1. **A shipped kind fits** → use it. `ZonedLine`, `Bars`, `StackedArea`, `MultiLine`, `DualPanel`,
14
+ `Heatmap`, `Donut`, `BandStrip`, `MirroredBars`. Read their props before assuming a gap: dual
15
+ axis, zones, thresholds, refLines, per-point markers, folding, per-series formatting and
16
+ `isPending` are all declarative already.
17
+ 2. **This is the SECOND instance of a shape with no kind** → extract a kind and migrate both call
18
+ sites. Not on the first, not past the third.
19
+ 3. **Genuinely unique** → stay bespoke in the page's own chart file, composing the primitive
20
+ directly. Not in a shared kinds directory.
19
21
 
20
- 1. **Reuse a shipped kind.** basalt-ui ships `ZonedLine`, `Bars`, `StackedArea`, `MultiLine`,
21
- `DualPanel`, `Heatmap`, `Donut`, `BandStrip`, `MirroredBars`. If the shape matches, use it —
22
- props are declarative (`data`, `chartId`, `getX`, `series`/`positiveBars`, one `AxisConfig` per
23
- axis, zones/thresholds/refLines as plain arrays). Done.
24
- - **`ZonedLine`** — a single-series line with zone bands, thresholds, x-zones, refLines.
25
- - **`Bars`** — 1+ stacked positive/negative bar series, optional line overlays, dual-axis config.
26
- - **`StackedArea`** — opaque stacked bands, auto cumulative-top y-domain.
27
- - **`MultiLine`** — N series on a shared y-axis (or two, via `y2`): legend-hover dimming,
28
- per-series synced dots, dashed companion (MA) lines, per-point markers (PR stars / status
29
- dots), zones + refLines, fixed or auto domain (also covers z-score/σ charts via a fixed
30
- symmetric domain + zero refLine). Replaces bespoke multi-line / relative-progression /
31
- training-load / strength-composite / fitness-trends.
32
- - **`DualPanel`** — top line-pane + bottom signed-histogram pane sharing ONE x-scale and ONE
33
- cursor; optional fill-between two top lines, zones, refLines. Replaces bespoke divergence /
34
- momentum.
35
- - **`Heatmap`** — category×category intensity grid (`color-mix` alpha), per-cell tooltip,
36
- optional gradient legend strip. Replaces bespoke time-of-day.
37
- - **`Donut`** — proportional donut, optional `centerContent` overlay.
38
- - **`BandStrip`** — 1-D categorical bands, no y dimension: one rect per slot,
39
- `getBand(d) => { state, fill?, absentFraction?, marker? }`. `series` is the state set and
40
- drives legend, fill and the one derived tooltip row. Replaces a bespoke state/availability
41
- strip.
42
- - **`MirroredBars`** — two BAR panes over one x scale and one baseline, each in its own domain
43
- (`up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`). Not `DualPanel`, whose top
44
- pane is a LINE and whose bottom takes one SIGNED accessor.
45
- 2. **Extract a new kind (second instance of a pattern).** When you are about to build the _second_
46
- chart of a shape that has no shipped kind, extract a kind. Don't extract on the first; don't
47
- wait past the third. A new kind lives in the consumer's chart kinds dir (or, if generic enough
48
- to promote, gets contributed to basalt-ui `src/charts/kinds/`). Migrate both call sites onto it.
49
- A single-plot kind composes `CartesianChart`, drawing only marks; a multi-pane or non-cartesian
50
- kind composes `ChartFrame` directly (see "Compose the primitives" below).
51
- 3. **Stay bespoke (genuinely unique).** A one-off shape (e.g. a dual-panel MACD, or two series on
52
- genuinely different scales that don't fit any kind's config surface) composes `CartesianChart`
53
- or `ChartFrame` directly in the page's chart file — not in `kinds/`.
22
+ **Extracting a kind? Prove it by porting.** Move real call sites onto it and ship the list of what
23
+ it could NOT express. A demo page is written against the API that already exists, so it proves
24
+ nothing.
54
25
 
55
- **Extracting a kind? Prove it by porting.** A kind is proven by moving a real consumer's call
56
- sites onto it and reporting what it could NOT express — not by a demo page, which is written
57
- against the API that already exists. The two band kinds were built that way (linewatch, 1884 → 957
58
- source lines, 11 `hand-rolled-plot` waivers retired, two live `NaN`-geometry bugs found). Ship the
59
- "still cannot express" list with the kind.
26
+ ## 2. Register the series (this is consumer data)
60
27
 
61
- > Anti-pattern: a single `<Chart type="..." config={...} />` god-component that switches by kind.
62
- > That is the Recharts trap. Prefer N small kinds. **If a chart doesn't fit the primitives, add a
63
- > kind — don't loosen the primitives.**
64
-
65
- A good kind's props: `data`, `chartId`, generic `getX` accessor, `height`/`aspectRatio`/`fill`,
66
- zones / thresholds / refLines arrays, one `AxisConfig` object per axis, `series: ChartSeries<T>[]`
67
- as the single source of truth for color/label/legend/tooltip. Bespoke escape hatches
68
- (`tooltip.extraRows`, `tooltip.prependRows`) are fine; they must not grow into a god-object config.
69
-
70
- `ZonedLine` and `MultiLine` also take `xZones?: XZoneSpec[]` — vertical time-window bands, the
71
- counterpart to the horizontal zones above. `{ from?, to?, fill }` bounds are `getX` **domain keys**
72
- (the label string, not a date/timestamp); an omitted bound is the plot edge, a key missing from the
73
- domain skips the band rather than clamping to one.
74
-
75
- ## Register a consumer series (VX.series is app-side)
76
-
77
- The framework ships generic primitives and the framework palette (semantic / status / neutral /
78
- surface) — **but not a domain series tree.** `VX.series` does not exist in basalt-ui; argo's
79
- `SERIES`/`ACTIVITY`/`USAGE_*` maps stayed app-side. Each consumer rebuilds its own series in **one
80
- guard-exempt file** (e.g. `src/lib/series.ts`), using the shipped extensibility helpers from
81
- `basalt-ui/tokens`:
28
+ `VX.series` does not exist in the framework — which metric owns which hue is domain data, declared
29
+ in ONE guard-exempt file per app:
82
30
 
83
31
  ```ts
84
- // src/lib/series.ts — the ONE file exempt from `basalt-ui check-theme` (it IS the palette source)
85
- import { defineSeries, groupTokens, seriesTokens, buildPaletteCss } from 'basalt-ui/tokens'
86
-
87
- // 1. Author the per-theme pairs (hue keeps identity, shifts shade across schemes).
88
- const HEALTH = defineSeries({
89
- hrv: { light: '#634DBF', dark: '#7C6BD6' }, // violet — lighter on dark to avoid glow
90
- restingHr: { light: '#C22762', dark: '#E0639A' },
91
- })
92
- const WALKING = defineSeries({
93
- intensityMin: { light: '#3F7D4F', dark: '#5FA372' }, // forest green — muted status hue
94
- })
95
-
96
- // 2. Turn the maps into namespaced token refs (var(--vx-health-hrv), ...).
97
- // seriesTokens(map, prefix?) for a flat namespace; groupTokens(name, map) namespaces under name.
98
- export const SERIES = {
99
- ...seriesTokens(HEALTH), // → { hrv: 'var(--vx-hrv)', restingHr: 'var(--vx-restingHr)' }
100
- walking: groupTokens('walking', WALKING), // → { intensityMin: 'var(--vx-walking-intensityMin)', ... }
101
- }
32
+ // src/lib/series.ts — the one file exempt from check-theme, because it IS the palette source
33
+ import { buildPaletteCss, defineSeries, groupTokens, seriesTokens } from 'basalt-ui/tokens'
102
34
 
103
- // 3. Feed the same maps into the palette CSS so the vars exist in both schemes.
104
- export const PALETTE_CSS = buildPaletteCss({
105
- groups: { health: HEALTH, walking: WALKING },
106
- derived: ['--vx-hrv-area: 12%'], // optional scheme-independent derived vars
107
- })
35
+ const HEALTH = defineSeries({ hrv: { light: '…', dark: '…' } }) // lighter on dark, deeper on light
36
+ export const SERIES = { ...seriesTokens(HEALTH), walking: groupTokens('walking', WALKING) }
37
+ export const PALETTE_CSS = buildPaletteCss({ groups: { '': HEALTH, 'walking-': WALKING } })
108
38
  ```
109
39
 
110
- - `seriesTokens` / `groupTokens` are **exact-keyed**: a typo or a stale key fails `tsc`, so the
111
- token surface can't silently drift from the palette data.
112
- - `defineSeries` is the typed authoring entry — it returns the map (sugar; pairs feed both the CSS
113
- and the tokens).
114
- - Inject `PALETTE_CSS` once (BasaltProvider injects the framework palette; the consumer appends its
115
- series CSS — or passes `injectPalette={false}` and head-injects both).
116
- - Then charts read `SERIES.hrv`, never a hex. This is what keeps `basalt-ui check-theme` green.
117
- - A series with nothing visible to point at (flat at the domain floor, all-zero) can still carry a
118
- qualifier: pass `note` on the series' `SeriesStyle` (flows to `LegendEntry.note` via
119
- `deriveLegend`) and `ChartLegend` renders it, muted, after the label.
40
+ `seriesTokens`/`groupTokens` are exact-keyed, so a stale or typo'd key fails `tsc`. Feed the SAME
41
+ maps to `buildPaletteCss` (or `BasaltProvider`'s `paletteOptions.groups`, keyed by var PREFIX — the
42
+ trailing dash matters) or the refs point at variables nothing declares.
120
43
 
121
- Theme tuning of these series happens in the theme lab — pass them as `groups` to
122
- `ThemeLabControls` (see `/basalt-design`).
44
+ ## 3. Compose the primitive
123
45
 
124
- ## Compose the primitives (never hand-roll)
125
-
126
- Every chart — kind or bespoke — composes `CartesianChart` for a single plot rect, or `ChartFrame`
127
- directly for a multi-pane / non-cartesian shape — five of them ship: `DualPanel`'s two panes,
128
- `MirroredBars`' two bar panes, `BandStrip`'s missing y dimension, `Heatmap`'s grid, `Donut`'s ring.
129
- Either way the contract is the same: the primitive owns margin, scales, grid,
130
- axes, legend, the shared cursor, and the tooltip; you supply `series` and draw ONLY marks. This is
131
- mandatory, not a convention — `basalt/hand-rolled-plot` fails the build on a bypass (see "Respect
132
- the boundary" below); it is easier to compose it than to work around it.
133
-
134
- **The common case — a single-plot chart:**
46
+ A single-plot chart composes `CartesianChart` and draws only marks in the render prop — margins,
47
+ scales, axes, grid, the shared cursor, crosshair, dots and tooltip are already done:
135
48
 
136
49
  ```tsx
137
- import { CartesianChart, LinePath, curveMonotoneX, VX } from 'basalt-ui/charts'
138
- import type { ChartSeries } from 'basalt-ui/charts'
139
- import { SERIES } from '../lib/series'
140
-
141
- type HrvPoint = { date: string; hrv: number }
142
-
143
- const HRV_SERIES: ChartSeries<HrvPoint>[] = [
144
- { key: 'hrv', label: 'HRV', color: SERIES.hrv, mark: 'line', getValue: (d) => d.hrv },
145
- ]
146
-
147
- function HrvChart({ data }: { data: HrvPoint[] }) {
148
- return (
149
- <CartesianChart
150
- data={data}
151
- chartId="hrv"
152
- getX={(d) => d.date}
153
- series={HRV_SERIES}
154
- y={{ domain: 'auto', format: (v) => `${Math.round(v)} ms` }}
155
- height={240}
156
- >
157
- {({ xScale, yScale, visible }) =>
158
- visible.map((s) => (
159
- <LinePath<HrvPoint>
160
- key={s.key}
161
- data={data}
162
- x={(d) => xScale(d.date) ?? 0}
163
- y={(d) => yScale(s.getValue(d) ?? 0)}
164
- stroke={s.color}
165
- strokeWidth={VX.lineWidth}
166
- curve={curveMonotoneX}
167
- />
168
- ))
169
- }
170
- </CartesianChart>
171
- )
172
- }
50
+ <CartesianChart data={data} chartId="hrv" getX={(d) => d.date} series={HRV} y={{ domain: 'auto' }} height={240}>
51
+ {({ xScale, yScale, visible }) =>
52
+ visible.map((s) => <LinePath key={s.key} data={data} x={…} y={…} stroke={s.color} />)
53
+ }
54
+ </CartesianChart>
173
55
  ```
174
56
 
175
- `CartesianChart` draws the grid, both axes, the shared cursor + crosshair + dots, and the derived
176
- tooltip for you — `HrvChart` above draws nothing but the line: no hover wiring, no manual
177
- `<AxisLeft>`/`<AxisBottom>`, no tooltip assembly.
178
-
179
- **Dual-axis is `y2`, not a flag.** Passing `y2` is what turns on the right axis (margin widens by
180
- measurement); a series opts in with `axis: 'right'` on its `ChartSeries` entry. See
181
- `apps/playground/src/demo/ChartsPage.tsx`'s `SessionsRevenueChart` for a full worked example (two
182
- series on genuinely different scales — session counts vs. `$k` revenue — composed directly from
183
- `CartesianChart` because no shipped kind's config surface covers a dual-axis line pair).
184
-
185
- Never substitute: hand-rolled legend markup, a raw `<AxisLeft>`/`<AxisBottom>`/`<AxisRight>`, a
186
- direct `@visx/tooltip` import, or an `rgba()` fill. Sparklines (`charts/sparklines/`) are the one
187
- exemption from the `CartesianChart`/legend/tooltip composition — but they still use `VX.*` tokens.
188
-
189
- ## The cursor is shared by default
190
-
191
- No provider needed. `useChartCursor` reads a module-level external store, so every chart on the
192
- page — kind or bespoke, `CartesianChart` or `ChartFrame` — shares one cursor out of the box, and
193
- resolution is **domain-aware** (nearest parsed date/number within a chart's own step, not exact
194
- string match), so a chart that folds its calendar into weekly buckets still tracks a hover from an
195
- unfolded daily sibling.
196
-
197
- Wrap a subtree in `ChartCursorScope` to **isolate** it onto a private cursor instead — the inverse
198
- of the old opt-in pattern, for e.g. two independent dashboards rendered side by side over the same
199
- calendar that must NOT sync with each other:
200
-
201
- ```tsx
202
- import { ChartCursorScope } from 'basalt-ui/charts'
203
- ;<ChartCursorScope>
204
- <ChartCard>…</ChartCard>
205
- <ChartCard>…</ChartCard>
206
- </ChartCursorScope>
207
- ```
208
-
209
- ## Respect the boundary (oxlint-enforced)
210
-
211
- - **`@visx/*` may only be imported inside chart files.** The shipped consumer oxlint preset bans
212
- direct `@visx/*` outside `**/charts/**`. Need a raw visx primitive for a bespoke chart? Pull it
213
- from basalt-ui's curated re-export in `basalt-ui/charts`, or keep the chart under a charts dir.
214
- - **basalt-ui's own `./charts` and `./tokens` are Mantine-free** — zero `@mantine/*` imports,
215
- keeping the token layer upstream of Mantine inside the framework itself (`cssVariablesResolver`
216
- reads `--vx-*` tokens to bind Mantine's surfaces to them; a chart importing `@mantine/*` directly
217
- would fork chrome and charts apart), AND letting `basalt-ui/charts`/`basalt-ui/tokens` resolve and
218
- render with no `@mantine/*` installed (real, CI-tested — `scripts/pack-test.sh`'s
219
- "charts/tokens-only (no-Mantine) resolution + render" step). This is a basalt-internal invariant,
220
- not a rule enforced on your own app code — compose the shipped primitives from
221
- `basalt-ui/charts`/`basalt-ui/tokens` and you inherit it for free.
222
- - **No raw color literals** anywhere except the one guard-exempt series file (and a deliberate
223
- `theme-allow` line). `basalt-ui check-theme` is the teeth; run it before committing.
224
- - **No raw visx axes.** `basalt-ui check-theme`'s `raw-visx-axis` guard fails the build on a raw
225
- `<AxisLeft>`/`<AxisBottom>`/`<AxisRight>` inside any `/charts/` file — use the tokenized
226
- `AxisLeftNumeric` / `AxisRightNumeric` / `AxisBottomDate` (escape via `theme-allow`).
227
- - **No hand-rolled plot assembly.** `basalt/hand-rolled-plot` fails the build on a chart-assembly
228
- primitive (`AxisLeftNumeric`, `AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`)
229
- rendered in a file that doesn't compose `CartesianChart`, per NODE. Escape:
230
- `theme-allow hand-rolled-plot — <why>` on the one node, or `theme-allow-file hand-rolled-plot —
231
- <why>` anywhere in the file — how a genuinely non-single-plot shape (`DualPanel`, `MirroredBars`,
232
- `BandStrip`; `Heatmap` and `Donut` render no assembly primitive and need none) declares itself.
233
- `-file` is the 1.21.0 spelling and it is required. The file that defines `CartesianChart` is
234
- exempt definitionally.
235
- - **No hand-authored legends.** `basalt/chart-legend-literal` fails the build on a hand-written
236
- array literal passed to `ChartLegend`'s `items` — derive it from the same `series` the chart draws
237
- (`deriveLegend`, or let `ChartFrame`/`CartesianChart` do it) so it can't name a series the plot no
238
- longer draws.
239
- - Both ship at `warn` in the consumer preset (`configs/oxlint.json`) for one minor and `error`
240
- repo-local (the "grace minor" doctrine in `packages/basalt-ui/CLAUDE.md`) — promoting to `error`
241
- in the next minor.
242
-
243
- ## Checklist for a new chart
244
-
245
- - Picked the right path: reused a kind / extracted on the second instance / stayed bespoke for a
246
- true one-off.
247
- - Composed `CartesianChart` (single plot) or `ChartFrame` directly (multi-pane / non-cartesian) —
248
- never hand-assembled margin/scales/axes/cursor/tooltip.
249
- - Any new metric color is a `{ light, dark }` pair in the consumer series file, exposed via
250
- `seriesTokens`/`groupTokens`, and wired into `PALETTE_CSS` via `buildPaletteCss`.
251
- - Charts read `SERIES.*` / `VX.*` — `basalt-ui check-theme` green, zero raw hex.
252
- - Marks draw `ctx.visible`, never the `series` prop, so a legend toggle actually removes them.
253
- - `@visx/*` stays inside chart files; chart code imports no `@mantine/*`.
254
- - Tuned both schemes via the theme lab where the hue needed it (defer to `/basalt-design`).
57
+ - **Draw `visible`, never the `series` prop** — otherwise a legend toggle hides nothing.
58
+ - **Dual axis is `y2`**, not a flag; a series opts in with `axis: 'right'`.
59
+ - **A multi-pane, radial or matrix shape composes `ChartFrame`** + `useChartCursor` + `autoMargin` +
60
+ `ChartTooltipFloat` and declares itself with `theme-allow-file hand-rolled-plot — <why>`.
61
+ - **Size is `height` / `aspectRatio` / `fill`** — one of the three, nothing else. Charts measure
62
+ themselves.
63
+ - Never substitute a raw visx axis, a hand-written legend array, a `@visx/tooltip` import, an
64
+ `rgba()` fill or a hex.
65
+
66
+ ## 4. Verify
67
+
68
+ - `oxlint .` green — no `basalt/hand-rolled-plot`, no `basalt/chart-legend-literal`, no
69
+ `basalt/chart-in-raw-surface`, no `@visx/*` import outside a `charts/` segment.
70
+ - `basalt-ui check-theme` green — no raw hex, no raw axis, every chart entry point has an
71
+ `ariaLabel`.
72
+ - Every new color is a `{ light, dark }` pair in the series file and wired into the palette CSS.
73
+ - Tooltip rows and the legend are DERIVED from `series`; the plot draws `visible`.
74
+ - Checked in both schemes, and with a pending query (`isPending`, not `data ?? []`).