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
@@ -3,6 +3,8 @@ import { Drawer, Menu, NavLink, ScrollArea, Stack, Text, UnstyledButton } from "
3
3
  import { useReducedMotion } from "@mantine/hooks";
4
4
  import { Fragment as Fragment2, useState } from "react";
5
5
  import { NavCountBadge } from "./nav-count-badge.js";
6
+ import { SidebarBlockToneDot } from "./sidebar-blocks.js";
7
+ import { sidebarBlockMobile } from "./sidebar-block-model.js";
6
8
  import { useBasaltSpacing } from "../theme/index.js";
7
9
  import classes from "./app-mobile-nav.module.css";
8
10
  const MENU_MIDDLEWARES = { flip: false, shift: { padding: 8 } };
@@ -134,9 +136,11 @@ function MobileNav({
134
136
  model,
135
137
  config,
136
138
  account,
137
- settingsMenuItems
139
+ settingsMenuItems,
140
+ blocks
138
141
  }) {
139
142
  const [openKey, setOpenKey] = useState(null);
143
+ const [openBlockKey, setOpenBlockKey] = useState(null);
140
144
  const reduceMotion = useReducedMotion();
141
145
  const { step } = useBasaltSpacing();
142
146
  const close = () => setOpenKey(null);
@@ -145,6 +149,61 @@ function MobileNav({
145
149
  target.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
146
150
  };
147
151
  const extraRows = [...accountRows(account), ...settingsRows(settingsMenuItems)];
152
+ const moreBlocks = (blocks ?? []).filter(
153
+ (block) => block.kind !== "custom" && sidebarBlockMobile(block) === "more" && (block.kind !== "list" || block.items.length > 0)
154
+ );
155
+ const listBlocks = moreBlocks.filter((block) => block.kind === "list");
156
+ const activateBlock = (block) => {
157
+ close();
158
+ if (block.kind === "list") {
159
+ setOpenBlockKey(block.key);
160
+ return;
161
+ }
162
+ block.onClick?.();
163
+ };
164
+ const menuBlockRow = (block) => /* @__PURE__ */ jsx(Menu.Item, { className: classes.menuItem, onClick: () => activateBlock(block), children: blockRowLabel(block) }, block.key);
165
+ const sheetBlockRow = (block) => /* @__PURE__ */ jsx(
166
+ NavLink,
167
+ {
168
+ classNames: { root: classes.row },
169
+ label: blockRowLabel(block),
170
+ onClick: () => activateBlock(block)
171
+ },
172
+ block.key
173
+ );
174
+ const blockItemRow = (item) => {
175
+ const lead = item.icon ?? (item.tone !== void 0 ? /* @__PURE__ */ jsx(SidebarBlockToneDot, { tone: item.tone }) : void 0);
176
+ const meta = item.meta !== void 0 ? /* @__PURE__ */ jsx(Text, { component: "span", className: classes.rowMeta, children: item.meta }) : void 0;
177
+ const interactive = item.Anchor !== void 0 || item.href !== void 0 || item.onClick !== void 0;
178
+ if (!interactive) {
179
+ return /* @__PURE__ */ jsxs("div", { className: classes.row, children: [
180
+ lead,
181
+ /* @__PURE__ */ jsx(Text, { component: "span", children: item.label }),
182
+ meta
183
+ ] }, item.key);
184
+ }
185
+ const shared = {
186
+ classNames: { root: classes.row },
187
+ label: item.label,
188
+ ...lead !== void 0 && { leftSection: lead },
189
+ ...meta !== void 0 && { rightSection: meta },
190
+ onClick: () => {
191
+ item.onClick?.();
192
+ setOpenBlockKey(null);
193
+ }
194
+ };
195
+ const Anchor = item.Anchor;
196
+ if (Anchor) return /* @__PURE__ */ jsx(NavLink, { component: Anchor, ...shared }, item.key);
197
+ return /* @__PURE__ */ jsx(
198
+ NavLink,
199
+ {
200
+ component: "a",
201
+ ...item.href !== void 0 && { href: item.href },
202
+ ...shared
203
+ },
204
+ item.key
205
+ );
206
+ };
148
207
  const hasCount = (slot) => slot.kind === "link" ? Boolean(slot.item.count) : slot.groups.some((g) => groupHasCount(g.items));
149
208
  const tabInner = (slot) => /* @__PURE__ */ jsxs(Fragment, { children: [
150
209
  /* @__PURE__ */ jsxs("span", { className: classes.tabIcon, children: [
@@ -271,9 +330,9 @@ function MobileNav({
271
330
  ...group.label ? [/* @__PURE__ */ jsx(Menu.Label, { children: group.label }, `${group.key}-label`)] : [],
272
331
  ...rows(group.items, 0, menuRow)
273
332
  ]),
274
- slot.isMore && extraRows.length > 0 && slotHasRows(slot) ? /* @__PURE__ */ jsx(Menu.Divider, {}) : null,
275
- slot.isMore ? extraRows.map(menuActionRow) : null,
276
- slot.isMore ? config?.moreExtra : null
333
+ slot.isMore && moreBlocks.length + extraRows.length > 0 && slotHasRows(slot) ? /* @__PURE__ */ jsx(Menu.Divider, {}) : null,
334
+ slot.isMore ? moreBlocks.map(menuBlockRow) : null,
335
+ slot.isMore ? extraRows.map(menuActionRow) : null
277
336
  ] })
278
337
  ]
279
338
  },
@@ -304,17 +363,45 @@ function MobileNav({
304
363
  sheetGroupLabel(group),
305
364
  rows(group.items, 0, sheetRow)
306
365
  ] }, group.key)),
307
- slot.isMore && extraRows.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 2, children: extraRows.map(sheetActionRow) }) : null,
308
- slot.isMore ? config?.moreExtra : null
366
+ slot.isMore && moreBlocks.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 2, children: moreBlocks.map(sheetBlockRow) }) : null,
367
+ slot.isMore && extraRows.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 2, children: extraRows.map(sheetActionRow) }) : null
309
368
  ] }) })
