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,7 +1,7 @@
1
1
  /**
2
2
  * Unit tests for checkSource — the pure (text, relPath, cfg) → Finding[] core.
3
3
  *
4
- * Covers all 25 guard kinds. Co-located with the guard, excluded from tsc
4
+ * Covers all 27 guard kinds. Co-located with the guard, excluded from tsc
5
5
  * (tsconfig exclude: src/**\/*.test.ts), run via `bun test`.
6
6
  *
7
7
  * The walker/reporter half is covered by the integration test in
@@ -1916,15 +1916,16 @@ describe('severity', () => {
1916
1916
  expect(f.find((v) => v.kind === 'raw-spacing')?.severity).toBe('error')
1917
1917
  })
1918
1918
 
1919
- // The grace ledger. Five kinds landed warn-only in the round-4 guard minor; deleting an entry
1920
- // from GRACE_PERIOD_KINDS is the promotion, and it shows up here as an explicit diff.
1919
+ // The grace ledger. These five kinds landed warn-only in the round-4 guard minor and sat there
1920
+ // for five minors with nothing tracking the promise (D4) — GRACE_PERIOD_KINDS is now empty and
1921
+ // the C16 version gate in grace.test.ts is what stops a future entry sitting unpromoted again.
1921
1922
  it.each([
1922
1923
  'theme-allow-unscoped',
1923
1924
  'surface-shadow-override',
1924
1925
  'css-raw-surface',
1925
1926
  'inline-font-size',
1926
1927
  'hidden-inline-style',
1927
- ])('%s is still in its grace minor (warn)', (kind) => {
1928
+ ])('%s is promoted past its grace minor (error)', (kind) => {
1928
1929
  const cases: Record<string, [string, string]> = {
1929
1930
  'theme-allow-unscoped': ['const x = 1 // theme-allow', PATH],
1930
1931
  'surface-shadow-override': ['const s = { boxShadow: `0 0 0 2px ${VX.accent}` }', PATH],
@@ -1937,7 +1938,7 @@ describe('severity', () => {
1937
1938
  }
1938
1939
  const [src, path] = cases[kind] as [string, string]
1939
1940
  const hit = find(src, path).find((v) => v.kind === kind)
1940
- expect(hit?.severity).toBe('warn')
1941
+ expect(hit?.severity).toBe('error')
1941
1942
  })
1942
1943
 
1943
1944
  it('ships every OTHER kind past its grace period', () => {
@@ -2139,9 +2140,9 @@ describe('theme-allow scoping', () => {
2139
2140
  expect(f).toHaveLength(0)
2140
2141
  })
2141
2142
 
2142
- it('ships theme-allow-unscoped as a warning, not a build failure', () => {
2143
+ it('ships theme-allow-unscoped as an error (promoted, C16)', () => {
2143
2144
  const f = find(`const x = 1 // theme-allow`)
2144
- expect(f.every((v) => v.severity === 'warn')).toBe(true)
2145
+ expect(f.every((v) => v.severity === 'error')).toBe(true)
2145
2146
  })
2146
2147
 
2147
2148
  // The annotation itself lives in a comment; a `theme-allow` in real code must not waive anything.
@@ -2370,9 +2371,9 @@ describe('surface-shadow-override', () => {
2370
2371
  expect(kinds(f)).not.toContain('surface-shadow-override')
2371
2372
  })
2372
2373
 
2373
- it('ships as a warning for its grace minor', () => {
2374
+ it('is an error now that its grace minor has ended (promoted, C16)', () => {
2374
2375
  const f = find('const s = { boxShadow: `0 0 0 2px ${VX.accent}` }')
2375
- expect(f.find((v) => v.kind === 'surface-shadow-override')?.severity).toBe('warn')
2376
+ expect(f.find((v) => v.kind === 'surface-shadow-override')?.severity).toBe('error')
2376
2377
  })
2377
2378
  })
2378
2379
 
@@ -2435,6 +2436,147 @@ describe('inline-font-size', () => {
2435
2436
  })
2436
2437
  })
2437
2438
 
2439
+ // ── 26. in-body-page-title (the text lane of law C8) ─────────────────────────
2440
+
2441
+ describe('in-body-page-title', () => {
2442
+ it('flags <Title order={1}> in the body', () => {
2443
+ expect(kinds(find(`<Title order={1}>Analytics</Title>`))).toContain('in-body-page-title')
2444
+ })
2445
+
2446
+ it('flags order={2} too', () => {
2447
+ expect(kinds(find(`<Title order={2}>Analytics</Title>`))).toContain('in-body-page-title')
2448
+ })
2449
+
2450
+ // The formatted default — the reason this kind is a bounded TAG scan and not a line regex.
2451
+ it('flags a Title formatted across lines', () => {
2452
+ const f = find(`<Title\n order={1}\n mb="sm"\n>\n Analytics\n</Title>`)
2453
+ expect(kinds(f)).toContain('in-body-page-title')
2454
+ })
2455
+
2456
+ it('does NOT flag order={3} — a section heading', () => {
2457
+ expect(kinds(find(`<Title order={3}>Sessions</Title>`))).not.toContain('in-body-page-title')
2458
+ })
2459
+
2460
+ it('does NOT flag a Title with no order', () => {
2461
+ expect(kinds(find(`<Title>Sessions</Title>`))).not.toContain('in-body-page-title')
2462
+ })
2463
+
2464
+ it('does NOT flag anything under a content/ path segment', () => {
2465
+ const f = find(`<Title order={1}>Doc</Title>`, 'src/content/Guide.tsx')
2466
+ expect(kinds(f)).not.toContain('in-body-page-title')
2467
+ })
2468
+
2469
+ // File-scoped and coarser than the plugin's node-level ancestry, on purpose: a document heading
2470
+ // told to become a breadcrumb is the WRONG advice, so the text lane errs silent.
2471
+ it('does NOT flag a file that renders Prose', () => {
2472
+ const f = find(`<Prose>\n <Title order={1}>Doc</Title>\n</Prose>`)
2473
+ expect(kinds(f)).not.toContain('in-body-page-title')
2474
+ })
2475
+
2476
+ it('honours a theme-allow', () => {
2477
+ const f = find(
2478
+ `// theme-allow in-body-page-title — a shell-less print view\n<Title order={1}>P</Title>`,
2479
+ )
2480
+ expect(kinds(f)).not.toContain('in-body-page-title')
2481
+ })
2482
+
2483
+ // Promoted at 1.27.0 — its GRACE_PERIOD_KINDS entry is gone, and so is the plugin rule of the
2484
+ // same id from PLUGIN_RULE_GRACE. One law, two lanes, one promotion.
2485
+ it('lands error now that the grace entry is gone (C16)', () => {
2486
+ const f = find(`<Title order={1}>Analytics</Title>`)
2487
+ expect(f.find((x) => x.kind === 'in-body-page-title')?.severity).toBe('error')
2488
+ })
2489
+ })
2490
+
2491
+ // ── 27. raw-selection-control (the text lane of law C1) ──────────────────────
2492
+
2493
+ describe('raw-selection-control', () => {
2494
+ it.each(['SegmentedControl', 'Select', 'MultiSelect', 'NativeSelect', 'TagsInput', 'Chip.Group'])(
2495
+ 'flags a raw <%s>',
2496
+ (tag) => {
2497
+ expect(kinds(find(`<${tag} data={[]} />`))).toContain('raw-selection-control')
2498
+ },
2499
+ )
2500
+
2501
+ it('reports the tag it found, not the whole line', () => {
2502
+ const f = find(`<Select data={[]} />`)
2503
+ expect(f.find((x) => x.kind === 'raw-selection-control')?.token).toBe('<Select')
2504
+ })
2505
+
2506
+ it.each(['SettingsRow', 'Modal', 'Drawer', 'Menu.Dropdown'])(
2507
+ 'does NOT flag one inside a %s window',
2508
+ (host) => {
2509
+ const f = find(`<${host}>\n <Select data={[]} />\n</${host}>`)
2510
+ expect(kinds(f)).not.toContain('raw-selection-control')
2511
+ },
2512
+ )
2513
+
2514
+ // The window is 12 lines; past it the approximation would start swallowing the NEXT row's control.
2515
+ it('DOES flag one 20 lines below the host tag — the window is bounded', () => {
2516
+ const f = find(`<Modal>\n${' <Text>x</Text>\n'.repeat(20)} <Select data={[]} />\n</Modal>`)
2517
+ expect(kinds(f)).toContain('raw-selection-control')
2518
+ })
2519
+
2520
+ it('does NOT flag anything in a file importing @mantine/form', () => {
2521
+ const f = find(`import { useForm } from '@mantine/form'\n<Select data={[]} />`)
2522
+ expect(kinds(f)).not.toContain('raw-selection-control')
2523
+ })
2524
+
2525
+ it('does NOT flag the file that DEFINES a basalt control', () => {
2526
+ const f = find(`export function ViewTabs() {\n return <SegmentedControl data={[]} />\n}`)
2527
+ expect(kinds(f)).not.toContain('raw-selection-control')
2528
+ })
2529
+
2530
+ it('does NOT flag a bound basalt control', () => {
2531
+ expect(kinds(find(`<SelectFilter field={f} label="Channel" />`))).not.toContain(
2532
+ 'raw-selection-control',
2533
+ )
2534
+ })
2535
+
2536
+ it('honours a theme-allow', () => {
2537
+ const f = find(
2538
+ `{/* theme-allow raw-selection-control — a one-off admin picker */}\n<Select data={[]} />`,
2539
+ )
2540
+ expect(kinds(f)).not.toContain('raw-selection-control')
2541
+ })
2542
+
2543
+ // The CROSS-FILE exemption — the same basename convention the plugin's `control-outside-home`
2544
+ // applies, so both lanes agree on the same file (one law, two lanes, one exemption). argo carried
2545
+ // 9 of these: a control in a modal/form module whose `<Modal>` is rendered by the parent, which is
2546
+ // outside the 12-line host window because the host tag is not in the file at all.
2547
+ it.each([
2548
+ 'src/edit-session-modal.tsx',
2549
+ 'src/filters-drawer.tsx',
2550
+ 'src/column-popover.tsx',
2551
+ 'src/detail-panel.tsx',
2552
+ 'src/booking-form.tsx',
2553
+ ])('does NOT flag %s — the overlay filename convention', (relPath) => {
2554
+ expect(kinds(find(`<Select data={[]} />`, relPath))).not.toContain('raw-selection-control')
2555
+ })
2556
+
2557
+ it('still flags a page module beside them', () => {
2558
+ expect(kinds(find(`<Select data={[]} />`, 'src/bookings-page.tsx'))).toContain(
2559
+ 'raw-selection-control',
2560
+ )
2561
+ })
2562
+
2563
+ // Basename only, and it needs the leading subject: a `modal/` DIRECTORY holds the page pieces
2564
+ // around the modals too, and a bare `modal.tsx` is a page module in every consumer that has one.
2565
+ it.each(['src/modal/session.tsx', 'src/modal.tsx'])(
2566
+ 'is a subject-prefixed BASENAME convention, not %s',
2567
+ (relPath) => {
2568
+ expect(kinds(find(`<Select data={[]} />`, relPath))).toContain('raw-selection-control')
2569
+ },
2570
+ )
2571
+
2572
+ // Still warn at 1.27.0 — this kind and its AST twin `basalt/control-outside-home` are the C1 pair
2573
+ // that moved to `promote: '1.28.0'` while the other wave-6 entries promoted.
2574
+ it('lands warn while the grace entry stands (C16)', () => {
2575
+ const f = find(`<Select data={[]} />`)
2576
+ expect(f.find((x) => x.kind === 'raw-selection-control')?.severity).toBe('warn')
2577
+ })
2578
+ })
2579
+
2438
2580
  // ── 26. hidden-inline-style ──────────────────────────────────────────────────
2439
2581
 
2440
2582
  describe('hidden-inline-style', () => {
@@ -2475,12 +2617,12 @@ export const C = () => <div style={labelStyle} />`,
2475
2617
  expect(kinds(f)).not.toContain('hidden-inline-style')
