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
  * ./guard — headless policy core. Mantine-free, dependency-free.
3
3
  *
4
- * GUARD_RULES: the closed registry of all 25 violation kinds.
4
+ * GUARD_RULES: the closed registry of all 27 violation kinds.
5
5
  * checkSource: pure (text, relPath, cfg) → Finding[]. No FS, no walk, no console.
6
6
  */
7
7
  import type { Finding, GuardConfig, GuardKind, GuardSeverity } from './types'
@@ -483,6 +483,68 @@ const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g
483
483
  // for a sub-floor fontSize.
484
484
  const RAW_FORM_CONTROL_TAG = /<(?:input|select|textarea)\b(?:=>|[^>])*?>/g
485
485
 
486
+ // ── Control-home patterns (docs/CONTROLS-SPEC.md §6 — the text lane of laws C1/C8) ──────────────
487
+ //
488
+ // Bounded full-text tag scans, the CARD_SURFACE_TAG / CHART_ENTRY_POINT_TAG shape: a `<Title>` and a
489
+ // `<Select>` are both routinely formatted across lines, so a per-line regex would see neither.
490
+
491
+ /** A `<Title …>` opening tag, however it is wrapped. `order` is judged separately, on the tag text. */
492
+ const PAGE_TITLE_TAG = /<Title(?![\w.])(?:=>|[^>])*?>/g
493
+ /** `order={1}` / `order={2}` — the two orders that name a PAGE rather than a section. */
494
+ const PAGE_TITLE_ORDER_PROP = /\border\s*=\s*\{\s*([12])\s*\}/
495
+
496
+ /**
497
+ * A raw Mantine selection control's opening tag — the text-lane twin of the oxlint plugin's
498
+ * RAW_FILTER_TAGS. `Chip.Group` carries a dot, so the name class allows one.
499
+ */
500
+ const RAW_SELECTION_CONTROL_TAG =
501
+ /<(SegmentedControl|Select|MultiSelect|NativeSelect|DatePickerInput|DateInput|TagsInput|Chip\.Group)(?![\w])(?:=>|[^>])*?>/g
502
+
503
+ /**
504
+ * A tag whose presence makes an `order={1|2}` Title a DOCUMENT heading rather than a page title.
505
+ * File-scoped on purpose — see the scan's own comment for why the coarser test is the honest one.
506
+ */
507
+ const PROSE_CONTEXT_TAG = /<(?:Prose|ArticleLayout|Modal|Drawer)(?![\w])/
508
+
509
+ /** An opening tag for one of the declared non-homes — a settings row, an overlay, a composer. */
510
+ const CONTROL_HOST_TAG =
511
+ /<(?:SettingsRow|Modal|Drawer|Popover\.Dropdown|Menu\.Dropdown|Composer)(?![\w])/
512
+
513
+ /**
514
+ * How far ABOVE a control the host-tag window reaches, in lines.
515
+ *
516
+ * The text lane has no ancestry, so "inside a SettingsRow" is approximated by "a SettingsRow opens
517
+ * within the last N lines". 12 covers every row shape in the five consumer repos (label +
518
+ * description + the `control={` line); past that the window would start swallowing the next row's
519
+ * control, which is the direction that makes the kind silent rather than noisy. The plugin rule
520
+ * (`basalt/control-outside-home`) is the one that answers this exactly — this kind exists for the
521
+ * PreToolUse hook lane, which sees one file's TEXT and no AST at all.
522
+ */
523
+ const CONTROL_HOST_WINDOW_LINES = 12
524
+
525
+ /**
526
+ * A file whose BASENAME declares it is an overlay's or a form's own body — the cross-file half of
527
+ * {@link CONTROL_HOST_TAG}, and the same regex the plugin's `OVERLAY_CONVENTION_FILE` applies to
528
+ * `basalt/control-outside-home` (one law, two lanes, one exemption).
529
+ *
530
+ * Law C1's cross-file case is advisory by declaration (`docs/CONTROLS-SPEC.md` §6, "Honest
531
+ * coverage"), and this kind was paying for it: a `<Select>` inside `edit-session-modal.tsx` whose
532
+ * `<Modal>` is rendered by the parent route is outside BOTH the 12-line host window and any
533
+ * ancestry walk, because the host tag is not in the file at all. argo carried 9 of them.
534
+ *
535
+ * The trade is that a whole file goes unscanned on a naming convention — the same bargain
536
+ * {@link MANTINE_FORM_IMPORT} already buys, and a smaller one than promoting the kind with 9 known
537
+ * false positives. Basename only: a `modal/` DIRECTORY holds the page pieces around the modals too.
538
+ */
539
+ const OVERLAY_CONVENTION_FILE = /(?:^|\/)[^/]*-(?:modal|drawer|popover|panel|form)\.[jt]sx$/
540
+
541
+ /** A name whose DECLARATION means this file DEFINES a basalt control rather than consuming one. */
542
+ const CONTROL_OWNER_DEF =
543
+ /\b(?:function|const|class)\s+(?:RangeFilter|CompareFilter|SelectFilter|MultiSelectFilter|NumberFilter|SearchFilter|ToggleFilter|ViewTabs|FilterSet|FilterPill|SyncButton|ActionGroup|OverflowMenu|CtlSlot)(?![\w])/
544
+
545
+ /** `@mantine/form` — a form is C1's third home, and its inputs are not filters. */
546
+ const MANTINE_FORM_IMPORT = /from\s+['"]@mantine\/form['"]/
547
+
486
548
  // A Mantine `styles={{ input: {...} }}` per-part style — the `input` key specifically targets the
487
549
  // rendered <input>/<textarea> part of TextInput/Select/Textarea/etc. Requires `input` be the FIRST
488
550
  // key in the styles object (see the sub-16-input-font doc comment for the scoping trade-off this
@@ -541,20 +603,32 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
541
603
  severity: {},
542
604
  }