310
369
  }
311
370
  )
312
371
  ] }, slot.key);
313
- return /* @__PURE__ */ jsx("nav", { className: classes.bar, "aria-label": "Primary", children: model.slots.map((slot) => {
314
- if (slot.kind === "link") return linkTab(slot);
315
- if (slot.kind === "menu") return menuSlot(slot);
316
- return sheetSlot(slot);
317
- }) });
372
+ const blockSheet = (block) => /* @__PURE__ */ jsx(
373
+ Drawer,
374
+ {
375
+ opened: openBlockKey === block.key,
376
+ onClose: () => setOpenBlockKey(null),
377
+ position: "bottom",
378
+ padding: "md",
379
+ title: block.label,
380
+ classNames: {
381
+ content: classes.sheet,
382
+ title: classes.sheetTitle,
383
+ header: classes.sheetHeader
384
+ },
385
+ transitionProps: {
386
+ transition: "slide-up",
387
+ duration: reduceMotion ? 0 : 220,
388
+ timingFunction: "cubic-bezier(0.32, 0.72, 0, 1)"
389
+ },
390
+ children: /* @__PURE__ */ jsx(ScrollArea.Autosize, { mah: "62dvh", type: "scroll", children: /* @__PURE__ */ jsx(Stack, { gap: 2, children: block.items.map(blockItemRow) }) })
391
+ },
392
+ `block-${block.key}`
393
+ );
394
+ return /* @__PURE__ */ jsxs("nav", { className: classes.bar, "aria-label": "Primary", children: [
395
+ model.slots.map((slot) => {
396
+ if (slot.kind === "link") return linkTab(slot);
397
+ if (slot.kind === "menu") return menuSlot(slot);
398
+ return sheetSlot(slot);
399
+ }),
400
+ listBlocks.map(blockSheet)
401
+ ] });
402
+ }
403
+ function blockRowLabel(block) {
404
+ return block.kind === "list" ? `${block.label} \xB7 ${block.count ?? block.items.length}` : `${block.label} \xB7 ${block.value} of ${block.total}`;
318
405
  }
319
406
  function slotHasRows(slot) {
320
407
  return slot.groups.some((group) => group.items.length > 0);
@@ -252,6 +252,14 @@
252
252
  background-color: color-mix(in srgb, var(--vx-ink) 10%, transparent);
253
253
  }
