basalt-ui 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +18 -0
  3. package/MIGRATING.md +546 -20
  4. package/README.md +45 -32
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/settings-section.d.ts +6 -4
  134. package/dist/dashboard/settings-section.d.ts.map +1 -1
  135. package/dist/dashboard/settings-section.js +23 -16
  136. package/dist/dashboard/settings-section.module.css +10 -0
  137. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  138. package/dist/dashboard/settings-section.test.d.ts +2 -0
  139. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  140. package/dist/dashboard/stat-card.d.ts +44 -9
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +80 -48
  143. package/dist/dashboard/stat-card.module.css +82 -0
  144. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  145. package/dist/data/data-table.d.ts +37 -15
  146. package/dist/data/data-table.d.ts.map +1 -1
  147. package/dist/data/data-table.js +88 -58
  148. package/dist/guard/grace.test.d.ts +20 -0
  149. package/dist/guard/grace.test.d.ts.map +1 -0
  150. package/dist/guard/index.d.ts +66 -3
  151. package/dist/guard/index.d.ts.map +1 -1
  152. package/dist/guard/index.js +82 -7
  153. package/dist/guard/types.d.ts +3 -3
  154. package/dist/guard/types.d.ts.map +1 -1
  155. package/dist/index.d.ts +12 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +13 -10
  158. package/dist/nav/types.d.ts +78 -2
  159. package/dist/nav/types.d.ts.map +1 -1
  160. package/dist/notifications/bell.d.ts +4 -1
  161. package/dist/notifications/bell.d.ts.map +1 -1
  162. package/dist/notifications/index.d.ts +1 -1
  163. package/dist/router-tanstack/field.d.ts +14 -0
  164. package/dist/router-tanstack/field.d.ts.map +1 -0
  165. package/dist/router-tanstack/field.js +4 -0
  166. package/dist/router-tanstack/index.d.ts +6 -2
  167. package/dist/router-tanstack/index.d.ts.map +1 -1
  168. package/dist/router-tanstack/index.js +4 -0
  169. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  170. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  171. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  172. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  173. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/search-param-store.js +16 -45
  175. package/dist/router-tanstack/search-store.d.ts +81 -0
  176. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  177. package/dist/router-tanstack/search-store.js +187 -0
  178. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  179. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  181. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  182. package/dist/router-tanstack/use-nav.d.ts +0 -2
  183. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  184. package/dist/router-tanstack/use-nav.js +34 -12
  185. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  186. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  187. package/dist/section/index.d.ts +2 -0
  188. package/dist/section/index.d.ts.map +1 -0
  189. package/dist/section/index.js +4 -0
  190. package/dist/section/section.d.ts +24 -0
  191. package/dist/section/section.d.ts.map +1 -0
  192. package/dist/section/section.js +126 -0
  193. package/dist/section/section.module.css +104 -0
  194. package/dist/section/section.module.css.d.ts +11 -0
  195. package/dist/section/section.test.d.ts +2 -0
  196. package/dist/section/section.test.d.ts.map +1 -0
  197. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  198. package/dist/shell/app-breadcrumbs.js +18 -15
  199. package/dist/shell/app-header.module.css +77 -37
  200. package/dist/shell/app-header.module.css.d.ts +2 -1
  201. package/dist/shell/app-mobile-nav.d.ts +9 -2
  202. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  203. package/dist/shell/app-mobile-nav.js +98 -11
  204. package/dist/shell/app-mobile-nav.module.css +8 -0
  205. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  206. package/dist/shell/app-sidebar.d.ts +55 -28
  207. package/dist/shell/app-sidebar.d.ts.map +1 -1
  208. package/dist/shell/app-sidebar.js +116 -67
  209. package/dist/shell/app-sidebar.module.css +303 -20
  210. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  211. package/dist/shell/index.d.ts +55 -21
  212. package/dist/shell/index.d.ts.map +1 -1
  213. package/dist/shell/index.js +40 -18
  214. package/dist/shell/mobile-nav-model.d.ts +13 -2
  215. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  216. package/dist/shell/mobile-nav-model.js +15 -1
  217. package/dist/shell/nav-count-badge.d.ts +1 -1
  218. package/dist/shell/page-bar.d.ts +51 -0
  219. package/dist/shell/page-bar.d.ts.map +1 -0
  220. package/dist/shell/page-bar.js +154 -0
  221. package/dist/shell/page-bar.module.css +178 -0
  222. package/dist/shell/page-bar.module.css.d.ts +17 -0
  223. package/dist/shell/page-bar.test.d.ts +2 -0
  224. package/dist/shell/page-bar.test.d.ts.map +1 -0
  225. package/dist/shell/sidebar-block-model.d.ts +48 -0
  226. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  227. package/dist/shell/sidebar-block-model.js +39 -0
  228. package/dist/shell/sidebar-blocks.d.ts +32 -0
  229. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  230. package/dist/shell/sidebar-blocks.js +169 -0
  231. package/dist/shell/sidebar-search.d.ts +13 -1
  232. package/dist/shell/sidebar-search.d.ts.map +1 -1
  233. package/dist/shell/sidebar-search.js +34 -1
  234. package/dist/shell/sidebar-search.module.css +26 -0
  235. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  236. package/dist/state/fields.d.ts +392 -0
  237. package/dist/state/fields.d.ts.map +1 -0
  238. package/dist/state/fields.js +369 -0
  239. package/dist/state/fields.test.d.ts +2 -0
  240. package/dist/state/fields.test.d.ts.map +1 -0
  241. package/dist/state/persisted.d.ts +48 -0
  242. package/dist/state/persisted.d.ts.map +1 -0
  243. package/dist/state/persisted.js +105 -0
  244. package/dist/state.d.ts +11 -56
  245. package/dist/state.d.ts.map +1 -1
  246. package/dist/state.js +14 -120
  247. package/dist/styles.css +13 -0
  248. package/dist/surfaces.d.ts +106 -24
  249. package/dist/surfaces.d.ts.map +1 -1
  250. package/dist/surfaces.js +155 -19
  251. package/dist/theme/controls.module.css +16 -1
  252. package/dist/theme/ctl-theme.d.ts +32 -0
  253. package/dist/theme/ctl-theme.d.ts.map +1 -0
  254. package/dist/theme/ctl-theme.js +54 -0
  255. package/dist/theme/ctl-theme.test.d.ts +2 -0
  256. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  257. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  258. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  259. package/dist/theme/divider-law.test.d.ts +2 -0
  260. package/dist/theme/divider-law.test.d.ts.map +1 -0
  261. package/dist/theme/icon-slot.d.ts +44 -0
  262. package/dist/theme/icon-slot.d.ts.map +1 -0
  263. package/dist/theme/icon-slot.js +21 -0
  264. package/dist/theme/icon-slot.module.css +47 -0
  265. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  266. package/dist/theme/icon-slot.test.d.ts +2 -0
  267. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  268. package/dist/theme/index.d.ts +1 -0
  269. package/dist/theme/index.d.ts.map +1 -1
  270. package/dist/theme/index.js +82 -5
  271. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  272. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  273. package/dist/theme/segmented-control.module.css +20 -0
  274. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  275. package/dist/tokens/index.d.ts +12 -0
  276. package/dist/tokens/index.d.ts.map +1 -1
  277. package/dist/tokens/index.js +38 -8
  278. package/dist/tokens/palette.d.ts +97 -61
  279. package/dist/tokens/palette.d.ts.map +1 -1
  280. package/dist/tokens/palette.js +76 -38
  281. package/dist/tokens.css +10 -2
  282. package/dist/widget-header/delta-badge.d.ts +13 -0
  283. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  284. package/dist/widget-header/delta-badge.js +29 -0
  285. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  286. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  287. package/dist/widget-header/index.d.ts +3 -0
  288. package/dist/widget-header/index.d.ts.map +1 -0
  289. package/dist/widget-header/index.js +6 -0
  290. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  291. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  292. package/dist/widget-header/widget-header.d.ts +37 -0
  293. package/dist/widget-header/widget-header.d.ts.map +1 -0
  294. package/dist/widget-header/widget-header.js +77 -0
  295. package/dist/widget-header/widget-header.module.css +335 -0
  296. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  297. package/dist/widget-header/widget-header.test.d.ts +2 -0
  298. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  299. package/llms.txt +30 -17
  300. package/package.json +14 -1
  301. package/src/charts/cursor/resolve.test.ts +29 -1
  302. package/src/charts/cursor/resolve.ts +38 -6
  303. package/src/charts/cursor/scope.tsx +6 -3
  304. package/src/charts/cursor/store.test.ts +19 -1
  305. package/src/charts/cursor/store.ts +10 -5
  306. package/src/charts/hooks/useChartCursor.ts +16 -8
  307. package/src/charts/index.ts +2 -2
  308. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  309. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  310. package/src/charts/primitives/ChartCard.tsx +86 -146
  311. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  312. package/src/cli/check-coverage.test.ts +297 -0
  313. package/src/cli/check-theme.test.ts +38 -14
  314. package/src/cli/index.ts +392 -16
  315. package/src/cli/init-sync.test.ts +97 -0
  316. package/src/connectivity/connectivity-indicator.tsx +16 -9
  317. package/src/content/index.ts +16 -3
  318. package/src/content/prose.module.css +7 -28
  319. package/src/content/prose.module.css.test.ts +8 -24
  320. package/src/controls/actions.module.css +62 -0
  321. package/src/controls/actions.module.css.d.ts +13 -0
  322. package/src/controls/actions.test.tsx +252 -0
  323. package/src/controls/actions.tsx +655 -0
  324. package/src/controls/actions.type-guard.test.ts +63 -0
  325. package/src/controls/compare-filter.tsx +64 -0
  326. package/src/controls/control-group.test.tsx +229 -0
  327. package/src/controls/control-group.tsx +63 -0
  328. package/src/controls/controls.module.css +542 -0
  329. package/src/controls/controls.module.css.d.ts +35 -0
  330. package/src/controls/controls.router.test.tsx +637 -0
  331. package/src/controls/enum-filter.tsx +159 -0
  332. package/src/controls/filter-context.tsx +153 -0
  333. package/src/controls/filter-pill.tsx +120 -0
  334. package/src/controls/filter-set.test.tsx +472 -0
  335. package/src/controls/filter-set.tsx +225 -0
  336. package/src/controls/filter-sheet.tsx +285 -0
  337. package/src/controls/glyphs.tsx +81 -0
  338. package/src/controls/index.ts +48 -0
  339. package/src/controls/multi-select-filter.tsx +140 -0
  340. package/src/controls/peer-boundary.test.ts +85 -0
  341. package/src/controls/props-contract.test.tsx +106 -0
  342. package/src/controls/range-filter.tsx +184 -0
  343. package/src/controls/search-filter.tsx +123 -0
  344. package/src/controls/select-filter.tsx +89 -0
  345. package/src/controls/sync-button.module.css +63 -0
  346. package/src/controls/sync-button.module.css.d.ts +11 -0
  347. package/src/controls/sync-button.test.tsx +183 -0
  348. package/src/controls/sync-button.tsx +197 -0
  349. package/src/controls/toggle-filter.tsx +60 -0
  350. package/src/controls/view-tabs.tsx +110 -0
  351. package/src/controls-dates/date-range-picker.tsx +42 -0
  352. package/src/controls-dates/index.ts +8 -0
  353. package/src/dashboard/delta-badge.tsx +4 -74
  354. package/src/dashboard/settings-section.module.css +10 -0
  355. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  356. package/src/dashboard/settings-section.test.tsx +56 -0
  357. package/src/dashboard/settings-section.tsx +33 -30
  358. package/src/dashboard/stat-card.module.css +82 -0
  359. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  360. package/src/dashboard/stat-card.test.tsx +67 -1
  361. package/src/dashboard/stat-card.tsx +209 -68
  362. package/src/data/data-table.test.tsx +96 -11
  363. package/src/data/data-table.tsx +220 -84
  364. package/src/guard/check-source.test.ts +126 -13
  365. package/src/guard/grace.test.ts +149 -0
  366. package/src/guard/index.ts +172 -29
  367. package/src/guard/types.ts +4 -2
  368. package/src/index.ts +46 -5
  369. package/src/nav/types.ts +84 -2
  370. package/src/notifications/bell.tsx +6 -3
  371. package/src/notifications/index.ts +1 -1
  372. package/src/router-tanstack/field.ts +31 -0
  373. package/src/router-tanstack/index.ts +24 -2
  374. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  375. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  376. package/src/router-tanstack/search-param-store.ts +42 -159
  377. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  378. package/src/router-tanstack/search-store.test.ts +418 -0
  379. package/src/router-tanstack/search-store.ts +380 -0
  380. package/src/router-tanstack/use-nav.test.tsx +187 -0
  381. package/src/router-tanstack/use-nav.ts +72 -14
  382. package/src/section/index.ts +1 -0
  383. package/src/section/section.module.css +104 -0
  384. package/src/section/section.module.css.d.ts +11 -0
  385. package/src/section/section.test.tsx +204 -0
  386. package/src/section/section.tsx +220 -0
  387. package/src/shell/app-breadcrumbs.tsx +44 -31
  388. package/src/shell/app-header.module.css +77 -37
  389. package/src/shell/app-header.module.css.d.ts +2 -1
  390. package/src/shell/app-mobile-nav.module.css +8 -0
  391. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  392. package/src/shell/app-mobile-nav.tsx +147 -8
  393. package/src/shell/app-sidebar-account.tsx +2 -2
  394. package/src/shell/app-sidebar.module.css +303 -20
  395. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  396. package/src/shell/app-sidebar.test.tsx +456 -41
  397. package/src/shell/app-sidebar.tsx +276 -140
  398. package/src/shell/index.test.tsx +291 -5
  399. package/src/shell/index.tsx +137 -38
  400. package/src/shell/mobile-nav-model.test.ts +228 -7
  401. package/src/shell/mobile-nav-model.ts +53 -3
  402. package/src/shell/nav-count-badge.tsx +1 -1
  403. package/src/shell/page-bar.module.css +178 -0
  404. package/src/shell/page-bar.module.css.d.ts +17 -0
  405. package/src/shell/page-bar.test.tsx +316 -0
  406. package/src/shell/page-bar.tsx +315 -0
  407. package/src/shell/sidebar-block-model.ts +83 -0
  408. package/src/shell/sidebar-blocks.tsx +301 -0
  409. package/src/shell/sidebar-search.module.css +26 -0
  410. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  411. package/src/shell/sidebar-search.tsx +71 -2
  412. package/src/state/fields.test.tsx +258 -0
  413. package/src/state/fields.ts +892 -0
  414. package/src/state/persisted.ts +211 -0
  415. package/src/state.ts +46 -240
  416. package/src/styles.css +13 -0
  417. package/src/surfaces.ts +203 -36
  418. package/src/theme/controls.module.css +16 -1
  419. package/src/theme/ctl-theme.test.tsx +275 -0
  420. package/src/theme/ctl-theme.tsx +138 -0
  421. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  422. package/src/theme/density-relations.test.ts +106 -51
  423. package/src/theme/divider-law.test.ts +259 -0
  424. package/src/theme/icon-slot.module.css +47 -0
  425. package/src/theme/icon-slot.module.css.d.ts +8 -0
  426. package/src/theme/icon-slot.test.tsx +275 -0
  427. package/src/theme/icon-slot.tsx +59 -0
  428. package/src/theme/index.ts +165 -3
  429. package/src/theme/layout-rhythm-css.test.ts +72 -0
  430. package/src/theme/radius.test.ts +11 -1
  431. package/src/theme/segmented-control.module.css +20 -0
  432. package/src/theme/shadow-surfaces.test.ts +20 -7
  433. package/src/theme/spacing.test.ts +75 -27
  434. package/src/theme/use-basalt-spacing.ts +2 -2
  435. package/src/tokens/density.test.ts +7 -25
  436. package/src/tokens/index.ts +48 -8
  437. package/src/tokens/palette.ts +107 -73
  438. package/src/widget-header/delta-badge.test.tsx +60 -0
  439. package/src/widget-header/delta-badge.tsx +57 -0
  440. package/src/widget-header/index.ts +2 -0
  441. package/src/widget-header/no-mantine.test.ts +33 -0
  442. package/src/widget-header/widget-header.module.css +335 -0
  443. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  444. package/src/widget-header/widget-header.test.tsx +187 -0
  445. package/src/widget-header/widget-header.tsx +168 -0
  446. package/agent/rules/basalt-agent.md +0 -452
  447. package/agent/rules/basalt-app.md +0 -246
  448. package/agent/rules/basalt-commands.md +0 -320
  449. package/agent/rules/basalt-content.md +0 -409
  450. package/agent/rules/basalt-data.md +0 -329
  451. package/agent/rules/basalt-forms.md +0 -186
  452. package/agent/rules/basalt-notifications.md +0 -264
  453. package/agent/rules/basalt-query.md +0 -187
  454. package/agent/rules/basalt-router.md +0 -336
  455. package/dist/content/article-filter-bar.d.ts +0 -40
  456. package/dist/content/article-filter-bar.d.ts.map +0 -1
  457. package/dist/content/article-filter-bar.js +0 -56
  458. package/dist/shell/page-header.d.ts +0 -13
  459. package/dist/shell/page-header.d.ts.map +0 -1
  460. package/dist/shell/page-header.js +0 -23
  461. package/src/content/article-filter-bar.tsx +0 -88
  462. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The sidebar-block law — every default a `SidebarBlock` leaves unstated, plus the two persisted