543
605
 
606
+ /**
607
+ * A grace-ledger entry: `since` and `promote` are plain `x.y.z` semver strings (C16,
608
+ * `docs/CONTROLS-SPEC.md` §1). `grace.test.ts` fails the build once `package.json`'s version
609
+ * reaches `promote` while the entry is still here — the version-gate that replaces the honor
610
+ * system a bare promotion-note string used to run on. `since` must be strictly before `promote`,
611
+ * also asserted there.
612
+ */
613
+ export type GraceEntry = { since: string; promote: string; why: string }
614
+
544
615
  /**
545
616
  * Kinds still inside their grace minor — reported, not fatal, until the next minor promotes them.
546
617
  * See {@link GuardSeverity} for the doctrine and why basalt-ui in particular needs it.
547
618
  *
548
- * The value is the promotion note, so the entry carries its own expiry rather than relying on
549
- * someone remembering. Adding a kind here is part of shipping it; removing the entry IS the
550
- * promotion, and belongs in its own commit so the changelog says enforcement got stricter.
619
+ * Adding a kind here is part of shipping it; removing the entry IS the promotion, and belongs in
620
+ * its own commit so the changelog says enforcement got stricter. `grace.test.ts` is the other half
621
+ * of "belongs in its own commit": it fails once `package.json`'s version reaches an entry's
622
+ * `promote` while the entry is still here, so a kind can no longer sit at `warn` with nothing
623
+ * tracking it — which is exactly what happened to the five entries this ledger shipped in the
624
+ * round-4 guard minor (D4, `docs/CONTROLS-SPEC.md` §6): all five stayed `warn` for five minors
625
+ * because deleting the entry was the only enforcement, and nothing checked that anyone had.
551
626
  *
552
627
  * The runway is measured in consumer UPGRADES, not in version numbers: `mantine-shade-index`
553
628
  * (the table's first tenant, promoted and gone) sat here across two minors because 1.8.0 shipped
554
629
  * the same day as 1.7.0 and 1.9.0 carried the chart batch the same consumer was upgrading for —
555
630
  * bundling a build-breaking promotion into the minor they take for the fixes turns a routine bump
556
- * into an unplanned refactor, which is exactly what the grace minor exists to prevent. The five
557
- * entries below all landed together in the round-4 guard minor and all promote one minor later.
631
+ * into an unplanned refactor, which is exactly what the grace minor exists to prevent.
558
632
  *
559
633
  * **A `SCANNABLE_EXT` widening cannot be expressed here, and 1.23.2's was deliberately not.**
560
634
  * Adding `.astro`/`.jsx`/`.vue` widens the FILE SET for all 25 kinds at once — there is no kind to
@@ -572,29 +646,25 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
572
646
  * with `theme-allow`, `basalt.severity` and `exemptRules` all still available.
573
647
  *
574
648
  * @example
575
- * const GRACE: Partial<Record<GuardKind, string>> = {
576
- * 'raw-font-family': 'introduced 1.4.0 — promote to error in 1.5.0',
649
+ * const GRACE: Partial<Record<GuardKind, GraceEntry>> = {
650
+ * 'raw-font-family': { since: '1.4.0', promote: '1.5.0', why: 'introduced 1.4.0 — …' },
577
651
  * }
578
652
  */