254
254
 
255
+ /* Trailing meta on a block-item row (a timestamp, an owner) — the mobile twin of the sidebar's
256
+ * `.blockRowMeta`, so the same `SidebarBlockItem.meta` reads the same on both surfaces. */
257
+ .rowMeta {
258
+ flex: none;
259
+ font-size: var(--vx-text-micro);
260
+ color: var(--vx-faint);
261
+ }
262
+
255
263
  /*
256
264
  * Active = neutral surface fill + plain text color. Never the identity blue (DESIGN.md). --nl-color
257
265
  * forces NavLink's active text/icon color (otherwise the accent) to the plain text color. The
@@ -19,5 +19,6 @@ declare const classes: {
19
19
  readonly sheetHeader: string
20
20
  readonly row: string
21
21
  readonly rowNested: string
22
+ readonly rowMeta: string
22
23
  }
23
24
  export default classes
@@ -1,49 +1,76 @@
1
- import type { ReactNode } from 'react';
2
1
  import type { BrandConfig, SettingsMenuItem } from './index.js';
3
- import type { SidebarSection } from '../nav/types.js';
4
- import type { BasaltAccountProps } from './account-types.js';
5
- import type { SidebarSearchConfig } from './sidebar-search.js';
2
+ import type { SidebarBlock, SidebarSection } from '../nav/types.js';
3
+ import type { AccountMenuItem, BasaltAccountProps } from './account-types.js';
4
+ import type { SidebarSearchActions, SidebarSearchConfig } from './sidebar-search.js';
6
5
  export type AppSidebarProps = {
7
- brand: BrandConfig;
6
+ /**
7
+ * Brand identity. Supplying `menu` turns the brand row into a `Name ▾` workspace switcher —
8
+ * the entries are the existing `AccountMenuItem` shape, so a consumer already mapping account
9
+ * actions needs no second vocabulary.
10
+ */
11
+ brand: BrandConfig & {
12
+ menu?: AccountMenuItem[];
13
+ };
8
14
  sections: SidebarSection[];
9
15
  collapsed: boolean;
10
16
  onToggleCollapse: () => void;
11
17
  /**
12
18
  * Footer settings-menu entries (theme switcher, devtools, …) — supplied by the consumer.
13
- * The pinned footer "Settings" row renders ONLY when this is a non-empty list — apps that put
14
- * Settings in a nav section (the common case) omit this and get no duplicate footer row. The
15
- * `brand.version` label renders inside this menu, so it also only shows when the row does.
19
+ * The pinned footer row renders ONLY when this is a non-empty list — apps that put Settings in a
20
+ * nav section (the common case) omit this and get no duplicate footer row.
21
+ *
22
+ * **Three entries or fewer render FLAT**, as one link row each (Settings · Integrations · Invite
23
+ * teammates), and four or more collapse into the single gear "Settings" menu. A menu that opens
24
+ * to show two rows costs a click for nothing; a footer of eight rows costs the nav its height.
25
+ * That threshold is basalt's — `docs/CONTROLS-SPEC.md` §2.3 — and `settingsMenu` is the override
26
+ * for the cases the count cannot see. `brand.version` renders as a faint label under the flat
27
+ * rows, and inside the dropdown in the menu form.
16
28
  */
17
29
  settingsMenuItems?: SettingsMenuItem[];