3
+ * fold-key grammars.
4
+ *
5
+ * PURE BY CONTRACT, for the same reason `mobile-nav-model.ts` is: three consumers read these
6
+ * defaults and none of them may disagree. `app-sidebar.tsx` decides where a block renders and
7
+ * whether the rail keeps it, `sidebar-blocks.tsx` paints it, and `mobile-nav-model.ts` counts the
8
+ * More-sheet rows it will produce — a count that picks `menu` vs `sheet` before any of it renders.
9
+ * A default re-derived per call site is how the count and the renderer drift apart (see
10
+ * `accountRowCount`'s doc for the same failure with account rows).
11
+ */
12
+ import type { SidebarBlock } from '../nav/types'
13
+
14
+ /** Envelope version for a persisted fold flag. Bump only if the value stops being a boolean. */
15
+ export const FOLD_VERSION = 1
16
+
17
+ /**
18
+ * `'nav'` renders after the nav sections inside the scroll region; `'bottom'` pins above the
19
+ * settings footer. A progress block is `'bottom'` — it is a pinned status row, not a nav tail.
20
+ */
21
+ export function sidebarBlockPlacement(block: SidebarBlock): 'nav' | 'bottom' {
22
+ if (block.kind === 'progress') return block.placement ?? 'bottom'
23
+ return block.placement ?? 'nav'
24
+ }
25
+
26
+ /**
27
+ * The collapsed-rail projection. A list block earns a dot only when it carries a `count` — a rail
28
+ * icon with no number behind it is a dot that means nothing — and a progress block a ring on the
29
+ * settings row. A `custom` block has no rail representation at all (arbitrary content in ~48px of
30
+ * icon column), which is exactly what `sidebarNavExtra` did before it.
31
+ */
32
+ export function sidebarBlockRail(block: SidebarBlock): 'dot' | 'ring' | 'hidden' {
33
+ if (block.kind === 'custom') return 'hidden'
34
+ if (block.kind === 'progress') return block.rail ?? 'ring'
35
+ // A dot has to sit ON something. With no `icon` there is no rail node to badge, and with no
36
+ // `count` there is no number behind the dot — either way the block leaves the rail rather than
37
+ // occupying ~48px of icon column with a label the rail has already hidden. This overrides an
38
+ // explicit `rail: 'dot'` too: that combination is a config mistake, not a shape to render blank.
39
+ if (block.icon === undefined || block.count === undefined) return 'hidden'
40
+ return block.rail ?? 'dot'
41
+ }
42
+
43
+ /**
44
+ * The mobile projection. A list is reachable from More by default — below `sm` the sidebar does
45
+ * not render at all, so a hidden list is unreachable, not merely unshown. A progress row defaults
46
+ * to `'hidden'`: it is ambient status, and every More row costs headroom the menu-vs-sheet
47
+ * threshold is measured against.
48
+ */
49
+ export function sidebarBlockMobile(block: SidebarBlock): 'more' | 'hidden' {
50
+ if (block.kind === 'custom') return 'hidden'
51
+ if (block.kind === 'progress') return block.mobile ?? 'hidden'
52
+ return block.mobile ?? 'more'
53
+ }
54
+
55
+ /** Rows a list block paints, honouring `max` — the "Show more" toggle reveals the remainder. */
56
+ export function sidebarBlockVisibleCount(itemCount: number, max: number | undefined): number {
57
+ if (max === undefined || max >= itemCount) return itemCount
58
+ return Math.max(0, max)
59
+ }
60
+
61
+ /**
62
+ * A section label as a storage-key segment. Lowercase, every run of non-alphanumerics collapsed to
63
+ * one `-`, no leading/trailing separator — `'Awaiting action'` → `'awaiting-action'`.
64
+ */
65
+ export function slugifyLabel(label: string): string {
66
+ return label
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, '-')
69
+ .replace(/^-+|-+$/g, '')
70
+ }
71
+
72
+ /**
73
+ * `createPersistedState` namespaces every key as `basalt:<key>`, so these return the UNNAMESPACED
74
+ * halves — the real storage keys are `basalt:sidebar-block:<key>` and
75
+ * `basalt:sidebar-section:<label-slug>` (`docs/CONTROLS-SPEC.md` §2.3).
76
+ */
77
+ export function sidebarBlockFoldKey(blockKey: string): string {
78
+ return `sidebar-block:${blockKey}`
79
+ }
80
+
81
+ export function sidebarSectionFoldKey(label: string): string {
82
+ return `sidebar-section:${slugifyLabel(label)}`
83
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Sidebar blocks — the renderer for `SidebarBlock[]` (`docs/CONTROLS-SPEC.md` §2.3, law C13).
3
+ *
4
+ * A block is the "Awaiting action" / "Recents" / "Getting started 1 of 5" shape every consumer
5
+ * hand-rolled into `sidebarNavExtra`: a micro-label, a short list of NON-destination rows, and
6
+ * sometimes a fold. Because it arrives as DATA, basalt owns the two projections a `ReactNode` slot
7
+ * could never express — the collapsed rail (a dot on the icon, a ring on the settings row) and the
8
+ * mobile More sheet (`app-mobile-nav.tsx`).
9
+ *
10
+ * Styling deliberately lives in `app-sidebar.module.css`, not in a module of its own: the
11
+ * collapsed-rail rules are `.root[data-collapsed] …` descendant selectors, and `.root` is that
12
+ * module's class. A second CSS module would hash `.block*` into a different name than the rail
13
+ * rules could ever select.
14
+ */
15
+ import { Collapse, NavLink, Progress, Text, UnstyledButton } from '@mantine/core'
16
+ import { useState } from 'react'
17
+ import type { ReactNode } from 'react'
18
+ import type {
19
+ SidebarBlockItem,
20
+ SidebarBlockTone,
21
+ SidebarCustomBlock,
22
+ SidebarListBlock,
23
+ SidebarProgressBlock,
24
+ } from '../nav/types'
25
+ import { NavCountBadge } from './nav-count-badge'
26
+ import { createPersistedState } from '../state'
27
+ import {
28
+ FOLD_VERSION,
29
+ sidebarBlockFoldKey,
30
+ sidebarBlockRail,
31
+ sidebarBlockVisibleCount,
32
+ } from './sidebar-block-model'
33
+ import { IconSlot } from '../theme/icon-slot'
34
+ import classes from './app-sidebar.module.css'
35
+
36
+ /**
37
+ * Fold/expand chevron. Lives here rather than in `app-sidebar.tsx` purely for direction: that
38
+ * module imports this one, so a glyph shared by a collapsible SECTION header and a collapsible
39
+ * BLOCK header has to sit on this side of the edge or the two files cycle.
40
+ */
41
+ export function IconChevron({ open }: { open: boolean }) {
42
+ return (
43
+ <svg
44
+ width={12}
45
+ height={12}
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth={2}
50
+ strokeLinecap="round"
51
+ strokeLinejoin="round"
52
+ aria-hidden
53
+ >
54
+ {open ? <path d="M6 9l6 6l6 -6" /> : <path d="M9 6l6 6l-6 6" />}
55
+ </svg>
56
+ )
57
+ }
58
+
59
+ /**
60
+ * One memoized `createPersistedState` store per fold key — the same module-scope factory pattern
61
+ * (and the same reason for it) as `collapseStore` in `index.tsx`: `createPersistedState` is a
62
+ * per-key FACTORY, so calling it during render would allocate a fresh store and a fresh
63
+ * `useSyncExternalStore` subscription on every commit. The FIRST caller's `initial` wins, which is
64
+ * what makes a `defaultCollapsed` a seed rather than a value that fights the persisted one.
65
+ */
66
+ const foldStores = new Map<string, () => readonly [boolean, (next: boolean) => void]>()
67
+
68
+ function foldStore(
69
+ key: string,
70
+ initial: boolean,
71
+ ): () => readonly [boolean, (next: boolean) => void] {
72
+ const cached = foldStores.get(key)
73
+ if (cached) return cached
74
+ const store = createPersistedState<boolean>({ key, version: FOLD_VERSION, initial })
75
+ foldStores.set(key, store)
76
+ return store
77
+ }
78
+
79
+ /**
80
+ * A persisted fold flag (`true` = folded). Exactly one hook call regardless of `key`, so swapping
81
+ * keys mid-life never moves the hook count.
82
+ *
83
+ * Every fold in the sidebar goes through this — block folds at `basalt:sidebar-block:<key>` and
84
+ * nav-section folds at `basalt:sidebar-section:<label-slug>`. Through 1.25.0 the section folds were
85
+ * one `useState` keyed by label, so every reload re-opened a section the user had closed.
86
+ */
87
+ export function usePersistedFold(
88
+ key: string,
89
+ initial: boolean,
90
+ ): readonly [boolean, (next: boolean) => void] {
91
+ return foldStore(key, initial)()
92
+ }
93
+
94
+ /**
95
+ * Block micro-label. Reuses `.sectionLabel` — the SAME class a nav section header uses, so a block
96
+ * heading and a section heading cannot drift apart (docs/DESIGN-SPEC.md §3).
97
+ */
98
+ function BlockLabel({ children }: { children: ReactNode }) {
99
+ return (
100
+ <Text component="div" className={classes.sectionLabel}>
101
+ {children}
102
+ </Text>
103
+ )
104
+ }
105
+
106
+ /** Status dot — the icon fallback for a toned row. Reads `--vx-status-*` through `data-tone`. */
107
+ export function SidebarBlockToneDot({ tone }: { tone: SidebarBlockTone }) {
108
+ return <span className={classes.blockToneDot} data-tone={tone} aria-hidden />
109
+ }
110
+
111
+ /** `value` of `total` as a ring — the collapsed rail's stand-in for a progress block. */
112
+ export function SidebarProgressRing({ value, total }: { value: number; total: number }) {
113
+ // r=8 in a 20x20 box: the circumference is a constant, so the arc is a dash offset rather than a
114
+ // path computed per render.
115
+ const CIRCUMFERENCE = 2 * Math.PI * 8
116
+ const ratio = total > 0 ? Math.min(1, Math.max(0, value / total)) : 0
117
+ return (
118
+ <span className={classes.blockRing} data-basalt-rail-ring>
119
+ <svg width={20} height={20} viewBox="0 0 20 20" aria-hidden>
120
+ <circle cx={10} cy={10} r={8} fill="none" stroke="var(--vx-divider)" strokeWidth={2} />
121
+ <circle
122
+ cx={10}
123
+ cy={10}
124
+ r={8}
125
+ fill="none"
126
+ stroke="var(--vx-accent)"
127
+ strokeWidth={2}
128
+ strokeLinecap="round"
129
+ strokeDasharray={CIRCUMFERENCE}
130
+ strokeDashoffset={CIRCUMFERENCE * (1 - ratio)}
131
+ transform="rotate(-90 10 10)"
132
+ />
133
+ </svg>
134
+ </span>
135
+ )
136
+ }
137
+
138
+ /**
139
+ * One list row. A row with no `Anchor`, `href` or `onClick` is NOT a link — the "Recents" shape is
140
+ * plain text, and wrapping it in an `<a>` with nowhere to go is what makes a keyboard walk through
141
+ * the sidebar land on dead stops.
142
+ */
143
+ function BlockRow({ item }: { item: SidebarBlockItem }) {
144
+ // Through `IconSlot` like every other `icon` prop in the framework: a block row's leading mark is
145
+ // a consumer-supplied glyph, and the row's 20px rhythm cannot be at the mercy of what size that
146
+ // glyph declares for itself (`theme/icon-slot.tsx`). The tone dot is basalt's own and already
147
+ // square, but it goes through the same box so the two cases align to the same column.
148
+ const rawLead =
149
+ item.icon ?? (item.tone !== undefined ? <SidebarBlockToneDot tone={item.tone} /> : undefined)
150
+ const lead = rawLead === undefined ? undefined : <IconSlot>{rawLead}</IconSlot>
151
+ const meta =
152
+ item.meta !== undefined ? (
153
+ <Text component="span" className={classes.blockRowMeta}>
154
+ {item.meta}
155
+ </Text>
156
+ ) : undefined
157
+
158
+ const interactive =
159
+ item.Anchor !== undefined || item.href !== undefined || item.onClick !== undefined
160
+ if (!interactive) {
161
+ return (
162
+ <div className={`${classes.blockRow} ${classes.blockRowStatic}`}>
163
+ {lead}
164
+ <Text component="span" className={classes.blockRowLabel}>
165
+ {item.label}
166
+ </Text>
167
+ {meta}
168
+ </div>
169
+ )
170
+ }
171
+
172
+ const shared = {
173
+ classNames: { root: classes.blockRow },
174
+ label: item.label,
175
+ ...(lead !== undefined && { leftSection: lead }),
176
+ ...(meta !== undefined && { rightSection: meta }),
177
+ ...(item.onClick !== undefined && { onClick: item.onClick }),
178
+ }
179
+ const Anchor = item.Anchor
180
+ if (Anchor) return <NavLink component={Anchor} {...shared} />
181
+ return <NavLink component="a" {...(item.href !== undefined && { href: item.href })} {...shared} />
182
+ }
183
+
184
+ /**
185
+ * A `kind: 'list'` block. Two independent states, deliberately: the FOLD is persisted (a user who
186
+ * closes "Recents" keeps it closed across reloads) while "Show more" is ephemeral — it is a
187
+ * momentary reach into a longer list, not a preference, and persisting it would quietly grow the
188
+ * sidebar for good.
189
+ */
190
+ function ListBlock({ block }: { block: SidebarListBlock }) {
191
+ const [folded, setFolded] = usePersistedFold(sidebarBlockFoldKey(block.key), false)
192
+ const [showAll, setShowAll] = useState(false)
193
+
194
+ const rail = sidebarBlockRail(block)
195
+ const visible = showAll
196
+ ? block.items.length
197
+ : sidebarBlockVisibleCount(block.items.length, block.max)
198
+ const hidden = block.items.length - visible
199
+
200
+ const label = (
201
+ <>
202
+ {block.icon !== undefined && (
203
+ <span className={classes.blockIcon}>
204
+ {block.icon}
205
+ {rail === 'dot' && <span className={classes.blockDot} aria-hidden />}
206
+ </span>
207
+ )}
208
+ <BlockLabel>{block.label}</BlockLabel>
209
+ {block.count ? <NavCountBadge count={block.count} /> : null}
210
+ </>
211
+ )
212
+
213
+ const body = (
214
+ <div className={classes.blockItems}>
215
+ {block.items.slice(0, visible).map((item) => (
216
+ <BlockRow key={item.key} item={item} />
217
+ ))}
218
+ {hidden > 0 || showAll ? (
219
+ <UnstyledButton
220
+ className={classes.blockMore}
221
+ onClick={() => setShowAll(!showAll)}
222
+ aria-expanded={showAll}
223
+ >
224
+ {showAll ? 'Show less' : 'Show more'}
225
+ </UnstyledButton>
226
+ ) : null}
227
+ </div>
228
+ )
229
+
230
+ return (
231
+ <div className={classes.block} data-rail={rail}>
232
+ {block.collapsible === true ? (
233
+ <>
234
+ <UnstyledButton
235
+ className={`${classes.blockHeader} ${classes.blockHeaderButton}`}
236
+ onClick={() => setFolded(!folded)}
237
+ aria-expanded={!folded}
238
+ >
239
+ {label}
240
+ <IconChevron open={!folded} />
241
+ </UnstyledButton>
242
+ <Collapse expanded={!folded}>{body}</Collapse>
243
+ </>
244
+ ) : (
245
+ <>
246
+ <div className={classes.blockHeader}>{label}</div>
247
+ {body}
248
+ </>
249
+ )}
250
+ </div>
251
+ )
252
+ }
253
+
254
+ /** A `kind: 'progress'` block — the "Getting started 1 of 5" row pinned above the footer. */
255
+ function ProgressBlock({ block }: { block: SidebarProgressBlock }) {
256
+ const percent =
257
+ block.total > 0 ? Math.min(100, Math.max(0, (block.value / block.total) * 100)) : 0
258
+ const inner = (
259
+ <>
260
+ <div className={classes.blockProgressHead}>
261
+ <BlockLabel>{block.label}</BlockLabel>
262
+ <Text component="span" className={classes.blockRowMeta}>
263
+ {block.value} of {block.total}
264
+ </Text>
265
+ </div>
266
+ <Progress value={percent} size="xs" aria-label={block.label} />
267
+ </>
268
+ )
269
+
270
+ return (
271
+ <div className={classes.block} data-rail={sidebarBlockRail(block)}>
272
+ {block.onClick !== undefined ? (
273
+ <UnstyledButton className={classes.blockProgress} onClick={block.onClick}>
274
+ {inner}
275
+ </UnstyledButton>
276
+ ) : (
277
+ <div className={classes.blockProgress}>{inner}</div>
278
+ )}
279
+ </div>
280
+ )
281
+ }
282
+
283
+ /** A `kind: 'custom'` block — consumer content, hidden on the rail exactly as `navExtra` was. */
284
+ function CustomBlock({ block }: { block: SidebarCustomBlock }) {
285
+ return (
286
+ <div className={classes.block} data-rail="hidden">
287
+ {block.node}
288
+ </div>
289
+ )
290
+ }
291
+
292
+ /** One block, whichever kind it is. `AppSidebar` places it; this decides nothing but the paint. */
293
+ export function SidebarBlockView({
294
+ block,
295
+ }: {
296
+ block: SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock
297
+ }) {
298
+ if (block.kind === 'list') return <ListBlock block={block} />
299
+ if (block.kind === 'progress') return <ProgressBlock block={block} />
300
+ return <CustomBlock block={block} />
301
+ }
@@ -5,6 +5,18 @@
5
5
  * button as a matched pair, so the two must never drift onto different depth tokens.
6
6
  */
7
7
 
8
+ /*
9
+ * The row that appears ONLY when `search.actions` is supplied — one or two icon-only buttons beside
10
+ * the trigger. Without actions the trigger stays the row's sole node (no wrapper), so the DOM of
11
+ * every consumer that doesn't use them is untouched.
12
+ */
13
+ .row {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--vx-space-sidebar-search-gap);
17
+ min-width: 0;
18
+ }
19
+
8
20
  .trigger {
9
21
  display: flex;
10
22
  align-items: center;
@@ -74,3 +86,17 @@
74
86
  background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent) !important;
