basalt-ui 1.25.0 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +26 -0
  3. package/MIGRATING.md +815 -23
  4. package/README.md +45 -32
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +160 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +118 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1465 -36
  17. package/configs/oxlint-plugin.test.ts +1190 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +39 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +93 -0
  75. package/dist/controls/filter-context.d.ts +60 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +20 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +46 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/number-filter.d.ts +26 -0
  99. package/dist/controls/number-filter.d.ts.map +1 -0
  100. package/dist/controls/number-filter.js +143 -0
  101. package/dist/controls/peer-boundary.test.d.ts +2 -0
  102. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  103. package/dist/controls/props-contract.test.d.ts +2 -0
  104. package/dist/controls/props-contract.test.d.ts.map +1 -0
  105. package/dist/controls/range-filter.d.ts +43 -0
  106. package/dist/controls/range-filter.d.ts.map +1 -0
  107. package/dist/controls/range-filter.js +135 -0
  108. package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
  109. package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
  110. package/dist/controls/search-filter.d.ts +10 -0
  111. package/dist/controls/search-filter.d.ts.map +1 -0
  112. package/dist/controls/search-filter.js +81 -0
  113. package/dist/controls/select-filter.d.ts +74 -0
  114. package/dist/controls/select-filter.d.ts.map +1 -0
  115. package/dist/controls/select-filter.js +10 -0
  116. package/dist/controls/sync-button.d.ts +38 -0
  117. package/dist/controls/sync-button.d.ts.map +1 -0
  118. package/dist/controls/sync-button.js +119 -0
  119. package/dist/controls/sync-button.module.css +63 -0
  120. package/dist/controls/sync-button.module.css.d.ts +11 -0
  121. package/dist/controls/sync-button.test.d.ts +2 -0
  122. package/dist/controls/sync-button.test.d.ts.map +1 -0
  123. package/dist/controls/toggle-filter.d.ts +9 -0
  124. package/dist/controls/toggle-filter.d.ts.map +1 -0
  125. package/dist/controls/toggle-filter.js +42 -0
  126. package/dist/controls/view-tabs.d.ts +23 -0
  127. package/dist/controls/view-tabs.d.ts.map +1 -0
  128. package/dist/controls/view-tabs.js +59 -0
  129. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  130. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  131. package/dist/controls-dates/date-range-picker.js +18 -0
  132. package/dist/controls-dates/index.d.ts +9 -0
  133. package/dist/controls-dates/index.d.ts.map +1 -0
  134. package/dist/controls-dates/index.js +4 -0
  135. package/dist/dashboard/delta-badge.d.ts +6 -12
  136. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  137. package/dist/dashboard/delta-badge.js +1 -50
  138. package/dist/dashboard/index.d.ts +1 -1
  139. package/dist/dashboard/index.d.ts.map +1 -1
  140. package/dist/dashboard/index.js +3 -1
  141. package/dist/dashboard/settings-section.d.ts +6 -4
  142. package/dist/dashboard/settings-section.d.ts.map +1 -1
  143. package/dist/dashboard/settings-section.js +23 -16
  144. package/dist/dashboard/settings-section.module.css +10 -0
  145. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  146. package/dist/dashboard/settings-section.test.d.ts +2 -0
  147. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  148. package/dist/dashboard/stat-card.d.ts +95 -9
  149. package/dist/dashboard/stat-card.d.ts.map +1 -1
  150. package/dist/dashboard/stat-card.js +108 -49
  151. package/dist/dashboard/stat-card.module.css +153 -0
  152. package/dist/dashboard/stat-card.module.css.d.ts +15 -0
  153. package/dist/data/data-table.d.ts +37 -15
  154. package/dist/data/data-table.d.ts.map +1 -1
  155. package/dist/data/data-table.js +96 -58
  156. package/dist/guard/grace.test.d.ts +20 -0
  157. package/dist/guard/grace.test.d.ts.map +1 -0
  158. package/dist/guard/index.d.ts +66 -3
  159. package/dist/guard/index.d.ts.map +1 -1
  160. package/dist/guard/index.js +78 -7
  161. package/dist/guard/types.d.ts +3 -3
  162. package/dist/guard/types.d.ts.map +1 -1
  163. package/dist/index.d.ts +13 -3
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js +13 -10
  166. package/dist/nav/types.d.ts +78 -2
  167. package/dist/nav/types.d.ts.map +1 -1
  168. package/dist/notifications/bell.d.ts +4 -1
  169. package/dist/notifications/bell.d.ts.map +1 -1
  170. package/dist/notifications/index.d.ts +1 -1
  171. package/dist/router-tanstack/field.d.ts +14 -0
  172. package/dist/router-tanstack/field.d.ts.map +1 -0
  173. package/dist/router-tanstack/field.js +4 -0
  174. package/dist/router-tanstack/index.d.ts +6 -2
  175. package/dist/router-tanstack/index.d.ts.map +1 -1
  176. package/dist/router-tanstack/index.js +4 -0
  177. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  178. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  179. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  180. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  181. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  182. package/dist/router-tanstack/search-param-store.js +16 -45
  183. package/dist/router-tanstack/search-store.d.ts +84 -0
  184. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  185. package/dist/router-tanstack/search-store.js +271 -0
  186. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  187. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  188. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  189. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  190. package/dist/router-tanstack/use-nav.d.ts +0 -2
  191. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  192. package/dist/router-tanstack/use-nav.js +34 -12
  193. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  194. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  195. package/dist/section/index.d.ts +2 -0
  196. package/dist/section/index.d.ts.map +1 -0
  197. package/dist/section/index.js +4 -0
  198. package/dist/section/section.d.ts +24 -0
  199. package/dist/section/section.d.ts.map +1 -0
  200. package/dist/section/section.js +126 -0
  201. package/dist/section/section.module.css +104 -0
  202. package/dist/section/section.module.css.d.ts +11 -0
  203. package/dist/section/section.test.d.ts +2 -0
  204. package/dist/section/section.test.d.ts.map +1 -0
  205. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  206. package/dist/shell/app-breadcrumbs.js +18 -15
  207. package/dist/shell/app-header.module.css +77 -37
  208. package/dist/shell/app-header.module.css.d.ts +2 -1
  209. package/dist/shell/app-mobile-nav.d.ts +9 -2
  210. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  211. package/dist/shell/app-mobile-nav.js +98 -11
  212. package/dist/shell/app-mobile-nav.module.css +8 -0
  213. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  214. package/dist/shell/app-sidebar.d.ts +55 -28
  215. package/dist/shell/app-sidebar.d.ts.map +1 -1
  216. package/dist/shell/app-sidebar.js +116 -67
  217. package/dist/shell/app-sidebar.module.css +303 -20
  218. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  219. package/dist/shell/index.d.ts +55 -21
  220. package/dist/shell/index.d.ts.map +1 -1
  221. package/dist/shell/index.js +40 -18
  222. package/dist/shell/mobile-nav-model.d.ts +13 -2
  223. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  224. package/dist/shell/mobile-nav-model.js +15 -1
  225. package/dist/shell/nav-count-badge.d.ts +1 -1
  226. package/dist/shell/page-bar.d.ts +51 -0
  227. package/dist/shell/page-bar.d.ts.map +1 -0
  228. package/dist/shell/page-bar.js +154 -0
  229. package/dist/shell/page-bar.module.css +178 -0
  230. package/dist/shell/page-bar.module.css.d.ts +17 -0
  231. package/dist/shell/page-bar.test.d.ts +2 -0
  232. package/dist/shell/page-bar.test.d.ts.map +1 -0
  233. package/dist/shell/sidebar-block-model.d.ts +48 -0
  234. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  235. package/dist/shell/sidebar-block-model.js +39 -0
  236. package/dist/shell/sidebar-blocks.d.ts +32 -0
  237. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  238. package/dist/shell/sidebar-blocks.js +169 -0
  239. package/dist/shell/sidebar-search.d.ts +13 -1
  240. package/dist/shell/sidebar-search.d.ts.map +1 -1
  241. package/dist/shell/sidebar-search.js +34 -1
  242. package/dist/shell/sidebar-search.module.css +26 -0
  243. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  244. package/dist/state/fields.d.ts +565 -0
  245. package/dist/state/fields.d.ts.map +1 -0
  246. package/dist/state/fields.js +405 -0
  247. package/dist/state/fields.test.d.ts +2 -0
  248. package/dist/state/fields.test.d.ts.map +1 -0
  249. package/dist/state/persisted.d.ts +60 -0
  250. package/dist/state/persisted.d.ts.map +1 -0
  251. package/dist/state/persisted.js +123 -0
  252. package/dist/state/persisted.test.d.ts +2 -0
  253. package/dist/state/persisted.test.d.ts.map +1 -0
  254. package/dist/state.d.ts +11 -56
  255. package/dist/state.d.ts.map +1 -1
  256. package/dist/state.js +14 -120
  257. package/dist/styles.css +13 -0
  258. package/dist/surfaces.d.ts +106 -24
  259. package/dist/surfaces.d.ts.map +1 -1
  260. package/dist/surfaces.js +156 -19
  261. package/dist/theme/controls.module.css +16 -1
  262. package/dist/theme/ctl-theme.d.ts +32 -0
  263. package/dist/theme/ctl-theme.d.ts.map +1 -0
  264. package/dist/theme/ctl-theme.js +54 -0
  265. package/dist/theme/ctl-theme.test.d.ts +2 -0
  266. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  267. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  268. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  269. package/dist/theme/divider-law.test.d.ts +2 -0
  270. package/dist/theme/divider-law.test.d.ts.map +1 -0
  271. package/dist/theme/icon-slot.d.ts +44 -0
  272. package/dist/theme/icon-slot.d.ts.map +1 -0
  273. package/dist/theme/icon-slot.js +21 -0
  274. package/dist/theme/icon-slot.module.css +47 -0
  275. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  276. package/dist/theme/icon-slot.test.d.ts +2 -0
  277. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  278. package/dist/theme/index.d.ts +1 -0
  279. package/dist/theme/index.d.ts.map +1 -1
  280. package/dist/theme/index.js +82 -5
  281. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  282. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  283. package/dist/theme/segmented-control.module.css +20 -0
  284. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  285. package/dist/tokens/index.d.ts +12 -0
  286. package/dist/tokens/index.d.ts.map +1 -1
  287. package/dist/tokens/index.js +38 -8
  288. package/dist/tokens/palette.d.ts +97 -61
  289. package/dist/tokens/palette.d.ts.map +1 -1
  290. package/dist/tokens/palette.js +76 -38
  291. package/dist/tokens.css +10 -2
  292. package/dist/widget-header/delta-badge.d.ts +13 -0
  293. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  294. package/dist/widget-header/delta-badge.js +29 -0
  295. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  296. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  297. package/dist/widget-header/index.d.ts +3 -0
  298. package/dist/widget-header/index.d.ts.map +1 -0
  299. package/dist/widget-header/index.js +6 -0
  300. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  301. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  302. package/dist/widget-header/widget-header.d.ts +67 -0
  303. package/dist/widget-header/widget-header.d.ts.map +1 -0
  304. package/dist/widget-header/widget-header.js +89 -0
  305. package/dist/widget-header/widget-header.module.css +350 -0
  306. package/dist/widget-header/widget-header.module.css.d.ts +25 -0
  307. package/dist/widget-header/widget-header.test.d.ts +2 -0
  308. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  309. package/llms.txt +30 -17
  310. package/package.json +14 -1
  311. package/src/charts/cursor/resolve.test.ts +29 -1
  312. package/src/charts/cursor/resolve.ts +38 -6
  313. package/src/charts/cursor/scope.tsx +6 -3
  314. package/src/charts/cursor/store.test.ts +19 -1
  315. package/src/charts/cursor/store.ts +10 -5
  316. package/src/charts/hooks/useChartCursor.ts +16 -8
  317. package/src/charts/index.ts +2 -2
  318. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  319. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  320. package/src/charts/primitives/ChartCard.tsx +86 -146
  321. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  322. package/src/cli/check-coverage.test.ts +297 -0
  323. package/src/cli/check-theme.test.ts +38 -14
  324. package/src/cli/index.ts +392 -16
  325. package/src/cli/init-sync.test.ts +97 -0
  326. package/src/connectivity/connectivity-indicator.tsx +16 -9
  327. package/src/content/index.ts +16 -3
  328. package/src/content/prose.module.css +7 -28
  329. package/src/content/prose.module.css.test.ts +8 -24
  330. package/src/controls/actions.module.css +62 -0
  331. package/src/controls/actions.module.css.d.ts +13 -0
  332. package/src/controls/actions.test.tsx +252 -0
  333. package/src/controls/actions.tsx +655 -0
  334. package/src/controls/actions.type-guard.test.ts +63 -0
  335. package/src/controls/compare-filter.tsx +64 -0
  336. package/src/controls/control-group.test.tsx +229 -0
  337. package/src/controls/control-group.tsx +63 -0
  338. package/src/controls/controls.module.css +542 -0
  339. package/src/controls/controls.module.css.d.ts +35 -0
  340. package/src/controls/controls.router.test.tsx +1053 -0
  341. package/src/controls/enum-filter.tsx +170 -0
  342. package/src/controls/filter-context.tsx +158 -0
  343. package/src/controls/filter-pill.tsx +120 -0
  344. package/src/controls/filter-set.test.tsx +472 -0
  345. package/src/controls/filter-set.tsx +225 -0
  346. package/src/controls/filter-sheet.tsx +285 -0
  347. package/src/controls/glyphs.tsx +81 -0
  348. package/src/controls/index.ts +49 -0
  349. package/src/controls/multi-select-filter.tsx +140 -0
  350. package/src/controls/number-filter.tsx +291 -0
  351. package/src/controls/peer-boundary.test.ts +85 -0
  352. package/src/controls/props-contract.test.tsx +137 -0
  353. package/src/controls/range-filter.tsx +237 -0
  354. package/src/controls/range-filter.type-guard.test.tsx +122 -0
  355. package/src/controls/search-filter.tsx +123 -0
  356. package/src/controls/select-filter.tsx +89 -0
  357. package/src/controls/sync-button.module.css +63 -0
  358. package/src/controls/sync-button.module.css.d.ts +11 -0
  359. package/src/controls/sync-button.test.tsx +183 -0
  360. package/src/controls/sync-button.tsx +197 -0
  361. package/src/controls/toggle-filter.tsx +60 -0
  362. package/src/controls/view-tabs.tsx +110 -0
  363. package/src/controls-dates/date-range-picker.tsx +42 -0
  364. package/src/controls-dates/index.ts +8 -0
  365. package/src/dashboard/delta-badge.tsx +4 -74
  366. package/src/dashboard/index.ts +6 -1
  367. package/src/dashboard/settings-section.module.css +10 -0
  368. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  369. package/src/dashboard/settings-section.test.tsx +56 -0
  370. package/src/dashboard/settings-section.tsx +33 -30
  371. package/src/dashboard/stat-card.module.css +153 -0
  372. package/src/dashboard/stat-card.module.css.d.ts +15 -0
  373. package/src/dashboard/stat-card.test.tsx +298 -1
  374. package/src/dashboard/stat-card.tsx +289 -67
  375. package/src/data/data-table.test.tsx +96 -11
  376. package/src/data/data-table.tsx +228 -84
  377. package/src/guard/check-source.test.ts +159 -13
  378. package/src/guard/grace.test.ts +158 -0
  379. package/src/guard/index.ts +185 -29
  380. package/src/guard/types.ts +4 -2
  381. package/src/index.ts +51 -5
  382. package/src/nav/types.ts +84 -2
  383. package/src/notifications/bell.tsx +6 -3
  384. package/src/notifications/index.ts +1 -1
  385. package/src/router-tanstack/field.ts +35 -0
  386. package/src/router-tanstack/index.ts +28 -2
  387. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  388. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  389. package/src/router-tanstack/search-param-store.ts +42 -159
  390. package/src/router-tanstack/search-store.router.test.tsx +882 -0
  391. package/src/router-tanstack/search-store.test.ts +578 -0
  392. package/src/router-tanstack/search-store.ts +546 -0
  393. package/src/router-tanstack/use-nav.test.tsx +187 -0
  394. package/src/router-tanstack/use-nav.ts +72 -14
  395. package/src/section/index.ts +1 -0
  396. package/src/section/section.module.css +104 -0
  397. package/src/section/section.module.css.d.ts +11 -0
  398. package/src/section/section.test.tsx +204 -0
  399. package/src/section/section.tsx +220 -0
  400. package/src/shell/app-breadcrumbs.tsx +44 -31
  401. package/src/shell/app-header.module.css +77 -37
  402. package/src/shell/app-header.module.css.d.ts +2 -1
  403. package/src/shell/app-mobile-nav.module.css +8 -0
  404. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  405. package/src/shell/app-mobile-nav.tsx +147 -8
  406. package/src/shell/app-sidebar-account.tsx +2 -2
  407. package/src/shell/app-sidebar.module.css +303 -20
  408. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  409. package/src/shell/app-sidebar.test.tsx +456 -41
  410. package/src/shell/app-sidebar.tsx +276 -140
  411. package/src/shell/index.test.tsx +291 -5
  412. package/src/shell/index.tsx +137 -38
  413. package/src/shell/mobile-nav-model.test.ts +228 -7
  414. package/src/shell/mobile-nav-model.ts +53 -3
  415. package/src/shell/nav-count-badge.tsx +1 -1
  416. package/src/shell/page-bar.module.css +178 -0
  417. package/src/shell/page-bar.module.css.d.ts +17 -0
  418. package/src/shell/page-bar.test.tsx +316 -0
  419. package/src/shell/page-bar.tsx +315 -0
  420. package/src/shell/sidebar-block-model.ts +83 -0
  421. package/src/shell/sidebar-blocks.tsx +301 -0
  422. package/src/shell/sidebar-search.module.css +26 -0
  423. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  424. package/src/shell/sidebar-search.tsx +71 -2
  425. package/src/state/fields.test.tsx +512 -0
  426. package/src/state/fields.ts +1150 -0
  427. package/src/state/persisted.test.ts +91 -0
  428. package/src/state/persisted.ts +261 -0
  429. package/src/state.ts +52 -240
  430. package/src/styles.css +13 -0
  431. package/src/surfaces.ts +204 -36
  432. package/src/theme/controls.module.css +16 -1
  433. package/src/theme/ctl-theme.test.tsx +275 -0
  434. package/src/theme/ctl-theme.tsx +138 -0
  435. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  436. package/src/theme/density-relations.test.ts +106 -51
  437. package/src/theme/divider-law.test.ts +259 -0
  438. package/src/theme/icon-slot.module.css +47 -0
  439. package/src/theme/icon-slot.module.css.d.ts +8 -0
  440. package/src/theme/icon-slot.test.tsx +275 -0
  441. package/src/theme/icon-slot.tsx +59 -0
  442. package/src/theme/index.ts +165 -3
  443. package/src/theme/layout-rhythm-css.test.ts +72 -0
  444. package/src/theme/radius.test.ts +11 -1
  445. package/src/theme/segmented-control.module.css +20 -0
  446. package/src/theme/shadow-surfaces.test.ts +20 -7
  447. package/src/theme/spacing.test.ts +75 -27
  448. package/src/theme/use-basalt-spacing.ts +2 -2
  449. package/src/tokens/density.test.ts +7 -25
  450. package/src/tokens/index.ts +48 -8
  451. package/src/tokens/palette.ts +107 -73
  452. package/src/widget-header/delta-badge.test.tsx +60 -0
  453. package/src/widget-header/delta-badge.tsx +57 -0
  454. package/src/widget-header/index.ts +2 -0
  455. package/src/widget-header/no-mantine.test.ts +33 -0
  456. package/src/widget-header/widget-header.module.css +350 -0
  457. package/src/widget-header/widget-header.module.css.d.ts +25 -0
  458. package/src/widget-header/widget-header.test.tsx +187 -0
  459. package/src/widget-header/widget-header.tsx +213 -0
  460. package/agent/rules/basalt-agent.md +0 -452
  461. package/agent/rules/basalt-app.md +0 -246
  462. package/agent/rules/basalt-commands.md +0 -320
  463. package/agent/rules/basalt-content.md +0 -409
  464. package/agent/rules/basalt-data.md +0 -329
  465. package/agent/rules/basalt-forms.md +0 -186
  466. package/agent/rules/basalt-notifications.md +0 -264
  467. package/agent/rules/basalt-query.md +0 -187
  468. package/agent/rules/basalt-router.md +0 -336
  469. package/dist/content/article-filter-bar.d.ts +0 -40
  470. package/dist/content/article-filter-bar.d.ts.map +0 -1
  471. package/dist/content/article-filter-bar.js +0 -56
  472. package/dist/shell/page-header.d.ts +0 -13
  473. package/dist/shell/page-header.d.ts.map +0 -1
  474. package/dist/shell/page-header.js +0 -23
  475. package/src/content/article-filter-bar.tsx +0 -88
  476. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,405 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { createPersistedState, readPersistedValue, writePersistedValue } from "./persisted.js";