579
- const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
580
- 'theme-allow-unscoped':
581
- 'introduced in the round-4 guard minor — promote to error one minor later. Every consumer has ' +
582
- 'bare `theme-allow` comments today (basalt itself has ~20), and landing this as an error would ' +
583
- 'fail every one of their builds on an upgrade they took for the fixes.',
584
- 'surface-shadow-override':
585
- 'introduced in the round-4 guard minor — promote to error one minor later. Catches the shape a ' +
586
- 'TOKEN-FLUENT consumer writes, so the code it rejects is code that looks correct and passed ' +
587
- 'every previous release.',
588
- 'css-raw-surface':
589
- 'introduced in the round-4 guard minor — promote to error one minor later. The surface kinds ' +
590
- 'reaching CSS at all is the "existing kind, new file type" case the doctrine names explicitly.',
591
- 'inline-font-size':
592
- 'introduced in the round-4 guard minor — promote to error one minor later. The oxlint plugin ' +
593
- 'has enforced the same thing as `basalt/no-raw-font-size` for a while, so a consumer running ' +
594
- 'both sees nothing new; one running check-theme alone gets a minor of runway.',
595
- 'hidden-inline-style':
596
- 'introduced in the round-4 guard minor — promote by DELETING the kind and folding its two ' +
597
- 'scans into `raw-html-layout`, which is where they belong once the widening has had its minor.',
653
+ export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
654
+ 'raw-selection-control': {
655
+ since: '1.26.0',
656
+ promote: '1.28.0',
657
+ why:
658
+ 'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
659
+ 'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
660
+ 'reading in the guard, and the reason this one lands warn rather than error even though its ' +
661
+ 'law is settled. It moved from 1.27.0 to 1.28.0 with its AST twin, `basalt/control-outside-' +
662
+ 'home`, whose PLUGIN_RULE_GRACE entry carries the measurement: the wave-7 run left 9 warns ' +
663
+ 'in argo, all of them a control in a modal/form module whose `<Modal>` is rendered by the ' +
664
+ 'PARENT — law C1 cross-file, which neither lane can see. `OVERLAY_CONVENTION_FILE` (this ' +
665
+ 'file, and the same regex in the plugin) exempts that declared naming convention in both ' +
666
+ 'lanes; 1.28.0 is when the remainder is re-measured. One law, two lanes, one promotion.',
667
+ },
598
668
  }
599
669
 
600
670
  /** A kind's effective severity: consumer override first, then the grace table, then `error`. */
@@ -762,6 +832,16 @@ export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
762
832
  'hand-rolled-shell',
763
833
  'shadow-basalt-export',
764
834
  'raw-scroll-container',
835
+ 'hand-rolled-filter',
836
+ 'control-outside-home',
837
+ 'control-size-literal',
838
+ 'page-bar-budget',
839
+ 'responsive-twin',
840
+ 'search-literal-link',
841
+ 'use-search-from-literal',
842
+ // `in-body-page-title` is deliberately ABSENT: it is a plugin rule AND a guard kind under one id
843
+ // (one law, two lanes, one annotation), and this set is the ids OUTSIDE `checkSource`'s reach.
844
+ // `check-source.test.ts` asserts the two registries stay disjoint.
765
845
  'visx-boundary',
766
846
  'visx-tooltip',
767
847
  'token-layer-boundary',
@@ -1388,6 +1468,11 @@ export const TOKENS_ONLY_DISABLED_KINDS: ReadonlySet<GuardKind> = new Set([
1388
1468
  'chart-missing-aria-label',
1389
1469
  'raw-form-control',
1390
1470
  'sub-16-input-font',
1471
+ // Both wave-6 control kinds: `<Title>` is a Mantine component and the remedy for either one is a
1472
+ // Mantine-rendered home (PageBar / WidgetHeader) or a basalt control over @mantine/core. Neither
1473
+ // sentence is followable in a consumer that installs the token layer and nothing else.
1474
+ 'in-body-page-title',
1475
+ 'raw-selection-control',
1391
1476
  ])
1392
1477
 
1393
1478
  // A `/` opens a regex literal (not division) when the previous significant token is one of these
@@ -1608,7 +1693,7 @@ type GuardRule = {
1608
1693
  }
1609
1694
 
