basalt-ui 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +25 -0
  3. package/MIGRATING.md +547 -21
  4. package/README.md +46 -33
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/query-state.d.ts +1 -1
  134. package/dist/dashboard/settings-section.d.ts +6 -4
  135. package/dist/dashboard/settings-section.d.ts.map +1 -1
  136. package/dist/dashboard/settings-section.js +23 -16
  137. package/dist/dashboard/settings-section.module.css +10 -0
  138. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  139. package/dist/dashboard/settings-section.test.d.ts +2 -0
  140. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  141. package/dist/dashboard/stat-card.d.ts +44 -9
  142. package/dist/dashboard/stat-card.d.ts.map +1 -1
  143. package/dist/dashboard/stat-card.js +80 -48
  144. package/dist/dashboard/stat-card.module.css +82 -0
  145. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  146. package/dist/data/data-table.d.ts +102 -20
  147. package/dist/data/data-table.d.ts.map +1 -1
  148. package/dist/data/data-table.js +166 -65
  149. package/dist/data/table.d.ts +1 -1
  150. package/dist/data/table.d.ts.map +1 -1
  151. package/dist/guard/grace.test.d.ts +20 -0
  152. package/dist/guard/grace.test.d.ts.map +1 -0
  153. package/dist/guard/index.d.ts +66 -3
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +82 -7
  156. package/dist/guard/types.d.ts +3 -3
  157. package/dist/guard/types.d.ts.map +1 -1
  158. package/dist/index.d.ts +12 -2
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +13 -10
  161. package/dist/nav/types.d.ts +78 -2
  162. package/dist/nav/types.d.ts.map +1 -1
  163. package/dist/notifications/bell.d.ts +4 -1
  164. package/dist/notifications/bell.d.ts.map +1 -1
  165. package/dist/notifications/index.d.ts +1 -1
  166. package/dist/router-tanstack/field.d.ts +14 -0
  167. package/dist/router-tanstack/field.d.ts.map +1 -0
  168. package/dist/router-tanstack/field.js +4 -0
  169. package/dist/router-tanstack/index.d.ts +6 -2
  170. package/dist/router-tanstack/index.d.ts.map +1 -1
  171. package/dist/router-tanstack/index.js +4 -0
  172. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  173. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  175. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  176. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  177. package/dist/router-tanstack/search-param-store.js +16 -45
  178. package/dist/router-tanstack/search-store.d.ts +81 -0
  179. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.js +187 -0
  181. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  182. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  183. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  184. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  185. package/dist/router-tanstack/use-nav.d.ts +0 -2
  186. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  187. package/dist/router-tanstack/use-nav.js +34 -12
  188. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  189. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  190. package/dist/section/index.d.ts +2 -0
  191. package/dist/section/index.d.ts.map +1 -0
  192. package/dist/section/index.js +4 -0
  193. package/dist/section/section.d.ts +24 -0
  194. package/dist/section/section.d.ts.map +1 -0
  195. package/dist/section/section.js +126 -0
  196. package/dist/section/section.module.css +104 -0
  197. package/dist/section/section.module.css.d.ts +11 -0
  198. package/dist/section/section.test.d.ts +2 -0
  199. package/dist/section/section.test.d.ts.map +1 -0
  200. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  201. package/dist/shell/app-breadcrumbs.js +18 -15
  202. package/dist/shell/app-header.module.css +77 -37
  203. package/dist/shell/app-header.module.css.d.ts +2 -1
  204. package/dist/shell/app-mobile-nav.d.ts +9 -2
  205. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  206. package/dist/shell/app-mobile-nav.js +98 -11
  207. package/dist/shell/app-mobile-nav.module.css +8 -0
  208. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  209. package/dist/shell/app-sidebar.d.ts +55 -28
  210. package/dist/shell/app-sidebar.d.ts.map +1 -1
  211. package/dist/shell/app-sidebar.js +116 -67
  212. package/dist/shell/app-sidebar.module.css +303 -20
  213. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  214. package/dist/shell/index.d.ts +55 -21
  215. package/dist/shell/index.d.ts.map +1 -1
  216. package/dist/shell/index.js +40 -18
  217. package/dist/shell/mobile-nav-model.d.ts +13 -2
  218. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  219. package/dist/shell/mobile-nav-model.js +15 -1
  220. package/dist/shell/nav-count-badge.d.ts +1 -1
  221. package/dist/shell/page-bar.d.ts +51 -0
  222. package/dist/shell/page-bar.d.ts.map +1 -0
  223. package/dist/shell/page-bar.js +154 -0
  224. package/dist/shell/page-bar.module.css +178 -0
  225. package/dist/shell/page-bar.module.css.d.ts +17 -0
  226. package/dist/shell/page-bar.test.d.ts +2 -0
  227. package/dist/shell/page-bar.test.d.ts.map +1 -0
  228. package/dist/shell/sidebar-block-model.d.ts +48 -0
  229. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  230. package/dist/shell/sidebar-block-model.js +39 -0
  231. package/dist/shell/sidebar-blocks.d.ts +32 -0
  232. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  233. package/dist/shell/sidebar-blocks.js +169 -0
  234. package/dist/shell/sidebar-search.d.ts +13 -1
  235. package/dist/shell/sidebar-search.d.ts.map +1 -1
  236. package/dist/shell/sidebar-search.js +34 -1
  237. package/dist/shell/sidebar-search.module.css +26 -0
  238. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  239. package/dist/state/fields.d.ts +392 -0
  240. package/dist/state/fields.d.ts.map +1 -0
  241. package/dist/state/fields.js +369 -0
  242. package/dist/state/fields.test.d.ts +2 -0
  243. package/dist/state/fields.test.d.ts.map +1 -0
  244. package/dist/state/persisted.d.ts +48 -0
  245. package/dist/state/persisted.d.ts.map +1 -0
  246. package/dist/state/persisted.js +105 -0
  247. package/dist/state.d.ts +11 -56
  248. package/dist/state.d.ts.map +1 -1
  249. package/dist/state.js +14 -120
  250. package/dist/styles.css +13 -0
  251. package/dist/surfaces.d.ts +106 -24
  252. package/dist/surfaces.d.ts.map +1 -1
  253. package/dist/surfaces.js +155 -19
  254. package/dist/theme/controls.module.css +16 -1
  255. package/dist/theme/ctl-theme.d.ts +32 -0
  256. package/dist/theme/ctl-theme.d.ts.map +1 -0
  257. package/dist/theme/ctl-theme.js +54 -0
  258. package/dist/theme/ctl-theme.test.d.ts +2 -0
  259. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  260. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  261. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  262. package/dist/theme/divider-law.test.d.ts +2 -0
  263. package/dist/theme/divider-law.test.d.ts.map +1 -0
  264. package/dist/theme/icon-slot.d.ts +44 -0
  265. package/dist/theme/icon-slot.d.ts.map +1 -0
  266. package/dist/theme/icon-slot.js +21 -0
  267. package/dist/theme/icon-slot.module.css +47 -0
  268. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  269. package/dist/theme/icon-slot.test.d.ts +2 -0
  270. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  271. package/dist/theme/index.d.ts +1 -0
  272. package/dist/theme/index.d.ts.map +1 -1
  273. package/dist/theme/index.js +82 -5
  274. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  275. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  276. package/dist/theme/segmented-control.module.css +20 -0
  277. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  278. package/dist/tokens/index.d.ts +12 -0
  279. package/dist/tokens/index.d.ts.map +1 -1
  280. package/dist/tokens/index.js +38 -8
  281. package/dist/tokens/palette.d.ts +97 -61
  282. package/dist/tokens/palette.d.ts.map +1 -1
  283. package/dist/tokens/palette.js +76 -38
  284. package/dist/tokens.css +10 -2
  285. package/dist/widget-header/delta-badge.d.ts +13 -0
  286. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  287. package/dist/widget-header/delta-badge.js +29 -0
  288. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  289. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  290. package/dist/widget-header/index.d.ts +3 -0
  291. package/dist/widget-header/index.d.ts.map +1 -0
  292. package/dist/widget-header/index.js +6 -0
  293. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  294. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  295. package/dist/widget-header/widget-header.d.ts +37 -0
  296. package/dist/widget-header/widget-header.d.ts.map +1 -0
  297. package/dist/widget-header/widget-header.js +77 -0
  298. package/dist/widget-header/widget-header.module.css +335 -0
  299. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  300. package/dist/widget-header/widget-header.test.d.ts +2 -0
  301. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  302. package/llms.txt +30 -17
  303. package/package.json +14 -1
  304. package/src/charts/cursor/resolve.test.ts +29 -1
  305. package/src/charts/cursor/resolve.ts +38 -6
  306. package/src/charts/cursor/scope.tsx +6 -3
  307. package/src/charts/cursor/store.test.ts +19 -1
  308. package/src/charts/cursor/store.ts +10 -5
  309. package/src/charts/hooks/useChartCursor.ts +16 -8
  310. package/src/charts/index.ts +2 -2
  311. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  312. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  313. package/src/charts/primitives/ChartCard.tsx +86 -146
  314. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  315. package/src/cli/check-coverage.test.ts +297 -0
  316. package/src/cli/check-theme.test.ts +38 -14
  317. package/src/cli/index.ts +392 -16
  318. package/src/cli/init-sync.test.ts +97 -0
  319. package/src/connectivity/connectivity-indicator.tsx +16 -9
  320. package/src/content/index.ts +16 -3
  321. package/src/content/prose.module.css +7 -28
  322. package/src/content/prose.module.css.test.ts +8 -24
  323. package/src/controls/actions.module.css +62 -0
  324. package/src/controls/actions.module.css.d.ts +13 -0
  325. package/src/controls/actions.test.tsx +252 -0
  326. package/src/controls/actions.tsx +655 -0
  327. package/src/controls/actions.type-guard.test.ts +63 -0
  328. package/src/controls/compare-filter.tsx +64 -0
  329. package/src/controls/control-group.test.tsx +229 -0
  330. package/src/controls/control-group.tsx +63 -0
  331. package/src/controls/controls.module.css +542 -0
  332. package/src/controls/controls.module.css.d.ts +35 -0
  333. package/src/controls/controls.router.test.tsx +637 -0
  334. package/src/controls/enum-filter.tsx +159 -0
  335. package/src/controls/filter-context.tsx +153 -0
  336. package/src/controls/filter-pill.tsx +120 -0
  337. package/src/controls/filter-set.test.tsx +472 -0
  338. package/src/controls/filter-set.tsx +225 -0
  339. package/src/controls/filter-sheet.tsx +285 -0
  340. package/src/controls/glyphs.tsx +81 -0
  341. package/src/controls/index.ts +48 -0
  342. package/src/controls/multi-select-filter.tsx +140 -0
  343. package/src/controls/peer-boundary.test.ts +85 -0
  344. package/src/controls/props-contract.test.tsx +106 -0
  345. package/src/controls/range-filter.tsx +184 -0
  346. package/src/controls/search-filter.tsx +123 -0
  347. package/src/controls/select-filter.tsx +89 -0
  348. package/src/controls/sync-button.module.css +63 -0
  349. package/src/controls/sync-button.module.css.d.ts +11 -0
  350. package/src/controls/sync-button.test.tsx +183 -0
  351. package/src/controls/sync-button.tsx +197 -0
  352. package/src/controls/toggle-filter.tsx +60 -0
  353. package/src/controls/view-tabs.tsx +110 -0
  354. package/src/controls-dates/date-range-picker.tsx +42 -0
  355. package/src/controls-dates/index.ts +8 -0
  356. package/src/dashboard/delta-badge.tsx +4 -74
  357. package/src/dashboard/query-state.tsx +1 -1
  358. package/src/dashboard/settings-section.module.css +10 -0
  359. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  360. package/src/dashboard/settings-section.test.tsx +56 -0
  361. package/src/dashboard/settings-section.tsx +33 -30
  362. package/src/dashboard/stat-card.module.css +82 -0
  363. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  364. package/src/dashboard/stat-card.test.tsx +67 -1
  365. package/src/dashboard/stat-card.tsx +209 -68
  366. package/src/data/data-table.test.tsx +282 -6
  367. package/src/data/data-table.tsx +429 -94
  368. package/src/data/table.ts +2 -0
  369. package/src/guard/check-source.test.ts +126 -13
  370. package/src/guard/grace.test.ts +149 -0
  371. package/src/guard/index.ts +172 -29
  372. package/src/guard/types.ts +4 -2
  373. package/src/index.ts +46 -5
  374. package/src/nav/types.ts +84 -2
  375. package/src/notifications/bell.tsx +6 -3
  376. package/src/notifications/index.ts +1 -1
  377. package/src/router-tanstack/field.ts +31 -0
  378. package/src/router-tanstack/index.ts +24 -2
  379. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  380. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  381. package/src/router-tanstack/search-param-store.ts +42 -159
  382. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  383. package/src/router-tanstack/search-store.test.ts +418 -0
  384. package/src/router-tanstack/search-store.ts +380 -0
  385. package/src/router-tanstack/use-nav.test.tsx +187 -0
  386. package/src/router-tanstack/use-nav.ts +72 -14
  387. package/src/section/index.ts +1 -0
  388. package/src/section/section.module.css +104 -0
  389. package/src/section/section.module.css.d.ts +11 -0
  390. package/src/section/section.test.tsx +204 -0
  391. package/src/section/section.tsx +220 -0
  392. package/src/shell/app-breadcrumbs.tsx +44 -31
  393. package/src/shell/app-header.module.css +77 -37
  394. package/src/shell/app-header.module.css.d.ts +2 -1
  395. package/src/shell/app-mobile-nav.module.css +8 -0
  396. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  397. package/src/shell/app-mobile-nav.tsx +147 -8
  398. package/src/shell/app-sidebar-account.tsx +2 -2
  399. package/src/shell/app-sidebar.module.css +303 -20
  400. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  401. package/src/shell/app-sidebar.test.tsx +456 -41
  402. package/src/shell/app-sidebar.tsx +276 -140
  403. package/src/shell/index.test.tsx +291 -5
  404. package/src/shell/index.tsx +137 -38
  405. package/src/shell/mobile-nav-model.test.ts +228 -7
  406. package/src/shell/mobile-nav-model.ts +53 -3
  407. package/src/shell/nav-count-badge.tsx +1 -1
  408. package/src/shell/page-bar.module.css +178 -0
  409. package/src/shell/page-bar.module.css.d.ts +17 -0
  410. package/src/shell/page-bar.test.tsx +316 -0
  411. package/src/shell/page-bar.tsx +315 -0
  412. package/src/shell/sidebar-block-model.ts +83 -0
  413. package/src/shell/sidebar-blocks.tsx +301 -0
  414. package/src/shell/sidebar-search.module.css +26 -0
  415. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  416. package/src/shell/sidebar-search.tsx +71 -2
  417. package/src/state/fields.test.tsx +258 -0
  418. package/src/state/fields.ts +892 -0
  419. package/src/state/persisted.ts +211 -0
  420. package/src/state.ts +46 -240
  421. package/src/styles.css +13 -0
  422. package/src/surfaces.ts +203 -36
  423. package/src/theme/controls.module.css +16 -1
  424. package/src/theme/ctl-theme.test.tsx +275 -0
  425. package/src/theme/ctl-theme.tsx +138 -0
  426. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  427. package/src/theme/density-relations.test.ts +106 -51
  428. package/src/theme/divider-law.test.ts +259 -0
  429. package/src/theme/icon-slot.module.css +47 -0
  430. package/src/theme/icon-slot.module.css.d.ts +8 -0
  431. package/src/theme/icon-slot.test.tsx +275 -0
  432. package/src/theme/icon-slot.tsx +59 -0
  433. package/src/theme/index.ts +165 -3
  434. package/src/theme/layout-rhythm-css.test.ts +72 -0
  435. package/src/theme/radius.test.ts +11 -1
  436. package/src/theme/segmented-control.module.css +20 -0
  437. package/src/theme/shadow-surfaces.test.ts +20 -7
  438. package/src/theme/spacing.test.ts +75 -27
  439. package/src/theme/use-basalt-spacing.ts +2 -2
  440. package/src/tokens/density.test.ts +7 -25
  441. package/src/tokens/index.ts +48 -8
  442. package/src/tokens/palette.ts +107 -73
  443. package/src/widget-header/delta-badge.test.tsx +60 -0
  444. package/src/widget-header/delta-badge.tsx +57 -0
  445. package/src/widget-header/index.ts +2 -0
  446. package/src/widget-header/no-mantine.test.ts +33 -0
  447. package/src/widget-header/widget-header.module.css +335 -0
  448. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  449. package/src/widget-header/widget-header.test.tsx +187 -0
  450. package/src/widget-header/widget-header.tsx +168 -0
  451. package/agent/rules/basalt-agent.md +0 -452
  452. package/agent/rules/basalt-app.md +0 -246
  453. package/agent/rules/basalt-commands.md +0 -320
  454. package/agent/rules/basalt-content.md +0 -409
  455. package/agent/rules/basalt-data.md +0 -269
  456. package/agent/rules/basalt-forms.md +0 -186
  457. package/agent/rules/basalt-notifications.md +0 -264
  458. package/agent/rules/basalt-query.md +0 -187
  459. package/agent/rules/basalt-router.md +0 -336
  460. package/dist/content/article-filter-bar.d.ts +0 -40
  461. package/dist/content/article-filter-bar.d.ts.map +0 -1
  462. package/dist/content/article-filter-bar.js +0 -56
  463. package/dist/shell/page-header.d.ts +0 -13
  464. package/dist/shell/page-header.d.ts.map +0 -1
  465. package/dist/shell/page-header.js +0 -23
  466. package/src/content/article-filter-bar.tsx +0 -88
  467. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Compile-time proof that law C6's "exactly one primary" is a TYPE fact, not a runtime check.
