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
@@ -7,14 +7,49 @@ type BarSparklineProps = {
7
7
  width: number
8
8
  height: number
9
9
  color?: string
10
+ /**
11
+ * Gap between two bars, in px. `1` is the shipped look; `2` at ~10 bars reads as a discrete series
12
+ * rather than a histogram, which is the KPI-card treatment.
13
+ *
14
+ * @default 1
15
+ */
16
+ barGap?: number
17
+ /**
18
+ * Bar corner radius, in px. `0` is the shipped look.
19
+ *
20
+ * @default 0
21
+ */
22
+ barRadius?: number
23
+ /**
24
+ * Paints the LAST bar in `emphasisColor` at full opacity — "this is where the series ends", the
25
+ * one reading a trend beside a KPI value is actually for. Off by default: a chart that means
26
+ * something by its final point must say so.
27
+ *
28
+ * @default false
29
+ */
30
+ emphasizeLast?: boolean
31
+ /** The emphasised bar's fill. @default `VX.accent` */
32
+ emphasisColor?: string
10
33
  /** Accessible text alternative, applied as `aria-label` (+ `role="img"`) on the `<svg>`. */
11
34
  ariaLabel?: string
12
35
  }
13
36
 
14
37
  /** Quiet inline bar trend — defaults to `VX.faint`, matching the sparkline family's restrained
15
- * identity (docs/DESIGN-SPEC.md §5). */
16
- export function BarSparkline({ data, width, height, color, ariaLabel }: BarSparklineProps) {
38
+ * identity (docs/DESIGN-SPEC.md §5). Every geometry option below defaults to the shipped look, so
39
+ * an existing call site renders byte-identically. */
40
+ export function BarSparkline({
41
+ data,
42
+ width,
43
+ height,
44
+ color,
45
+ barGap = 1,
46
+ barRadius = 0,
47
+ emphasizeLast = false,
48
+ emphasisColor,
49
+ ariaLabel,
50
+ }: BarSparklineProps) {
17
51
  const fillColor = color ?? VX.faint
52
+ const lastColor = emphasisColor ?? VX.accent
18
53
  const a11yProps = ariaLabel !== undefined ? { role: 'img' as const, 'aria-label': ariaLabel } : {}
19
54
 
20
55
  const yScale = useMemo(() => {
@@ -25,12 +60,14 @@ export function BarSparkline({ data, width, height, color, ariaLabel }: BarSpark
25
60
  if (!data.length) return <svg width={width} height={height} {...a11yProps} />
26
61
 
27
62
  const step = width / data.length
28
- const barWidth = Math.max(step - 1, 1)
63
+ const barWidth = Math.max(step - barGap, 1)
64
+ const lastIndex = data.length - 1
29
65
 
30
66
  return (
31
67
  <svg width={width} height={height} {...a11yProps}>
32
68
  {data.map((v, i) => {
33
69
  const bh = Math.max(yScale(Math.max(v, 0)), 0)
70
+ const isLast = emphasizeLast && i === lastIndex
34
71
  return (
35
72
  <rect
36
73
  key={i}
@@ -38,8 +75,9 @@ export function BarSparkline({ data, width, height, color, ariaLabel }: BarSpark
38
75
  y={height - bh}
39
76
  width={barWidth}
40
77
  height={bh}
41
- fill={fillColor}
42
- fillOpacity={0.75}
78
+ {...(barRadius > 0 && { rx: barRadius })}
79
+ fill={isLast ? lastColor : fillColor}
80
+ fillOpacity={isLast ? 1 : 0.75}
43
81
  />
44
82
  )
45
83
  })}
@@ -0,0 +1,297 @@
1
+ /**
2
+ * `basalt-ui check-coverage` — the 11 assertions (SURFACES consistency + the agent-layer line
3
+ * budgets), and the generated `<!-- basalt:coverage -->` header each rule file carries
4
+ * (docs/CONTROLS-SPEC.md §7).
5
+ *
6
+ * The block exists because a rule file's own claim about what enforces it was prose, and prose
7
+ * drifted: a doc could name a guard kind and stay silent about the oxlint rule doing the real work,
8
+ * or claim coverage for a law nothing checks (D8). Generating it from SURFACES makes the claim a
9
+ * projection of the registry, and `--check` makes a stale claim a build failure.
10
+ *
11
+ * The pure half (`coverageFor` / `coverageBlock` / `applyCoverageBlock`) is tested directly; the fs
12
+ * half runs against a TEMP package root, never the real `agent/rules/**` — a test that wrote into
13
+ * the shipped rules would make `--write` untestable afterwards.
14
+ */
15
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
16
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { resolve } from 'node:path'
19
+
20
+ import { GUARD_RULES } from '../guard/index'
21
+ import { RULE_NAMES, SURFACES } from '../surfaces'
22
+ import {
23
+ COVERAGE_BLOCK_CLOSE,
24
+ COVERAGE_BLOCK_OPEN,
25
+ applyCoverageBlock,
26
+ checkCoverage,
27
+ coverageBlock,
28
+ coverageFor,
29
+ readCoverageBlock,
30
+ reconcileCoverageBlocks,
31
+ } from './index'
32
+
33
+ const FRONTMATTER = `---\nsource: basalt-ui\ndescription: d\npaths:\n - 'src/**'\n---\n`
34
+
35
+ let dir: string
36
+
37
+ beforeEach(() => {
38
+ dir = mkdtempSync(resolve(tmpdir(), 'basalt-check-coverage-'))
39
+ mkdirSync(resolve(dir, 'agent/rules'), { recursive: true })
40
+ })
41
+
42
+ afterEach(() => {
43
+ rmSync(dir, { recursive: true, force: true })
44
+ })
45
+
46
+ /** Writes one rule file into the temp package root and returns its path. */
47
+ function writeRuleFile(rule: string, body: string): string {
48
+ const path = resolve(dir, `agent/rules/basalt-${rule}.md`)
49
+ writeFileSync(path, body)
50
+ return path
51
+ }
52
+
53
+ // ── the 11 assertions ────────────────────────────────────────────────────────
54
+
55
+ describe('checkCoverage', () => {
56
+ it('passes against the live registry', () => {
57
+ expect(checkCoverage()).toBe(0)
58
+ })
59
+
60
+ it('rejects --write and --check together rather than picking one', () => {
61
+ expect(checkCoverage(['--write', '--check'])).toBe(1)
62
+ })
63
+
64
+ it('holds every shipped agent-layer file inside its budget, and the rules inside the total', () => {
65
+ // Assertion 10, read off the real layer rather than a fixture: the budgets ARE the doctrine, so
66
+ // the interesting failure is a shipped rule growing past one, not a synthetic file doing it.
67
+ const pkgRoot = resolve(import.meta.dir, '../..')
68
+ const lines = (rel: string) =>
69
+ readFileSync(resolve(pkgRoot, rel), 'utf8').split('\n').length - 1
70
+ const total = RULE_NAMES.reduce((sum, rule) => sum + lines(`agent/rules/basalt-${rule}.md`), 0)
71
+ expect(total).toBeLessThanOrEqual(1050)
72
+ expect(RULE_NAMES.length).toBe(6)
73
+ for (const skill of ['basalt-app', 'basalt-design', 'basalt-charts']) {
74
+ expect(lines(`agent/skills/${skill}/SKILL.md`)).toBeLessThanOrEqual(100)
75
+ }
76
+ expect(lines('agent/templates/CLAUDE-block.md.tpl')).toBeLessThanOrEqual(40)
77
+ expect(lines('agent/templates/DESIGN.md.tpl')).toBeLessThanOrEqual(45)
78
+ })
79
+ })
80
+
81
+ // ── coverageFor — the projection ─────────────────────────────────────────────
82
+
83
+ describe('coverageFor', () => {
84
+ // `rule` is many-to-one: `data` covers ./data, ./data/table and ./data/virtual, `mantine` covers
85
+ // `.` and ./connectivity. Reading one surface would understate the rule's coverage.
86
+ it('unions every surface carrying the rule', () => {
87
+ const mantine = coverageFor('mantine')
88
+ expect(mantine.pluginRules).toContain('hand-rolled-shell')
89
+ expect(mantine.guardKinds).toContain('raw-form-control')
90
+ })
91
+
92
+ it('names the control rules under the controls rule', () => {
93
+ expect(coverageFor('controls').pluginRules).toEqual([
94
+ 'control-outside-home',
95
+ 'control-size-literal',
96
+ 'hand-rolled-filter',
97
+ 'responsive-twin',
98
+ ])
99
+ })
100
+
101
+ it('carries the advisory laws verbatim, in spec order', () => {
102
+ const { advisoryLaws } = coverageFor('controls')
103
+ expect(advisoryLaws.length).toBe(4)
104
+ expect(advisoryLaws[0]).toContain('C1 as a cross-file law')
105
+ })
106
+
107
+ it('dedupes a kind two surfaces share', () => {
108
+ const { guardKinds } = coverageFor('charts')
109
+ expect(guardKinds.filter((k) => k === 'raw-hex')).toHaveLength(1)
110
+ })
111
+
112
+ it('unions the plugin rules of every surface sharing one rule name', () => {
113
+ // `batteries` is eight surfaces (query, forms, notifications, commands, data ×3, agent ×2,
114
+ // content, #app); only the agent pair carries plugin rules, and the header must name them.
115
+ const { guardKinds, pluginRules } = coverageFor('batteries')
116
+ expect(guardKinds).toEqual([])
117
+ expect(pluginRules).toEqual(['agent-no-raw-usechat', 'agent-resume-guard', 'ai-sdk-major'])
118
+ })
119
+
120
+ it('leaves no rule with nothing on either lane', () => {
121
+ // The 13→6 merge folded the eight `guardKinds: []` rules into one file, so every remaining rule
122
+ // is backed by something — a header saying otherwise would be a rule with no reason to ship.
123
+ for (const rule of RULE_NAMES) {
124
+ const { guardKinds, pluginRules } = coverageFor(rule)
125
+ expect([rule, guardKinds.length + pluginRules.length > 0]).toEqual([rule, true])
126
+ }
127
+ })
128
+ })
129
+
130
+ // ── coverageBlock — what the file actually carries ───────────────────────────
131
+
132
+ describe('coverageBlock', () => {
133
+ it('is HTML comments only, so it renders as nothing', () => {
134
+ for (const rule of RULE_NAMES) {
135
+ for (const line of coverageBlock(rule).split('\n')) {
136
+ expect([rule, line.startsWith('<!--') && line.endsWith('-->')]).toEqual([rule, true])
137
+ }
138
+ }
139
+ })
140
+
141
+ it('opens and closes with the delimiters', () => {
142
+ const block = coverageBlock('controls')
143
+ expect(block.startsWith(COVERAGE_BLOCK_OPEN)).toBe(true)
144
+ expect(block.endsWith(COVERAGE_BLOCK_CLOSE)).toBe(true)
145
+ })
146
+
147
+ it('names both lanes, prefixing the oxlint ids', () => {
148
+ const block = coverageBlock('controls')
149
+ expect(block).toContain('oxlint rules — basalt/control-outside-home')
150
+ expect(block).toContain('guard kinds — raw-selection-control')
151
+ })
152
+
153
+ // The empty lane still prints — `none` is a claim someone can check, an omitted section is not.
154
+ it('prints `none` for a lane with nothing on it', () => {
155
+ expect(coverageBlock('batteries')).toContain('guard kinds — none')
156
+ })
157
+
158
+ // A block that simply omitted the section would read as "nothing to declare" whether or not
159
+ // anyone had looked. `—` is a claim someone can check.
160
+ it('prints `not guarded: —` when a rule declares no advisory law', () => {
161
+ expect(coverageBlock('tokens')).toContain('<!-- not guarded: — -->')
162
+ })
163
+
164
+ it('prints one line per advisory law, so a diff points at the claim', () => {
165
+ const lines = coverageBlock('controls')
166
+ .split('\n')
167
+ .filter((l) => l.includes('not guarded'))
168
+ expect(lines).toHaveLength(4)
169
+ })
170
+
171
+ it('is stable — the same registry emits the same bytes', () => {
172
+ expect(coverageBlock('charts')).toBe(coverageBlock('charts'))
173
+ })
174
+ })
175
+
176
+ // ── applyCoverageBlock / readCoverageBlock ──────────────────────────────────
177
+
178
+ describe('applyCoverageBlock', () => {
179
+ const block = coverageBlock('controls')
180
+
181
+ it('inserts below the YAML frontmatter — the paths: header stays first', () => {
182
+ const out = applyCoverageBlock(`${FRONTMATTER}\n# Title\n`, block)
183
+ expect(out.startsWith(FRONTMATTER)).toBe(true)
184
+ expect(out).toContain(block)
185
+ expect(out.indexOf(block)).toBeLessThan(out.indexOf('# Title'))
186
+ })
187
+
188
+ it('replaces an existing block instead of stacking a second one', () => {
189
+ const stale = `${COVERAGE_BLOCK_OPEN}\n<!-- backed by: nothing -->\n${COVERAGE_BLOCK_CLOSE}`
190
+ const out = applyCoverageBlock(`${FRONTMATTER}\n${stale}\n\n# Title\n`, block)
191
+ expect(out).toContain(block)
192
+ expect(out).not.toContain('backed by: nothing')
193
+ expect(out.split(COVERAGE_BLOCK_OPEN)).toHaveLength(2)
194
+ })
195
+
196
+ it('is idempotent', () => {
197
+ const once = applyCoverageBlock(`${FRONTMATTER}\n# T\n`, block)
198
+ expect(applyCoverageBlock(once, block)).toBe(once)
199
+ })
200
+
201
+ it('prepends when there is no frontmatter', () => {
202
+ expect(applyCoverageBlock('# T\n', block).startsWith(block)).toBe(true)
203
+ })
204
+
205
+ it('readCoverageBlock returns null for a file with no block', () => {
206
+ expect(readCoverageBlock(`${FRONTMATTER}\n# T\n`)).toBeNull()
207
+ })
208
+ })
209
+
210
+ // ── reconcileCoverageBlocks — --write / --check ─────────────────────────────
211
+
212
+ describe('reconcileCoverageBlocks', () => {
213
+ /** Every rule file the temp root needs, so only the block state under test varies. */
214
+ function seedRuleFiles(body: (rule: string) => string): void {
215
+ for (const rule of RULE_NAMES) writeRuleFile(rule, body(rule))
216
+ }
217
+
218
+ it('--write inserts a block into every rule file', () => {
219
+ seedRuleFiles(() => `${FRONTMATTER}\n# T\n`)
220
+ const { failures, notes } = reconcileCoverageBlocks(dir, 'write')
221
+ expect(failures).toEqual([])
222
+ expect(notes).toHaveLength(RULE_NAMES.length)
223
+ const text = readFileSync(resolve(dir, 'agent/rules/basalt-controls.md'), 'utf8')
224
+ expect(text).toContain(coverageBlock('controls'))
225
+ })
226
+
227
+ it('--write is idempotent — a second run reports unchanged and rewrites nothing', () => {
228
+ seedRuleFiles(() => `${FRONTMATTER}\n# T\n`)
229
+ reconcileCoverageBlocks(dir, 'write')
230
+ const before = readFileSync(resolve(dir, 'agent/rules/basalt-controls.md'), 'utf8')
231
+ const { notes } = reconcileCoverageBlocks(dir, 'write')
232
+ expect(notes.every((n) => n.endsWith('unchanged'))).toBe(true)
233
+ expect(readFileSync(resolve(dir, 'agent/rules/basalt-controls.md'), 'utf8')).toBe(before)
234
+ })
235
+
236
+ it('--check passes once the blocks are written', () => {
237
+ seedRuleFiles(() => `${FRONTMATTER}\n# T\n`)
238
+ reconcileCoverageBlocks(dir, 'write')
239
+ expect(reconcileCoverageBlocks(dir, 'check').failures).toEqual([])
240
+ })
241
+
242
+ // The wave-6 requirement: the blocks are inserted by the agent-layer wave, and a gate that failed
243
+ // before they exist would have to land disabled — which is how a gate stays disabled.
244
+ it('--check REPORTS a file with no block yet, and does not fail it', () => {
245
+ seedRuleFiles(() => `${FRONTMATTER}\n# T\n`)
246
+ const { failures, notes } = reconcileCoverageBlocks(dir, 'check')
247
+ expect(failures).toEqual([])
248
+ expect(notes).toHaveLength(RULE_NAMES.length)
249
+ expect(notes[0]).toContain('no <!-- basalt:coverage --> block yet')
250
+ })
251
+
252
+ it('--check FAILS a block that disagrees with SURFACES', () => {
253
+ seedRuleFiles(() => `${FRONTMATTER}\n# T\n`)
254
+ reconcileCoverageBlocks(dir, 'write')
255
+ const path = resolve(dir, 'agent/rules/basalt-controls.md')
256
+ writeFileSync(path, readFileSync(path, 'utf8').replace('basalt/hand-rolled-filter', 'basalt/x'))
257
+ const { failures } = reconcileCoverageBlocks(dir, 'check')
258
+ expect(failures).toHaveLength(1)
259
+ expect(failures[0]).toContain('basalt-controls.md')
260
+ })
261
+
262
+ it('skips a missing rule file — assertion 2 already reports that', () => {
263
+ expect(reconcileCoverageBlocks(dir, 'check')).toEqual({ failures: [], notes: [] })
264
+ })
265
+ })
266
+
267
+ // ── the registry itself ─────────────────────────────────────────────────────
268
+
269
+ describe('SURFACES.pluginRules', () => {
270
+ it('is required on every doctrine surface — [] is legal, absent is not', () => {
271
+ for (const [key, spec] of Object.entries(SURFACES)) {
272
+ if (spec.kind !== 'doctrine') continue
273
+ expect([key, Array.isArray(spec.pluginRules)]).toEqual([key, true])
274
+ }
275
+ })
276
+ })
277
+
278
+ describe('SURFACES.guardKinds — assertion 11', () => {
279
+ // The twin of assertion 9 for the text lane. Two wave-6 kinds shipped on no surface at all, so
280
+ // `basalt-controls.md` printed "guard kinds — none" while the guard was scanning for
281
+ // `raw-selection-control` — a generated header claiming less coverage than exists is D8 in the
282
+ // other direction, and it cannot be hand-corrected.
283
+ it('places every GUARD_RULES kind on at least one doctrine surface', () => {
284
+ const surfaced = new Set<string>()
285
+ for (const spec of Object.values(SURFACES)) {
286
+ if (spec.kind !== 'doctrine') continue
287
+ for (const kind of spec.guardKinds) surfaced.add(kind)
288
+ }
289
+ const orphans = Object.keys(GUARD_RULES).filter((kind) => !surfaced.has(kind))
290
+ expect(orphans).toEqual([])
291
+ })
292
+
293
+ it('names the two wave-6 control kinds on the surfaces whose headers must claim them', () => {
294
+ expect(coverageFor('controls').guardKinds).toContain('raw-selection-control')
295
+ expect(coverageFor('mantine').guardKinds).toContain('in-body-page-title')
296
+ })
297
+ })
@@ -84,7 +84,7 @@ describe('check-theme raw-surface', () => {
84
84
 
85
85
  it('does NOT flag a line carrying a theme-allow comment', () => {
86
86
  fixture(
87
- `export const C = () => <div style={{ borderRadius: 8 }} /> // theme-allow: legacy widget\n`,
87
+ `export const C = () => <div style={{ borderRadius: 8 }} /> // theme-allow raw-surface — legacy widget\n`,
88
88
  )
89
89
  const { code, err } = run()
90
90
  expect(code).toBe(0)
@@ -110,10 +110,11 @@ describe('check-theme raw-surface', () => {
110
110
  })
111
111
 
112
112
  it('does NOT flag a var(--…) boxShadow token (regression)', () => {
113
- fixture(`export const C = () => <div style={{ boxShadow: 'var(--mantine-shadow-sm)' }} />\n`)
113
+ fixture(`export const C = () => <div style={{ boxShadow: 'var(--vx-shadow-card)' }} />\n`)
114
114
  const { code, err } = run()
115
115
  expect(code).toBe(0)
116
116
  expect(err).not.toContain('raw-surface')
117
+ expect(err).not.toContain('surface-shadow-override')
117
118
  })
118
119
 
119
120
  it('does NOT flag a Mantine radius="md" prop (not inline surface styling)', () => {
@@ -216,7 +217,7 @@ describe('check-theme off-system-surface-var', () => {
216
217
 
217
218
  it('does NOT flag a line carrying a theme-allow comment', () => {
218
219
  fixture(
219
- `export const C = () => <Box bg="var(--mantine-color-gray-3)" /> // theme-allow: legacy\n`,
220
+ `export const C = () => <Box bg="var(--mantine-color-gray-3)" /> // theme-allow off-system-surface-var — legacy\n`,
220
221
  )
221
222
  const { code, err } = run()
222
223
  expect(code).toBe(0)
@@ -266,7 +267,9 @@ describe('check-theme raw-html-layout', () => {
266
267
  })
267
268
 
268
269
  it('does NOT flag a line carrying a theme-allow comment', () => {
269
- fixture(`export const C = () => <div style={{ display: 'flex' }} /> // theme-allow\n`)
270
+ fixture(
271
+ `export const C = () => <div style={{ display: 'flex' }} /> // theme-allow raw-html-layout inline-display — legacy\n`,
272
+ )
270
273
  const { code, err } = run()
271
274
  expect(code).toBe(0)
272
275
  expect(err).not.toContain('raw-html-layout')
@@ -377,7 +380,7 @@ describe('check-theme raw-visx-axis', () => {
377
380
  it('does NOT flag a chart-path <AxisLeft> carrying a theme-allow comment', () => {
378
381
  fixtureAt(
379
382
  'charts/kinds/Foo.tsx',
380
- `export const Foo = () => <AxisLeft scale={s} /> // theme-allow: bespoke dual-panel\n`,
383
+ `export const Foo = () => <AxisLeft scale={s} /> // theme-allow raw-visx-axis — bespoke dual-panel\n`,
381
384
  )
382
385
  const { code, err } = run()
383
386
  expect(code).toBe(0)
@@ -490,7 +493,7 @@ describe('check-theme unframed-chart', () => {
490
493
  it('does NOT flag a line carrying a theme-allow comment', () => {
491
494
  fixtureAt(
492
495
  'charts/kinds/Foo.tsx',
493
- `export const Foo = () => <ChartLegend items={[{ key: 'a', label: 'A' }]} /> // theme-allow: bespoke legend\n`,
496
+ `export const Foo = () => <ChartLegend items={[{ key: 'a', label: 'A' }]} /> // theme-allow unframed-chart — bespoke legend\n`,
494
497
  )
495
498
  const { code, err } = run()
496
499
  expect(code).toBe(0)
@@ -538,7 +541,9 @@ describe('check-theme raw-form-control', () => {
538
541
  })
539
542
 
540
543
  it('does NOT flag a line carrying a theme-allow comment', () => {
541
- fixture(`export const C = () => <input type="text" /> // theme-allow: legacy widget\n`)
544
+ fixture(
545
+ `export const C = () => <input type="text" /> // theme-allow raw-form-control — legacy widget\n`,
546
+ )
542
547
  const { code, err } = run()
543
548
  expect(code).toBe(0)
544
549
  expect(err).not.toContain('raw-form-control')
@@ -563,34 +568,46 @@ describe('check-theme sub-16-input-font', () => {
563
568
  expect(err).toContain('sub-16-input-font')
564
569
  })
565
570
 
566
- it('does NOT flag a fontSize below 16 on a <Text> (not a form control)', () => {
571
+ it('does NOT flag sub-16-input-font on a <Text> (not a form control)', () => {
572
+ // The literal itself is still a raw font-size, so `inline-font-size` (an independent kind,
573
+ // no `sub16InputFont` gate) reports it — only `sub-16-input-font`'s form-control scoping is
574
+ // under test here.
567
575
  fixture(`export const C = () => <Text style={{ fontSize: 12 }}>caption</Text>\n`)
568
576
  const { code, err } = run()
569
- expect(code).toBe(0)
577
+ expect(code).toBe(1)
570
578
  expect(err).not.toContain('sub-16-input-font')
579
+ expect(err).toContain('inline-font-size')
571
580
  })
572
581
 
573
- it('does NOT flag a fontSize below 16 on a <span> (not a form control)', () => {
582
+ it('does NOT flag sub-16-input-font on a <span> (not a form control)', () => {
583
+ // Same as the <Text> case above: `inline-font-size` fires independently of the form-control
584
+ // scoping this test verifies for `sub-16-input-font`.
574
585
  fixture(`export const C = () => <span style={{ fontSize: 11 }}>micro</span>\n`)
575
586
  const { code, err } = run()
576
- expect(code).toBe(0)
587
+ expect(code).toBe(1)
577
588
  expect(err).not.toContain('sub-16-input-font')
589
+ expect(err).toContain('inline-font-size')
578
590
  })
579
591
 
580
592
  it('respects the sub16InputFont:false config knob', () => {
593
+ // `inline-font-size` has no config knob and fires on any raw literal regardless — only
594
+ // `sub-16-input-font` itself is under test here.
581
595
  fixture(`export const C = () => <input style={{ fontSize: 12 }} />\n`, {
582
596
  roots: ['src'],
583
597
  rawFormControl: false,
584
598
  sub16InputFont: false,
585
599
  })
586
600
  const { code, err } = run()
587
- expect(code).toBe(0)
601
+ expect(code).toBe(1)
588
602
  expect(err).not.toContain('sub-16-input-font')
603
+ expect(err).toContain('inline-font-size')
589
604
  })
590
605
 
591
606
  it('does NOT flag a line carrying a theme-allow comment', () => {
607
+ // Names both kinds the literal can trip: sub-16-input-font (scoped by rawFormControl above)
608
+ // and inline-font-size (ungated, fires on the literal regardless of element).
592
609
  fixture(
593
- `export const C = () => <input style={{ fontSize: 12 }} /> // theme-allow: legacy widget\n`,
610
+ `export const C = () => <input style={{ fontSize: 12 }} /> // theme-allow sub-16-input-font inline-font-size — legacy widget\n`,
594
611
  { roots: ['src'], rawFormControl: false },
595
612
  )
596
613
  const { code, err } = run()
@@ -646,11 +663,18 @@ describe('check-theme scans .css', () => {
646
663
  })
647
664
 
648
665
  it('does NOT false-positive a JSX-shaped rule on genuine CSS', () => {
649
- fixtureAt('theme/controls.module.css', '.foo {\n display: flex;\n border-radius: 8px;\n}\n')
666
+ // `border-radius: 8px` is a genuine, unrelated `css-raw-surface` violation (a raw literal
667
+ // with no `--vx-radius-*` token) — the point of this test is that the JSX-shaped rules
668
+ // (`inline-display`, `raw-html-layout`) do not also fire on plain CSS syntax.
669
+ fixtureAt(
670
+ 'theme/controls.module.css',
671
+ '.foo {\n display: flex;\n border-radius: var(--vx-radius-card);\n}\n',
672
+ )
650
673
  const { code, err } = run()
651
674
  expect(code).toBe(0)
652
675
  expect(err).not.toContain('inline-display')
653
676
  expect(err).not.toContain('raw-html-layout')
677
+ expect(err).not.toContain('css-raw-surface')
654
678
  })
655
679
  })
656
680