basalt-ui 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +18 -0
  3. package/MIGRATING.md +546 -20
  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 +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/settings-section.d.ts +6 -4
  134. package/dist/dashboard/settings-section.d.ts.map +1 -1
  135. package/dist/dashboard/settings-section.js +23 -16
  136. package/dist/dashboard/settings-section.module.css +10 -0
  137. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  138. package/dist/dashboard/settings-section.test.d.ts +2 -0
  139. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  140. package/dist/dashboard/stat-card.d.ts +44 -9
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +80 -48
  143. package/dist/dashboard/stat-card.module.css +82 -0
  144. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  145. package/dist/data/data-table.d.ts +37 -15
  146. package/dist/data/data-table.d.ts.map +1 -1
  147. package/dist/data/data-table.js +88 -58
  148. package/dist/guard/grace.test.d.ts +20 -0
  149. package/dist/guard/grace.test.d.ts.map +1 -0
  150. package/dist/guard/index.d.ts +66 -3
  151. package/dist/guard/index.d.ts.map +1 -1
  152. package/dist/guard/index.js +82 -7
  153. package/dist/guard/types.d.ts +3 -3
  154. package/dist/guard/types.d.ts.map +1 -1
  155. package/dist/index.d.ts +12 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +13 -10
  158. package/dist/nav/types.d.ts +78 -2
  159. package/dist/nav/types.d.ts.map +1 -1
  160. package/dist/notifications/bell.d.ts +4 -1
  161. package/dist/notifications/bell.d.ts.map +1 -1
  162. package/dist/notifications/index.d.ts +1 -1
  163. package/dist/router-tanstack/field.d.ts +14 -0
  164. package/dist/router-tanstack/field.d.ts.map +1 -0
  165. package/dist/router-tanstack/field.js +4 -0
  166. package/dist/router-tanstack/index.d.ts +6 -2
  167. package/dist/router-tanstack/index.d.ts.map +1 -1
  168. package/dist/router-tanstack/index.js +4 -0
  169. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  170. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  171. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  172. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  173. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/search-param-store.js +16 -45
  175. package/dist/router-tanstack/search-store.d.ts +81 -0
  176. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  177. package/dist/router-tanstack/search-store.js +187 -0
  178. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  179. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  181. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  182. package/dist/router-tanstack/use-nav.d.ts +0 -2
  183. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  184. package/dist/router-tanstack/use-nav.js +34 -12
  185. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  186. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  187. package/dist/section/index.d.ts +2 -0
  188. package/dist/section/index.d.ts.map +1 -0
  189. package/dist/section/index.js +4 -0
  190. package/dist/section/section.d.ts +24 -0
  191. package/dist/section/section.d.ts.map +1 -0
  192. package/dist/section/section.js +126 -0
  193. package/dist/section/section.module.css +104 -0
  194. package/dist/section/section.module.css.d.ts +11 -0
  195. package/dist/section/section.test.d.ts +2 -0
  196. package/dist/section/section.test.d.ts.map +1 -0
  197. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  198. package/dist/shell/app-breadcrumbs.js +18 -15
  199. package/dist/shell/app-header.module.css +77 -37
  200. package/dist/shell/app-header.module.css.d.ts +2 -1
  201. package/dist/shell/app-mobile-nav.d.ts +9 -2
  202. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  203. package/dist/shell/app-mobile-nav.js +98 -11
  204. package/dist/shell/app-mobile-nav.module.css +8 -0
  205. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  206. package/dist/shell/app-sidebar.d.ts +55 -28
  207. package/dist/shell/app-sidebar.d.ts.map +1 -1
  208. package/dist/shell/app-sidebar.js +116 -67
  209. package/dist/shell/app-sidebar.module.css +303 -20
  210. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  211. package/dist/shell/index.d.ts +55 -21
  212. package/dist/shell/index.d.ts.map +1 -1
  213. package/dist/shell/index.js +40 -18
  214. package/dist/shell/mobile-nav-model.d.ts +13 -2
  215. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  216. package/dist/shell/mobile-nav-model.js +15 -1
  217. package/dist/shell/nav-count-badge.d.ts +1 -1
  218. package/dist/shell/page-bar.d.ts +51 -0
  219. package/dist/shell/page-bar.d.ts.map +1 -0
  220. package/dist/shell/page-bar.js +154 -0
  221. package/dist/shell/page-bar.module.css +178 -0
  222. package/dist/shell/page-bar.module.css.d.ts +17 -0
  223. package/dist/shell/page-bar.test.d.ts +2 -0
  224. package/dist/shell/page-bar.test.d.ts.map +1 -0
  225. package/dist/shell/sidebar-block-model.d.ts +48 -0
  226. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  227. package/dist/shell/sidebar-block-model.js +39 -0
  228. package/dist/shell/sidebar-blocks.d.ts +32 -0
  229. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  230. package/dist/shell/sidebar-blocks.js +169 -0
  231. package/dist/shell/sidebar-search.d.ts +13 -1
  232. package/dist/shell/sidebar-search.d.ts.map +1 -1
  233. package/dist/shell/sidebar-search.js +34 -1
  234. package/dist/shell/sidebar-search.module.css +26 -0
  235. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  236. package/dist/state/fields.d.ts +392 -0
  237. package/dist/state/fields.d.ts.map +1 -0
  238. package/dist/state/fields.js +369 -0
  239. package/dist/state/fields.test.d.ts +2 -0
  240. package/dist/state/fields.test.d.ts.map +1 -0
  241. package/dist/state/persisted.d.ts +48 -0
  242. package/dist/state/persisted.d.ts.map +1 -0
  243. package/dist/state/persisted.js +105 -0
  244. package/dist/state.d.ts +11 -56
  245. package/dist/state.d.ts.map +1 -1
  246. package/dist/state.js +14 -120
  247. package/dist/styles.css +13 -0
  248. package/dist/surfaces.d.ts +106 -24
  249. package/dist/surfaces.d.ts.map +1 -1
  250. package/dist/surfaces.js +155 -19
  251. package/dist/theme/controls.module.css +16 -1
  252. package/dist/theme/ctl-theme.d.ts +32 -0
  253. package/dist/theme/ctl-theme.d.ts.map +1 -0
  254. package/dist/theme/ctl-theme.js +54 -0
  255. package/dist/theme/ctl-theme.test.d.ts +2 -0
  256. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  257. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  258. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  259. package/dist/theme/divider-law.test.d.ts +2 -0
  260. package/dist/theme/divider-law.test.d.ts.map +1 -0
  261. package/dist/theme/icon-slot.d.ts +44 -0
  262. package/dist/theme/icon-slot.d.ts.map +1 -0
  263. package/dist/theme/icon-slot.js +21 -0
  264. package/dist/theme/icon-slot.module.css +47 -0
  265. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  266. package/dist/theme/icon-slot.test.d.ts +2 -0
  267. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  268. package/dist/theme/index.d.ts +1 -0
  269. package/dist/theme/index.d.ts.map +1 -1
  270. package/dist/theme/index.js +82 -5
  271. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  272. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  273. package/dist/theme/segmented-control.module.css +20 -0
  274. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  275. package/dist/tokens/index.d.ts +12 -0
  276. package/dist/tokens/index.d.ts.map +1 -1
  277. package/dist/tokens/index.js +38 -8
  278. package/dist/tokens/palette.d.ts +97 -61
  279. package/dist/tokens/palette.d.ts.map +1 -1
  280. package/dist/tokens/palette.js +76 -38
  281. package/dist/tokens.css +10 -2
  282. package/dist/widget-header/delta-badge.d.ts +13 -0
  283. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  284. package/dist/widget-header/delta-badge.js +29 -0
  285. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  286. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  287. package/dist/widget-header/index.d.ts +3 -0
  288. package/dist/widget-header/index.d.ts.map +1 -0
  289. package/dist/widget-header/index.js +6 -0
  290. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  291. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  292. package/dist/widget-header/widget-header.d.ts +37 -0
  293. package/dist/widget-header/widget-header.d.ts.map +1 -0
  294. package/dist/widget-header/widget-header.js +77 -0
  295. package/dist/widget-header/widget-header.module.css +335 -0
  296. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  297. package/dist/widget-header/widget-header.test.d.ts +2 -0
  298. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  299. package/llms.txt +30 -17
  300. package/package.json +14 -1
  301. package/src/charts/cursor/resolve.test.ts +29 -1
  302. package/src/charts/cursor/resolve.ts +38 -6
  303. package/src/charts/cursor/scope.tsx +6 -3
  304. package/src/charts/cursor/store.test.ts +19 -1
  305. package/src/charts/cursor/store.ts +10 -5
  306. package/src/charts/hooks/useChartCursor.ts +16 -8
  307. package/src/charts/index.ts +2 -2
  308. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  309. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  310. package/src/charts/primitives/ChartCard.tsx +86 -146
  311. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  312. package/src/cli/check-coverage.test.ts +297 -0
  313. package/src/cli/check-theme.test.ts +38 -14
  314. package/src/cli/index.ts +392 -16
  315. package/src/cli/init-sync.test.ts +97 -0
  316. package/src/connectivity/connectivity-indicator.tsx +16 -9
  317. package/src/content/index.ts +16 -3
  318. package/src/content/prose.module.css +7 -28
  319. package/src/content/prose.module.css.test.ts +8 -24
  320. package/src/controls/actions.module.css +62 -0
  321. package/src/controls/actions.module.css.d.ts +13 -0
  322. package/src/controls/actions.test.tsx +252 -0
  323. package/src/controls/actions.tsx +655 -0
  324. package/src/controls/actions.type-guard.test.ts +63 -0
  325. package/src/controls/compare-filter.tsx +64 -0
  326. package/src/controls/control-group.test.tsx +229 -0
  327. package/src/controls/control-group.tsx +63 -0
  328. package/src/controls/controls.module.css +542 -0
  329. package/src/controls/controls.module.css.d.ts +35 -0
  330. package/src/controls/controls.router.test.tsx +637 -0
  331. package/src/controls/enum-filter.tsx +159 -0
  332. package/src/controls/filter-context.tsx +153 -0
  333. package/src/controls/filter-pill.tsx +120 -0
  334. package/src/controls/filter-set.test.tsx +472 -0
  335. package/src/controls/filter-set.tsx +225 -0
  336. package/src/controls/filter-sheet.tsx +285 -0
  337. package/src/controls/glyphs.tsx +81 -0
  338. package/src/controls/index.ts +48 -0
  339. package/src/controls/multi-select-filter.tsx +140 -0
  340. package/src/controls/peer-boundary.test.ts +85 -0
  341. package/src/controls/props-contract.test.tsx +106 -0
  342. package/src/controls/range-filter.tsx +184 -0
  343. package/src/controls/search-filter.tsx +123 -0
  344. package/src/controls/select-filter.tsx +89 -0
  345. package/src/controls/sync-button.module.css +63 -0
  346. package/src/controls/sync-button.module.css.d.ts +11 -0
  347. package/src/controls/sync-button.test.tsx +183 -0
  348. package/src/controls/sync-button.tsx +197 -0
  349. package/src/controls/toggle-filter.tsx +60 -0
  350. package/src/controls/view-tabs.tsx +110 -0
  351. package/src/controls-dates/date-range-picker.tsx +42 -0
  352. package/src/controls-dates/index.ts +8 -0
  353. package/src/dashboard/delta-badge.tsx +4 -74
  354. package/src/dashboard/settings-section.module.css +10 -0
  355. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  356. package/src/dashboard/settings-section.test.tsx +56 -0
  357. package/src/dashboard/settings-section.tsx +33 -30
  358. package/src/dashboard/stat-card.module.css +82 -0
  359. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  360. package/src/dashboard/stat-card.test.tsx +67 -1
  361. package/src/dashboard/stat-card.tsx +209 -68
  362. package/src/data/data-table.test.tsx +96 -11
  363. package/src/data/data-table.tsx +220 -84
  364. package/src/guard/check-source.test.ts +126 -13
  365. package/src/guard/grace.test.ts +149 -0
  366. package/src/guard/index.ts +172 -29
  367. package/src/guard/types.ts +4 -2
  368. package/src/index.ts +46 -5
  369. package/src/nav/types.ts +84 -2
  370. package/src/notifications/bell.tsx +6 -3
  371. package/src/notifications/index.ts +1 -1
  372. package/src/router-tanstack/field.ts +31 -0
  373. package/src/router-tanstack/index.ts +24 -2
  374. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  375. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  376. package/src/router-tanstack/search-param-store.ts +42 -159
  377. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  378. package/src/router-tanstack/search-store.test.ts +418 -0
  379. package/src/router-tanstack/search-store.ts +380 -0
  380. package/src/router-tanstack/use-nav.test.tsx +187 -0
  381. package/src/router-tanstack/use-nav.ts +72 -14
  382. package/src/section/index.ts +1 -0
  383. package/src/section/section.module.css +104 -0
  384. package/src/section/section.module.css.d.ts +11 -0
  385. package/src/section/section.test.tsx +204 -0
  386. package/src/section/section.tsx +220 -0
  387. package/src/shell/app-breadcrumbs.tsx +44 -31
  388. package/src/shell/app-header.module.css +77 -37
  389. package/src/shell/app-header.module.css.d.ts +2 -1
  390. package/src/shell/app-mobile-nav.module.css +8 -0
  391. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  392. package/src/shell/app-mobile-nav.tsx +147 -8
  393. package/src/shell/app-sidebar-account.tsx +2 -2
  394. package/src/shell/app-sidebar.module.css +303 -20
  395. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  396. package/src/shell/app-sidebar.test.tsx +456 -41
  397. package/src/shell/app-sidebar.tsx +276 -140
  398. package/src/shell/index.test.tsx +291 -5
  399. package/src/shell/index.tsx +137 -38
  400. package/src/shell/mobile-nav-model.test.ts +228 -7
  401. package/src/shell/mobile-nav-model.ts +53 -3
  402. package/src/shell/nav-count-badge.tsx +1 -1
  403. package/src/shell/page-bar.module.css +178 -0
  404. package/src/shell/page-bar.module.css.d.ts +17 -0
  405. package/src/shell/page-bar.test.tsx +316 -0
  406. package/src/shell/page-bar.tsx +315 -0
  407. package/src/shell/sidebar-block-model.ts +83 -0
  408. package/src/shell/sidebar-blocks.tsx +301 -0
  409. package/src/shell/sidebar-search.module.css +26 -0
  410. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  411. package/src/shell/sidebar-search.tsx +71 -2
  412. package/src/state/fields.test.tsx +258 -0
  413. package/src/state/fields.ts +892 -0
  414. package/src/state/persisted.ts +211 -0
  415. package/src/state.ts +46 -240
  416. package/src/styles.css +13 -0
  417. package/src/surfaces.ts +203 -36
  418. package/src/theme/controls.module.css +16 -1
  419. package/src/theme/ctl-theme.test.tsx +275 -0
  420. package/src/theme/ctl-theme.tsx +138 -0
  421. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  422. package/src/theme/density-relations.test.ts +106 -51
  423. package/src/theme/divider-law.test.ts +259 -0
  424. package/src/theme/icon-slot.module.css +47 -0
  425. package/src/theme/icon-slot.module.css.d.ts +8 -0
  426. package/src/theme/icon-slot.test.tsx +275 -0
  427. package/src/theme/icon-slot.tsx +59 -0
  428. package/src/theme/index.ts +165 -3
  429. package/src/theme/layout-rhythm-css.test.ts +72 -0
  430. package/src/theme/radius.test.ts +11 -1
  431. package/src/theme/segmented-control.module.css +20 -0
  432. package/src/theme/shadow-surfaces.test.ts +20 -7
  433. package/src/theme/spacing.test.ts +75 -27
  434. package/src/theme/use-basalt-spacing.ts +2 -2
  435. package/src/tokens/density.test.ts +7 -25
  436. package/src/tokens/index.ts +48 -8
  437. package/src/tokens/palette.ts +107 -73
  438. package/src/widget-header/delta-badge.test.tsx +60 -0
  439. package/src/widget-header/delta-badge.tsx +57 -0
  440. package/src/widget-header/index.ts +2 -0
  441. package/src/widget-header/no-mantine.test.ts +33 -0
  442. package/src/widget-header/widget-header.module.css +335 -0
  443. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  444. package/src/widget-header/widget-header.test.tsx +187 -0
  445. package/src/widget-header/widget-header.tsx +168 -0
  446. package/agent/rules/basalt-agent.md +0 -452
  447. package/agent/rules/basalt-app.md +0 -246
  448. package/agent/rules/basalt-commands.md +0 -320
  449. package/agent/rules/basalt-content.md +0 -409
  450. package/agent/rules/basalt-data.md +0 -329
  451. package/agent/rules/basalt-forms.md +0 -186
  452. package/agent/rules/basalt-notifications.md +0 -264
  453. package/agent/rules/basalt-query.md +0 -187
  454. package/agent/rules/basalt-router.md +0 -336
  455. package/dist/content/article-filter-bar.d.ts +0 -40
  456. package/dist/content/article-filter-bar.d.ts.map +0 -1
  457. package/dist/content/article-filter-bar.js +0 -56
  458. package/dist/shell/page-header.d.ts +0 -13
  459. package/dist/shell/page-header.d.ts.map +0 -1
  460. package/dist/shell/page-header.js +0 -23
  461. package/src/content/article-filter-bar.tsx +0 -88
  462. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The shared body behind `SelectFilter` and `CompareFilter` — one closed-enum filter over