3
+ *
4
+ * `ActionGroupProps.primary` is singular, so a second one is unwritable: an array is rejected, and
5
+ * a duplicate object key is a tsc error in its own right. This mirrors the
6
+ * `apps/playground/src/*.type-guard.ts` convention (one `@ts-expect-error` per bad line, proven by
7
+ * `tsc --noEmit`) but lives beside the type, exactly as `agent-chat/virtualize.type-guard.test.ts`
8
+ * does — and for the same packaging reason: a bare `*.type-guard.ts` under `src/` would be picked
9
+ * up by tsup's build glob and ship an unused fixture, while `*.test.ts` is excluded from it.
10
+ */
11
+ import { describe, expect, test } from 'bun:test'
12
+ import type { ActionGroupProps, BarAction, GlobalAction } from './actions'
13
+
14
+ function accept(props: ActionGroupProps): ActionGroupProps {
15
+ return props
16
+ }
17
+
18
+ function acceptAction(action: BarAction): BarAction {
19
+ return action
20
+ }
21
+
22
+ function acceptGlobal(action: GlobalAction): GlobalAction {
23
+ return action
24
+ }
25
+
26
+ // ── Valid — must type-check with no error ─────────────────────────────────────
27
+
28
+ accept({})
29
+ accept({ primary: { key: 'a', label: 'A' } })
30
+ accept({ primary: { key: 'a', label: 'A' }, secondary: [{ key: 'b', label: 'B' }] })
31
+ accept({ secondary: [{ key: 'm', kind: 'menu', label: 'Export as', items: [] }] })
32
+ accept({ primary: { key: 'c', kind: 'custom', node: null } })
33
+ acceptAction({ key: 'a', label: 'A', mobile: 'hidden', danger: true })
34
+ acceptGlobal({ key: 'g', node: null, mobile: 'bar' })
35
+
36
+ // ── Invalid — each MUST be a tsc error, one directive per bad line ────────────
37
+
38
+ const TWO: BarAction[] = [
39
+ { key: 'a', label: 'A' },
40
+ { key: 'b', label: 'B' },
41
+ ]
42
+ // @ts-expect-error a SECOND primary is unwritable: `primary` is one action, never a list
43
+ accept({ primary: TWO })
44
+ // @ts-expect-error `secondary` is the list; a single action is not one
45
+ accept({ secondary: { key: 'b', label: 'B' } })
46
+ // @ts-expect-error a `kind: 'menu'` group has no mobile placement — it always folds
47
+ acceptAction({ key: 'm', kind: 'menu', label: 'M', items: [], mobile: 'bar' })
48
+ // @ts-expect-error a `kind: 'custom'` action carries a node, never a label
49
+ acceptAction({ key: 'c', kind: 'custom', node: null, label: 'C' })
50
+ // @ts-expect-error `label` is required on a button-shaped action
51
+ acceptAction({ key: 'a' })
52
+ // @ts-expect-error 'sheet' is not a mobile placement
53
+ acceptAction({ key: 'a', label: 'A', mobile: 'sheet' })
54
+ // @ts-expect-error a GlobalAction is data, not a bare node
55
+ acceptGlobal({ node: null })
56
+
57
+ describe('ActionGroupProps (type-guard)', () => {
58
+ test('is a compile-time-only fixture — see the @ts-expect-error directives above', () => {
59
+ // No runtime behavior to assert: the proof is that this file type-checks at all. A single
60
+ // trivial assertion keeps this a normal bun:test file rather than an empty-suite oddity.
61
+ expect(true).toBe(true)
62
+ })
63
+ })
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `CompareFilter` — the period-comparison enum every dashboard grows
3
+ * (`docs/CONTROLS-SPEC.md` §3). A `SelectFilter` narrowed to the three values basalt names, so the
4
+ * label, the option order and the fallback are the same on every page instead of three per app.
5
+ *
6
+ * @example
7
+ * // compare: field.enum(['none', 'previous', 'year'], 'none')
8
+ * <CompareFilter field={analytics.field.compare} />
9
+ */
10
+ import type { ReactNode } from 'react'
11
+ import type { EnumField, FieldHandle } from '../state'
12
+ import { EnumFilter } from './enum-filter'
13
+ import { SwapGlyph } from './glyphs'
14
+
15
+ /** The three comparison bases — declare the field as `field.enum(COMPARE_VALUES, 'none')`. */
16
+ export type CompareValue = 'none' | 'previous' | 'year'
17
+
18
+ /** The declared value order, so a consumer's `field.enum` and this control cannot disagree. */
19
+ export const COMPARE_VALUES: readonly CompareValue[] = ['none', 'previous', 'year']
20
+
21
+ /**
22
+ * The three option labels basalt names — the other half of "the same on every page instead of three
23
+ * per app", which this control promised and did not deliver.
24
+ *
25
+ * `field.enum` labels an option with the raw VALUE until a consumer calls `store.labels()`, so a
26
+ * `CompareFilter` over an unlabelled field printed `none` / `previous` / `year` in its popover and,
27
+ * now that the pill reads the value (see `EnumFilter`'s doc), in the bar too. Every app then wrote
28
+ * the same three strings into its own `labels()` call, which is three chances to disagree about what
29
+ * `year` means.
30
+ *
31
+ * These are DEFAULTS, not overrides: a field whose label differs from its value has been labelled
32
+ * deliberately, and that label wins. Detecting it as `label !== value` is exactly the signal
33
+ * `optionsFor` leaves behind (`state/fields.ts` — `label: map?.[value] ?? value`), so nothing new
34
+ * has to be threaded through the store to ask the question.
35
+ */
36
+ export const COMPARE_LABELS: Readonly<Record<CompareValue, string>> = {
37
+ none: 'No comparison',
38
+ previous: 'Previous period',
39
+ year: 'Same period last year',
40
+ }
41
+
42
+ export type CompareFilterProps = {
43
+ readonly field: FieldHandle<EnumField<CompareValue>>
44
+ /** @default 'Compare' */
45
+ readonly label?: string
46
+ /** Leading pill icon. Defaults to an arrows-swap glyph — like `RangeFilter`'s calendar, the mark
47
+ * is part of what makes the pill legible as a period comparison. */
48
+ readonly icon?: ReactNode
49
+ }
50
+
51
+ export function CompareFilter({ field, label = 'Compare', icon }: CompareFilterProps): ReactNode {
52
+ // Read through the handle's own options so a consumer's `labels()` and its option ORDER both
53
+ // survive; only an UNLABELLED row (label === value) takes basalt's default string.
54
+ const options = field.options.map((option) =>
55
+ option.label === option.value && isCompareValue(option.value)
56
+ ? { value: option.value, label: COMPARE_LABELS[option.value] }
57
+ : option,
58
+ )
59
+ return <EnumFilter field={field} label={label} icon={icon ?? <SwapGlyph />} options={options} />
60
+ }
61
+
62
+ function isCompareValue(value: string): value is CompareValue {
63
+ return (COMPARE_VALUES as readonly string[]).includes(value)
64
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * `ControlGroup` — the joined-control primitive (`docs/CONTROLS-SPEC.md` §3), and `ActionGroup`'s
3
+ * derived use of it.
4
+ *
5
+ * The gap it closes: every consumer that wanted a `‹ Today ›` stepper hand-built one — a `Group
6
+ * gap={0}` plus per-child `radius` and `style={{ marginLeft: -1 }}`, restated on each page, each
7
+ * getting the focus-ring z-index wrong in its own way. `basalt/shadow-basalt-export` now names the
8
+ * three words that fork tends to arrive under (`ButtonGroup`, `ButtonRow`, `JoinedButtons`).
9
+ *
10
+ * The GEOMETRY is CSS, and CSS-module hashes are unavailable under `bun test`, so it is asserted
11
+ * from the module text (the idiom `filter-set.test.tsx` and `theme/layout-rhythm-css.test.ts` use).
12
+ * The PARTITION — which adjacent actions get joined — is real logic and is rendered.
13
+ */
14
+ import { ActionIcon, Button, MantineProvider } from '@mantine/core'
15
+ import { render } from '@testing-library/react'
16
+ import { describe, expect, test } from 'bun:test'
17
+ import { readFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import type { ReactNode } from 'react'
20
+ import { ActionGroup } from './actions'
21
+ import type { BarAction } from './actions'
22
+ import { ControlGroup } from './control-group'
23
+
24
+ const CSS = readFileSync(join(import.meta.dir, 'controls.module.css'), 'utf8')
25
+ const JOINED = CSS.slice(CSS.indexOf('/* ── ControlGroup'))
26
+
27
+ function mount(node: ReactNode) {
28
+ return render(<MantineProvider>{node}</MantineProvider>)
29
+ }
30
+
31
+ const desktop = () => document.querySelector('.mantine-visible-from-sm')
32
+ const mobile = () => document.querySelector('.mantine-hidden-from-sm')
33
+
34
+ /** The joined boxes inside one variant of the row. */
35
+ function groups(root: Element | null): Element[] {
36
+ return [...(root?.querySelectorAll('[data-gap]') ?? [])]
37
+ }
38
+
39
+ function Glyph(): ReactNode {
40
+ return <svg viewBox="0 0 24 24" />
41
+ }
42
+
43
+ describe('the joined box', () => {
44
+ test('renders one element carrying the gap mode, defaulting to "none"', () => {
45
+ const { container } = mount(
46
+ <ControlGroup>
47
+ <Button variant="default">Today</Button>
48
+ </ControlGroup>,
49
+ )
50
+ expect(container.querySelector('[data-gap]')?.getAttribute('data-gap')).toBe('none')
51
+ })
52
+
53
+ test('gap="tight" is the un-joined mode, for a set that must stay separately pressable', () => {
54
+ const { container } = mount(
55
+ <ControlGroup gap="tight">
56
+ <Button variant="default">A</Button>
57
+ <Button variant="default">B</Button>
58
+ </ControlGroup>,
59
+ )
60
+ expect(container.querySelector('[data-gap]')?.getAttribute('data-gap')).toBe('tight')
61
+ expect(JOINED).toContain("[data-gap='tight'] {\n gap: var(--vx-space-control-gap")
62
+ })
63
+
64
+ test('joined children collapse their two adjacent borders into one', () => {
65
+ // 2px down the middle reads as a gap someone forgot to close, not as a divider.
66
+ expect(JOINED).toContain("[data-gap='none'] > * + * {\n margin-inline-start: -1px;")
67
+ })
68
+
69
+ test('the radius lives on the outer ends only — that is what makes three boxes one box', () => {
70
+ expect(JOINED).toContain("[data-gap='none'] > *:not(:first-child):not(:last-child) {")
71
+ expect(JOINED).toContain('border-radius: 0;')
72
+ expect(JOINED).toContain("[data-gap='none'] > *:first-child:not(:only-child) {")
73
+ expect(JOINED).toContain("[data-gap='none'] > *:last-child:not(:only-child) {")
74
+ })
75
+
76
+ test('a one-child group keeps its normal shape — :only-child is excluded on both ends', () => {
77
+ // A conditional render that dropped two of three children must not lose half its corners.
78
+ expect(JOINED).toContain(':first-child:not(:only-child)')
79
+ expect(JOINED).toContain(':last-child:not(:only-child)')
80
+ })
81
+
82
+ test('the internal hairline is drawn here, because the default border is transparent', () => {
83
+ // `cssVariablesResolver` points `--mantine-color-default-border` at transparent on purpose
84
+ // (DESIGN-SPEC §8). The 12%-ink value is `.pill`'s, so a joined pill row and a joined button row
85
+ // read as one family.
86
+ expect(JOINED).toContain("[data-gap='none'] > [data-variant='default'] {")
87
+ expect(JOINED).toContain('color-mix(in srgb, var(--vx-ink) 12%, transparent)')
88
+ })
89
+
90
+ test('a filled child keeps its fill edge — the border rule is scoped to the default variant', () => {
91
+ expect(JOINED).not.toMatch(/\[data-gap='none'\] > \* \{\s*border-color/)
92
+ })
93
+
94
+ test('hover/focus/active raise the child so its shared edge paints whole', () => {
95
+ expect(JOINED).toContain("[data-gap='none'] > *:focus-visible,")
96
+ expect(JOINED).toContain('z-index: 1;')
97
+ expect(JOINED).toContain('position: relative;')
98
+ })
99
+
100
+ test('the group never wraps — a joined set that broke mid-box is worse than an overflow', () => {
101
+ const rule = JOINED.slice(
102
+ JOINED.indexOf('.group {'),
103
+ JOINED.indexOf('}', JOINED.indexOf('.group {')),
104
+ )
105
+ expect(rule).toContain('flex-wrap: nowrap')
106
+ expect(rule).toContain('flex: none')
107
+ })
108
+
109
+ test('it adds no role and no label — a joined LOOK is not a semantic group', () => {
110
+ const { container } = mount(
111
+ <ControlGroup>
112
+ <ActionIcon variant="default" aria-label="Previous">
113
+ <Glyph />
114
+ </ActionIcon>
115
+ <Button variant="default">Today</Button>
116
+ </ControlGroup>,
117
+ )
118
+ const box = container.querySelector('[data-gap]')
119
+ expect(box?.hasAttribute('role')).toBe(false)
120
+ expect(box?.hasAttribute('aria-label')).toBe(false)
121
+ })
122
+ })
123
+
124
+ describe('ActionGroup joins what belongs together, and nothing else', () => {
125
+ const stepper: BarAction[] = [
126
+ { key: 'prev', label: 'Previous period', icon: <Glyph />, group: true },
127
+ { key: 'today', label: 'Today', group: true },
128
+ { key: 'next', label: 'Next period', icon: <Glyph />, group: true },
129
+ ]
130
+
131
+ test('a run of group:true secondaries becomes ONE joined box on desktop', () => {
132
+ mount(<ActionGroup secondary={stepper} />)
133
+ const joined = groups(desktop())
134
+ expect(joined.length).toBe(1)
135
+ expect(joined[0]?.querySelectorAll('button').length).toBe(3)
136
+ })
137
+
138
+ test('a joined member with an icon renders icon-only, its label demoted to the a11y name', () => {
139
+ // `‹ Today ›` is ONE control whose middle segment names it; the arrows' content is their
140
+ // direction. Labelled, the same three actions measured 304px against 118px in Chrome.
141
+ mount(<ActionGroup secondary={stepper} />)
142
+ const row = desktop()
143
+ const prev = row?.querySelector('[aria-label="Previous period"]')
144
+ expect(prev).not.toBeNull()
145
+ expect(prev?.textContent).toBe('')
146
+ // The one member with no icon keeps its label — the rule reads what the caller supplied.
147
+ expect(row?.textContent).toBe('Today')
148
+ })
149
+
150
+ test('a joined set with NO icons keeps every label — the icon-only rule is not a width policy', () => {
151
+ mount(
152
+ <ActionGroup
153
+ secondary={[
154
+ { key: 'abs', label: 'Absolute', group: true },
155
+ { key: 'rate', label: 'Rate', group: true },
156
+ ]}
157
+ />,
158
+ )
159
+ expect(groups(desktop()).length).toBe(1)
160
+ expect(desktop()?.textContent).toBe('AbsoluteRate')
161
+ })
162
+
163
+ test('independent secondaries are NOT joined — the default row is unchanged', () => {
164
+ mount(
165
+ <ActionGroup
166
+ secondary={[
167
+ { key: 'export', label: 'Export CSV' },
168
+ { key: 'accounts', label: 'Accounts', icon: <Glyph /> },
169
+ ]}
170
+ />,
171
+ )
172
+ expect(groups(desktop()).length).toBe(0)
173
+ })
174
+
175
+ test('a non-group action between two group ones splits the run in two', () => {
176
+ // Two runs of one is the same row it was before — the group flag is per-member on purpose, so a
177
+ // set that forgets one member degrades to the old row instead of joining the wrong things.
178
+ mount(
179
+ <ActionGroup
180
+ secondary={[
181
+ { key: 'prev', label: 'Prev', group: true },
182
+ { key: 'loose', label: 'Loose' },
183
+ { key: 'next', label: 'Next', group: true },
184
+ ]}
185
+ />,
186
+ )
187
+ expect(groups(desktop()).length).toBe(0)
188
+ })
189
+
190
+ test('adjacent ICON-ONLY entries join on the MOBILE bar without any flag', () => {
191
+ // Below `sm` an icon-bearing action renders as an `ActionIcon` and nothing else, so two in a row
192
+ // are two 30px squares with a gap — three boxes' worth of border for two actions, exactly where
193
+ // width is scarcest.
194
+ mount(
195
+ <ActionGroup
196
+ secondary={[
197
+ { key: 'a', label: 'Filter', icon: <Glyph />, mobile: 'bar' },
198
+ { key: 'b', label: 'Sort', icon: <Glyph />, mobile: 'bar' },
199
+ ]}
200
+ />,
201
+ )
202
+ expect(groups(mobile()).length).toBe(1)
203
+ // …and NOT on desktop, where the same two actions are labelled buttons that are not related.
204
+ expect(groups(desktop()).length).toBe(0)
205
+ })
206
+
207
+ test('the mobile PRIMARY is never joined to a sibling — a fill edge against a border edge', () => {
208
+ mount(
209
+ <ActionGroup
210
+ primary={{ key: 'save', label: 'Save', icon: <Glyph /> }}
211
+ secondary={[{ key: 'a', label: 'Filter', icon: <Glyph />, mobile: 'bar' }]}
212
+ />,
213
+ )
214
+ expect(groups(mobile()).length).toBe(0)
215
+ })
216
+
217
+ test('a kind:"custom" entry breaks the run — basalt does not draw that node', () => {
218
+ mount(
219
+ <ActionGroup
220
+ secondary={[
221
+ { key: 'prev', label: 'Prev', group: true },
222
+ { key: 'chip', kind: 'custom', node: <span>live</span> },
223
+ { key: 'next', label: 'Next', group: true },
224
+ ]}
225
+ />,
226
+ )
227
+ expect(groups(desktop()).length).toBe(0)
228
+ })
229
+ })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `ControlGroup` — adjacent controls that act on ONE thing, joined into one box
3
+ * (`docs/CONTROLS-SPEC.md` §3). Blueprint's `ControlGroup` and Mantine's own `Button.Group` are the
4
+ * same primitive; neither works here, because basalt's tier holds `FilterPill`s, `Button`s,
5
+ * `ActionIcon`s and `TextInput`s in the same row and Mantine's `Button.Group` joins Buttons only
6
+ * (it declares its own `section-*` size vars, which the `ctl` tier does not define — see
7
+ * `theme/ctl-tier-coverage.test.ts`'s note on why `*Group` directories are excluded from the scan).
8
+ *
9
+ * **What it is for.** A `‹ Today ›` date stepper, a `− 1 +` quantity, an `Absolute | Rate` pair — a
10
+ * set of controls a reader should read as ONE control with several affordances. The joined box is the
11
+ * statement: these three act on the same value, and the two dividers inside it are the only place a
12
+ * hairline is a divider rather than a border (`docs/DESIGN-SPEC.md` §8 — depth is a shadow). A row of
13
+ * INDEPENDENT actions is not this; that is `ActionGroup`, and joining it would claim a relationship
14
+ * that is not there.
15
+ *
16
+ * **Presentational by design — no `role`, no `aria-label`.** A joined LOOK is not a semantic group:
17
+ * each child is already a button or an input with its own accessible name, and wrapping them in a
18
+ * `role="group"` adds a level to the a11y tree that announces nothing a reader needs. Where the set
19
+ * genuinely IS one control with one name — a range picker, a tab set — basalt already ships that
20
+ * control (`RangeFilter`, `ViewTabs`) with the right semantics, and reaching for `ControlGroup`
21
+ * instead is `basalt/hand-rolled-filter`.
22
+ *
23
+ * @example
24
+ * // The one shape this exists for: three affordances, one value.
25
+ * <ControlGroup>
26
+ * <ActionIcon variant="default" aria-label="Previous period" onClick={back}><IconLeft /></ActionIcon>
27
+ * <Button variant="default" onClick={today}>Today</Button>
28
+ * <ActionIcon variant="default" aria-label="Next period" onClick={next}><IconRight /></ActionIcon>
29
+ * </ControlGroup>
30
+ */
31
+ import { Box } from '@mantine/core'
32
+ import type { ReactNode } from 'react'
33
+ import classes from './controls.module.css'
34
+
35
+ export type ControlGroupProps = {
36
+ readonly children: ReactNode
37
+ /**
38
+ * `none` (the default) JOINS the children: no gap, one shared hairline between each pair, and the
39
+ * corner radius only on the outer ends, so the set reads as one box.
40
+ *
41
+ * `tight` keeps the tier's own control gap and only guarantees the alignment and the no-wrap. It
42
+ * is for a set that belongs together but must stay separately pressable — three destructive
43
+ * actions, or a pair where one child is `filled` and joining it would put a fill edge against a
44
+ * border edge.
45
+ *
46
+ * @default 'none'
47
+ */
48
+ readonly gap?: 'none' | 'tight'
49
+ readonly className?: string
50
+ }
51
+
52
+ export function ControlGroup({ children, gap = 'none', className }: ControlGroupProps): ReactNode {
53
+ return (
54
+ // A Mantine `Box`, not a raw `<div>` — `basalt/raw-html-layout` flags layout styling on a bare
55
+ // HTML tag, the same reason `CtlSlot` uses one.
56
+ <Box
57
+ className={className === undefined ? classes.group : `${classes.group} ${className}`}
58
+ data-gap={gap}
59
+ >
60
+ {children}
61
+ </Box>
62
+ )
63
+ }