1610
1695
  /**
1611
- * The closed registry of all 25 guard kinds. The triad test asserts
1696
+ * The closed registry of all 27 guard kinds. The triad test asserts
1612
1697
  * `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
1613
1698
  *
1614
1699
  * raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
@@ -1827,7 +1912,23 @@ export const GUARD_RULES = {
1827
1912
  enabled: (cfg: GuardConfig) => cfg.rawHtmlLayout,
1828
1913
  appliesTo: (relPath) => !isChartFile(relPath) && !relPath.endsWith('.css'),
1829
1914
  message:
1830
- 'Raw HTML element with inline layout/surface styling that the line scan cannot see — the tag is formatted across lines, or the style object is hoisted to a const. Same violation as raw-html-layout: use a Mantine layout primitive (Box/Flex/Grid/Stack/Group). Its own kind for one minor so the widening lands as a warning; it merges into raw-html-layout at the promotion.',
1915
+ 'Raw HTML element with inline layout/surface styling that the line scan cannot see — the tag is formatted across lines, or the style object is hoisted to a const. Same violation as raw-html-layout: use a Mantine layout primitive (Box/Flex/Grid/Stack/Group). Promoted to error at 1.26.0 (C16) with severity only — the kind stayed standalone rather than folding into raw-html-layout as originally planned; the merge is still open, tracked as a follow-up, not done here.',
1916
+ },
1917
+ 'in-body-page-title': {
1918
+ kind: 'in-body-page-title',
1919
+ pattern: PAGE_TITLE_TAG, // handled inline (full-text tag-scoped scan); entry keeps the registry complete
1920
+ // JSX-tag-shaped (`<Title order={1}>`) — never appears in CSS text.
1921
+ appliesTo: (relPath) => !relPath.endsWith('.css'),
1922
+ message:
1923
+ 'In-body page title — a page is named ONCE, by the breadcrumb (staticData.title) or by PageBar.title in a shell-less app, and every section/card/table title is a WidgetHeader (docs/CONTROLS-SPEC.md law C8). An <Title order={1|2}> in the body is a second name for the same page, and it drifts. Prose / ArticleLayout / an overlay and anything under a content/ path are document headings and never report. Same law the oxlint plugin enforces as basalt/in-body-page-title — one id, two lanes, so one theme-allow covers both.',
1924
+ },
1925
+ 'raw-selection-control': {
1926
+ kind: 'raw-selection-control',
1927
+ pattern: RAW_SELECTION_CONTROL_TAG, // handled inline (full-text tag-scoped scan + host window)
1928
+ // JSX-tag-shaped (`<Select …>`) — never appears in CSS text.
1929
+ appliesTo: (relPath) => !relPath.endsWith('.css'),
1930
+ message:
1931
+ 'Raw selection control with no home — a filter or tab belongs in a PageBar / Section / WidgetHeader slot and takes a `field` (a FieldHandle), so it owns the URL write and the localStorage mirror instead of carrying value/onChange (docs/CONTROLS-SPEC.md laws C1–C3). Use the bound control from basalt-ui/controls. A settings row, an overlay and an @mantine/form file are the declared non-homes and never report. This is the TEXT lane of basalt/control-outside-home, which answers the same question against the real AST — where the two disagree, the plugin is right.',
1831
1932
  },
1832
1933
  'inline-font-size': {
1833
1934
  kind: 'inline-font-size',
@@ -2249,6 +2350,61 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
2249
2350
  }
2250
2351
  }
2251
2352
 
2353
+ // in-body-page-title — full-text tag scan (the card-with-border shape), because a `<Title>` with
2354
+ // its props on separate lines is the formatted default. `order` is judged on the tag's own text.
2355
+ //
2356
+ // Two file-level exemptions, both deliberately coarser than the plugin's node-level ones and both
2357
+ // false-NEGATIVE-only: anything under a `content/` path segment, and any file that renders
2358
+ // `<Prose>` / `<ArticleLayout>` / an overlay anywhere in it. A text scan has no ancestry, so
2359
+ // "under Prose" is not answerable per node — and a document heading told to become a breadcrumb is
2360
+ // the wrong advice, not merely noise. `basalt/in-body-page-title` (the AST lane, same id) is the
2361
+ // half that scopes this exactly.
2362
+ if (
2363
+ ruleApplies('in-body-page-title', relPath) &&
2364
+ !relPath.split('/').includes('content') &&
2365
+ !PROSE_CONTEXT_TAG.test(codeText)
2366
+ ) {
2367
+ for (const m of codeText.matchAll(PAGE_TITLE_TAG)) {
2368
+ const order = PAGE_TITLE_ORDER_PROP.exec(m[0])
2369
+ if (order === null) continue
2370
+ const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
2371
+ const endLine = startLine + (m[0].split('\n').length - 1)
2372
+ if (isAllowedInRange(startLine, endLine, 'in-body-page-title')) continue
2373
+ findings.push({
2374
+ relPath,
2375
+ line: startLine,
2376
+ token: `<Title order={${order[1]}}`,
2377
+ kind: 'in-body-page-title',
2378
+ })
2379
+ }
2380
+ }
2381
+
2382
+ // raw-selection-control — full-text tag scan plus the host WINDOW (see CONTROL_HOST_WINDOW_LINES).
2383
+ // Three file-level exemptions mirror the plugin rule's, so the two lanes agree on the same file:
2384
+ // a file that DEFINES a basalt control cannot be told to use one, a file importing
2385
+ // `@mantine/form` is a form — C1's third home, whose inputs are not filters — and a file NAMED as
2386
+ // an overlay body carries its `<Modal>` in the parent, where no scan of this file can reach it.
2387
+ if (
2388
+ ruleApplies('raw-selection-control', relPath) &&
2389
+ !OVERLAY_CONVENTION_FILE.test(relPath) &&
2390
+ !CONTROL_OWNER_DEF.test(codeText) &&
2391
+ !MANTINE_FORM_IMPORT.test(codeText)
2392
+ ) {
2393
+ for (const m of codeText.matchAll(RAW_SELECTION_CONTROL_TAG)) {
2394
+ const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
2395
+ const windowStart = Math.max(0, startLine - 1 - CONTROL_HOST_WINDOW_LINES)
2396
+ if (CONTROL_HOST_TAG.test(codeLines.slice(windowStart, startLine).join('\n'))) continue
2397
+ const endLine = startLine + (m[0].split('\n').length - 1)
2398
+ if (isAllowedInRange(startLine, endLine, 'raw-selection-control')) continue
2399
+ findings.push({
2400
+ relPath,
2401
+ line: startLine,
2402
+ token: `<${m[1] ?? 'Select'}`,
2403
+ kind: 'raw-selection-control',
2404
+ })
2405
+ }
2406
+ }
2407
+
2252
2408
  // sub-16-input-font — two full-text scans (same shape as the tag-scoped scans above): (a) a raw
2253
2409
  // form-control's own inline `style={{ fontSize: N }}`, (b) a Mantine `styles={{ input: { fontSize:
2254
2410
  // N } }}` per-part style. Either shape is now DEAD CODE against the `!important` floor in
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
3
3
  *
4
- * GuardKind is the closed set of 25 violation kinds the theme guard can emit.
4
+ * GuardKind is the closed set of 27 violation kinds the theme guard can emit.
5
5
  * Finding is the structured result per violation, replacing the old `Violation` shape.
6
6
  * GuardConfig is the per-run configuration that drives checkSource.
7
7
  */