2476
2618
  })
2477
2619
 
2478
- it('ships as a warning for its grace minor', () => {
2620
+ it('is an error now that its grace minor has ended (promoted, C16)', () => {
2479
2621
  const f = find(
2480
2622
  `const wrapperStyle = { position: 'relative', width: '100%' }
2481
2623
  export const C = () => <div style={wrapperStyle} />`,
2482
2624
  )
2483
- expect(f.find((v) => v.kind === 'hidden-inline-style')?.severity).toBe('warn')
2625
+ expect(f.find((v) => v.kind === 'hidden-inline-style')?.severity).toBe('error')
2484
2626
  })
2485
2627
  })
2486
2628
 
@@ -2638,7 +2780,7 @@ describe('markup files (index.html / *.webmanifest)', () => {
2638
2780
  * Round 9: `.astro` and `.vue` became scannable and fell through to the `ts` dialect, so `<!-- … -->`
2639
2781
  * was never stripped — a `theme-allow` written in an HTML comment waived nothing there, and a color
2640
2782
  * inside a commented-out block still reported. They resolve as `sfc` now: BOTH comment dialects,
2641
- * and the full 25-kind set (not `MARKUP_KINDS` — an `.astro` template is JSX-shaped and a `.vue`
2783
+ * and the full 27-kind set (not `MARKUP_KINDS` — an `.astro` template is JSX-shaped and a `.vue`
2642
2784
  * `<script setup>` is real TS).
2643
2785
  */
2644
2786
  describe('single-file components (.astro / .vue)', () => {
@@ -2770,11 +2912,15 @@ describe("profile: 'tokens-only'", () => {
2770
2912
  'css-raw-surface': false,
2771
2913
  'inline-font-size': false,
2772
2914
  'hidden-inline-style': true,
2915
+ // The two wave-6 control kinds: `<Title>` is a Mantine component, and the remedy for either is
2916
+ // a Mantine-rendered home (PageBar / WidgetHeader) or a basalt control over @mantine/core.
2917
+ 'in-body-page-title': true,
2918
+ 'raw-selection-control': true,
2773
2919
  }
2774
2920
 
2775
2921
  it('classifies every kind in the registry — the table is exhaustive', () => {
2776
2922
  expect(Object.keys(MANTINE_COUPLED).toSorted()).toEqual(Object.keys(GUARD_RULES).toSorted())
2777
- expect(Object.keys(MANTINE_COUPLED)).toHaveLength(25)
2923
+ expect(Object.keys(MANTINE_COUPLED)).toHaveLength(27)
2778
2924
  })
2779
2925
 
2780
2926
  it('the disabled set is exactly the Mantine-coupled half — a complete partition', () => {
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The C16 version gate (`docs/CONTROLS-SPEC.md` §1): a `GRACE_PERIOD_KINDS` entry fails the build
3
+ * once `package.json`'s version reaches its `promote`, and every entry must declare `since` before
4
+ * `promote`. This is the mechanism that replaces the honor system — deleting an entry used to be
5
+ * the ONLY enforcement of "warn for one minor", and nothing checked that anyone had (D4:
6
+ * `docs/archive/CONTROLS-SYNTHESIS.md`). Five kinds landed together in the round-4 guard minor and sat
7
+ * at `warn` for five minors before this test existed.
8
+ *
9
+ * `assertGraceLedger` is tested directly against synthetic entries below rather than only through
10
+ * the real ledger: every pre-existing entry promoted or moved to `PLUGIN_RULE_ADVISORY`'s guard-side
11
+ * sibling, and an `it.each` over the empty ledger that left behind ran zero assertions, which proved
12
+ * nothing about the gate actually firing. The ledger carries ONE wave-6 control kind at 1.27.0 —
13
+ * `raw-selection-control`, re-dated to `promote: '1.28.0'` with its AST twin
14
+ * `basalt/control-outside-home` (`in-body-page-title` promoted on schedule and its entry is gone) —
15
+ * and the real-ledger check below runs against it.
16
+ *
17
+ * This gate measures the version already PUBLISHED, so it can only go red after the release that
18
+ * shipped a due entry. `scripts/check-grace.ts` is the other end — `scripts/release.sh` runs it
19
+ * against the version the dry run computed, before the release is cut.
20
+ *
21
+ * Excluded from tsc (tsconfig exclude: src/**\/*.test.ts), run via `bun test`.
22
+ */
23
+ import { describe, expect, it } from 'bun:test'
24
+ import { readFileSync } from 'node:fs'
25
+ import { resolve } from 'node:path'
26
+ import { GRACE_PERIOD_KINDS } from './index'
27
+
28
+ const PACKAGE_JSON_PATH = resolve(import.meta.dirname, '..', '..', 'package.json')
29
+ const CURRENT_VERSION = (JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8')) as { version: string })
30
+ .version
31
+
32
+ type GraceEntry = { since: string; promote: string; why: string }
33
+
34
+ /**
35
+ * Tiny local semver compare — no dependency. Assumes plain `x.y.z`, no pre-release identifiers
36
+ * (basalt-ui bans majors and ships no pre-releases). Returns negative when `a` sorts before `b`,
37
+ * positive when after, zero when equal. Compares each component numerically, not lexically, so
38
+ * `'1.9.0' < '1.10.0'` and `'1.3.0' < '1.26.0'` — a string compare would get both backwards.
39
+ */
40
+ export function compareSemver(a: string, b: string): number {
41
+ const partsA = a.split('.').map(Number)
42
+ const partsB = b.split('.').map(Number)
43
+ for (let i = 0; i < 3; i++) {
44
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0)
45
+ if (diff !== 0) return diff > 0 ? 1 : -1
46
+ }
47
+ return 0
48
+ }
49
+
50
+ /**
51
+ * The C16 gate itself, extracted so it can be exercised against a synthetic ledger as well as the
52
+ * real one. Throws naming the first offending entry when either invariant breaks: `since` must
53
+ * precede `promote` for every entry, and no entry's `promote` may be `<=` the given `version`.
54
+ */
55
+ export function assertGraceLedger(ledger: Record<string, GraceEntry>, version: string): void {
56
+ for (const [id, entry] of Object.entries(ledger)) {
57
+ if (compareSemver(entry.since, entry.promote) >= 0) {
58
+ throw new Error(
59
+ `${id}: \`since\` (${entry.since}) must be before \`promote\` (${entry.promote}).`,
60
+ )
61
+ }
62
+ if (compareSemver(version, entry.promote) >= 0) {
63
+ throw new Error(
64
+ `${id}: package.json is at ${version}, which has reached its promote version ` +
65
+ `${entry.promote} — promote to error or extend \`promote\` with a reason.`,
66
+ )
67
+ }
68
+ }
69
+ }
70
+
71
+ describe('compareSemver', () => {
72
+ it('orders multi-digit components numerically, not lexically', () => {
73
+ expect(compareSemver('1.9.0', '1.10.0')).toBeLessThan(0)
74
+ expect(compareSemver('1.10.0', '1.9.0')).toBeGreaterThan(0)
75
+ expect(compareSemver('1.3.0', '1.26.0')).toBeLessThan(0)
76
+ expect(compareSemver('1.26.0', '1.3.0')).toBeGreaterThan(0)
77
+ })
78
+
79
+ it('treats equal versions as equal', () => {
80
+ expect(compareSemver('1.25.0', '1.25.0')).toBe(0)
81
+ })
82
+ })
83
+
84
+ describe('assertGraceLedger', () => {
85
+ it('does not throw on a well-formed entry not yet due', () => {
86
+ const ledger = { 'fake-kind': { since: '1.0.0', promote: '2.0.0', why: 'synthetic' } }
87
+ expect(() => assertGraceLedger(ledger, '1.25.0')).not.toThrow()
88
+ })
89
+
90
+ it('throws once the given version reaches promote', () => {
91
+ const ledger = { 'fake-kind': { since: '1.0.0', promote: '1.25.0', why: 'synthetic' } }
92
+ expect(() => assertGraceLedger(ledger, '1.25.0')).toThrow(/fake-kind/)
93
+ })
94
+
95
+ it('throws once the given version is past promote', () => {
96
+ const ledger = { 'fake-kind': { since: '1.0.0', promote: '1.9.0', why: 'synthetic' } }
97
+ expect(() => assertGraceLedger(ledger, '1.10.0')).toThrow(/fake-kind/)
98
+ })
99
+
100
+ it('throws when since does not precede promote', () => {
101
+ const ledger = { 'fake-kind': { since: '1.5.0', promote: '1.5.0', why: 'synthetic' } }
102
+ expect(() => assertGraceLedger(ledger, '1.0.0')).toThrow(/since/)
103
+ })
104
+ })
105
+
106
+ describe('GRACE_PERIOD_KINDS — version gate (C16)', () => {
107
+ it('the real ledger passes the gate against the real package version', () => {
108
+ expect(() =>
109
+ assertGraceLedger(GRACE_PERIOD_KINDS as Record<string, GraceEntry>, CURRENT_VERSION),
110
+ ).not.toThrow()
111
+ })
112
+ })
113
+
114
+ // ── the release-time half: scripts/check-grace.ts ────────────────────────────
115
+ // Exercised as a PROCESS, because that is how `scripts/release.sh` consumes it — the exit code is
116
+ // the whole interface, and a script that printed the right words with status 0 would gate nothing.
117
+
118
+ const CHECK_GRACE = resolve(import.meta.dirname, '..', '..', 'scripts', 'check-grace.ts')
119
+
120
+ function runCheckGrace(...args: string[]): { code: number; stderr: string; stdout: string } {
121
+ const result = Bun.spawnSync(['bun', CHECK_GRACE, ...args])
122
+ return {
123
+ code: result.exitCode,
124
+ stderr: result.stderr.toString(),
125
+ stdout: result.stdout.toString(),
126
+ }
127
+ }
128
+
129
+ describe('scripts/check-grace.ts', () => {
130
+ it('passes for a version below every promote', () => {
131
+ const { code, stdout } = runCheckGrace('1.0.0')
132
+ expect([code, stdout.includes('clear of all')]).toEqual([0, true])
133
+ })
134
+
135
+ it('refuses a version that has reached a promote, naming the entries', () => {
136
+ const { code, stderr } = runCheckGrace('9.9.9')
137
+ expect(code).toBe(1)
138
+ expect(stderr).toContain('raw-selection-control')
139
+ expect(stderr).toContain('basalt/control-outside-home')
140
+ })
141
+
142
+ // 1.27.0 promoted five of the six wave-6 plugin rules and one of the two guard kinds; what is
143
+ // LEFT is the C1 pair (`raw-selection-control` + `basalt/control-outside-home`), re-dated to
144
+ // 1.28.0 against a measurement rather than a hunch — see either entry's `why`. So the release
145
+ // this file used to refuse now passes, and the refusal moved with the pair.
146
+ it('passes 1.27.0 — the five entries due there promoted', () => {
147
+ expect(runCheckGrace('1.27.0').code).toBe(0)
148
+ })
149
+
150
+ it('refuses the version the remaining C1 pair is due in (1.28.0)', () => {
151
+ expect(runCheckGrace('1.28.0').code).toBe(1)
152
+ })
153
+
154
+ it('exits 2 on a missing or malformed version rather than passing vacuously', () => {
155
+ expect(runCheckGrace().code).toBe(2)
156
+ expect(runCheckGrace('not-a-version').code).toBe(2)
157
+ })
158
+ })