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
@@ -0,0 +1,93 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button, Radio, Stack } from "@mantine/core";
3
+ import classes from "./controls.module.css";
4
+ import { useFilterRegistration, useFilterSurface } from "./filter-context.js";
5
+ import { FilterPill } from "./filter-pill.js";
6
+ import { SheetField, SheetOptionList, useControlName } from "./filter-sheet.js";
7
+ function EnumFilter({
8
+ field,
9
+ label,
10
+ icon,
11
+ clearable,
12
+ numeric,
13
+ options: optionsProp
14
+ }) {
15
+ const [value, setValue] = field.use();
16
+ const surface = useFilterSurface();
17
+ const isDefault = field.isDefault(value);
18
+ useFilterRegistration(!isDefault, () => {
19
+ field.clear();
20
+ });
21
+ const options = optionsProp ?? field.options;
22
+ const selected = options.find((option) => option.value === value);
23
+ const inSheet = surface === "sheet";
24
+ const { labelId } = useControlName(label, inSheet);
25
+ if (inSheet) {
26
+ return /* @__PURE__ */ jsx(SheetField, { label, labelId, children: /* @__PURE__ */ jsx(
27
+ SheetOptionList,
28
+ {
29
+ mode: "single",
30
+ labelId,
31
+ selected: [value],
32
+ options,
33
+ onToggle: (next) => {
34
+ setValue(next);
35
+ }
36
+ }
37
+ ) });
38
+ }
39
+ const body = (
40
+ // Mantine's OWN `label` prop, not an `aria-label`: `Radio.Group` overwrites both `aria-label`
41
+ // and `aria-labelledby` with its own `aria-labelledby` pointing at the `Input.Wrapper` label it
42
+ // renders — which, with no `label` passed, points at an element that does not exist. So the only
43
+ // way to name this group is to give it the label. It doubles as the heading in both surfaces:
44
+ // the group needs the FILTER's name ('Currency'), which the pill cannot supply because the pill
45
+ // reads the VALUE ('EUR') once set.
46
+ /* @__PURE__ */ jsxs(
47
+ Radio.Group,
48
+ {
49
+ label,
50
+ classNames: { label: classes.groupLabel },
51
+ value,
52
+ onChange: (next) => {
53
+ setValue(next);
54
+ },
55
+ children: [
56
+ /* @__PURE__ */ jsx(Stack, { gap: 2, children: options.map((option) => /* @__PURE__ */ jsx(
57
+ Radio,
58
+ {
59
+ value: option.value,
60
+ label: option.label,
61
+ ...option.disabled === true && { disabled: true }
62
+ },
63
+ option.value
64
+ )) }),
65
+ clearable === true && !isDefault && /* @__PURE__ */ jsx(
66
+ Button,
67
+ {
68
+ variant: "subtle",
69
+ size: "ctl",
70
+ onClick: () => {
71
+ field.clear();
72
+ },
73
+ children: "Clear"
74
+ }
75
+ )
76
+ ]
77
+ }
78
+ )
79
+ );
80
+ return /* @__PURE__ */ jsx(
81
+ FilterPill,
82
+ {
83
+ label: selected?.label ?? label,
84
+ active: !isDefault,
85
+ ...numeric === true && { numeric: true },
86
+ ...icon !== void 0 && { icon },
87
+ children: /* @__PURE__ */ jsx("div", { className: classes.optionList, children: body })
88
+ }
89
+ );
90
+ }
91
+ export {
92
+ EnumFilter
93
+ };
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from 'react';
2
+ /** Which form a control renders. Provided by `FilterSet`; `'pill'` when there is no `FilterSet`. */
3
+ export type FilterSurface = 'pill' | 'sheet';
4
+ /**
5
+ * The form this control should render — the ONE thing a control branches on.
6
+ *
7
+ * @example
8
+ * const surface = useFilterSurface()
9
+ * if (surface === 'sheet') return <SheetField label={label}>…</SheetField>
10
+ */
11
+ export declare function useFilterSurface(): FilterSurface;
12
+ /** What one filter reports about itself, keyed internally by a `useId()` per mount. */
13
+ export type FilterRegistration = {
14
+ /** `!field.isDefault(value)` — the only input to `Filters (n)` and the pill's accent border. */
15
+ readonly isActive: boolean;
16
+ /** Writes the field back to its fallback. Called by `Reset all`. */
17
+ readonly reset: () => void;
18
+ };
19
+ type FilterRegistry = {
20
+ register(key: string, registration: FilterRegistration): void;
21
+ unregister(key: string): void;
22
+ };
23
+ export type FilterSetScopeProps = {
24
+ readonly surface: FilterSurface;
25
+ /** `null` on a second copy of the same children (the fold dropdown, the sheet). */
26
+ readonly registry: FilterRegistry | null;
27
+ readonly children: ReactNode;
28
+ };
29
+ /** Provides both contexts at once — every place FilterSet mounts children goes through this. */
30
+ export declare function FilterSetScope({ surface, registry, children }: FilterSetScopeProps): ReactNode;
31
+ /**
32
+ * Report this filter's active/reset state to the enclosing `FilterSet`. Call it unconditionally —
33
+ * it no-ops outside a registering scope (a bare filter with no `FilterSet`, the fold dropdown, the
34
+ * sheet), which is what keeps it hook-safe in every control.
35
+ *
36
+ * The reset is `field.clear()`, never `setValue(field.fallback)`: writing the fallback back is a
37
+ * WRITE, so it persists the default as if the user had chosen it — the URL keeps `?window=7d`, the
38
+ * localStorage mirror keeps its key, and a later change to the field's fallback no longer reaches
39
+ * either. `clear()` removes the value instead, on whichever lane the field is on.
40
+ *
41
+ * @example
42
+ * const [value] = field.use()
43
+ * useFilterRegistration(!field.isDefault(value), () => field.clear())
44
+ */
45
+ export declare function useFilterRegistration(isActive: boolean, reset: () => void): void;
46
+ export type FilterCensus = {
47
+ readonly registry: FilterRegistry;
48
+ /** How many registered filters are non-default — `Filters (n)`. */
49
+ readonly activeCount: number;
50
+ /** Resets every registered filter. */
51
+ readonly resetAll: () => void;
52
+ };
53
+ /**
54
+ * The registry side, for `FilterSet` itself. The map is a ref (registration happens in children's
55
+ * effects, which must not re-render the parent mid-commit) and the COUNT is state, so only a real
56
+ * change in the count re-renders the bar.
57
+ */
58
+ export declare function useFilterCensus(): FilterCensus;
59
+ export {};
60
+ //# sourceMappingURL=filter-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-context.d.ts","sourceRoot":"","sources":["../../src/controls/filter-context.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,oGAAoG;AACpG,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAA;AAI5C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAED,uFAAuF;AACvF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC7D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAID,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,mBAAmB,GAAG,SAAS,CAM9F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI,CAkBhF;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;IACjC,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAiC9C"}
@@ -0,0 +1,69 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useId,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react";
12
+ const FilterSurfaceContext = createContext("pill");
13
+ function useFilterSurface() {
14
+ return useContext(FilterSurfaceContext);
15
+ }
16
+ const FilterRegistryContext = createContext(null);
17
+ function FilterSetScope({ surface, registry, children }) {
18
+ return /* @__PURE__ */ jsx(FilterSurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(FilterRegistryContext.Provider, { value: registry, children }) });
19
+ }
20
+ function useFilterRegistration(isActive, reset) {
21
+ const registry = useContext(FilterRegistryContext);
22
+ const key = useId();
23
+ const resetRef = useRef(reset);
24
+ resetRef.current = reset;
25
+ const stableReset = useCallback(() => {
26
+ resetRef.current();
27
+ }, []);
28
+ useEffect(() => {
29
+ if (registry === null) return;
30
+ registry.register(key, { isActive, reset: stableReset });
31
+ return () => {
32
+ registry.unregister(key);
33
+ };
34
+ }, [registry, key, isActive, stableReset]);
35
+ }
36
+ function useFilterCensus() {
37
+ const entries = useRef(/* @__PURE__ */ new Map());
38
+ const [activeCount, setActiveCount] = useState(0);
39
+ const sync = useCallback(() => {
40
+ let next = 0;
41
+ for (const entry of entries.current.values()) {
42
+ if (entry.isActive) next += 1;
43
+ }
44
+ setActiveCount((current) => current === next ? current : next);
45
+ }, []);
46
+ const registry = useMemo(
47
+ () => ({
48
+ register(key, registration) {
49
+ entries.current.set(key, registration);
50
+ sync();
51
+ },
52
+ unregister(key) {
53
+ entries.current.delete(key);
54
+ sync();
55
+ }
56
+ }),
57
+ [sync]
58
+ );
59
+ const resetAll = useCallback(() => {
60
+ for (const entry of entries.current.values()) entry.reset();
61
+ }, []);
62
+ return { registry, activeCount, resetAll };
63
+ }
64
+ export {
65
+ FilterSetScope,
66
+ useFilterCensus,
67
+ useFilterRegistration,
68
+ useFilterSurface
69
+ };
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+ export type FilterPillProps = {
3
+ readonly label: string;
4
+ readonly icon?: ReactNode;
5
+ /** Non-default → accent-tinted border. Derived from `field.isDefault(value)`, never hand-set. */
6
+ readonly active?: boolean;
7
+ /** Mono label, for a numeric preset set (`7d` / `30d`) — the same law `[data-numeric]` applies
8
+ * to a numeric SegmentedControl label (`theme/segmented-control.module.css`). */
9
+ readonly numeric?: boolean;
10
+ /** Popover body. Omit for a press-to-act pill. */
11
+ readonly children?: ReactNode;
12
+ readonly onClick?: () => void;
13
+ /** Accessible name when the visible label is a bare count (`+2`). */
14
+ readonly ariaLabel?: string;
15
+ /**
16
+ * `aria-pressed` for the press-to-act mode (`ToggleFilter`) — without it the on/off state is
17
+ * carried by a border colour and nothing else. Deliberately NOT set on a popover-target pill,
18
+ * where `aria-expanded` (which Mantine's `Popover.Target` supplies) is the correct state.
19
+ */
20
+ readonly pressed?: boolean;
21
+ readonly className?: string;
22
+ /** Set by `FilterSet` on its own fold/sheet pills; a filter never passes either of these. */
23
+ readonly hideGlyph?: boolean;
24
+ /** `data-shown` — the CSS hook that reveals the `+N` fold pill once folding is needed. */
25
+ readonly shown?: boolean;
26
+ };
27
+ export declare function FilterPill({ label, icon, active, numeric, children, onClick, ariaLabel, pressed, className, hideGlyph, shown, }: FilterPillProps): ReactNode;
28
+ //# sourceMappingURL=filter-pill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-pill.d.ts","sourceRoot":"","sources":["../../src/controls/filter-pill.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;sFACkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,SAAS,EACT,OAAO,EACP,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE,eAAe,GAAG,SAAS,CA2D7B"}
@@ -0,0 +1,70 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button, Popover } from "@mantine/core";
3
+ import { useState } from "react";
4
+ import { IconSlot } from "../theme/icon-slot.js";
5
+ import classes from "./controls.module.css";
6
+ import { UpDownGlyph } from "./glyphs.js";
7
+ function FilterPill({
8
+ label,
9
+ icon,
10
+ active,
11
+ numeric,
12
+ children,
13
+ onClick,
14
+ ariaLabel,
15
+ pressed,
16
+ className,
17
+ hideGlyph,
18
+ shown
19
+ }) {
20
+ const [opened, setOpened] = useState(false);
21
+ const target = /* @__PURE__ */ jsx(
22
+ Button,
23
+ {
24
+ variant: "default",
25
+ size: "ctl",
26
+ className: className === void 0 ? classes.pill : `${classes.pill} ${className}`,
27
+ classNames: { label: classes.pillLabel },
28
+ ...active === true && { "data-active": true },
29
+ ...numeric === true && { "data-numeric": true },
30
+ ...ariaLabel !== void 0 && { "aria-label": ariaLabel },
31
+ ...pressed !== void 0 && children === void 0 && { "aria-pressed": pressed },
32
+ ...shown === true && { "data-shown": true },
33
+ ...icon !== void 0 && {
34
+ // `IconSlot` owns the 16px box and the optical centring; `.pillIcon` adds only the pill's
35
+ // muted-then-ink colour law. A consumer's glyph can no longer set this row's height.
36
+ leftSection: /* @__PURE__ */ jsx(IconSlot, { className: classes.pillIcon, children: icon })
37
+ },
38
+ ...hideGlyph !== true && {
39
+ rightSection: /* @__PURE__ */ jsx(IconSlot, { className: classes.glyph, children: /* @__PURE__ */ jsx(UpDownGlyph, {}) })
40
+ },
41
+ onClick: onClick ?? (() => {
42
+ setOpened((current) => !current);
43
+ }),
44
+ children: label
45
+ }
46
+ );
47
+ if (children === void 0) return target;
48
+ return /* @__PURE__ */ jsxs(
49
+ Popover,
50
+ {
51
+ opened,
52
+ onChange: setOpened,
53
+ onDismiss: () => {
54
+ setOpened(false);
55
+ },
56
+ position: "bottom-start",
57
+ withinPortal: true,
58
+ shadow: "md",
59
+ radius: "sm",
60
+ trapFocus: true,
61
+ children: [
62
+ /* @__PURE__ */ jsx(Popover.Target, { children: target }),
63
+ /* @__PURE__ */ jsx(Popover.Dropdown, { className: classes.dropdown, children })
64
+ ]
65
+ }
66
+ );
67
+ }
68
+ export {
69
+ FilterPill
70
+ };
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react';
2
+ export type FilterSetProps = {
3
+ readonly children: ReactNode;
4
+ /**
5
+ * How many children stay inline pills below `sm`. The rest are reachable only through the
6
+ * `Filters (n)` sheet — which always holds every child, inline ones included.
7
+ *
8
+ * @default 1
9
+ */
10
+ readonly inline?: number;
11
+ };
12
+ export declare function FilterSet({ children, inline }: FilterSetProps): ReactNode;
13
+ //# sourceMappingURL=filter-set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-set.d.ts","sourceRoot":"","sources":["../../src/controls/filter-set.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,OAAO,CAAA;AAYjD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAU,EAAE,EAAE,cAAc,GAAG,SAAS,CAoF7E"}
@@ -0,0 +1,125 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Children, isValidElement, useCallback, useEffect, useRef, useState } from "react";
3
+ import classes from "./controls.module.css";
4
+ import { FilterSetScope, useFilterCensus } from "./filter-context.js";
5
+ import { FilterPill } from "./filter-pill.js";
6
+ import { FilterSheet } from "./filter-sheet.js";
7
+ import { FunnelGlyph } from "./glyphs.js";
8
+ const FOLD_PILL_WIDTH = 52;
9
+ const FALLBACK_GAP = 6;
10
+ function FilterSet({ children, inline = 1 }) {
11
+ const items = Children.toArray(children);
12
+ const { registry, activeCount, resetAll } = useFilterCensus();
13
+ const [sheetOpened, setSheetOpened] = useState(false);
14
+ const rowRef = useRef(null);
15
+ const fit = useOverflowFit(rowRef, items.length);
16
+ const openSheet = useCallback(() => {
17
+ setSheetOpened(true);
18
+ }, []);
19
+ const closeSheet = useCallback(() => {
20
+ setSheetOpened(false);
21
+ }, []);
22
+ if (items.length === 0) return null;
23
+ const folded = items.slice(fit);
24
+ const anyFoldedOnMobile = items.length > inline;
25
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
26
+ /* @__PURE__ */ jsxs("div", { className: classes.pillRow, ref: rowRef, "data-filter-row": true, children: [
27
+ /* @__PURE__ */ jsx(FilterSetScope, { surface: "pill", registry, children: items.map((child, index) => (
28
+ // `Children.toArray` already stamped a stable `.0`/`.1`-style key on every child, so
29
+ // the slot wrapper reuses it rather than inventing an array-index key.
30
+ /* @__PURE__ */ jsx(
31
+ "div",
32
+ {
33
+ className: classes.pillSlot,
34
+ "data-filter-slot": true,
35
+ ...index < inline && { "data-inline": true },
36
+ ...index >= fit && { "data-folded": true },
37
+ children: child
38
+ },
39
+ slotKey(child, index)
40
+ )
41
+ )) }),
42
+ /* @__PURE__ */ jsx(
43
+ FilterPill,
44
+ {
45
+ label: `+${folded.length}`,
46
+ ariaLabel: `${folded.length} more filters`,
47
+ className: classes.foldPill,
48
+ hideGlyph: true,
49
+ shown: folded.length > 0,
50
+ children: /* @__PURE__ */ jsx("div", { className: classes.optionList, children: /* @__PURE__ */ jsx(FilterSetScope, { surface: "sheet", registry: null, children: folded }) })
51
+ }
52
+ ),
53
+ anyFoldedOnMobile && /* @__PURE__ */ jsx(
54
+ FilterPill,
55
+ {
56
+ label: activeCount > 0 ? `Filters (${activeCount})` : "Filters",
57
+ active: activeCount > 0,
58
+ icon: /* @__PURE__ */ jsx(FunnelGlyph, {}),
59
+ className: classes.sheetPill,
60
+ hideGlyph: true,
61
+ onClick: openSheet
62
+ }
63
+ )
64
+ ] }),
65
+ anyFoldedOnMobile && /* @__PURE__ */ jsx(FilterSheet, { opened: sheetOpened, onClose: closeSheet, onResetAll: resetAll, children: /* @__PURE__ */ jsx(FilterSetScope, { surface: "sheet", registry: null, children: items }) })
66
+ ] });
67
+ }
68
+ function useOverflowFit(rowRef, count) {
69
+ const [fit, setFit] = useState(count);
70
+ const widths = useRef([]);
71
+ useEffect(() => {
72
+ const row = rowRef.current;
73
+ if (row === null) return;
74
+ if (typeof ResizeObserver === "undefined") {
75
+ setFit(count);
76
+ return;
77
+ }
78
+ const measure = () => {
79
+ const slots = [...row.querySelectorAll("[data-filter-slot]")];
80
+ slots.forEach((slot, index) => {
81
+ const width = slot.offsetWidth;
82
+ if (width > 0) widths.current[index] = width;
83
+ });
84
+ const available = row.clientWidth;
85
+ if (available <= 0) {
86
+ setFit(count);
87
+ return;
88
+ }
89
+ const gap = readGap(row);
90
+ const bare = fitCount(widths.current, count, gap, available, 0);
91
+ setFit(
92
+ bare === count ? count : fitCount(widths.current, count, gap, available, FOLD_PILL_WIDTH)
93
+ );
94
+ };
95
+ measure();
96
+ const observer = new ResizeObserver(measure);
97
+ observer.observe(row);
98
+ for (const slot of row.querySelectorAll("[data-filter-slot]")) {
99
+ observer.observe(slot);
100
+ }
101
+ return () => {
102
+ observer.disconnect();
103
+ };
104
+ }, [rowRef, count]);
105
+ return fit;
106
+ }
107
+ function fitCount(widths, count, gap, available, reserve) {
108
+ let used = reserve === 0 ? 0 : reserve + gap;
109
+ for (let index = 0; index < count; index += 1) {
110
+ used += (index === 0 ? 0 : gap) + (widths[index] ?? 0);
111
+ if (used > available) return index;
112
+ }
113
+ return count;
114
+ }
115
+ function slotKey(child, index) {
116
+ if (isValidElement(child) && child.key !== null) return child.key;
117
+ return String(index);
118
+ }
119
+ function readGap(row) {
120
+ const parsed = Number.parseFloat(getComputedStyle(row).columnGap);
121
+ return Number.isFinite(parsed) ? parsed : FALLBACK_GAP;
122
+ }
123
+ export {
124
+ FilterSet
125
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=filter-set.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-set.test.d.ts","sourceRoot":"","sources":["../../src/controls/filter-set.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,117 @@
1
+ import type { ReactNode } from 'react';
2
+ export type FilterSheetProps = {
3
+ readonly opened: boolean;
4
+ readonly onClose: () => void;
5
+ readonly onResetAll: () => void;
6
+ readonly title?: string;
7
+ readonly children: ReactNode;
8
+ };
9
+ /**
10
+ * `Reset all` rides the HEADER, beside the title and the close ×, not a footer.
11
+ *
12
+ * A bottom drawer's footer is the one region a phone cannot promise is on screen: the sheet grows
13
+ * with its content, so with six filters open the reset button sat below the fold and the only way to
14
+ * reach it was to scroll past every row it would have undone. The header is fixed by construction.
15
+ * It also removes the sheet's last horizontal rule — a footer needs separating from the rows above
16
+ * it, a header does not (`docs/CONTROLS-SPEC.md` §2.1: hairlines between OPTION rows only).
17
+ */
18
+ export declare function FilterSheet({ opened, onClose, onResetAll, title, children, }: FilterSheetProps): ReactNode;
19
+ /** One row of a {@link SheetOptionList}. */
20
+ export type SheetOption = {
21
+ readonly value: string;
22
+ readonly label: string;
23
+ readonly disabled?: boolean;
24
+ };
25
+ export type SheetOptionListProps = {
26
+ /** `single` renders radio semantics, `multi` checkbox semantics. */
27
+ readonly mode: 'single' | 'multi';
28
+ /** The selected value(s) — a one-element array in `single` mode. */
29
+ readonly selected: readonly string[];
30
+ readonly options: readonly SheetOption[];
31
+ readonly onToggle: (value: string) => void;
32
+ /** The `SheetField` heading this list is named by. */
33
+ readonly labelId: string;
34
+ };
35
+ /**
36
+ * The sheet's ONE list form (`docs/CONTROLS-SPEC.md` §2.1): every option is a 44px full-width row,
37
+ * label left, a check at the trailing edge when selected, a 1px hairline between rows and nowhere
38
+ * else.
39
+ *
40
+ * **It replaced a `SegmentedControl` and a Mantine `Radio` list, for two different reasons.** The
41
+ * range presets rendered as a `orientation="vertical"` SegmentedControl, which in a full-width sheet
42
+ * is a stack of left-aligned labels inside a tinted track — it read as a broken control, not as a
43
+ * choice, and it carried the track's own inset so it aligned with nothing else in the sheet. The
44
+ * enum/multi filters rendered Mantine `Radio`/`Checkbox` rows, which put a 20px dot or box in the
45
+ * leading position of every row; at six filters the sheet was a column of controls rather than a
46
+ * column of options, and the leading marks pushed every label off the sheet's own text column.
47
+ *
48
+ * The semantics are a real native `<input>` inside the row's `<label>`, visually hidden — not
49
+ * `role="radio"` on a div. A hidden native input keeps the arrow-key group behaviour, the
50
+ * form-association and the announced state for free, and `<label>` wrapping it makes the whole 44px
51
+ * box the hit target (C15) without a single `onClick` on a non-interactive element.
52
+ */
53
+ export declare function SheetOptionList({ mode, selected, options, onToggle, labelId, }: SheetOptionListProps): ReactNode;
54
+ export type SheetDisclosureProps = {
55
+ readonly label: string;
56
+ readonly expanded: boolean;
57
+ readonly onToggle: () => void;
58
+ readonly children: ReactNode;
59
+ };
60
+ /**
61
+ * A 44px row that expands its own body inline — the sheet's answer to a control too large to sit in
62
+ * the list unconditionally (`RangeFilter`'s custom date picker, which is a whole calendar). Same row
63
+ * geometry as a {@link SheetOptionList} option, so the disclosure reads as the list's last entry
64
+ * rather than as a different kind of thing.
65
+ */
66
+ export declare function SheetDisclosure({ label, expanded, onToggle, children, }: SheetDisclosureProps): ReactNode;
67
+ export type SheetFieldProps = {
68
+ readonly label: string;
69
+ /** Stamped on the heading so a control can point `aria-labelledby` at it — see `useControlName`. */
70
+ readonly labelId?: string;
71
+ readonly children: ReactNode;
72
+ };
73
+ /** A labelled block inside the sheet — the sheet form's outer shell for every control. */
74
+ export declare function SheetField({ label, labelId, children }: SheetFieldProps): ReactNode;
75
+ /**
76
+ * Names a control that has no VISIBLE Mantine `label`, and does it differently per surface — which
77
+ * is the whole reason this is one helper instead of an `aria-label` sprinkled per call site.
78
+ *
79
+ * In the SHEET the visible `SheetField` heading is right there, so the control points at it
80
+ * (`aria-labelledby`) and the heading stops being decoration. In the PILL surface there is no
81
+ * visible name to point at — the pill text is the VALUE (`EUR`, `30d`, `3 channels`) — so the
82
+ * control carries the filter's own name instead. Either way the announced name is what is being
83
+ * filtered, not what it currently holds.
84
+ *
85
+ * The id is generated by the CONTROL, not by `SheetField`, because the control renders its own
86
+ * `SheetField`: an id published downward through context could never reach the sibling that needs it.
87
+ *
88
+ * @example
89
+ * const { labelId, nameProps } = useControlName(label, inSheet)
90
+ * <Radio.Group {...nameProps} …/>
91
+ * // and, in the sheet branch: <SheetField label={label} labelId={labelId}>…</SheetField>
92
+ */
93
+ export declare function useControlName(label: string, inSheet: boolean): {
94
+ labelId: string;
95
+ nameProps: {
96
+ 'aria-labelledby': string;
97
+ } | {
98
+ 'aria-label': string;
99
+ };
100
+ };
101
+ export type SheetRowProps = {
102
+ readonly children: ReactNode;
103
+ };
104
+ /** One 44px full-width row (`--vx-space-sheet-row-height`). */
105
+ export declare function SheetRow({ children }: SheetRowProps): ReactNode;
106
+ /** The class every sheet-form list item shares, for controls that render their own row element. */
107
+ export declare const sheetRowClass: string;
108
+ /**
109
+ * `classNames` for a Radio/Checkbox/Switch rendered as a sheet row — stretches its `body` and
110
+ * `<label>` across the full 44px so the whole row is the touch target, not the middle 20px of it.
111
+ */
112
+ export declare const sheetRowClassNames: {
113
+ readonly root: string;
114
+ readonly body: string;
115
+ readonly label: string;
116
+ };
117
+ //# sourceMappingURL=filter-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-sheet.d.ts","sourceRoot":"","sources":["../../src/controls/filter-sheet.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,IAAI,CAAA;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,UAAU,EACV,KAAiB,EACjB,QAAQ,GACT,EAAE,gBAAgB,GAAG,SAAS,CAoB9B;AAqBD,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAA;IACjC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,OAAO,GACR,EAAE,oBAAoB,GAAG,SAAS,CA+ClC;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,SAAS,CAoBlC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,oGAAoG;IACpG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,SAAS,CASnF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf;IACD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE;QAAE,iBAAiB,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAA;CACpE,CAMA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,GAAG,SAAS,CAE/D;AAED,mGAAmG;AACnG,eAAO,MAAM,aAAa,QAAmB,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAIrB,CAAA"}