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,81 @@
1
+ import type { AnyField, FieldHandle, SearchValues, StoredValues } from '../state.js';
2
+ export type CreateSearchStoreOptions<S extends Record<string, AnyField>> = {
3
+ /** localStorage key (namespaced `basalt:<key>` automatically). One entry per store. */
4
+ key: string;
5
+ /** The fields, built with `field.*`. The key of each is its default URL param name. */
6
+ fields: S;
7
+ /** Envelope version — bump when the field set changes to discard stale localStorage. */
8
+ version?: number;
9
+ };
10
+ export type SearchStore<S extends Record<string, AnyField>> = {
11
+ /**
12
+ * Pass directly to `createFileRoute({ validateSearch })`. Returns every URL-lane param, resolved
13
+ * URL ⊳ localStorage ⊳ fallback — a route wired to it therefore always has the params, which is
14
+ * what makes `use()`'s "does this route own the field" check trivial and exact.
15
+ */
16
+ validateSearch: (raw: Record<string, unknown>) => SearchValues<S>;
17
+ /**
18
+ * The click-time `search:` value for a nav link. Pass it BY REFERENCE
19
+ * (`search: store.linkSearch`) inside `defineNav` — never a literal object, which pins the
20
+ * fallback on every click and is why one consumer's reader had zero call sites (C10, A2).
21
+ */
22
+ linkSearch: () => SearchValues<S>;
23
+ /** Plain read for non-React contexts (a redirect, a loader, a guard, a test). */
24
+ readStored: () => Partial<StoredValues<S>>;
25
+ /** The per-field handles every basalt control takes instead of `value`/`onChange` (C2). */
26
+ field: {
27
+ [K in keyof S]: FieldHandle<S[K]>;
28
+ };
29
+ /** Option labels for enum / multi / range fields. Chainable — call it once, at definition. */
30
+ labels: (map: Partial<{
31
+ [K in keyof S]: Record<string, string>;
32
+ }>) => SearchStore<S>;
33
+ /** Every URL-lane param, resolved — the object a page hands to its query. */
34
+ useValues: () => SearchValues<S>;
35
+ /** How many fields differ from their fallback — the `n` in a `Filters (n)` pill. */
36
+ useActiveCount: () => number;
37
+ /** Resets every field to its fallback: one navigate, one storage write. */
38
+ useReset: () => () => void;
39
+ };
40
+ /**
41
+ * The public store plus the one member only the deprecated wrappers need: `markReaderWired`, so a
42
+ * consumer whose only reader is the wrapper's own `useStore()` still counts as wired and never sees
43
+ * the pinned-link warning it never used to see.
44
+ *
45
+ * @internal — absent from `SearchStore`, so it is invisible on `createSearchStore`'s return type.
46
+ */
47
+ export type InternalSearchStore<S extends Record<string, AnyField>> = SearchStore<S> & {
48
+ markReaderWired: () => void;
49
+ };
50
+ /**
51
+ * The store, plus the one knob the deprecated wrappers need: `legacyValueField` keeps the
52
+ * enum-only stores' single-value storage layout so an already-persisted selection still resolves.
53
+ * Not exported from the barrel — `createSearchStore` is the public door.
54
+ */
55
+ export declare function buildSearchStore<const S extends Record<string, AnyField>>(o: CreateSearchStoreOptions<S> & {
56
+ legacyValueField?: string;
57
+ }): InternalSearchStore<S>;
58
+ /**
59
+ * One store per page (or per feature), over typed fields. `key` is the localStorage key; each
60
+ * field's map key is its default URL param name.
61
+ *
62
+ * @example
63
+ * export const analytics = createSearchStore({
64
+ * key: 'analytics',
65
+ * fields: {
66
+ * range: field.range({ presets: ['7d', '30d', '90d'], fallback: '30d', custom: true }),
67
+ * compare: field.enum(['none', 'previous'], 'none'),
68
+ * channels: field.multi(CHANNELS, []),
69
+ * },
70
+ * }).labels({ range: { '7d': 'Last 7 days', '30d': 'Last 30 days', '90d': 'Last 90 days' } })
71
+ *
72
+ * export const Route = createFileRoute('/analytics')({
73
+ * validateSearch: analytics.validateSearch,
74
+ * loaderDeps: ({ search }) => search,
75
+ * })
76
+ *
77
+ * // in the page: <RangeFilter field={analytics.field.range} />
78
+ * // in the nav: linkOptions({ to: '/analytics', search: analytics.linkSearch })
79
+ */
80
+ export declare function createSearchStore<const S extends Record<string, AnyField>>(o: CreateSearchStoreOptions<S>): SearchStore<S>;
81
+ //# sourceMappingURL=search-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-store.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-store.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAc,YAAY,EAAE,MAAM,UAAU,CAAA;AAI7F,MAAM,MAAM,wBAAwB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI;IACzE,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAA;IACX,uFAAuF;IACvF,MAAM,EAAE,CAAC,CAAA;IACT,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI;IAC5D;;;;OAIG;IACH,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAAA;IACjE;;;;OAIG;IACH,UAAU,EAAE,MAAM,YAAY,CAAC,CAAC,CAAC,CAAA;IACjC,iFAAiF;IACjF,UAAU,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,2FAA2F;IAC3F,KAAK,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAA;IAC5C,8FAA8F;IAC9F,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;KAAE,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,CAAA;IACpF,6EAA6E;IAC7E,SAAS,EAAE,MAAM,YAAY,CAAC,CAAC,CAAC,CAAA;IAChC,oFAAoF;IACpF,cAAc,EAAE,MAAM,MAAM,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,MAAM,IAAI,CAAA;CAC3B,CAAA;AA4DD;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IACrF,eAAe,EAAE,MAAM,IAAI,CAAA;CAC5B,CAAA;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EACvE,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D,mBAAmB,CAAC,CAAC,CAAC,CAoMxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EACxE,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAC7B,WAAW,CAAC,CAAC,CAAC,CAEhB"}
@@ -0,0 +1,187 @@
1
+ import { useNavigate, useSearch } from "@tanstack/react-router";
2
+ import { createStoreCore } from "../state.js";
3
+ function warnLinkPinsFallback(input) {
4
+ const { key, param, urlValue, storedValue } = input;
5
+ console.warn(
6
+ `[basalt-ui] createSearchStore('${key}'): the URL pinned \`${param}=${urlValue}\` (the fallback) while '${storedValue}' was persisted, and nothing has read this store back. A nav link is almost certainly declaring \`search: { ${param}: '${urlValue}' }\` at module scope, which overrides the stored value on every click \u2014 pass the store's own reader instead: \`search: <store>.linkSearch\`. (dev only)`
7
+ );
8
+ }
9
+ function useCurrentSearch() {
10
+ const search = useSearch({ strict: false });
11
+ return typeof search === "object" && search !== null ? search : {};
12
+ }
13
+ function describeValue(value) {
14
+ return typeof value === "string" ? value : JSON.stringify(value);
15
+ }
16
+ function warnWriteHasNowhereToGo(input) {
17
+ const { key, name, param } = input;
18
+ console.warn(
19
+ `[basalt-ui] createSearchStore('${key}'): wrote field '${name}' from a route that does not validate \`${param}\`, and the field is \`persist: false\` \u2014 so the write was dropped. Either render this control on the route wired to \`validateSearch\`, or give the field the localStorage lane (drop \`persist: false\`) so a write from elsewhere survives until the owning route reads it. (dev only)`
20
+ );
21
+ }
22
+ function isMemoryLane(entry) {
23
+ return !entry.codec.lane.url && !entry.codec.lane.persist;
24
+ }
25
+ function buildSearchStore(o) {
26
+ const core = createStoreCore(o);
27
+ let readerWired = false;
28
+ let warned = false;
29
+ const markReaderWired = () => {
30
+ readerWired = true;
31
+ };
32
+ const nowhereWarned = /* @__PURE__ */ new Set();
33
+ const flatten = (search, record) => {
34
+ const out = {};
35
+ for (const entry of core.urlEntries) {
36
+ Object.assign(out, entry.codec.toSearch(core.resolve(entry, search, record)));
37
+ }
38
+ return out;
39
+ };
40
+ const maybeWarn = (raw, record) => {
41
+ if (warned || readerWired || process.env["NODE_ENV"] === "production") return;
42
+ for (const entry of core.urlEntries) {
43
+ const fromUrl = entry.codec.fromSearch(raw);
44
+ if (fromUrl === null || !entry.codec.equals(fromUrl, entry.codec.fallback)) continue;
45
+ const stored = entry.codec.decode(record[entry.name]);
46
+ if (stored === null || entry.codec.equals(stored, fromUrl)) continue;
47
+ warned = true;
48
+ warnLinkPinsFallback({
49
+ key: o.key,
50
+ param: entry.codec.primary,
51
+ urlValue: describeValue(entry.codec.toSearch(fromUrl)[entry.codec.primary]),
52
+ storedValue: describeValue(entry.codec.toSearch(stored)[entry.codec.primary])
53
+ });
54
+ return;
55
+ }
56
+ };
57
+ const validateSearch = (raw) => {
58
+ const record = core.readRecord();
59
+ maybeWarn(raw, record);
60
+ return flatten(raw, record);
61
+ };
62
+ const linkSearch = () => {
63
+ readerWired = true;
64
+ return flatten(null, core.readRecord());
65
+ };
66
+ const readStored = () => {
67
+ readerWired = true;
68
+ return core.readStoredFlat();
69
+ };
70
+ const urlUse = (entry) => () => {
71
+ const search = useCurrentSearch();
72
+ const navigate = useNavigate();
73
+ const [record, setRecord] = core.usePersistedRecord();
74
+ markReaderWired();
75
+ const value = core.resolve(entry, search, record);
76
+ const validated = Object.hasOwn(search, entry.codec.primary);
77
+ return [
78
+ value,
79
+ (next) => {
80
+ if (validated) {
81
+ navigate({
82
+ to: ".",
83
+ search: (prev) => ({ ...prev, ...entry.codec.toSearch(next) }),
84
+ replace: entry.codec.lane.history === "replace",
85
+ // A filter lives halfway down a page as often as it lives in the bar, and the router's
86
+ // default scroll restoration treats this same-route search write as a navigation — so
87
+ // without this a select two screens down jumps the reader back to the top on every
88
+ // change. Never a prop: there is no filter write that WANTS the page to scroll.
89
+ resetScroll: false
90
+ });
91
+ }
92
+ if (entry.codec.lane.persist) {
93
+ core.writeField(setRecord, entry.name, next);
94
+ return;
95
+ }
96
+ if (validated || nowhereWarned.has(entry.name)) return;
97
+ if (process.env["NODE_ENV"] === "production") return;
98
+ nowhereWarned.add(entry.name);
99
+ warnWriteHasNowhereToGo({ key: o.key, name: entry.name, param: entry.codec.primary });
100
+ }
101
+ ];
102
+ };
103
+ const localUse = (entry) => () => {
104
+ const [record, setRecord] = core.usePersistedRecord();
105
+ markReaderWired();
106
+ return [
107
+ core.resolve(entry, null, record),
108
+ (next) => core.writeField(setRecord, entry.name, next)
109
+ ];
110
+ };
111
+ const memoryEntries = core.entries.filter(isMemoryLane);
112
+ const field = {};
113
+ for (const entry of core.entries) {
114
+ const use = entry.codec.lane.url ? urlUse(entry) : entry.codec.lane.persist ? localUse(entry) : core.memoryUse(entry);
115
+ field[entry.name] = core.handle(entry, use);
116
+ }
117
+ const useValues = () => {
118
+ const search = useCurrentSearch();
119
+ const [record] = core.usePersistedRecord();
120
+ markReaderWired();
121
+ return flatten(search, record);
122
+ };
123
+ const useActiveCount = () => {
124
+ const search = useCurrentSearch();
125
+ const [record] = core.usePersistedRecord();
126
+ core.useMemoryVersion();
127
+ let count = 0;
128
+ for (const entry of core.entries) {
129
+ const value = isMemoryLane(entry) ? core.readMemoryValue(entry) : core.resolve(entry, entry.codec.lane.url ? search : null, record);
130
+ if (!entry.codec.equals(value, entry.codec.fallback)) count += 1;
131
+ }
132
+ return count;
133
+ };
134
+ const useReset = () => {
135
+ const search = useCurrentSearch();
136
+ const navigate = useNavigate();
137
+ const [, setRecord] = core.usePersistedRecord();
138
+ return () => {
139
+ const validatedEntries = core.urlEntries.filter(
140
+ (entry) => Object.hasOwn(search, entry.codec.primary)
141
+ );
142
+ if (validatedEntries.length > 0) {
143
+ const patch = {};
144
+ for (const entry of validatedEntries) {
145
+ Object.assign(patch, entry.codec.toSearch(entry.codec.fallback));
146
+ }
147
+ navigate({
148
+ to: ".",
149
+ search: (prev) => ({ ...prev, ...patch }),
150
+ replace: true,
151
+ // Same reason as a single field's write above — a `Reset all` pressed from the mobile
152
+ // sheet must not also scroll the page it was pressed on.
153
+ resetScroll: false
154
+ });
155
+ }
156
+ if (memoryEntries.length > 0) core.resetMemory();
157
+ if (!core.anyPersisted) return;
158
+ const next = { ...core.readRecord() };
159
+ for (const entry of core.entries) {
160
+ if (entry.codec.lane.persist) next[entry.name] = entry.codec.fallback;
161
+ }
162
+ setRecord(next);
163
+ };
164
+ };
165
+ const store = {
166
+ validateSearch,
167
+ linkSearch,
168
+ readStored,
169
+ field,
170
+ labels: (map) => {
171
+ core.setLabels(map);
172
+ return store;
173
+ },
174
+ useValues,
175
+ useActiveCount,
176
+ useReset,
177
+ markReaderWired
178
+ };
179
+ return store;
180
+ }
181
+ function createSearchStore(o) {
182
+ return buildSearchStore(o);
183
+ }
184
+ export {
185
+ buildSearchStore,
186
+ createSearchStore
187
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search-store.router.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-store.router.test.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-store.router.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-store.test.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/search-store.test.ts"],"names":[],"mappings":""}
@@ -10,8 +10,6 @@ export type UseNavOptions<G extends ReadonlyArray<AnyNavGroup>> = {
10
10
  badges?: Partial<Record<NavItemId<G>, number | ReactNode>>;
11
11
  /** Override active detection entirely. */
12
12
  isActive?: (item: AnyNavItem) => boolean;
13
- /** Passed straight through into the returned `mobileNav`. */
14
- moreExtra?: ReactNode;
15
13
  getScrollElement?: () => HTMLElement | null;
16
14
  };
17
15
  /** Spread straight onto `<BasaltShell {...nav} />` — both keys are `BasaltShellProps` keys. */
@@ -1 +1 @@
1
- {"version":3,"file":"use-nav.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/use-nav.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnD,OAAO,KAAK,EAAE,eAAe,EAA0B,cAAc,EAAE,MAAM,cAAc,CAAA;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEpF,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,aAAa,CAAC,WAAW,CAAC,IAAI;IAChE;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAA;IACxC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;CAC5C,CAAA;AAED,+FAA+F;AAC/F,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,SAAS,EAAE,eAAe,CAAA;CAAE,CAAA;AAEnF;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,WAAW,CAAC,EAC1C,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC1C,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAqG9D"}
1
+ {"version":3,"file":"use-nav.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/use-nav.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnD,OAAO,KAAK,EAAE,eAAe,EAA0B,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3F,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEpF,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,aAAa,CAAC,WAAW,CAAC,IAAI;IAChE;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAA;IACxC,gBAAgB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;CAC5C,CAAA;AAED,+FAA+F;AAC/F,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;IAAC,SAAS,EAAE,eAAe,CAAA;CAAE,CAAA;AAEnF;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,WAAW,CAAC,EAC1C,KAAK,CAAC,CAAC,SAAS,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC1C,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAgK9D"}
@@ -1,5 +1,6 @@
1
1
  import { createElement, useMemo } from "react";
2
2
  import { Link, useMatchRoute, useRouterState } from "@tanstack/react-router";
3
+ import { flattenNav } from "./nav.js";
3
4
  function useNav(config, opts) {
4
5
  const matchRoute = useMatchRoute();
5
6
  const href = useRouterState({ select: (s) => s.location.href });
@@ -7,29 +8,52 @@ function useNav(config, opts) {
7
8
  const isActiveOverride = opts?.isActive;
8
9
  const sections = useMemo(() => {
9
10
  const badgeOf = (id) => badges?.[id];
10
- const isActive = (item) => isActiveOverride?.(item) ?? Boolean(
11
+ const matchOf = (item) => isActiveOverride?.(item) ?? Boolean(
11
12
  matchRoute({
12
13
  to: item.link.to,
13
14
  ...item.link.params !== void 0 && { params: item.link.params },
14
- // Prefix match by default so a parent stays lit on its children; `'/'` would then match
15
- // everything, so it is always exact.
16
15
  fuzzy: item.exact !== true && item.link.to !== "/"
17
16
  })
18
17
  );
18
+ const allItems = flattenNav(config);
19
+ const specificity = (to) => to.split("/").filter(Boolean).length;
20
+ const matchedById = /* @__PURE__ */ new Map();
21
+ let winnerId;
22
+ let winnerTo;
23
+ for (const item of allItems) {
24
+ const matched = matchOf(item);
25
+ matchedById.set(item.id, matched);
26
+ if (matched && (winnerTo === void 0 || specificity(item.link.to) > specificity(winnerTo))) {
27
+ winnerId = item.id;
28
+ winnerTo = item.link.to;
29
+ }
30
+ }
31
+ const resolveState = (item) => {
32
+ if (item.id === winnerId) return { active: true, ancestor: false };
33
+ const matched = matchedById.get(item.id) ?? false;
34
+ const ancestor = winnerTo !== void 0 && matched && winnerTo.startsWith(`${item.link.to}/`);
35
+ return { active: false, ancestor };
36
+ };
19
37
  const toSidebarItem = (item) => {
20
- const Anchor = (props) => createElement(Link, { ...item.link, ...props });
38
+ const Anchor = (props) => createElement(Link, {
39
+ ...item.link,
40
+ activeOptions: { exact: true },
41
+ ...props
42
+ });
21
43
  const badge = badgeOf(item.id);
22
44
  const children = item.children?.map(toSidebarItem);
45
+ const { active, ancestor } = resolveState(item);
23
46
  return {
24
47
  key: item.id,
25
48
  label: item.label,
26
49
  icon: item.icon,
27
50
  Anchor,
28
- // Per-destination active only. Rolling a child's active state up into its parent is the
29
- // MOBILE SLOT's rule (§2.3.12) and belongs to `projectMobileNav`, which reads `children`
30
- // anyway — doing it here too would also light a desktop parent row whose own route does
31
- // not match.
32
- active: isActive(item),
51
+ // Exactly one destination in the WHOLE definition reads `active` (`resolveState` above).
52
+ // Rolling that up into a mobile SLOT (any descendant active → the slot reads active) is
53
+ // still `projectMobileNav`'s job, which reads `children` for that. `ancestor` is the
54
+ // desktop-only middle state: a matched item that sits on the winner's path but isn't it.
55
+ active,
56
+ ...ancestor && { ancestor },
33
57
  ...item.short !== void 0 && { short: item.short },
34
58
  ...item.mobile !== void 0 && { mobile: item.mobile },
35
59
  ...item.disabled !== void 0 && { disabled: item.disabled },
@@ -47,7 +71,6 @@ function useNav(config, opts) {
47
71
  }));
48
72
  }, [config, matchRoute, href, badges, isActiveOverride]);
49
73
  const mobile = config.mobile;
50
- const moreExtra = opts?.moreExtra;
51
74
  const getScrollElement = opts?.getScrollElement;
52
75
  const mobileNav = useMemo(() => {
53
76
  const groupLabelById = new Map(config.groups.map((g) => [g.id, g.label]));
@@ -60,10 +83,9 @@ function useNav(config, opts) {
60
83
  ...mobile?.maxTabs !== void 0 && { maxTabs: mobile.maxTabs },
61
84
  ...mobile?.menuMax !== void 0 && { menuMax: mobile.menuMax },
62
85
  ...mobile?.moreLabel !== void 0 && { moreLabel: mobile.moreLabel },
63
- ...moreExtra !== void 0 && { moreExtra },
64
86
  ...getScrollElement !== void 0 && { getScrollElement }
65
87
  };
66
- }, [config, mobile, moreExtra, getScrollElement]);
88
+ }, [config, mobile, getScrollElement]);
67
89
  return { sections, mobileNav };
68
90
  }
69
91
  export {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-nav.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-nav.test.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/use-nav.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export { Section, type SectionProps } from './section.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/section/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { Section } from "./section.js";
2
+ export {
3
+ Section
4
+ };
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { WidgetHeaderProps } from '../widget-header/index.js';
3
+ export type SectionProps = Omit<WidgetHeaderProps, 'tier'> & {
4
+ /** Rendered in the header row, in its own `CtlSlot` — hidden while the section is collapsed. */
5
+ tabs?: ReactNode;
6
+ /** Renders a chevron toggle in the header; the header stays drawn when closed, only `children`
7
+ * (and `tabs`) unmount. @default false */
8
+ collapsible?: boolean;
9
+ /** Persists the fold state at `basalt:section:<persistKey>`. Omit for a local, unpersisted fold
10
+ * (`useState`). */
11
+ persistKey?: string;
12
+ /** The fold state a section opens on, respected only while no persisted value exists — a section
13
+ * a reader has closed stays closed. `false` is what a long secondary block wants. @default true */
14
+ defaultOpen?: boolean;
15
+ /** Rendered directly under the header and ALWAYS visible, collapsed or not: a collapsed section
16
+ * still states its headline figures, which is what makes collapsing it a real option. Keep it to
17
+ * one row — the body is what `children` is for. */
18
+ summary?: ReactNode;
19
+ /** Turns the root into a scroll anchor (`id` + `scroll-margin-top` cleared for both sticky bars). */
20
+ id?: string;
21
+ children: ReactNode;
22
+ };
23
+ export declare function Section({ title, icon, subtitle, info, value, delta, deltaPeriod, sparkline, count, actions, tabs, collapsible, persistKey, defaultOpen, summary, id, children, }: SectionProps): import("react").JSX.Element;
24
+ //# sourceMappingURL=section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/section/section.tsx"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IAC3D,gGAAgG;IAChG,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;8CAC0C;IAC1C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;uBACmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;uGACmG;IACnG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;uDAEmD;IACnD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,qGAAqG;IACrG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAgFD,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,KAAK,EACL,OAAO,EACP,IAAI,EACJ,WAAmB,EACnB,UAAU,EACV,WAAkB,EAClB,OAAO,EACP,EAAE,EACF,QAAQ,GACT,EAAE,YAAY,+BAiDd"}
@@ -0,0 +1,126 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Children, useId, useMemo, useState } from "react";
3
+ import { CtlSlot } from "../theme/index.js";
4
+ import { createPersistedState } from "../state.js";
5
+ import { WidgetHeader } from "../widget-header/index.js";
6
+ import classes from "./section.module.css";
7
+ function isDev() {
8
+ return process.env["NODE_ENV"] !== "production";
9
+ }
10
+ function warnPastActionBudget(title, actions) {
11
+ if (!isDev()) return;
12
+ const count = Children.count(actions);
13
+ if (count <= 3) return;
14
+ console.warn(
15
+ `Section "${title}": ${count} actions exceeds the \u22643 budget (docs/CONTROLS-SPEC.md C6) \u2014 move the rest behind a menu.`
16
+ );
17
+ }
18
+ const UNPERSISTED_KEY = "__local__";
19
+ function useSectionOpen(persistKey, defaultOpen) {
20
+ const [localOpen, setLocalOpen] = useState(defaultOpen);
21
+ const usePersistedOpen = useMemo(
22
+ () => createPersistedState({
23
+ key: `section:${persistKey ?? UNPERSISTED_KEY}`,
24
+ version: 1,
25
+ initial: defaultOpen
26
+ }),
27
+ [persistKey, defaultOpen]
28
+ );
29
+ const [persistedOpen, setPersistedOpen] = usePersistedOpen();
30
+ if (persistKey !== void 0) return [persistedOpen, setPersistedOpen];
31
+ return [localOpen, setLocalOpen];
32
+ }
33
+ function ChevronToggle({
34
+ open,
35
+ onToggle,
36
+ controls
37
+ }) {
38
+ return /* @__PURE__ */ jsx(
39
+ "button",
40
+ {
41
+ type: "button",
42
+ className: classes.chevron,
43
+ "data-open": open,
44
+ "aria-expanded": open,
45
+ "aria-controls": controls,
46
+ "aria-label": open ? "Collapse section" : "Expand section",
47
+ onClick: onToggle,
48
+ children: /* @__PURE__ */ jsx(
49
+ "svg",
50
+ {
51
+ width: 12,
52
+ height: 12,
53
+ viewBox: "0 0 24 24",
54
+ fill: "none",
55
+ stroke: "currentColor",
56
+ strokeWidth: 2.5,
57
+ "aria-hidden": "true",
58
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6", strokeLinecap: "round", strokeLinejoin: "round" })
59
+ }
60
+ )
61
+ }
62
+ );
63
+ }
64
+ function Section({
65
+ title,
66
+ icon,
67
+ subtitle,
68
+ info,
69
+ value,
70
+ delta,
71
+ deltaPeriod,
72
+ sparkline,
73
+ count,
74
+ actions,
75
+ tabs,
76
+ collapsible = false,
77
+ persistKey,
78
+ defaultOpen = true,
79
+ summary,
80
+ id,
81
+ children
82
+ }) {
83
+ const [open, setOpen] = useSectionOpen(persistKey, defaultOpen);
84
+ const bodyId = useId();
85
+ if (actions !== void 0) warnPastActionBudget(title, actions);
86
+ const headerActions = tabs !== void 0 || actions !== void 0 || collapsible ? /* @__PURE__ */ jsxs(Fragment, { children: [
87
+ tabs !== void 0 && open && /* @__PURE__ */ jsx(CtlSlot, { children: tabs }),
88
+ actions !== void 0 && /* @__PURE__ */ jsx(CtlSlot, { children: actions }),
89
+ collapsible && /* @__PURE__ */ jsx(ChevronToggle, { open, onToggle: () => setOpen(!open), controls: bodyId })
90
+ ] }) : void 0;
91
+ return /* @__PURE__ */ jsxs(
92
+ "div",
93
+ {
94
+ className: classes.root,
95
+ ...id !== void 0 && { id },
96
+ ...id !== void 0 && {
97
+ style: {
98
+ scrollMarginTop: "calc(var(--app-shell-header-height, 0px) + var(--basalt-page-bar-h, 0px))"
99
+ }
100
+ },
101
+ children: [
102
+ /* @__PURE__ */ jsx(
103
+ WidgetHeader,
104
+ {
105
+ tier: "section",
106
+ title,
107
+ ...icon !== void 0 && { icon },
108
+ ...subtitle !== void 0 && { subtitle },
109
+ ...info !== void 0 && { info },
110
+ ...value !== void 0 && { value },
111
+ ...delta !== void 0 && { delta },
112
+ ...deltaPeriod !== void 0 && { deltaPeriod },
113
+ ...sparkline !== void 0 && { sparkline },
114
+ ...count !== void 0 && { count },
115
+ ...headerActions !== void 0 && { actions: headerActions }
116
+ }
117
+ ),
118
+ summary !== void 0 && /* @__PURE__ */ jsx("div", { className: classes.summary, children: summary }),
119
+ open && /* @__PURE__ */ jsx("div", { id: bodyId, className: classes.body, children })
120
+ ]
121
+ }
122
+ );
123
+ }
124
+ export {
125
+ Section
126
+ };