3
+ function rangeField(o, lane) {
4
+ const { fallback } = o;
5
+ return {
6
+ kind: "range",
7
+ presets: o.presets,
8
+ // A thunk fallback stays a thunk — wrapped so the lazy value is read at the same moment every
9
+ // other lane reads one, not once here at definition.
10
+ fallback: typeof fallback === "function" ? () => ({ preset: fallback() }) : { preset: fallback },
11
+ custom: o.custom ?? false,
12
+ params: o.params ?? {},
13
+ window: o.window ?? {},
14
+ lane: resolveLane(lane)
15
+ };
16
+ }
17
+ const field = {
18
+ /** A closed string enum — a tab, a compare mode, a single-select filter. */
19
+ enum(values, fallback, lane) {
20
+ return { kind: "enum", values, fallback, lane: resolveLane(lane) };
21
+ },
22
+ /** An any-of set over the same closed enum. Decoding is deduped and canonically re-sorted. */
23
+ multi(values, fallback = [], lane) {
24
+ return { kind: "multi", values, fallback, lane: resolveLane(lane) };
25
+ },
26
+ range: rangeField,
27
+ /** A number — pagination, a threshold. Out-of-range input clamps to `min`/`max`. */
28
+ number(o, lane) {
29
+ return {
30
+ kind: "number",
31
+ fallback: o.fallback,
32
+ min: o.min,
33
+ max: o.max,
34
+ int: o.int === true,
35
+ lane: resolveLane(lane)
36
+ };
37
+ },
38
+ /** A toggle. Accepts `true`/`false` and the strings `'true'`/`'false'` from a hand-typed URL. */
39
+ boolean(fallback, lane) {
40
+ return { kind: "boolean", fallback, lane: resolveLane(lane) };
41
+ },
42
+ /** Free text — a search box. Defaults to `history: 'replace'` like every other field. */
43
+ string(o, lane) {
44
+ return {
45
+ kind: "string",
46
+ fallback: o?.fallback ?? "",
47
+ max: o?.max,
48
+ lane: resolveLane(lane)
49
+ };
50
+ }
51
+ };
52
+ function resolveLane(lane) {
53
+ return {
54
+ url: lane?.url !== false,
55
+ persist: lane?.persist !== false,
56
+ history: lane?.history === "push" ? "push" : "replace"
57
+ };
58
+ }
59
+ function resolveFallback(raw) {
60
+ return typeof raw === "function" ? raw() : raw;
61
+ }
62
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T[\d:.]+(?:Z|[+-]\d{2}:\d{2})?)?$/;
63
+ function isIsoDate(raw) {
64
+ return typeof raw === "string" && ISO_DATE_RE.test(raw) && !Number.isNaN(Date.parse(raw));
65
+ }
66
+ const NUMERIC_RE = /^-?\d+(\.\d+)?$/;
67
+ function decodeBoolean(raw) {
68
+ if (typeof raw === "boolean") return raw;
69
+ if (raw === "true") return true;
70
+ if (raw === "false") return false;
71
+ return null;
72
+ }
73
+ function sameList(a, b) {
74
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
75
+ return a.length === b.length && a.every((entry, i) => entry === b[i]);
76
+ }
77
+ function resolveFieldCodec(name, f, opts) {
78
+ switch (f.kind) {
79
+ case "enum": {
80
+ const allowed = new Set(f.values);
81
+ const decode = (raw) => typeof raw === "string" && allowed.has(raw) ? raw : null;
82
+ return single(name, f, decode, { optionValues: f.values });
83
+ }
84
+ case "multi": {
85
+ const present = new Set(f.values);
86
+ const decode = (raw) => {
87
+ if (!Array.isArray(raw)) return null;
88
+ const picked = /* @__PURE__ */ new Set();
89
+ for (const entry of raw) {
90
+ if (typeof entry === "string" && present.has(entry)) picked.add(entry);
91
+ }
92
+ return f.values.filter((value) => picked.has(value));
93
+ };
94
+ const decodeUrl = opts?.legacyMultiEmpty === true ? (raw) => {
95
+ const value = decode(raw);
96
+ return Array.isArray(value) && value.length === 0 ? null : value;
97
+ } : decode;
98
+ return single(name, f, decode, {
99
+ optionValues: f.values,
100
+ equals: sameList,
101
+ decodeUrl
102
+ });
103
+ }
104
+ case "range": {
105
+ const presetParam = f.params.preset ?? name;
106
+ const fromParam = f.params.from ?? "from";
107
+ const toParam = f.params.to ?? "to";
108
+ const allowed = new Set(f.presets);
109
+ const decode = (raw) => {
110
+ if (typeof raw !== "object" || raw === null) return null;
111
+ const value = raw;
112
+ if (typeof value.preset !== "string") return null;
113
+ if (value.preset === "custom") {
114
+ if (!f.custom || !isIsoDate(value.from) || !isIsoDate(value.to)) return null;
115
+ return { preset: "custom", from: value.from, to: value.to };
116
+ }
117
+ return allowed.has(value.preset) ? { preset: value.preset } : null;
118
+ };
119
+ return {
120
+ kind: "range",
121
+ lane: f.lane,
122
+ params: [presetParam, fromParam, toParam],
123
+ primary: presetParam,
124
+ get fallback() {
125
+ return resolveFallback(f.fallback);
126
+ },
127
+ windows: f.window,
128
+ decode,
129
+ fromSearch: (search) => decode({
130
+ preset: search[presetParam],
131
+ from: search[fromParam],
132
+ to: search[toParam]
133
+ }),
134
+ toSearch: (value) => {
135
+ const range = value;
136
+ return {
137
+ [presetParam]: range.preset,
138
+ [fromParam]: range.from,
139
+ [toParam]: range.to
140
+ };
141
+ },
142
+ equals: (a, b) => {
143
+ const x = a;
144
+ const y = b;
145
+ if (x === null || y === null) return x === y;
146
+ return x.preset === y.preset && x.from === y.from && x.to === y.to;
147
+ },
148
+ optionValues: f.custom ? [...f.presets, "custom"] : f.presets
149
+ };
150
+ }
151
+ case "number": {
152
+ const decode = (raw) => {
153
+ const parsed = typeof raw === "number" ? raw : typeof raw === "string" && NUMERIC_RE.test(raw) ? Number(raw) : NaN;
154
+ if (!Number.isFinite(parsed)) return null;
155
+ if (f.int && !Number.isInteger(parsed)) return null;
156
+ let value = parsed;
157
+ if (f.min !== void 0 && value < f.min) value = f.min;
158
+ if (f.max !== void 0 && value > f.max) value = f.max;
159
+ return value;
160
+ };
161
+ return single(name, f, decode, { bounds: { min: f.min, max: f.max, int: f.int } });
162
+ }
163
+ case "boolean": {
164
+ return single(name, f, decodeBoolean);
165
+ }
166
+ case "string": {
167
+ const decode = (raw) => {
168
+ if (typeof raw !== "string") return null;
169
+ return f.max !== void 0 && raw.length > f.max ? raw.slice(0, f.max) : raw;
170
+ };
171
+ return single(name, f, decode);
172
+ }
173
+ default: {
174
+ throw new Error("basalt-ui: createSearchStore received a field of an unknown kind");
175
+ }
176
+ }
177
+ }
178
+ function single(name, f, decode, extra) {
179
+ const decodeUrl = extra?.decodeUrl ?? decode;
180
+ return {
181
+ kind: f.kind,
182
+ lane: f.lane,
183
+ params: [name],
184
+ primary: name,
185
+ get fallback() {
186
+ return resolveFallback(f.fallback);
187
+ },
188
+ ...extra?.bounds !== void 0 && { bounds: extra.bounds },
189
+ decode,
190
+ fromSearch: (search) => decodeUrl(search[name]),
191
+ toSearch: (value) => ({ [name]: value }),
192
+ equals: extra?.equals ?? ((a, b) => a === b),
193
+ optionValues: extra?.optionValues ?? []
194
+ };
195
+ }
196
+ function isRecord(raw) {
197
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw);
198
+ }
199
+ function assignDefined(into, from) {
200
+ for (const [key, value] of Object.entries(from)) {
201
+ if (value !== void 0) into[key] = value;
202
+ }
203
+ }
204
+ function resolveField(entry, search, record) {
205
+ const fromUrl = search === null ? null : entry.codec.fromSearch(search);
206
+ if (fromUrl !== null) return fromUrl;
207
+ if (entry.codec.lane.persist) {
208
+ const stored = entry.codec.decode(record[entry.name]);
209
+ if (stored !== null) return stored;
210
+ }
211
+ return entry.codec.fallback;
212
+ }
213
+ function createMemoryLane() {
214
+ const values = /* @__PURE__ */ new Map();
215
+ const listeners = /* @__PURE__ */ new Set();
216
+ let version = 0;
217
+ const bump = () => {
218
+ version += 1;
219
+ for (const cb of listeners) cb();
220
+ };
221
+ return {
222
+ subscribe: (cb) => {
223
+ listeners.add(cb);
224
+ return () => {
225
+ listeners.delete(cb);
226
+ };
227
+ },
228
+ getVersion: () => version,
229
+ // `undefined` is the only "never written" marker the lane needs: no field kind has `undefined`
230
+ // as a legal value, so it can never collide with a written one.
231
+ resolve: (entry) => {
232
+ const stored = values.get(entry.name);
233
+ return stored === void 0 ? entry.codec.fallback : stored;
234
+ },
235
+ write: (name, value) => {
236
+ values.set(name, value);
237
+ bump();
238
+ },
239
+ remove: (name) => {
240
+ if (!values.delete(name)) return;
241
+ bump();
242
+ },
243
+ clear: () => {
244
+ if (values.size === 0) return;
245
+ values.clear();
246
+ bump();
247
+ }
248
+ };
249
+ }
250
+ function assertNoParamCollision(key, entries) {
251
+ const owner = /* @__PURE__ */ new Map();
252
+ for (const entry of entries) {
253
+ for (const param of entry.codec.params) {
254
+ const existing = owner.get(param);
255
+ if (existing !== void 0 && existing !== entry.name) {
256
+ throw new Error(
257
+ `basalt-ui: createSearchStore('${key}'): fields '${existing}' and '${entry.name}' both own the URL param '${param}' \u2014 rename one via field.range({ params: { ... } }).`
258
+ );
259
+ }
260
+ owner.set(param, entry.name);
261
+ }
262
+ }
263
+ }
264
+ function createStoreCore(o) {
265
+ const version = o.version ?? 1;
266
+ const legacy = o.legacyValueField;
267
+ const entries = Object.entries(o.fields).map(([name, f]) => ({
268
+ name,
269
+ codec: resolveFieldCodec(name, f, { legacyMultiEmpty: legacy !== void 0 })
270
+ }));
271
+ assertNoParamCollision(o.key, entries);
272
+ const urlEntries = entries.filter((entry) => entry.codec.lane.url);
273
+ const anyPersisted = entries.some((entry) => entry.codec.lane.persist);
274
+ const labels = /* @__PURE__ */ new Map();
275
+ const usePersistedRaw = createPersistedState({ key: o.key, version, initial: null });
276
+ const memory = createMemoryLane();
277
+ const toRecord = (raw) => {
278
+ if (legacy !== void 0) return raw === null || raw === void 0 ? {} : { [legacy]: raw };
279
+ return isRecord(raw) ? raw : {};
280
+ };
281
+ const fromRecord = (record) => legacy !== void 0 ? record[legacy] ?? null : record;
282
+ const readRecord = () => toRecord(readPersistedValue(o.key, version));
283
+ const optionsFor = (entry) => {
284
+ const map = labels.get(entry.name);
285
+ return entry.codec.optionValues.map((value) => ({ value, label: map?.[value] ?? value }));
286
+ };
287
+ const clearField = (name) => {
288
+ const record = readRecord();
289
+ if (!Object.hasOwn(record, name)) return;
290
+ const next = { ...record };
291
+ delete next[name];
292
+ writePersistedValue(o.key, version, fromRecord(next));
293
+ };
294
+ const writeField = (setRecord, name, value) => {
295
+ setRecord({ ...readRecord(), [name]: value });
296
+ };
297
+ const handle = (entry, use, clear) => {
298
+ const { codec } = entry;
299
+ const built = {
300
+ kind: codec.kind,
301
+ use,
302
+ clear,
303
+ isDefault: (v) => codec.equals(v, codec.fallback)
304
+ };
305
+ if (codec.kind === "range") {
306
+ built["toWindow"] = (v) => {
307
+ if (v.preset === "custom" && v.from !== void 0 && v.to !== void 0) {
308
+ return { from: v.from, to: v.to };
309
+ }
310
+ const windows = codec.windows;
311
+ if (windows !== void 0 && Object.hasOwn(windows, v.preset)) {
312
+ const resolver = windows[v.preset];
313
+ if (resolver !== void 0) return resolver(/* @__PURE__ */ new Date());
314
+ }
315
+ return { window: v.preset };
316
+ };
317
+ }
318
+ if (codec.kind === "number") {
319
+ const bounds = codec.bounds;
320
+ built["min"] = bounds?.min;
321
+ built["max"] = bounds?.max;
322
+ built["int"] = bounds?.int ?? false;
323
+ }
324
+ Object.defineProperty(built, "options", {
325
+ enumerable: true,
326
+ get: () => optionsFor(entry)
327
+ });
328
+ Object.defineProperty(built, "fallback", {
329
+ enumerable: true,
330
+ get: () => codec.fallback
331
+ });
332
+ return built;
333
+ };
334
+ return {
335
+ entries,
336
+ urlEntries,
337
+ anyPersisted,
338
+ usePersistedRecord() {
339
+ const [raw, setRaw] = usePersistedRaw();
340
+ return [toRecord(raw), (next) => setRaw(fromRecord(next))];
341
+ },
342
+ readRecord,
343
+ memoryUse(entry) {
344
+ return () => {
345
+ useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion);
346
+ return [memory.resolve(entry), (next) => memory.write(entry.name, next)];
347
+ };
348
+ },
349
+ readMemoryValue: (entry) => memory.resolve(entry),
350
+ useMemoryVersion: () => useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion),
351
+ resetMemory: () => memory.clear(),
352
+ clearField,
353
+ clearMemory: (name) => memory.remove(name),
354
+ setLabels(map) {
355
+ for (const [name, entry] of Object.entries(map)) {
356
+ if (entry !== void 0) labels.set(name, entry);
357
+ }
358
+ },
359
+ optionsFor,
360
+ resolve: resolveField,
361
+ writeField,
362
+ readStoredFlat() {
363
+ const record = readRecord();
364
+ const out = {};
365
+ for (const entry of entries) {
366
+ const stored = entry.codec.decode(record[entry.name]);
367
+ if (stored !== null) assignDefined(out, entry.codec.toSearch(stored));
368
+ }
369
+ return out;
370
+ },
371
+ handle
372
+ };
373
+ }
374
+ function createLocalStore(o) {
375
+ const core = createStoreCore({ key: o.key, fields: o.fields, version: o.version });
376
+ const persistedUse = (entry) => () => {
377
+ const [record, setRecord] = core.usePersistedRecord();
378
+ return [
379
+ core.resolve(entry, null, record),
380
+ (next) => core.writeField(setRecord, entry.name, next)
381
+ ];
382
+ };
383
+ const field2 = {};
384
+ for (const entry of core.entries) {
385
+ const persisted = entry.codec.lane.persist;
386
+ const use = persisted ? persistedUse(entry) : core.memoryUse(entry);
387
+ const clear = persisted ? () => core.clearField(entry.name) : () => core.clearMemory(entry.name);
388
+ field2[entry.name] = core.handle(entry, use, clear);
389
+ }
390
+ const store = {
391
+ field: field2,
392
+ readStored: () => core.readStoredFlat(),
393
+ labels: (map) => {
394
+ core.setLabels(map);
395
+ return store;
396
+ }
397
+ };
398
+ return store;
399
+ }
400
+ export {
401
+ createLocalStore,
402
+ createStoreCore,
403
+ field,
404
+ resolveFieldCodec
405
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fields.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.test.d.ts","sourceRoot":"","sources":["../../src/state/fields.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ import type { StandardSchemaV1 } from '../register.js';
2
+ export type PersistedStateOptions<T> = {
3
+ /** localStorage key (will be namespaced as `basalt:<key>`). */
4
+ readonly key: string;
5
+ /** Envelope version — increment when the shape changes. */
6
+ readonly version: number;
7
+ /** Value to use when nothing is persisted, after migration fails, or on SSR. */
8
+ readonly initial: T;
9
+ /** Migrate a previous-version persisted value forward to the current shape. */
10
+ readonly migrate?: (persisted: unknown, fromVersion: number) => T;
11
+ /**
12
+ * Standard-Schema validate the (post-migrate) value. Invalid result OR a Promise (async schema)
13
+ * falls back to `initial` — sync storage can't await, so async validators are treated as invalid.
14
+ */
15
+ readonly schema?: StandardSchemaV1<unknown, T>;
16
+ };
17
+ /**
18
+ * Versioned, Standard-Schema-validated localStorage state via useSyncExternalStore (no zustand).
19
+ * SSR-safe (getServerSnapshot returns `initial`). Cross-tab via the storage event.
20
+ *
21
+ * Returns a factory hook — call it once per module, then use the returned hook in components.
22
+ * Common case is 3 lines.
23
+ *
24
+ * @example
25
+ * export const useFilterDraft = createPersistedState({ key: 'filters', version: 1, initial: DEFAULT, schema: FilterSchema })
26
+ * // in a component:
27
+ * const [draft, setDraft] = useFilterDraft()
28
+ */
29
+ export declare function createPersistedState<T>(opts: PersistedStateOptions<T>): () => readonly [T, (next: T) => void];
30
+ /**
31
+ * Read a value previously written by `createPersistedState` under the namespaced key
32
+ * `basalt:<key>`. Parses the versioned envelope shape `{ v, value }`. Returns `null` on
33
+ * miss, corruption, version mismatch, or SSR — no fallback is applied (call sites supply
34
+ * their own default).
35
+ *
36
+ * Primary use: `validateSearch` in TanStack Router (which runs outside React, so it can't
37
+ * call `usePersistedState`). Combine with a URL-param check to create a search-param store
38
+ * backed by localStorage.
39
+ *
40
+ * @param key — un-namespaced key (the same one passed to `createPersistedState`)
41
+ * @param version — if provided, the envelope version must match; stale envelopes return `null`
42
+ *
43
+ * @example
44
+ * const stored = readPersistedValue('dashboard-range', 1)
45
+ * // stored is '1d' | '7d' | '30d' | null
46
+ */
47
+ export declare function readPersistedValue(key: string, version?: number): unknown | null;
48
+ /**
49
+ * Write a value under the namespaced key `basalt:<key>` in the same versioned envelope
50
+ * `createPersistedState` writes, and wake every hook on that key — the write half of
51
+ * `readPersistedValue`, and plain in the same way: no React required.
52
+ *
53
+ * The one call site is a store field's `clear()`, which has to DELETE its key from the store's
54
+ * record from an event handler, where the hook's setter is not in reach. Not exported from the
55
+ * `basalt-ui/state` barrel: the two store factories are the API, this is their seam.
56
+ *
57
+ * @internal
58
+ */
59
+ export declare function writePersistedValue(key: string, version: number, value: unknown): void;
60
+ //# sourceMappingURL=persisted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persisted.d.ts","sourceRoot":"","sources":["../../src/state/persisted.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAOnD,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC,CAAA;IACjE;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;CAC/C,CAAA;AA4FD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAwEvC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAYhF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAItF"}
@@ -0,0 +1,123 @@
1
+ import { useSyncExternalStore } from "react";
2
+ const noopSubscribe = (_cb) => () => {
3
+ };
4
+ const channels = /* @__PURE__ */ new Map();
5
+ function notify(storageKey) {
6
+ const channel = channels.get(storageKey);
7
+ if (channel === void 0) return;
8
+ for (const listener of channel.listeners) listener();
9
+ }
10
+ function channelFor(storageKey) {
11
+ const existing = channels.get(storageKey);
12
+ if (existing !== void 0) return existing;
13
+ const created = { listeners: /* @__PURE__ */ new Set(), handler: null };
14
+ channels.set(storageKey, created);
15
+ return created;
16
+ }
17
+ function isEnvelope(raw) {
18
+ return typeof raw === "object" && raw !== null && "v" in raw && typeof raw["v"] === "number" && "value" in raw;
19
+ }
20
+ function parseStorage(raw, opts) {
21
+ if (raw === null) return opts.initial;
22
+ try {
23
+ const parsed = JSON.parse(raw);
24
+ if (!isEnvelope(parsed)) return opts.initial;
25
+ let value = parsed.value;
26
+ if (parsed.v !== opts.version) {
27
+ value = opts.migrate ? opts.migrate(parsed.value, parsed.v) : opts.initial;
28
+ }
29
+ if (opts.schema) {
30
+ const result = opts.schema["~standard"].validate(value);
31
+ if (result instanceof Promise) return opts.initial;
32
+ if (result.issues !== void 0) return opts.initial;
33
+ return result.value;
34
+ }
35
+ return value;
36
+ } catch {
37
+ return opts.initial;
38
+ }
39
+ }
40
+ function writeEnvelope(storageKey, version, value) {
41
+ try {
42
+ const envelope = { v: version, value };
43
+ window.localStorage.setItem(storageKey, JSON.stringify(envelope));
44
+ } catch {
45
+ }
46
+ }
47
+ function writeStorage(opts, next) {
48
+ writeEnvelope(`basalt:${opts.key}`, opts.version, next);
49
+ }
50
+ function createPersistedState(opts) {
51
+ const storageKey = `basalt:${opts.key}`;
52
+ const subscribe = (cb) => {
53
+ const channel = channelFor(storageKey);
54
+ channel.listeners.add(cb);
55
+ if (channel.handler === null) {
56
+ channel.handler = (e) => {
57
+ if (e.key === storageKey) notify(storageKey);
58
+ };
59
+ window.addEventListener("storage", channel.handler);
60
+ }
61
+ return () => {
62
+ channel.listeners.delete(cb);
63
+ if (channel.listeners.size === 0 && channel.handler !== null) {
64
+ window.removeEventListener("storage", channel.handler);
65
+ channel.handler = null;
66
+ }
67
+ };
68
+ };
69
+ let cachedRaw = null;
70
+ let cachedValue = opts.initial;
71
+ let primed = false;
72
+ const getSnapshot = () => {
73
+ let raw;
74
+ try {
75
+ raw = window.localStorage.getItem(storageKey);
76
+ } catch {
77
+ raw = null;
78
+ }
79
+ if (primed && raw === cachedRaw) return cachedValue;
80
+ cachedRaw = raw;
81
+ cachedValue = parseStorage(raw, opts);
82
+ primed = true;
83
+ return cachedValue;
84
+ };
85
+ const getServerSnapshot = () => opts.initial;
86
+ const setState = (next) => {
87
+ writeStorage(opts, next);
88
+ notify(storageKey);
89
+ };
90
+ const isServer = typeof window === "undefined";
91
+ return function usePersistedState() {
92
+ const value = useSyncExternalStore(
93
+ // SSR guard — subscribe must not reference `window` on the server.
94
+ isServer ? noopSubscribe : subscribe,
95
+ isServer ? getServerSnapshot : getSnapshot,
96
+ getServerSnapshot
97
+ );
98
+ return [value, setState];
99
+ };
100
+ }
101
+ function readPersistedValue(key, version) {
102
+ const storageKey = `basalt:${key}`;
103
+ try {
104
+ const raw = localStorage.getItem(storageKey);
105
+ if (raw === null) return null;
106
+ const envelope = JSON.parse(raw);
107
+ if (!isEnvelope(envelope)) return null;
108
+ if (version !== void 0 && envelope.v !== version) return null;
109
+ return envelope.value;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ function writePersistedValue(key, version, value) {
115
+ const storageKey = `basalt:${key}`;
116
+ writeEnvelope(storageKey, version, value);
117
+ notify(storageKey);
118
+ }
119
+ export {
120
+ createPersistedState,
121
+ readPersistedValue,
122
+ writePersistedValue
123
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=persisted.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persisted.test.d.ts","sourceRoot":"","sources":["../../src/state/persisted.test.ts"],"names":[],"mappings":""}
package/dist/state.d.ts CHANGED
@@ -1,62 +1,17 @@
1
- import type { StandardSchemaV1 } from './register.js';
2
1
  /**
3
- * Returns `true` when the browser reports an active network connection, `false` otherwise.
4
- * Backed by `useSyncExternalStore`: subscribes to window `online`/`offline` events.
5
- * SSR-safe — `getServerSnapshot` returns `true` (optimistic: assume online on the server).
2
+ * `basalt-ui/state` — the headless state surface: no Mantine, no router, no JSX.
6
3
  *
7
- * @deprecated Use `useConnectivity()` from `basalt-ui` instead — it aggregates browser,
8
- * React Query, SSE, and health-ping signals into a richer `{ status, details }` object.
9
- * `useOnlineStatus` remains for simple boolean needs without a provider dependency.
4
+ * Two halves, one barrel:
10
5
  *
11
- * @example
12
- * const isOnline = useOnlineStatus()
13
- * if (!isOnline) return <OfflineBanner />
14
- */
15
- export declare function useOnlineStatus(): boolean;
16
- export type PersistedStateOptions<T> = {
17
- /** localStorage key (will be namespaced as `basalt:<key>`). */
18
- readonly key: string;
19
- /** Envelope version — increment when the shape changes. */
20
- readonly version: number;
21
- /** Value to use when nothing is persisted, after migration fails, or on SSR. */
22
- readonly initial: T;
23
- /** Migrate a previous-version persisted value forward to the current shape. */
24
- readonly migrate?: (persisted: unknown, fromVersion: number) => T;
25
- /**
26
- * Standard-Schema validate the (post-migrate) value. Invalid result OR a Promise (async schema)
27
- * falls back to `initial` — sync storage can't await, so async validators are treated as invalid.
28
- */
29
- readonly schema?: StandardSchemaV1<unknown, T>;
30
- };
31
- /**
32
- * Versioned, Standard-Schema-validated localStorage state via useSyncExternalStore (no zustand).
33
- * SSR-safe (getServerSnapshot returns `initial`). Cross-tab via the storage event.
34
- *
35
- * Returns a factory hook — call it once per module, then use the returned hook in components.
36
- * Common case is 3 lines.
37
- *
38
- * @example
39
- * export const useFilterDraft = createPersistedState({ key: 'filters', version: 1, initial: DEFAULT, schema: FilterSchema })
40
- * // in a component:
41
- * const [draft, setDraft] = useFilterDraft()
42
- */
43
- export declare function createPersistedState<T>(opts: PersistedStateOptions<T>): () => readonly [T, (next: T) => void];
44
- /**
45
- * Read a value previously written by `createPersistedState` under the namespaced key
46
- * `basalt:<key>`. Parses the versioned envelope shape `{ v, value }`. Returns `null` on
47
- * miss, corruption, version mismatch, or SSR — no fallback is applied (call sites supply
48
- * their own default).
49
- *
50
- * Primary use: `validateSearch` in TanStack Router (which runs outside React, so it can't
51
- * call `usePersistedState`). Combine with a URL-param check to create a search-param store
52
- * backed by localStorage.
53
- *
54
- * @param key — un-namespaced key (the same one passed to `createPersistedState`)
55
- * @param version — if provided, the envelope version must match; stale envelopes return `null`
6
+ * - `./state/persisted` — `createPersistedState` / `readPersistedValue`, the versioned localStorage
7
+ * primitive every other basalt module persists through.
8
+ * - `./state/fields` — the store field vocabulary (`field.*`, `FieldHandle`, the lanes) and
9
+ * `createLocalStore`, the router-free store. `createSearchStore` (`basalt-ui/router-tanstack`)
10
+ * is the router-coupled factory over the same fields.
56
11
  *
57
- * @example
58
- * const stored = readPersistedValue('dashboard-range', 1)
59
- * // stored is '1d' | '7d' | '30d' | null
12
+ * The split is a file boundary, not a surface boundary: this barrel is what `package.json`'s
13
+ * `./state` export resolves to, and its named exports are pinned by `scripts/export-surface.json`.
60
14
  */
61
- export declare function readPersistedValue(key: string, version?: number): unknown | null;
15
+ export { createPersistedState, type PersistedStateOptions, readPersistedValue, } from './state/persisted.js';
16
+ export { field, createLocalStore, type LocalStore, type FieldLane, type ResolvedLane, type ResolveLane, type RangeValue, type RangeParams, type RangePresets, type RangeWindow, type RangeWindows, type FieldFallback, type FieldSetOptions, type EnumField, type MultiField, type RangeField, type NumberField, type BooleanField, type StringField, type AnyField, type FieldValue, type FieldOption, type FieldHandle, type SearchValues, type StoredValues, type FieldCodec, type StoreEntry, type StoreCoreOptions, type StoreCore, type FieldUse, type FieldWrite, resolveFieldCodec, createStoreCore, } from './state/fields.js';
62
17
  //# sourceMappingURL=state.d.ts.map