75
87
  color: var(--vx-ink) !important;
76
88
  }
89
+
90
+ /* Trailing search-row action — the same ghost treatment as `.ghostIcon` / `.railBtn`, at the `icon`
91
+ * tier (24px) the component asks for explicitly. Never re-points `--ai-size`: unlike `.railBtn`,
92
+ * this button is NOT a second state of the trigger and must not track its height. */
93
+ .actionBtn {
94
+ flex: none;
95
+ color: var(--vx-faint) !important;
96
+ border-radius: var(--vx-radius-ctrl) !important;
97
+ }
98
+
99
+ .actionBtn:hover {
100
+ background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent) !important;
101
+ color: var(--vx-ink) !important;
102
+ }
@@ -4,8 +4,10 @@
4
4
  * (`noUncheckedIndexedAccess`, `noPropertyAccessFromIndexSignature`, `exactOptionalPropertyTypes`).
5
5
  */
6
6
  declare const classes: {
7
+ readonly row: string
7
8
  readonly trigger: string
8
9
  readonly label: string
9
10
  readonly railBtn: string
11
+ readonly actionBtn: string
10
12
  }
11
13
  export default classes
@@ -8,8 +8,22 @@
8
8
  * surface + shadow-card, control radius, faint icon/label, mono `Kbd` shortcut hint.