3
+ * `field.enum` (`docs/CONTROLS-SPEC.md` §3). INTERNAL: the two exported wrappers differ only in
4
+ * their default label and in how narrowly they type `field`, so there is one implementation and no
5
+ * third public spelling of it.
6
+ *
7
+ * Both surfaces render the SAME `Radio.Group`, differing only in row height: a radio list states
8
+ * "one of these" without a second interaction (a nested Select inside a popover would), and it is
9
+ * the sheet form the spec prescribes — so it is the popover form too rather than inventing a second
10
+ * idiom for the same choice.
11
+ *
12
+ * **The pill label is ALWAYS the selected option's label, including at the field's default value.**
13
+ * It used to fall back to the FILTER's name while `isDefault(value)` held, and that is the bug this
14
+ * comment exists to keep fixed: a bar reading `Compare` over a field holding `'previous'` states
15
+ * something false — the popover said `Previous period` was selected and the pill said the filter had
16
+ * not been touched. A pill is a readout, so the value is the only thing it may print. `label` is the
17
+ * popover/sheet heading and the accessible name, nothing else; the ACTIVE state (touched vs at its
18
+ * default) is carried by the accent border, which is a second channel and does not need the text.
19
+ *
20
+ * `?? label` survives as a last resort for a value that is in the field but not in the rendered
21
+ * `options` — a runtime catalogue that has dropped a row while the URL still points at it. That is a
22
+ * data gap, not a default, and printing the filter's name is the least-wrong thing to do with it.
23
+ *
24
+ * `field` is typed as `ChoiceHandle` rather than a `FieldHandle<EnumField>`, and `options` may
25
+ * override the field's own rows — the two halves of `SelectFilter`'s runtime-catalogue shape (see
26
+ * its doc). Both public wrappers still type `field` exactly, so a wrong field kind is still a type
27
+ * error at the call site; the widening lives here, where nothing consumer-facing points at it.
28
+ */
29
+ import { Button, Radio, Stack } from '@mantine/core'
30
+ import type { ReactNode } from 'react'
31
+ import type { FieldOption } from '../state'
32
+ import classes from './controls.module.css'
33
+ import { useFilterRegistration, useFilterSurface } from './filter-context'
34
+ import { FilterPill } from './filter-pill'
35
+ import { SheetField, SheetOptionList, useControlName } from './filter-sheet'
36
+ import type { FilterOption } from './select-filter'
37
+
38
+ /**
39
+ * The slice of a `FieldHandle` this body actually binds to, generic over the VALUE rather than over
40
+ * the field kind. `FieldHandle<EnumField<T>>` satisfies it at `T` and `FieldHandle<StringField>` at
41
+ * `string`, which is what lets `SelectFilter` take either without this file knowing about kinds —
42
+ * and it stays a structural subset, so it can never widen into a second, hand-constructible handle.
43
+ */
44
+ export type ChoiceHandle<T extends string> = {
45
+ readonly fallback: T
46
+ readonly options: readonly FieldOption[]
47
+ use(): readonly [T, (next: T) => void]
48
+ isDefault(v: T): boolean
49
+ }
50
+
51
+ export type EnumFilterProps<T extends string> = {
52
+ readonly field: ChoiceHandle<T>
53
+ readonly label: string
54
+ readonly icon?: ReactNode
55
+ /** Adds a `Clear` action that writes the field back to its fallback. */
56
+ readonly clearable?: boolean
57
+ /**
58
+ * Overrides `field.options` at render — a runtime catalogue whose labels carry live data, or the
59
+ * only row source a `StringField` has. `| undefined` explicitly: an internal component is spread
60
+ * into, and `exactOptionalPropertyTypes` rejects a possibly-undefined member on a bare optional.
61
+ */
62
+ readonly options?: readonly FilterOption[] | undefined
63
+ }
64
+
65
+ export function EnumFilter<T extends string>({
66
+ field,
67
+ label,
68
+ icon,
69
+ clearable,
70
+ options: optionsProp,
71
+ }: EnumFilterProps<T>): ReactNode {
72
+ const [value, setValue] = field.use()
73
+ const surface = useFilterSurface()
74
+ const isDefault = field.isDefault(value)
75
+ useFilterRegistration(!isDefault, () => {
76
+ setValue(field.fallback)
77
+ })
78
+
79
+ // The prop wins whole, never merged: a catalogue that has dropped a value must be able to drop
80
+ // its row, which a merge with `field.options` would silently put back.
81
+ const options: readonly FilterOption[] = optionsProp ?? field.options
82
+ const selected = options.find((option) => option.value === value)
83
+ const inSheet = surface === 'sheet'
84
+ const { labelId } = useControlName(label, inSheet)
85
+
86
+ // The SHEET form is a `SheetOptionList` (44px rows, trailing check, hairline between rows), not
87
+ // the popover's `Radio.Group` — see `SheetOptionList`'s doc for why the leading radio dot had to
88
+ // go. Both still write through the same setter, so there is one behaviour and two surfaces.
89
+ if (inSheet) {
90
+ return (
91
+ <SheetField label={label} labelId={labelId}>
92
+ <SheetOptionList
93
+ mode="single"
94
+ labelId={labelId}
95
+ selected={[value]}
96
+ options={options}
97
+ onToggle={(next) => {
98
+ // Rendered from `options` — the field's declared values, or the runtime catalogue
99
+ // standing in for them; the cast restores what the codec already guarantees.
100
+ setValue(next as T)
101
+ }}
102
+ />
103
+ </SheetField>
104
+ )
105
+ }
106
+
107
+ const body = (
108
+ // Mantine's OWN `label` prop, not an `aria-label`: `Radio.Group` overwrites both `aria-label`
109
+ // and `aria-labelledby` with its own `aria-labelledby` pointing at the `Input.Wrapper` label it
110
+ // renders — which, with no `label` passed, points at an element that does not exist. So the only
111
+ // way to name this group is to give it the label. It doubles as the heading in both surfaces:
112
+ // the group needs the FILTER's name ('Currency'), which the pill cannot supply because the pill
113
+ // reads the VALUE ('EUR') once set.
114
+ <Radio.Group
115
+ label={label}
116
+ classNames={{ label: classes.groupLabel }}
117
+ value={value}
118
+ onChange={(next) => {
119
+ // `next` is a DOM string, and the radios are rendered from `options` — the field's own
120
+ // declared values, or the runtime catalogue standing in for them. Over a closed enum the
121
+ // cast restores what the codec already guarantees; over a `StringField` there is nothing to
122
+ // guarantee and nothing to widen, since every string is a legal value there.
123
+ setValue(next as T)
124
+ }}
125
+ >
126
+ <Stack gap={2}>
127
+ {options.map((option) => (
128
+ <Radio
129
+ key={option.value}
130
+ value={option.value}
131
+ label={option.label}
132
+ {...(option.disabled === true && { disabled: true })}
133
+ />
134
+ ))}
135
+ </Stack>
136
+ {clearable === true && !isDefault && (
137
+ <Button
138
+ variant="subtle"
139
+ size="ctl"
140
+ onClick={() => {
141
+ setValue(field.fallback)
142
+ }}
143
+ >
144
+ Clear
145
+ </Button>
146
+ )}
147
+ </Radio.Group>
148
+ )
149
+
150
+ return (
151
+ <FilterPill
152
+ label={selected?.label ?? label}
153
+ active={!isDefault}
154
+ {...(icon !== undefined && { icon })}
155
+ >
156
+ <div className={classes.optionList}>{body}</div>
157
+ </FilterPill>
158
+ )
159
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The two contexts `FilterSet` publishes and every control in this folder consumes
3
+ * (`docs/CONTROLS-SPEC.md` §3). Its own module so `filter-set.tsx` and `filter-sheet.tsx` can both
4
+ * read it without importing each other.
5
+ *
6
+ * **Surface** — `useFilterSurface()` is how a control decides its FORM, not a media query: `'pill'`
7
+ * inside the bar row (and inside the desktop `+N` fold), `'sheet'` inside the mobile `Filters (n)`
8
+ * Drawer. A control renders exactly one form per mount; the desktop/mobile swap WITHIN the pill
9
+ * form is CSS (`visibleFrom`/`hiddenFrom`), never JS (C9).
10
+ *
11
+ * **Registry** — `useFilterRegistration()` is what makes `Filters (n)` and `Reset all` derived
12
+ * rather than hand-passed: each filter reports `{ isActive, reset }` for itself, so `FilterSet`
13
+ * needs no list of its children's fields. Registration is deliberately scoped to ONE surface: the
14
+ * bar row keeps every child mounted on every viewport (hidden slots are `display: none`, never
15
+ * unmounted — see `controls.module.css`), so the row alone is a complete and stable census. The
16
+ * fold dropdown and the sheet mount a SECOND copy of the same children and therefore provide a
17
+ * `null` registry, which is what keeps one filter from counting twice.
18
+ */
19
+ import {
20
+ createContext,
21
+ useCallback,
22
+ useContext,
23
+ useEffect,
24
+ useId,
25
+ useMemo,
26
+ useRef,
27
+ useState,
28
+ } from 'react'
29
+ import type { ReactNode } from 'react'
30
+
31
+ /** Which form a control renders. Provided by `FilterSet`; `'pill'` when there is no `FilterSet`. */
32
+ export type FilterSurface = 'pill' | 'sheet'
33
+
34
+ const FilterSurfaceContext = createContext<FilterSurface>('pill')
35
+
36
+ /**
37
+ * The form this control should render — the ONE thing a control branches on.
38
+ *
39
+ * @example
40
+ * const surface = useFilterSurface()
41
+ * if (surface === 'sheet') return <SheetField label={label}>…</SheetField>
42
+ */
43
+ export function useFilterSurface(): FilterSurface {
44
+ return useContext(FilterSurfaceContext)
45
+ }
46
+
47
+ /** What one filter reports about itself, keyed internally by a `useId()` per mount. */
48
+ export type FilterRegistration = {
49
+ /** `!field.isDefault(value)` — the only input to `Filters (n)` and the pill's accent border. */
50
+ readonly isActive: boolean
51
+ /** Writes the field back to its fallback. Called by `Reset all`. */
52
+ readonly reset: () => void
53
+ }
54
+
55
+ type FilterRegistry = {
56
+ register(key: string, registration: FilterRegistration): void
57
+ unregister(key: string): void
58
+ }
59
+
60
+ const FilterRegistryContext = createContext<FilterRegistry | null>(null)
61
+
62
+ export type FilterSetScopeProps = {
63
+ readonly surface: FilterSurface
64
+ /** `null` on a second copy of the same children (the fold dropdown, the sheet). */
65
+ readonly registry: FilterRegistry | null
66
+ readonly children: ReactNode
67
+ }
68
+
69
+ /** Provides both contexts at once — every place FilterSet mounts children goes through this. */
70
+ export function FilterSetScope({ surface, registry, children }: FilterSetScopeProps): ReactNode {
71
+ return (
72
+ <FilterSurfaceContext.Provider value={surface}>
73
+ <FilterRegistryContext.Provider value={registry}>{children}</FilterRegistryContext.Provider>
74
+ </FilterSurfaceContext.Provider>
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Report this filter's active/reset state to the enclosing `FilterSet`. Call it unconditionally —
80
+ * it no-ops outside a registering scope (a bare filter with no `FilterSet`, the fold dropdown, the
81
+ * sheet), which is what keeps it hook-safe in every control.
82
+ *
83
+ * @example
84
+ * const [value, setValue] = field.use()
85
+ * useFilterRegistration(!field.isDefault(value), () => setValue(field.fallback))
86
+ */
87
+ export function useFilterRegistration(isActive: boolean, reset: () => void): void {
88
+ const registry = useContext(FilterRegistryContext)
89
+ const key = useId()
90
+ // A ref, so a `reset` closure that is fresh every render does not re-run the effect (and so
91
+ // `Reset all` always calls the LATEST setter, not the one captured at registration).
92
+ const resetRef = useRef(reset)
93
+ resetRef.current = reset
94
+ const stableReset = useCallback(() => {
95
+ resetRef.current()
96
+ }, [])
97
+
98
+ useEffect(() => {
99
+ if (registry === null) return
100
+ registry.register(key, { isActive, reset: stableReset })
101
+ return () => {
102
+ registry.unregister(key)
103
+ }
104
+ }, [registry, key, isActive, stableReset])
105
+ }
106
+
107
+ export type FilterCensus = {
108
+ readonly registry: FilterRegistry
109
+ /** How many registered filters are non-default — `Filters (n)`. */
110
+ readonly activeCount: number
111
+ /** Resets every registered filter. */
112
+ readonly resetAll: () => void
113
+ }
114
+
115
+ /**
116
+ * The registry side, for `FilterSet` itself. The map is a ref (registration happens in children's
117
+ * effects, which must not re-render the parent mid-commit) and the COUNT is state, so only a real
118
+ * change in the count re-renders the bar.
119
+ */
120
+ export function useFilterCensus(): FilterCensus {
121
+ const entries = useRef(new Map<string, FilterRegistration>())
122
+ const [activeCount, setActiveCount] = useState(0)
123
+
124
+ // The functional form, so React's own bail-out drops a write of the count already held — a
125
+ // register/unregister pair that leaves the count unchanged must not re-render the bar.
126
+ const sync = useCallback(() => {
127
+ let next = 0
128
+ for (const entry of entries.current.values()) {
129
+ if (entry.isActive) next += 1
130
+ }
131
+ setActiveCount((current) => (current === next ? current : next))
132
+ }, [])
133
+
134
+ const registry = useMemo<FilterRegistry>(
135
+ () => ({
136
+ register(key, registration) {
137
+ entries.current.set(key, registration)
138
+ sync()
139
+ },
140
+ unregister(key) {
141
+ entries.current.delete(key)
142
+ sync()
143
+ },
144
+ }),
145
+ [sync],
146
+ )
147
+
148
+ const resetAll = useCallback(() => {
149
+ for (const entry of entries.current.values()) entry.reset()
150
+ }, [])
151
+
152
+ return { registry, activeCount, resetAll }
153
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `FilterPill` — the one chip every filter in the bar row renders (`docs/CONTROLS-SPEC.md` §3):
3
+ * bordered, 30px, leading icon · label · trailing `⇅`. INTERNAL: it is not on the `./controls`
4
+ * barrel, because a consumer reaching for it directly is hand-rolling a filter
5
+ * (`basalt/hand-rolled-filter`) — the pill is what `RangeFilter`/`SelectFilter`/… ARE, not a
6
+ * building block to assemble one from.
7
+ *
8
+ * The 30px box is `variant="default" size="ctl"` and nothing else — the tier comes from the theme's
9
+ * `--button-height-ctl` var set (`docs/CONTROLS-SPEC.md` §5), so the pill is the same height whether
10
+ * or not the home wrapped its slot in `CtlSlot`, and it tracks the density knob for free.
11
+ *
12
+ * Two modes, one component: with `children` the pill is a `Popover` target (every enum-ish filter);
13
+ * with only `onClick` it is a plain button that acts on press (`ToggleFilter`, the `+N` fold's
14
+ * sibling, the `Filters (n)` sheet opener).
15
+ */
16
+ import { Button, Popover } from '@mantine/core'
17
+ import { useState } from 'react'
18
+ import type { ReactNode } from 'react'
19
+ import { IconSlot } from '../theme/icon-slot'
20
+ import classes from './controls.module.css'
21
+ import { UpDownGlyph } from './glyphs'
22
+
23
+ export type FilterPillProps = {
24
+ readonly label: string
25
+ readonly icon?: ReactNode
26
+ /** Non-default → accent-tinted border. Derived from `field.isDefault(value)`, never hand-set. */
27
+ readonly active?: boolean
28
+ /** Mono label, for a numeric preset set (`7d` / `30d`) — the same law `[data-numeric]` applies
29
+ * to a numeric SegmentedControl label (`theme/segmented-control.module.css`). */
30
+ readonly numeric?: boolean
31
+ /** Popover body. Omit for a press-to-act pill. */
32
+ readonly children?: ReactNode
33
+ readonly onClick?: () => void
34
+ /** Accessible name when the visible label is a bare count (`+2`). */
35
+ readonly ariaLabel?: string
36
+ /**
37
+ * `aria-pressed` for the press-to-act mode (`ToggleFilter`) — without it the on/off state is
38
+ * carried by a border colour and nothing else. Deliberately NOT set on a popover-target pill,
39
+ * where `aria-expanded` (which Mantine's `Popover.Target` supplies) is the correct state.
40
+ */
41
+ readonly pressed?: boolean
42
+ readonly className?: string
43
+ /** Set by `FilterSet` on its own fold/sheet pills; a filter never passes either of these. */
44
+ readonly hideGlyph?: boolean
45
+ /** `data-shown` — the CSS hook that reveals the `+N` fold pill once folding is needed. */
46
+ readonly shown?: boolean
47
+ }
48
+
49
+ export function FilterPill({
50
+ label,
51
+ icon,
52
+ active,
53
+ numeric,
54
+ children,
55
+ onClick,
56
+ ariaLabel,
57
+ pressed,
58
+ className,
59
+ hideGlyph,
60
+ shown,
61
+ }: FilterPillProps): ReactNode {
62
+ // Mantine's `Popover` does not open on target click — it is a controlled surface, unlike `Menu`.
63
+ // This is the only `useState` in the folder, and it holds an OVERLAY's open flag, not a filter's
64
+ // value (C3 is about the value; a popover that survives a reload would be a bug, not a feature).
65
+ const [opened, setOpened] = useState(false)
66
+
67
+ const target = (
68
+ <Button
69
+ variant="default"
70
+ size="ctl"
71
+ className={className === undefined ? classes.pill : `${classes.pill} ${className}`}
72
+ classNames={{ label: classes.pillLabel }}
73
+ {...(active === true && { 'data-active': true })}
74
+ {...(numeric === true && { 'data-numeric': true })}
75
+ {...(ariaLabel !== undefined && { 'aria-label': ariaLabel })}
76
+ {...(pressed !== undefined && children === undefined && { 'aria-pressed': pressed })}
77
+ {...(shown === true && { 'data-shown': true })}
78
+ {...(icon !== undefined && {
79
+ // `IconSlot` owns the 16px box and the optical centring; `.pillIcon` adds only the pill's
80
+ // muted-then-ink colour law. A consumer's glyph can no longer set this row's height.
81
+ leftSection: <IconSlot className={classes.pillIcon}>{icon}</IconSlot>,
82
+ })}
83
+ {...(hideGlyph !== true && {
84
+ rightSection: (
85
+ <IconSlot className={classes.glyph}>
86
+ <UpDownGlyph />
87
+ </IconSlot>
88
+ ),
89
+ })}
90
+ onClick={
91
+ onClick ??
92
+ (() => {
93
+ setOpened((current) => !current)
94
+ })
95
+ }
96
+ >
97
+ {label}
98
+ </Button>
99
+ )
100
+
101
+ if (children === undefined) return target
102
+
103
+ return (
104
+ <Popover
105
+ opened={opened}
106
+ onChange={setOpened}
107
+ onDismiss={() => {
108
+ setOpened(false)
109
+ }}
110
+ position="bottom-start"
111
+ withinPortal
112
+ shadow="md"
113
+ radius="sm"
114
+ trapFocus
115
+ >
116
+ <Popover.Target>{target}</Popover.Target>
117
+ <Popover.Dropdown className={classes.dropdown}>{children}</Popover.Dropdown>
118
+ </Popover>
119
+ )
120
+ }