30
+ /**
31
+ * Which footer form `settingsMenuItems` takes. `'auto'` (the default) is the count rule above;
32
+ * `'flat'` and `'menu'` force one regardless of how many entries there are.
33
+ *
34
+ * The override exists because the COUNT is not the whole question. Three rows that are each a
35
+ * theme radio group, a devtools switch and a density slider are three CONTROLS, not three
36
+ * destinations — flat they fill the footer with widgets and push the account row off the fold,
37
+ * and the count rule cannot tell them from three links. Equally, four short destinations may be
38
+ * worth their height on a tall sidebar. `'auto'` stays the default so nothing moves for a
39
+ * consumer that never had an opinion.
40
+ *
41
+ * DESKTOP FOOTER ONLY. Below `sm` there is no sidebar at all and the same entries are flat rows
42
+ * in the bottom bar's More surface either way — a dropdown inside that sheet would be a menu
43
+ * inside a menu.
44
+ *
45
+ * @default 'auto'
46
+ */
47
+ settingsMenu?: 'auto' | 'flat' | 'menu';
18
48
  /**
19
49
  * Optional account row rendered below the settings menu in the sidebar footer (see
20
- * `SidebarAccount` / `BasaltAccountProps`), separated by its own top hairline. Omitting it
21
- * reproduces today's footer unchanged.
50
+ * `SidebarAccount` / `BasaltAccountProps`) — no separating hairline, the row's own top padding
51
+ * supplies the separation. Omitting it reproduces today's footer unchanged.
22
52
  */
23
53
  account?: BasaltAccountProps;
24
54
  /**
25
55
  * Optional search field rendered directly below the brand and ABOVE the nav scroll region — a
26
- * fixed, non-scrolling row. Pair with basalt-ui/commands' openSpotlight.
56
+ * fixed, non-scrolling row. Pair with basalt-ui/commands' openSpotlight. `actions` adds up to two
57
+ * icon-only buttons to the right of that row.
27
58
  */
28
- search?: SidebarSearchConfig;
59
+ search?: SidebarSearchConfig & {
60
+ actions?: SidebarSearchActions;
61
+ };
29
62
  /**
30
- * Arbitrary content appended after `sections` inside the nav `ScrollArea` — a tree, a filter
31
- * panel, a project list, anything a set of `SidebarItem`s can't express. Renders as the last
32
- * child of the scrolling nav column, so a long list scrolls with the rest of the nav instead of
33
- * fighting it for height. Pass `sections={[]}` to use this slot exclusively; the section-spacing
34
- * rule only fires between adjacent children, so an empty `sections` produces no orphan divider or
35
- * dead padding above it.
63
+ * Declared blocks (`docs/CONTROLS-SPEC.md` §2.3, law C13) — an "Awaiting action" list, a
64
+ * "Recents" list, a "Getting started" progress row, or a `kind: 'custom'` node for a tree or
65
+ * filter panel no set of rows can express (`kind: 'custom'` is what replaced `navExtra`).
36
66
  *
37
- * Hidden on the collapsed desktop rail; still present in the mobile drawer, which opens at full
38
- * width. The rail is ~56px of icon buttons with no sensible representation for arbitrary
39
- * consumer content, so the CSS media query that drives the rail hides this slot the same way it
40
- * hides `.childList` — never a JS check on `collapsed`, since that one value is shared by the
41
- * rail AND the drawer (only the media query tells them apart). `SidebarSearch` gets to adapt
42
- * itself to the rail (it takes `collapsed` and renders its own icon-only form) because the shell
43
- * owns that control; it cannot adapt content it knows nothing about, so this slot hides instead
44
- * of squashing it.
67
+ * `placement: 'nav'` appends after `sections` INSIDE the nav scroll region, so a long list
68
+ * scrolls with the nav instead of fighting it for height; `'bottom'` pins above the settings
69
+ * footer. Because these are data and not a `ReactNode` slot, basalt owns both projections a slot
70
+ * could never express: the collapsed rail (a count dot on the icon, a ring on the settings row)
71
+ * and the mobile More sheet — see `sidebarBlockRail` / `sidebarBlockMobile`.
45
72
  */
46
- navExtra?: ReactNode;
73
+ blocks?: SidebarBlock[];
47
74
  };
48
- export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse, settingsMenuItems, account, search, navExtra, }: AppSidebarProps): import("react").JSX.Element;
75
+ export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse, settingsMenuItems, settingsMenu, account, search, blocks, }: AppSidebarProps): import("react").JSX.Element;
49
76
  //# sourceMappingURL=app-sidebar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,cAAc,CAAA;AAG/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAK3D,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAqQD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,QAAQ,GACT,EAAE,eAAe,+BAuJjB"}