9
9
  */
10
10
  import { ActionIcon, Kbd, Text, UnstyledButton } from '@mantine/core'
11
+ import type { ReactNode } from 'react'
12
+ import { OverflowMenu } from '../controls/actions'
13
+ import type { BarAction } from '../controls/actions'
11
14
  import classes from './sidebar-search.module.css'
12
15
 
16
+ /**
17
+ * The ⌘K row's trailing icon buttons — ONE or TWO, by type (`docs/CONTROLS-SPEC.md` §2.3).
18
+ *
19
+ * The tuple IS the enforcement: a third button pushes the search trigger under the width where its
20
+ * placeholder stays readable, and there is no third thing a sidebar search row is for. Each entry
21
+ * renders icon-only at the `icon` tier (24px, `--ai-size-icon`) with `label` demoted to the
22
+ * accessible name; a `kind: 'menu'` entry becomes an `OverflowMenu` kebab, a `kind: 'custom'` one
23
+ * is mounted verbatim.
24
+ */
25
+ export type SidebarSearchActions = [BarAction] | [BarAction, BarAction]
26
+
13
27
  export type SidebarSearchConfig = {
14
28
  /** Opens the search palette — e.g. `openSpotlight` from `basalt-ui/commands`. */
15
29
  onOpen: () => void
@@ -39,12 +53,55 @@ function IconSearch() {
39
53
  )
40
54
  }