8
8
 
9
- /** The 25 theme-guard violation kinds. */
9
+ /** The 27 theme-guard violation kinds. */
10
10
  export type GuardKind =
11
11
  | 'raw-hex'
12
12
  | 'raw-color-fn'
@@ -33,6 +33,8 @@ export type GuardKind =
33
33
  | 'css-raw-surface'
34
34
  | 'inline-font-size'
35
35
  | 'hidden-inline-style'
36
+ | 'in-body-page-title'
37
+ | 'raw-selection-control'
36
38
 
37
39
  /**
38
40
  * How hard a finding lands. `error` fails the build; `warn` reports and passes.
package/src/index.ts CHANGED
@@ -14,6 +14,14 @@ export {
14
14
  } from './provider'
15
15
  export { createBasaltTheme, baseTheme, cssVariablesResolver } from './theme'
16
16
  export type { BasaltFontsConfig, CreateBasaltThemeOptions } from './theme'
17
+ export { CTL_THEME, CtlSlot } from './theme'
18
+ export type { CtlSlotProps } from './theme'
19
+ /**
20
+ * The action vocabulary every home's `actions` slot takes (`docs/CONTROLS-SPEC.md` §2.1). The
21
+ * COMPONENTS that project it (`ActionGroup`, `OverflowMenu`, `SyncButton`, and the filter family)
22
+ * live on `basalt-ui/controls` — only the types a `PageBar`/`BasaltShell` prop mentions are here.
23
+ */
24
+ export type { BarAction, ActionGroupProps, GlobalAction } from './controls/actions'
17
25
  export { ThemeToggle, type ThemeToggleProps } from './theme-toggle'