1
+ {"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAe,cAAc,EAAE,MAAM,cAAc,CAAA;AAG7E,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAE1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAgBjF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,KAAK,EAAE,WAAW,GAAG;QAAE,IAAI,CAAC,EAAE,eAAe,EAAE,CAAA;KAAE,CAAA;IACjD,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;IACvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,mBAAmB,GAAG;QAAE,OAAO,CAAC,EAAE,oBAAoB,CAAA;KAAE,CAAA;IACjE;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAA;CACxB,CAAA;AA4VD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,YAAqB,EACrB,OAAO,EACP,MAAM,EACN,MAAM,GACP,EAAE,eAAe,+BAiKjB"}
@@ -14,10 +14,21 @@ import {
14
14
  UnstyledButton
15
15
  } from "@mantine/core";
16
16
  import { useDisclosure } from "@mantine/hooks";
17
- import { useEffect, useRef, useState } from "react";
17
+ import { useEffect, useRef } from "react";
18
18
  import { NavCountBadge } from "./nav-count-badge.js";
19
19
  import { SidebarAccount } from "./app-sidebar-account.js";
20
20
  import { SidebarSearch } from "./sidebar-search.js";
21
+ import {
22
+ IconChevron,
23
+ SidebarBlockView,
24
+ SidebarProgressRing,
25
+ usePersistedFold
26
+ } from "./sidebar-blocks.js";
27
+ import {
28
+ sidebarBlockPlacement,
29
+ sidebarBlockRail,
30
+ sidebarSectionFoldKey
31
+ } from "./sidebar-block-model.js";
21
32
  import { VX } from "../tokens/index.js";
22
33
  import { useBasaltSpacing } from "../theme/index.js";
23
34
  import classes from "./app-sidebar.module.css";
@@ -42,23 +53,6 @@ function IconCollapse({ collapsed }) {
42
53
  }
43
54
  );
44
55
  }