41
55
 
56
+ /** The first grapheme of a label — the fallback body for an action shipping no icon. */
57
+ function initial(label: string): string {
58
+ return [...label][0]?.toUpperCase() ?? '?'
59
+ }
60
+
61
+ /**
62
+ * One trailing action as an icon-only button. `size="icon"` is EXPLICIT here, not inherited: the
63
+ * search row is chrome, not a control home, so no `CtlSlot` wraps it and nothing else would resolve
64
+ * the tier (`--ai-size-icon`, declared by `cssVariablesResolver` — spec §5).
65
+ */
66
+ function SearchAction({ action }: { action: BarAction }): ReactNode {
67
+ if (action.kind === 'custom') return action.node
68
+ if (action.kind === 'menu') {
69
+ return <OverflowMenu actions={action.items} trigger="kebab" label={action.label} />
70
+ }
71
+ const shared = {
72
+ variant: 'subtle' as const,
73
+ size: 'icon',
74
+ className: classes.actionBtn,
75
+ 'aria-label': action.label,
76
+ disabled: action.disabled === true,
77
+ ...(action.onClick !== undefined && { onClick: action.onClick }),
78
+ }
79
+ const body = action.icon ?? initial(action.label)
80
+ const Anchor = action.Anchor
81
+ if (Anchor !== undefined) {
82
+ return (
83
+ <ActionIcon component={Anchor} {...shared}>
84
+ {body}
85
+ </ActionIcon>
86
+ )
87
+ }
88
+ if (action.link !== undefined) {
89
+ return (
90
+ <ActionIcon component="a" href={action.link.to} {...shared}>
91
+ {body}
92
+ </ActionIcon>
93
+ )
94
+ }
95
+ return <ActionIcon {...shared}>{body}</ActionIcon>
96
+ }
97
+
42
98
  export function SidebarSearch({
43
99
  onOpen,
44
100
  placeholder = 'Search…',
45
101
  shortcut,
102
+ actions,
46
103
  collapsed,
47
- }: SidebarSearchConfig & { collapsed?: boolean }) {
104
+ }: SidebarSearchConfig & { actions?: SidebarSearchActions; collapsed?: boolean }) {
48
105
  // SSR-safe mac detection, matching the shell's other shortcut-hint logic.
49
106
  const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPod|iPad/i.test(navigator.platform)
50
107
  const hint = shortcut ?? (isMac ? '⌘K' : 'Ctrl K')
@@ -63,7 +120,7 @@ export function SidebarSearch({
63
120
  )
64
121
  }
65
122
 
66
- return (
123
+ const trigger = (
67
124
  <UnstyledButton
68
125
  type="button"
69
126
  className={classes.trigger}
@@ -77,4 +134,16 @@ export function SidebarSearch({
77
134
  <Kbd size="xs">{hint}</Kbd>
78
135
  </UnstyledButton>
79
136
  )
137
+
138
+ // No wrapper without actions — the trigger stays the row's only node, exactly as it shipped.
139
+ if (actions === undefined) return trigger
140
+
141
+ return (
142
+ <div className={classes.row}>
143
+ {trigger}
144
+ {actions.map((action) => (
145
+ <SearchAction key={action.key} action={action} />
146
+ ))}
147
+ </div>
148
+ )
80
149
  }