18
26
  export { MOTION_DURATION, MOTION_SPRING, MOTION_EASE_STANDARD } from './motion'
19
27
  export {
@@ -22,6 +30,7 @@ export {
22
30
  AppSidebar,
23
31
  SidebarSearch,
24
32
  type SidebarSearchConfig,
33
+ type SidebarSearchActions,
25
34
  SidebarAccount,
26
35
  MobileNav,
27
36
  type MobileNavProps,
@@ -31,11 +40,16 @@ export {
31
40
  MOBILE_MENU_MAX_DEFAULT,
32
41
  MOBILE_MORE_KEY,
33
42
  AppBreadcrumbs,
34
- PageHeaderProvider,
35
- PageActions,
36
- PageActionsOutlet,
43
+ PageBar,
44
+ type PageBarProps,
37
45
  type SidebarSection,
38
46
  type SidebarItem,
47
+ type SidebarBlock,
48
+ type SidebarBlockItem,
49
+ type SidebarBlockTone,
50
+ type SidebarListBlock,
51
+ type SidebarProgressBlock,
52
+ type SidebarCustomBlock,
39
53
  type NavAnchor,
40
54
  type NavAnchorProps,
41
55
  type NavMobilePlacement,
@@ -72,19 +86,51 @@ export {
72
86
  type StandardSchemaV1,
73
87
  } from './register'
74
88
 
75
- // ── Persisted state + online hook (state.ts) ─────────────────────────────────────────────────────
89
+ // ── Persisted state + the headless store kernel (state.ts) ───────────────────────────────────────
90
+ // The `field.*` VALUE is deliberately not re-exported here — `basalt-ui/forms` already owns the
91
+ // name `field` on its own barrel. Import it from `basalt-ui/router-tanstack` (beside
92
+ // `createSearchStore`) or from `basalt-ui/state` (beside `createLocalStore`).
76
93
  export {
77
94
  createPersistedState,
78
95
  type PersistedStateOptions,
79
- useOnlineStatus,
80
96
  readPersistedValue,
97
+ createLocalStore,
98
+ type LocalStore,
99
+ type AnyField,
100
+ type BooleanField,
101
+ type EnumField,
102
+ type FieldFallback,
103
+ type FieldHandle,
104
+ type FieldLane,
105
+ type FieldOption,
106
+ type FieldSetOptions,
107
+ type FieldValue,
108
+ type MultiField,
109
+ type NumberField,
110
+ type RangeField,
111
+ type RangeParams,
112
+ type RangePresets,
113
+ type RangeValue,
114
+ type RangeWindow,
115
+ type RangeWindows,
116
+ type ResolvedLane,
117
+ type SearchValues,
118
+ type StoredValues,
119
+ type StringField,
81
120
  } from './state'
82
121
 
122
+ // ── WidgetHeader (unified section/widget/card heading primitive, docs/CONTROLS-SPEC.md §2.2) ─────
123
+ export { WidgetHeader, type WidgetHeaderProps } from './widget-header'
124
+
125
+ // ── Section (tier-2 heading composer over WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ───────────────
126
+ export { Section, type SectionProps } from './section'
127
+
83
128
  // ── Dashboard composites (KPI atoms + settings building blocks) ──────────────────────────────────
84
129
  export {
85
130
  DeltaBadge,
86
131
  type DeltaBadgeProps,
87
132
  StatCard,
133
+ type StatCardBreakdownRow,
88
134
  type StatCardProps,
89
135
  type StatCardTone,
90
136
  EmptyState,
package/src/nav/types.ts CHANGED
@@ -40,6 +40,8 @@ export type SidebarItem = {
40
40
  /** Plain href — the no-router fallback, and the breadcrumb parent target. */
41
41
  href?: string
42
42
  active?: boolean
43
+ /** A parent on the path to the active row. NOT active itself — never carries `aria-current`. */
44
+ ancestor?: boolean
43
45
  disabled?: boolean
44
46
  onClick?: (e: MouseEvent) => void
45
47
  /** Rich desktop badge (e.g. `NavCountBadge`). Desktop only. */
@@ -70,6 +72,88 @@ export type SidebarSection = {
70
72
  mobile?: false | NavSectionMobile
71
73
  }
72
74
 
75
+ /**
76
+ * Status tone on a sidebar-block item — the same three-member vocabulary as `StatCardTone`, whose
77
+ * members are `VX.status` keys by construction. Declared here rather than imported from
78
+ * `../dashboard/stat-card`: this module is the one `./router-tanstack` reads, and even a
79
+ * type-only edge into the Mantine layer would make `nav/types.d.ts` unresolvable without
80
+ * `@mantine/core` installed.
81
+ */
82
+ export type SidebarBlockTone = 'good' | 'warn' | 'bad'
83
+
84
+ /** One row inside a `kind: 'list'` block. Same router seam as `SidebarItem` — `Anchor`, then href. */
85
+ export type SidebarBlockItem = {
86
+ key: string
87
+ label: string
88
+ /** Trailing muted text (a timestamp, a count, an owner). */
89
+ meta?: string
90
+ icon?: ReactNode
91
+ /** Renders a status dot in place of a missing `icon`; reads `--vx-status-*`, never a hex. */
92
+ tone?: SidebarBlockTone
93
+ Anchor?: NavAnchor
94
+ href?: string
95
+ onClick?: () => void
96
+ }
97
+
98
+ /**
99
+ * A list of non-destination rows under a micro-label — "Awaiting action", "Recents", a project
100
+ * list. `count` is the header badge (and what earns a rail dot); `max` shows the first N with a
101
+ * "Show more" toggle past it.
102
+ */
103
+ export type SidebarListBlock = {
104
+ kind: 'list'
105
+ key: string
106
+ label: string
107
+ icon?: ReactNode
108
+ count?: number
109
+ /** Rows shown before the "Show more" toggle. Omit to show every row. */
110
+ max?: number
111
+ items: SidebarBlockItem[]
112
+ /** @default 'nav' */
113
+ placement?: 'nav' | 'bottom'
114
+ collapsible?: boolean
115
+ /** Collapsed-rail projection. @default 'dot' when `count` is set, `'hidden'` otherwise */
116
+ rail?: 'dot' | 'hidden'
117
+ /** @default 'more' */
118
+ mobile?: 'more' | 'hidden'
119
+ }
120
+
121
+ /** A "Getting started 1 of 5" progress row. Pinned above the settings footer; never in the nav. */
122
+ export type SidebarProgressBlock = {
123
+ kind: 'progress'
124
+ key: string
125
+ label: string
126
+ value: number
127
+ total: number
128
+ onClick?: () => void
129
+ /** @default 'bottom' — the only placement a progress row has. */
130
+ placement?: 'bottom'
131
+ /** Collapsed-rail projection: a ring on the settings row. @default 'ring' */
132
+ rail?: 'ring' | 'hidden'
133
+ /** @default 'hidden' */
134
+ mobile?: 'more' | 'hidden'
135
+ }
136
+
137
+ /**
138
+ * DESKTOP ONLY — arbitrary consumer content (a tree, a filter panel) that no set of rows can
139
+ * express. Replaced `BasaltShellProps.sidebarNavExtra`; hidden on the collapsed rail and absent
140
+ * from mobile for the same reason that prop was.
141
+ */
142
+ export type SidebarCustomBlock = {
143
+ kind: 'custom'
144
+ key: string
145
+ node: ReactNode
146
+ /** @default 'nav' */
147
+ placement?: 'nav' | 'bottom'
148
+ }
149
+
150
+ /**
151
+ * A sidebar block — DECLARED DATA, never a `ReactNode` slot (law C13, `docs/CONTROLS-SPEC.md`
152
+ * §2.3), which is what lets basalt own the collapsed-rail badge and the mobile More-sheet
153
+ * projection instead of each consumer hand-rolling both.
154
+ */
155
+ export type SidebarBlock = SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock
156
+
73
157
  export type MobileNavSurface = 'link' | 'menu' | 'sheet'
74
158
 
75
159
  /** One row inside a `menu`/`sheet` slot, grouped under its source section. */
@@ -108,8 +192,6 @@ export type MobileNavConfig = {
108
192
  menuMax?: number
109
193
  /** @default 'More' */
110
194
  moreLabel?: string
111
- /** Extra content pinned to the bottom of the More surface. */
112
- moreExtra?: ReactNode
113
195
  /** Scroll container for active-slot re-tap. @default document.scrollingElement */
114
196
  getScrollElement?: () => HTMLElement | null
115
197
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NotificationBell — ActionIcon + Indicator showing the unread Inbox count, opens a
3
- * NotificationCenter in a Popover. Designed for the shell `globalActions` slot (ReactNode).
3
+ * NotificationCenter in a Popover. Designed for the shell `globalActions` slot (`GlobalAction[]`).
4
4
  *
5
5
  * The badge counts unread Inbox items (errors / warnings / actionable notifications you haven't
6
6
  * seen) — not every toast that fired. Opening the bell does NOT mark anything read; items are
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * // In the shell's globalActions prop:
13
13
  * <BasaltShell
14
- * globalActions={<NotificationBell />}
14
+ * globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]}
15
15
  * {...rest}
16
16
  * />
17
17
  */
@@ -33,7 +33,10 @@ export type NotificationBellProps = {
33
33
  * Bell icon button with an unread-Inbox indicator. Opens a NotificationCenter popover.
34
34
  *
35
35
  * @example
36
- * <BasaltShell globalActions={<NotificationBell />} {...rest} />
36
+ * <BasaltShell
37
+ * globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]}
38
+ * {...rest}
39
+ * />
37
40
  */
38
41
  export function NotificationBell({ label = 'Notifications' }: NotificationBellProps) {
39
42
  const [opened, { open, toggle, close }] = useDisclosure(false)
@@ -20,7 +20,7 @@
20
20
  * notifyPromise(save(), { loading: 'Saving…', success: 'Saved', error: 'Failed' })
21
21
  *
22
22
  * // Shell bell (globalActions slot):
23
- * <BasaltShell globalActions={<NotificationBell />} ... />
23
+ * <BasaltShell globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]} ... />
24
24
  */
25
25
 
26
26
  // ── notify core ───────────────────────────────────────────────────────────────
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `field.*` — the field vocabulary `createSearchStore` takes, re-exported at the surface a consumer
3
+ * imports it from (`basalt-ui/router-tanstack`, beside the store factory).
4
+ *
5
+ * The builders and their codecs LIVE in `../state` on purpose: a field is declarative data with no
6
+ * router in it, and `createLocalStore` — the router-free lane, published on `basalt-ui/state` —
7
+ * has to build the same fields without pulling in `@tanstack/react-router`. One definition, two
8
+ * surfaces, no second description of the same param.
9
+ *
10
+ * Full field reference: the JSDoc on `field` itself, and `docs/CONTROLS-SPEC.md` §4.
11
+ */
12
+ export { field } from '../state'
13
+ export type {
14
+ AnyField,
15
+ BooleanField,
16
+ EnumField,
17
+ FieldFallback,
18
+ FieldHandle,
19
+ FieldLane,
20
+ FieldOption,
21
+ FieldSetOptions,
22
+ FieldValue,
23
+ MultiField,
24
+ NumberField,
25
+ RangeField,
26
+ RangeParams,
27
+ RangePresets,
28
+ RangeWindow,
29
+ RangeWindows,
30
+ RangeValue,
31
+ ResolvedLane,
32
+ SearchValues,
33
+ StoredValues,
34
+ StringField,
35
+ } from '../state'
@@ -3,10 +3,36 @@
3
3
  * Optional peer: @tanstack/react-router.
4
4
  * Carries the typed navigation definition (`defineNav`/`navGroup`/`navTarget`/`flattenNav`) and
5
5
  * `useNav`, which resolves it into `BasaltShell`'s `sections` + `mobileNav` props; plus the
6
- * StaticDataRouteOption augmentation (title/icon/navSection) for router-derived breadcrumbs and
7
- * the search-param stores — no Mantine, no JSX.
6
+ * StaticDataRouteOption augmentation (title/icon/navSection) for router-derived breadcrumbs, plus
7
+ * `createSearchStore` + the `field.*` vocabulary (and the two deprecated enum-only wrappers it
8
+ * replaced) — no Mantine, no JSX.
8
9
  */
9
10
 
11
+ export { createSearchStore, type CreateSearchStoreOptions, type SearchStore } from './search-store'
12
+ export { field } from './field'
13
+ export type {
14
+ AnyField,
15
+ BooleanField,
16
+ EnumField,
17
+ FieldFallback,
18
+ FieldHandle,
19
+ FieldLane,
20
+ FieldOption,
21
+ FieldSetOptions,
22
+ FieldValue,
23
+ MultiField,
24
+ NumberField,
25
+ RangeField,
26
+ RangeParams,
27
+ RangePresets,
28
+ RangeValue,
29
+ RangeWindow,
30
+ RangeWindows,
31
+ ResolvedLane,
32
+ SearchValues,
33
+ StoredValues,
34
+ StringField,
35
+ } from './field'
10
36
  export {
11
37
  createSearchParamStore,
12
38
  type SearchParamStore,