45
- function IconChevron({ open }) {
46
- return /* @__PURE__ */ jsx(
47
- "svg",
48
- {
49
- width: 12,
50
- height: 12,
51
- viewBox: "0 0 24 24",
52
- fill: "none",
53
- stroke: "currentColor",
54
- strokeWidth: 2,
55
- strokeLinecap: "round",
56
- strokeLinejoin: "round",
57
- "aria-hidden": true,
58
- children: open ? /* @__PURE__ */ jsx("path", { d: "M6 9l6 6l6 -6" }) : /* @__PURE__ */ jsx("path", { d: "M9 6l6 6l-6 6" })
59
- }
60
- );
61
- }
62
56
  function IconGear() {
63
57
  return /* @__PURE__ */ jsxs(
64
58
  "svg",
@@ -81,6 +75,31 @@ function IconGear() {
81
75
  }
82
76
  const HOVER_OPEN_DELAY = 150;
83
77
  const HOVER_CLOSE_DELAY = 200;
78
+ const FLAT_SETTINGS_MAX = 3;
79
+ function BrandName({
80
+ brand,
81
+ menuWidth
82
+ }) {
83
+ const label = /* @__PURE__ */ jsx(Text, { className: classes.brandName, fz: VX.text.xl, fw: 550, children: brand.name });
84
+ const menu = brand.menu;
85
+ if (menu === void 0 || menu.length === 0) return label;
86
+ return /* @__PURE__ */ jsxs(Menu, { position: "bottom-start", withArrow: true, width: menuWidth, zIndex: 500, children: [
87
+ /* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsxs(UnstyledButton, { className: classes.brandButton, "aria-label": `${brand.name} workspace`, children: [
88
+ label,
89
+ /* @__PURE__ */ jsx(IconChevron, { open: true })
90
+ ] }) }),
91
+ /* @__PURE__ */ jsx(Menu.Dropdown, { children: menu.map((entry) => /* @__PURE__ */ jsx(
92
+ Menu.Item,
93
+ {
94
+ leftSection: entry.icon,
95
+ ...entry.danger === true && { color: "red" },
96
+ onClick: entry.onClick,
97
+ children: entry.label
98
+ },
99
+ entry.key
100
+ )) })
101
+ ] });
102
+ }
84
103
  function hasActiveDescendant(item) {
85
104
  if (item.active) return true;
86
105
  return item.children?.some((c) => hasActiveDescendant(c)) ?? false;
@@ -93,6 +112,15 @@ function NavLinkBody({ item, active }) {
93
112
  leftSection: item.icon,
94
113
  rightSection: item.badge ?? (item.count ? /* @__PURE__ */ jsx(NavCountBadge, { count: item.count }) : void 0),
95
114
  active,
115
+ // Basalt is the one that stamps `aria-current`, not the theme and not a router `<Link>`'s own
116
+ // default (which a consumer's `SidebarItem.Anchor` may compute independently) — mirrors
117
+ // `app-mobile-nav.tsx`'s identical pattern so both surfaces agree on what "active" means.
118
+ "aria-current": active ? "page" : void 0,
119
+ // `item.ancestor` (a parent on the winner's path — `useNav`'s exclusivity pass) is NEVER
120
+ // active and never carries `aria-current`; it only earns a `data-ancestor` hook for CSS. A
121
+ // leaf item never has `ancestor` set, so this is a no-op there — shared here rather than
122
+ // duplicated per render path.
123
+ ...item.ancestor === true && { "data-ancestor": true },
96
124
  ...item.onClick !== void 0 && { onClick: item.onClick }
97
125
  };
98
126
  if (Anchor) {
@@ -100,8 +128,36 @@ function NavLinkBody({ item, active }) {
100
128
  }
101
129
  return /* @__PURE__ */ jsx(NavLink, { component: "a", ...item.href !== void 0 && { href: item.href }, ...shared });
102
130
  }
103
- function SectionLabel({ children, flush }) {
104
- return /* @__PURE__ */ jsx(Text, { component: "div", px: flush ? 0 : "xs", mb: flush ? 0 : 4, className: classes.sectionLabel, children });
131
+ function SectionLabel({ children }) {
132
+ return /* @__PURE__ */ jsx(Text, { component: "div", px: 0, mb: 0, className: classes.sectionLabel, children });
133
+ }
134
+ function NavSection({ section, collapsed }) {
135
+ const [folded, setFolded] = usePersistedFold(
136
+ sidebarSectionFoldKey(section.label),
137
+ Boolean(section.defaultCollapsed)
138
+ );
139
+ const items = section.items.map((item) => /* @__PURE__ */ jsx(NavItemRow, { item, collapsed }, item.key));
140
+ if (!section.collapsible) {
141
+ return /* @__PURE__ */ jsxs("div", { children: [
142
+ /* @__PURE__ */ jsx("div", { className: classes.sectionBand, children: /* @__PURE__ */ jsx(SectionLabel, { children: section.label }) }),
143
+ /* @__PURE__ */ jsx(Stack, { gap: 1, children: items })
144
+ ] });
145
+ }
146
+ return /* @__PURE__ */ jsxs("div", { children: [
147
+ /* @__PURE__ */ jsxs(
148
+ UnstyledButton,
149
+ {
150
+ className: `${classes.sectionBand} ${classes.sectionHeader}`,
151
+ onClick: () => setFolded(!folded),
152
+ "aria-expanded": !folded,
153
+ children: [
154
+ /* @__PURE__ */ jsx(SectionLabel, { children: section.label }),
155
+ /* @__PURE__ */ jsx(IconChevron, { open: !folded })
156
+ ]
157
+ }
158
+ ),
159
+ /* @__PURE__ */ jsx(Collapse, { expanded: !folded, children: /* @__PURE__ */ jsx(Stack, { gap: 1, children: items }) })
160
+ ] });
105
161
  }
106
162
  function NavItemRow({ item, collapsed }) {
107
163
  const [opened, { open, close }] = useDisclosure(false);
@@ -213,33 +269,51 @@ function AppSidebar({
213
269
  collapsed,
214
270
  onToggleCollapse,
215
271
  settingsMenuItems,
272
+ settingsMenu = "auto",
216
273
  account,
217
274
  search,
218
- navExtra
275
+ blocks
219
276
  }) {
220
277
  const { step } = useBasaltSpacing();
221
- const [collapsedSections, setCollapsedSections] = useState(
222
- () => Object.fromEntries(sections.map((s) => [s.label, Boolean(s.defaultCollapsed)]))
278
+ const navBlocks = (blocks ?? []).filter((block) => sidebarBlockPlacement(block) === "nav");
279
+ const bottomBlocks = (blocks ?? []).filter((block) => sidebarBlockPlacement(block) === "bottom");
280
+ const railRing = bottomBlocks.find(
281
+ (block) => block.kind === "progress" && sidebarBlockRail(block) === "ring"
223
282
  );
224
- const renderSectionItems = (section) => section.items.map((item) => /* @__PURE__ */ jsx(NavItemRow, { item, collapsed }, item.key));
225
- const hasSettingsMenu = (settingsMenuItems?.length ?? 0) > 0;
226
- const settingsRow = hasSettingsMenu ? /* @__PURE__ */ jsx(Group, { ...account ? {} : { className: classes.footer }, gap: "xs", wrap: "nowrap", children: /* @__PURE__ */ jsxs(Menu, { position: "right-start", withArrow: true, width: step.sidebarSettingsMenuWidth, zIndex: 500, children: [
283
+ const settingsItems = settingsMenuItems ?? [];
284
+ const versionLabel = brand.version !== void 0 ? `${brand.name} v${brand.version}` : void 0;
285
+ const ringMark = collapsed && railRing !== void 0 && railRing.kind === "progress" ? /* @__PURE__ */ jsx(SidebarProgressRing, { value: railRing.value, total: railRing.total }) : null;
286
+ const flatSettings = settingsMenu === "flat" || settingsMenu === "auto" && settingsItems.length <= FLAT_SETTINGS_MAX;
287
+ const settingsRow = settingsItems.length === 0 ? null : flatSettings ? /* @__PURE__ */ jsxs("div", { className: classes.footerLinks, children: [
288
+ settingsItems.map((entry, index) => /* @__PURE__ */ jsxs(
289
+ UnstyledButton,
290
+ {
291
+ className: classes.footerBtn,
292
+ onClick: entry.onClick,
293
+ "aria-label": entry.label,
294
+ children: [
295
+ /* @__PURE__ */ jsx("span", { className: classes.footerIconSlot, children: entry.icon ?? /* @__PURE__ */ jsx(IconGear, {}) }),
296
+ /* @__PURE__ */ jsx(Text, { className: classes.footerText, fz: VX.text.md, children: entry.label }),
297
+ index === 0 ? ringMark : null
298
+ ]
299
+ },
300
+ entry.key
301
+ )),
302
+ versionLabel !== void 0 && /* @__PURE__ */ jsx(Text, { component: "div", className: classes.footerVersion, children: versionLabel })
303
+ ] }) : /* @__PURE__ */ jsx(Group, { gap: "xs", wrap: "nowrap", children: /* @__PURE__ */ jsxs(Menu, { position: "right-start", withArrow: true, width: step.sidebarSettingsMenuWidth, zIndex: 500, children: [
227
304
  /* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsxs(UnstyledButton, { className: classes.footerBtn, "aria-label": "Settings", children: [
228
305
  /* @__PURE__ */ jsx(IconGear, {}),
229
- /* @__PURE__ */ jsx(Text, { className: classes.footerText, fz: VX.text.md, children: "Settings" })
306
+ /* @__PURE__ */ jsx(Text, { className: classes.footerText, fz: VX.text.md, children: "Settings" }),
307
+ ringMark
230
308
  ] }) }),
231
309
  /* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
232
- settingsMenuItems?.map((entry) => /* @__PURE__ */ jsx(Menu.Item, { leftSection: entry.icon, onClick: entry.onClick, children: entry.label }, entry.key)),
233
- brand.version && /* @__PURE__ */ jsxs(Fragment, { children: [
310
+ settingsItems.map((entry) => /* @__PURE__ */ jsx(Menu.Item, { leftSection: entry.icon, onClick: entry.onClick, children: entry.label }, entry.key)),
311
+ versionLabel !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
234
312
  /* @__PURE__ */ jsx(Menu.Divider, {}),
235
- /* @__PURE__ */ jsxs(Menu.Label, { children: [
236
- brand.name,
237
- " v",
238
- brand.version
239
- ] })
313
+ /* @__PURE__ */ jsx(Menu.Label, { children: versionLabel })
240
314
  ] })
241
315
  ] })
242
- ] }) }) : null;
316
+ ] }) });
243
317
  return /* @__PURE__ */ jsxs(Stack, { gap: 0, h: "100%", className: classes.root, "data-collapsed": collapsed || void 0, children: [
244
318
  /* @__PURE__ */ jsxs(Group, { className: classes.brand, gap: "sm", wrap: "nowrap", children: [
245
319
  /* @__PURE__ */ jsxs(Group, { className: classes.brandLead, gap: "sm", wrap: "nowrap", children: [
@@ -253,7 +327,7 @@ function AppSidebar({
253
327
  style: { display: "block" }
254
328
  }
255
329
  ),
256
- /* @__PURE__ */ jsx(Text, { className: classes.brandName, fz: VX.text.xl, fw: 550, children: brand.name })
330
+ /* @__PURE__ */ jsx(BrandName, { brand, menuWidth: step.sidebarAccountMenuWidth })
257
331
  ] }),
258
332
  /* @__PURE__ */ jsx(
259
333
  ActionIcon,
@@ -278,40 +352,15 @@ function AppSidebar({
278
352
  className: classes.navScroll,
279
353
  classNames: { viewport: classes.navViewport },
280
354
  children: /* @__PURE__ */ jsxs(Stack, { gap: 0, className: classes.nav, children: [
281
- sections.map((section) => {
282
- if (!section.collapsible) {
283
- return /* @__PURE__ */ jsxs("div", { children: [
284
- /* @__PURE__ */ jsx("div", { className: classes.sectionBand, children: /* @__PURE__ */ jsx(SectionLabel, { flush: true, children: section.label }) }),
285
- /* @__PURE__ */ jsx(Stack, { gap: 1, children: renderSectionItems(section) })
286
- ] }, section.label);
287
- }
288
- const isOpen = !collapsedSections[section.label];
289
- return /* @__PURE__ */ jsxs("div", { children: [
290
- /* @__PURE__ */ jsxs(
291
- UnstyledButton,
292
- {
293
- className: `${classes.sectionBand} ${classes.sectionHeader}`,
294
- onClick: () => setCollapsedSections((prev) => ({
295
- ...prev,
296
- [section.label]: !prev[section.label]
297
- })),
298
- "aria-expanded": isOpen,
299
- children: [
300
- /* @__PURE__ */ jsx(SectionLabel, { flush: true, children: section.label }),
301
- /* @__PURE__ */ jsx(IconChevron, { open: isOpen })
302
- ]
303
- }
304
- ),
305
- /* @__PURE__ */ jsx(Collapse, { expanded: isOpen, children: /* @__PURE__ */ jsx(Stack, { gap: 1, children: renderSectionItems(section) }) })
306
- ] }, section.label);
307
- }),
308
- navExtra !== void 0 && navExtra !== null && /* @__PURE__ */ jsx("div", { className: classes.navExtra, children: navExtra })
355
+ sections.map((section) => /* @__PURE__ */ jsx(NavSection, { section, collapsed }, section.label)),
356
+ navBlocks.map((block) => /* @__PURE__ */ jsx(SidebarBlockView, { block }, block.key))
309
357
  ] })
310
358
  }
311
359
  ),
312
- account ? /* @__PURE__ */ jsxs(Stack, { gap: 0, className: classes.footer, children: [
360
+ (bottomBlocks.length > 0 || settingsRow !== null || account !== void 0) && /* @__PURE__ */ jsxs(Stack, { gap: 0, className: classes.footer, children: [
361
+ bottomBlocks.map((block) => /* @__PURE__ */ jsx(SidebarBlockView, { block }, block.key)),
313
362
  settingsRow,
314
- /* @__PURE__ */ jsx(
363
+ account !== void 0 && /* @__PURE__ */ jsx(
315
364
  SidebarAccount,
316
365
  {
317
366
  state: account.state,
@@ -319,7 +368,7 @@ function AppSidebar({
319
368
  ...account.showEmail !== void 0 && { showEmail: account.showEmail }
320
369
  }
321
370
  )
322
- ] }) : settingsRow
371
+ ] })
323
372
  ] });
324
373
  }
325
374
  export {