basalt-ui 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +25 -0
  3. package/MIGRATING.md +547 -21
  4. package/README.md +46 -33
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/query-state.d.ts +1 -1
  134. package/dist/dashboard/settings-section.d.ts +6 -4
  135. package/dist/dashboard/settings-section.d.ts.map +1 -1
  136. package/dist/dashboard/settings-section.js +23 -16
  137. package/dist/dashboard/settings-section.module.css +10 -0
  138. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  139. package/dist/dashboard/settings-section.test.d.ts +2 -0
  140. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  141. package/dist/dashboard/stat-card.d.ts +44 -9
  142. package/dist/dashboard/stat-card.d.ts.map +1 -1
  143. package/dist/dashboard/stat-card.js +80 -48
  144. package/dist/dashboard/stat-card.module.css +82 -0
  145. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  146. package/dist/data/data-table.d.ts +102 -20
  147. package/dist/data/data-table.d.ts.map +1 -1
  148. package/dist/data/data-table.js +166 -65
  149. package/dist/data/table.d.ts +1 -1
  150. package/dist/data/table.d.ts.map +1 -1
  151. package/dist/guard/grace.test.d.ts +20 -0
  152. package/dist/guard/grace.test.d.ts.map +1 -0
  153. package/dist/guard/index.d.ts +66 -3
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +82 -7
  156. package/dist/guard/types.d.ts +3 -3
  157. package/dist/guard/types.d.ts.map +1 -1
  158. package/dist/index.d.ts +12 -2
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +13 -10
  161. package/dist/nav/types.d.ts +78 -2
  162. package/dist/nav/types.d.ts.map +1 -1
  163. package/dist/notifications/bell.d.ts +4 -1
  164. package/dist/notifications/bell.d.ts.map +1 -1
  165. package/dist/notifications/index.d.ts +1 -1
  166. package/dist/router-tanstack/field.d.ts +14 -0
  167. package/dist/router-tanstack/field.d.ts.map +1 -0
  168. package/dist/router-tanstack/field.js +4 -0
  169. package/dist/router-tanstack/index.d.ts +6 -2
  170. package/dist/router-tanstack/index.d.ts.map +1 -1
  171. package/dist/router-tanstack/index.js +4 -0
  172. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  173. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  175. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  176. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  177. package/dist/router-tanstack/search-param-store.js +16 -45
  178. package/dist/router-tanstack/search-store.d.ts +81 -0
  179. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.js +187 -0
  181. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  182. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  183. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  184. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  185. package/dist/router-tanstack/use-nav.d.ts +0 -2
  186. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  187. package/dist/router-tanstack/use-nav.js +34 -12
  188. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  189. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  190. package/dist/section/index.d.ts +2 -0
  191. package/dist/section/index.d.ts.map +1 -0
  192. package/dist/section/index.js +4 -0
  193. package/dist/section/section.d.ts +24 -0
  194. package/dist/section/section.d.ts.map +1 -0
  195. package/dist/section/section.js +126 -0
  196. package/dist/section/section.module.css +104 -0
  197. package/dist/section/section.module.css.d.ts +11 -0
  198. package/dist/section/section.test.d.ts +2 -0
  199. package/dist/section/section.test.d.ts.map +1 -0
  200. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  201. package/dist/shell/app-breadcrumbs.js +18 -15
  202. package/dist/shell/app-header.module.css +77 -37
  203. package/dist/shell/app-header.module.css.d.ts +2 -1
  204. package/dist/shell/app-mobile-nav.d.ts +9 -2
  205. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  206. package/dist/shell/app-mobile-nav.js +98 -11
  207. package/dist/shell/app-mobile-nav.module.css +8 -0
  208. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  209. package/dist/shell/app-sidebar.d.ts +55 -28
  210. package/dist/shell/app-sidebar.d.ts.map +1 -1
  211. package/dist/shell/app-sidebar.js +116 -67
  212. package/dist/shell/app-sidebar.module.css +303 -20
  213. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  214. package/dist/shell/index.d.ts +55 -21
  215. package/dist/shell/index.d.ts.map +1 -1
  216. package/dist/shell/index.js +40 -18
  217. package/dist/shell/mobile-nav-model.d.ts +13 -2
  218. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  219. package/dist/shell/mobile-nav-model.js +15 -1
  220. package/dist/shell/nav-count-badge.d.ts +1 -1
  221. package/dist/shell/page-bar.d.ts +51 -0
  222. package/dist/shell/page-bar.d.ts.map +1 -0
  223. package/dist/shell/page-bar.js +154 -0
  224. package/dist/shell/page-bar.module.css +178 -0
  225. package/dist/shell/page-bar.module.css.d.ts +17 -0
  226. package/dist/shell/page-bar.test.d.ts +2 -0
  227. package/dist/shell/page-bar.test.d.ts.map +1 -0
  228. package/dist/shell/sidebar-block-model.d.ts +48 -0
  229. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  230. package/dist/shell/sidebar-block-model.js +39 -0
  231. package/dist/shell/sidebar-blocks.d.ts +32 -0
  232. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  233. package/dist/shell/sidebar-blocks.js +169 -0
  234. package/dist/shell/sidebar-search.d.ts +13 -1
  235. package/dist/shell/sidebar-search.d.ts.map +1 -1
  236. package/dist/shell/sidebar-search.js +34 -1
  237. package/dist/shell/sidebar-search.module.css +26 -0
  238. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  239. package/dist/state/fields.d.ts +392 -0
  240. package/dist/state/fields.d.ts.map +1 -0
  241. package/dist/state/fields.js +369 -0
  242. package/dist/state/fields.test.d.ts +2 -0
  243. package/dist/state/fields.test.d.ts.map +1 -0
  244. package/dist/state/persisted.d.ts +48 -0
  245. package/dist/state/persisted.d.ts.map +1 -0
  246. package/dist/state/persisted.js +105 -0
  247. package/dist/state.d.ts +11 -56
  248. package/dist/state.d.ts.map +1 -1
  249. package/dist/state.js +14 -120
  250. package/dist/styles.css +13 -0
  251. package/dist/surfaces.d.ts +106 -24
  252. package/dist/surfaces.d.ts.map +1 -1
  253. package/dist/surfaces.js +155 -19
  254. package/dist/theme/controls.module.css +16 -1
  255. package/dist/theme/ctl-theme.d.ts +32 -0
  256. package/dist/theme/ctl-theme.d.ts.map +1 -0
  257. package/dist/theme/ctl-theme.js +54 -0
  258. package/dist/theme/ctl-theme.test.d.ts +2 -0
  259. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  260. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  261. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  262. package/dist/theme/divider-law.test.d.ts +2 -0
  263. package/dist/theme/divider-law.test.d.ts.map +1 -0
  264. package/dist/theme/icon-slot.d.ts +44 -0
  265. package/dist/theme/icon-slot.d.ts.map +1 -0
  266. package/dist/theme/icon-slot.js +21 -0
  267. package/dist/theme/icon-slot.module.css +47 -0
  268. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  269. package/dist/theme/icon-slot.test.d.ts +2 -0
  270. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  271. package/dist/theme/index.d.ts +1 -0
  272. package/dist/theme/index.d.ts.map +1 -1
  273. package/dist/theme/index.js +82 -5
  274. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  275. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  276. package/dist/theme/segmented-control.module.css +20 -0
  277. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  278. package/dist/tokens/index.d.ts +12 -0
  279. package/dist/tokens/index.d.ts.map +1 -1
  280. package/dist/tokens/index.js +38 -8
  281. package/dist/tokens/palette.d.ts +97 -61
  282. package/dist/tokens/palette.d.ts.map +1 -1
  283. package/dist/tokens/palette.js +76 -38
  284. package/dist/tokens.css +10 -2
  285. package/dist/widget-header/delta-badge.d.ts +13 -0
  286. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  287. package/dist/widget-header/delta-badge.js +29 -0
  288. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  289. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  290. package/dist/widget-header/index.d.ts +3 -0
  291. package/dist/widget-header/index.d.ts.map +1 -0
  292. package/dist/widget-header/index.js +6 -0
  293. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  294. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  295. package/dist/widget-header/widget-header.d.ts +37 -0
  296. package/dist/widget-header/widget-header.d.ts.map +1 -0
  297. package/dist/widget-header/widget-header.js +77 -0
  298. package/dist/widget-header/widget-header.module.css +335 -0
  299. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  300. package/dist/widget-header/widget-header.test.d.ts +2 -0
  301. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  302. package/llms.txt +30 -17
  303. package/package.json +14 -1
  304. package/src/charts/cursor/resolve.test.ts +29 -1
  305. package/src/charts/cursor/resolve.ts +38 -6
  306. package/src/charts/cursor/scope.tsx +6 -3
  307. package/src/charts/cursor/store.test.ts +19 -1
  308. package/src/charts/cursor/store.ts +10 -5
  309. package/src/charts/hooks/useChartCursor.ts +16 -8
  310. package/src/charts/index.ts +2 -2
  311. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  312. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  313. package/src/charts/primitives/ChartCard.tsx +86 -146
  314. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  315. package/src/cli/check-coverage.test.ts +297 -0
  316. package/src/cli/check-theme.test.ts +38 -14
  317. package/src/cli/index.ts +392 -16
  318. package/src/cli/init-sync.test.ts +97 -0
  319. package/src/connectivity/connectivity-indicator.tsx +16 -9
  320. package/src/content/index.ts +16 -3
  321. package/src/content/prose.module.css +7 -28
  322. package/src/content/prose.module.css.test.ts +8 -24
  323. package/src/controls/actions.module.css +62 -0
  324. package/src/controls/actions.module.css.d.ts +13 -0
  325. package/src/controls/actions.test.tsx +252 -0
  326. package/src/controls/actions.tsx +655 -0
  327. package/src/controls/actions.type-guard.test.ts +63 -0
  328. package/src/controls/compare-filter.tsx +64 -0
  329. package/src/controls/control-group.test.tsx +229 -0
  330. package/src/controls/control-group.tsx +63 -0
  331. package/src/controls/controls.module.css +542 -0
  332. package/src/controls/controls.module.css.d.ts +35 -0
  333. package/src/controls/controls.router.test.tsx +637 -0
  334. package/src/controls/enum-filter.tsx +159 -0
  335. package/src/controls/filter-context.tsx +153 -0
  336. package/src/controls/filter-pill.tsx +120 -0
  337. package/src/controls/filter-set.test.tsx +472 -0
  338. package/src/controls/filter-set.tsx +225 -0
  339. package/src/controls/filter-sheet.tsx +285 -0
  340. package/src/controls/glyphs.tsx +81 -0
  341. package/src/controls/index.ts +48 -0
  342. package/src/controls/multi-select-filter.tsx +140 -0
  343. package/src/controls/peer-boundary.test.ts +85 -0
  344. package/src/controls/props-contract.test.tsx +106 -0
  345. package/src/controls/range-filter.tsx +184 -0
  346. package/src/controls/search-filter.tsx +123 -0
  347. package/src/controls/select-filter.tsx +89 -0
  348. package/src/controls/sync-button.module.css +63 -0
  349. package/src/controls/sync-button.module.css.d.ts +11 -0
  350. package/src/controls/sync-button.test.tsx +183 -0
  351. package/src/controls/sync-button.tsx +197 -0
  352. package/src/controls/toggle-filter.tsx +60 -0
  353. package/src/controls/view-tabs.tsx +110 -0
  354. package/src/controls-dates/date-range-picker.tsx +42 -0
  355. package/src/controls-dates/index.ts +8 -0
  356. package/src/dashboard/delta-badge.tsx +4 -74
  357. package/src/dashboard/query-state.tsx +1 -1
  358. package/src/dashboard/settings-section.module.css +10 -0
  359. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  360. package/src/dashboard/settings-section.test.tsx +56 -0
  361. package/src/dashboard/settings-section.tsx +33 -30
  362. package/src/dashboard/stat-card.module.css +82 -0
  363. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  364. package/src/dashboard/stat-card.test.tsx +67 -1
  365. package/src/dashboard/stat-card.tsx +209 -68
  366. package/src/data/data-table.test.tsx +282 -6
  367. package/src/data/data-table.tsx +429 -94
  368. package/src/data/table.ts +2 -0
  369. package/src/guard/check-source.test.ts +126 -13
  370. package/src/guard/grace.test.ts +149 -0
  371. package/src/guard/index.ts +172 -29
  372. package/src/guard/types.ts +4 -2
  373. package/src/index.ts +46 -5
  374. package/src/nav/types.ts +84 -2
  375. package/src/notifications/bell.tsx +6 -3
  376. package/src/notifications/index.ts +1 -1
  377. package/src/router-tanstack/field.ts +31 -0
  378. package/src/router-tanstack/index.ts +24 -2
  379. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  380. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  381. package/src/router-tanstack/search-param-store.ts +42 -159
  382. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  383. package/src/router-tanstack/search-store.test.ts +418 -0
  384. package/src/router-tanstack/search-store.ts +380 -0
  385. package/src/router-tanstack/use-nav.test.tsx +187 -0
  386. package/src/router-tanstack/use-nav.ts +72 -14
  387. package/src/section/index.ts +1 -0
  388. package/src/section/section.module.css +104 -0
  389. package/src/section/section.module.css.d.ts +11 -0
  390. package/src/section/section.test.tsx +204 -0
  391. package/src/section/section.tsx +220 -0
  392. package/src/shell/app-breadcrumbs.tsx +44 -31
  393. package/src/shell/app-header.module.css +77 -37
  394. package/src/shell/app-header.module.css.d.ts +2 -1
  395. package/src/shell/app-mobile-nav.module.css +8 -0
  396. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  397. package/src/shell/app-mobile-nav.tsx +147 -8
  398. package/src/shell/app-sidebar-account.tsx +2 -2
  399. package/src/shell/app-sidebar.module.css +303 -20
  400. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  401. package/src/shell/app-sidebar.test.tsx +456 -41
  402. package/src/shell/app-sidebar.tsx +276 -140
  403. package/src/shell/index.test.tsx +291 -5
  404. package/src/shell/index.tsx +137 -38
  405. package/src/shell/mobile-nav-model.test.ts +228 -7
  406. package/src/shell/mobile-nav-model.ts +53 -3
  407. package/src/shell/nav-count-badge.tsx +1 -1
  408. package/src/shell/page-bar.module.css +178 -0
  409. package/src/shell/page-bar.module.css.d.ts +17 -0
  410. package/src/shell/page-bar.test.tsx +316 -0
  411. package/src/shell/page-bar.tsx +315 -0
  412. package/src/shell/sidebar-block-model.ts +83 -0
  413. package/src/shell/sidebar-blocks.tsx +301 -0
  414. package/src/shell/sidebar-search.module.css +26 -0
  415. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  416. package/src/shell/sidebar-search.tsx +71 -2
  417. package/src/state/fields.test.tsx +258 -0
  418. package/src/state/fields.ts +892 -0
  419. package/src/state/persisted.ts +211 -0
  420. package/src/state.ts +46 -240
  421. package/src/styles.css +13 -0
  422. package/src/surfaces.ts +203 -36
  423. package/src/theme/controls.module.css +16 -1
  424. package/src/theme/ctl-theme.test.tsx +275 -0
  425. package/src/theme/ctl-theme.tsx +138 -0
  426. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  427. package/src/theme/density-relations.test.ts +106 -51
  428. package/src/theme/divider-law.test.ts +259 -0
  429. package/src/theme/icon-slot.module.css +47 -0
  430. package/src/theme/icon-slot.module.css.d.ts +8 -0
  431. package/src/theme/icon-slot.test.tsx +275 -0
  432. package/src/theme/icon-slot.tsx +59 -0
  433. package/src/theme/index.ts +165 -3
  434. package/src/theme/layout-rhythm-css.test.ts +72 -0
  435. package/src/theme/radius.test.ts +11 -1
  436. package/src/theme/segmented-control.module.css +20 -0
  437. package/src/theme/shadow-surfaces.test.ts +20 -7
  438. package/src/theme/spacing.test.ts +75 -27
  439. package/src/theme/use-basalt-spacing.ts +2 -2
  440. package/src/tokens/density.test.ts +7 -25
  441. package/src/tokens/index.ts +48 -8
  442. package/src/tokens/palette.ts +107 -73
  443. package/src/widget-header/delta-badge.test.tsx +60 -0
  444. package/src/widget-header/delta-badge.tsx +57 -0
  445. package/src/widget-header/index.ts +2 -0
  446. package/src/widget-header/no-mantine.test.ts +33 -0
  447. package/src/widget-header/widget-header.module.css +335 -0
  448. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  449. package/src/widget-header/widget-header.test.tsx +187 -0
  450. package/src/widget-header/widget-header.tsx +168 -0
  451. package/agent/rules/basalt-agent.md +0 -452
  452. package/agent/rules/basalt-app.md +0 -246
  453. package/agent/rules/basalt-commands.md +0 -320
  454. package/agent/rules/basalt-content.md +0 -409
  455. package/agent/rules/basalt-data.md +0 -269
  456. package/agent/rules/basalt-forms.md +0 -186
  457. package/agent/rules/basalt-notifications.md +0 -264
  458. package/agent/rules/basalt-query.md +0 -187
  459. package/agent/rules/basalt-router.md +0 -336
  460. package/dist/content/article-filter-bar.d.ts +0 -40
  461. package/dist/content/article-filter-bar.d.ts.map +0 -1
  462. package/dist/content/article-filter-bar.js +0 -56
  463. package/dist/shell/page-header.d.ts +0 -13
  464. package/dist/shell/page-header.d.ts.map +0 -1
  465. package/dist/shell/page-header.js +0 -23
  466. package/src/content/article-filter-bar.tsx +0 -88
  467. package/src/shell/page-header.tsx +0 -35
@@ -0,0 +1,211 @@
1
+ /**
2
+ * createPersistedState — Mantine-free, React-only localStorage primitive.
3
+ *
4
+ * Versioned, Standard-Schema-validated state via useSyncExternalStore. SSR-safe.
5
+ * Cross-tab via the 'storage' event. Keys are namespaced 'basalt:*' so they never
6
+ * collide with the localstorage-theme guard pattern.
7
+ *
8
+ * One of the two halves behind the `basalt-ui/state` barrel (`../state.ts`); the field vocabulary
9
+ * and the two store factories built on this primitive live in `./fields`.
10
+ */
11
+ import { useSyncExternalStore } from 'react'
12
+ import type { StandardSchemaV1 } from '../register'
13
+
14
+ // Shared noop subscribe — passed to useSyncExternalStore on the server so no window access occurs.
15
+ const noopSubscribe =
16
+ (_cb: () => void): (() => void) =>
17
+ () => {}
18
+
19
+ export type PersistedStateOptions<T> = {
20
+ /** localStorage key (will be namespaced as `basalt:<key>`). */
21
+ readonly key: string
22
+ /** Envelope version — increment when the shape changes. */
23
+ readonly version: number
24
+ /** Value to use when nothing is persisted, after migration fails, or on SSR. */
25
+ readonly initial: T
26
+ /** Migrate a previous-version persisted value forward to the current shape. */
27
+ readonly migrate?: (persisted: unknown, fromVersion: number) => T
28
+ /**
29
+ * Standard-Schema validate the (post-migrate) value. Invalid result OR a Promise (async schema)
30
+ * falls back to `initial` — sync storage can't await, so async validators are treated as invalid.
31
+ */
32
+ readonly schema?: StandardSchemaV1<unknown, T>
33
+ }
34
+
35
+ /** The envelope stored in localStorage: `{ v: number, value: unknown }`. */
36
+ type Envelope = { v: number; value: unknown }
37
+
38
+ function isEnvelope(raw: unknown): raw is Envelope {
39
+ return (
40
+ typeof raw === 'object' &&
41
+ raw !== null &&
42
+ 'v' in raw &&
43
+ typeof (raw as Record<string, unknown>)['v'] === 'number' &&
44
+ 'value' in raw
45
+ )
46
+ }
47
+
48
+ /** Parse a raw localStorage string (or null) into a value, falling back to `initial` on any miss. */
49
+ function parseStorage<T>(raw: string | null, opts: PersistedStateOptions<T>): T {
50
+ if (raw === null) return opts.initial
51
+ try {
52
+ const parsed: unknown = JSON.parse(raw)
53
+ if (!isEnvelope(parsed)) return opts.initial
54
+
55
+ let value: unknown = parsed.value
56
+
57
+ if (parsed.v !== opts.version) {
58
+ value = opts.migrate ? opts.migrate(parsed.value, parsed.v) : opts.initial
59
+ }
60
+
61
+ if (opts.schema) {
62
+ const result = opts.schema['~standard'].validate(value)
63
+ // Async schema — can't await in sync storage path; fall back to initial
64
+ if (result instanceof Promise) return opts.initial
65
+ if (result.issues !== undefined) return opts.initial
66
+ // Narrowed to SuccessResult<T> — value is T
67
+ return result.value
68
+ }
69
+
70
+ return value as T
71
+ } catch {
72
+ return opts.initial
73
+ }
74
+ }
75
+
76
+ function writeStorage<T>(opts: PersistedStateOptions<T>, next: T): void {
77
+ const storageKey = `basalt:${opts.key}`
78
+ try {
79
+ const envelope: Envelope = { v: opts.version, value: next }
80
+ window.localStorage.setItem(storageKey, JSON.stringify(envelope))
81
+ } catch {
82
+ // Silently fail (storage full, private browsing, etc.)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Versioned, Standard-Schema-validated localStorage state via useSyncExternalStore (no zustand).
88
+ * SSR-safe (getServerSnapshot returns `initial`). Cross-tab via the storage event.
89
+ *
90
+ * Returns a factory hook — call it once per module, then use the returned hook in components.
91
+ * Common case is 3 lines.
92
+ *
93
+ * @example
94
+ * export const useFilterDraft = createPersistedState({ key: 'filters', version: 1, initial: DEFAULT, schema: FilterSchema })
95
+ * // in a component:
96
+ * const [draft, setDraft] = useFilterDraft()
97
+ */
98
+ export function createPersistedState<T>(
99
+ opts: PersistedStateOptions<T>,
100
+ ): () => readonly [T, (next: T) => void] {
101
+ const storageKey = `basalt:${opts.key}`
102
+
103
+ // One Set of subscriber callbacks — mirrors notifications/store.ts single-listener-to-Set pattern.
104
+ const listeners = new Set<() => void>()
105
+
106
+ // ONE module-scoped 'storage' event listener per key, registered lazily on first subscriber.
107
+ // Removed when the last subscriber unsubscribes (cleanup semantics preserved).
108
+ let storageHandler: ((e: StorageEvent) => void) | null = null
109
+
110
+ const subscribe = (cb: () => void): (() => void) => {
111
+ listeners.add(cb)
112
+
113
+ // Attach the shared window listener on the first subscriber.
114
+ if (storageHandler === null) {
115
+ storageHandler = (e: StorageEvent): void => {
116
+ if (e.key === storageKey) {
117
+ for (const listener of listeners) listener()
118
+ }
119
+ }
120
+ window.addEventListener('storage', storageHandler)
121
+ }
122
+
123
+ return () => {
124
+ listeners.delete(cb)
125
+ // Detach the shared window listener when the last subscriber unsubscribes.
126
+ if (listeners.size === 0 && storageHandler !== null) {
127
+ window.removeEventListener('storage', storageHandler)
128
+ storageHandler = null
129
+ }
130
+ }
131
+ }
132
+
133
+ // Snapshot cache — useSyncExternalStore requires getSnapshot to return a referentially STABLE
134
+ // value while the store is unchanged. parseStorage() allocates a fresh object/array on every
135
+ // call, so returning it raw makes React see an ever-changing snapshot for object/array state
136
+ // and loop until "Maximum update depth exceeded". We cache the parsed value keyed on the raw
137
+ // localStorage string: same string → same reference; a write (this tab or another) changes the
138
+ // string, so the next read re-parses exactly once. Primitive state was unaffected — this fixes
139
+ // the object/array case (chat history, form drafts).
140
+ let cachedRaw: string | null = null
141
+ let cachedValue: T = opts.initial
142
+ let primed = false
143
+
144
+ const getSnapshot = (): T => {
145
+ let raw: string | null
146
+ try {
147
+ raw = window.localStorage.getItem(storageKey)
148
+ } catch {
149
+ raw = null
150
+ }
151
+ if (primed && raw === cachedRaw) return cachedValue
152
+ cachedRaw = raw
153
+ cachedValue = parseStorage(raw, opts)
154
+ primed = true
155
+ return cachedValue
156
+ }
157
+ const getServerSnapshot = (): T => opts.initial
158
+
159
+ const setState = (next: T): void => {
160
+ writeStorage(opts, next)
161
+ // Notify in-tab listeners (the 'storage' event only fires in OTHER tabs)
162
+ for (const cb of listeners) cb()
163
+ }
164
+
165
+ // Detect SSR once at creation time — the environment doesn't change between renders.
166
+ const isServer = typeof window === 'undefined'
167
+
168
+ return function usePersistedState(): readonly [T, (next: T) => void] {
169
+ const value = useSyncExternalStore<T>(
170
+ // SSR guard — subscribe must not reference `window` on the server.
171
+ isServer ? noopSubscribe : subscribe,
172
+ isServer ? getServerSnapshot : getSnapshot,
173
+ getServerSnapshot,
174
+ )
175
+
176
+ return [value, setState] as const
177
+ }
178
+ }
179
+
180
+ // ── readPersistedValue — plain function, no React required ─────────────────────────────────
181
+
182
+ /**
183
+ * Read a value previously written by `createPersistedState` under the namespaced key
184
+ * `basalt:<key>`. Parses the versioned envelope shape `{ v, value }`. Returns `null` on
185
+ * miss, corruption, version mismatch, or SSR — no fallback is applied (call sites supply
186
+ * their own default).
187
+ *
188
+ * Primary use: `validateSearch` in TanStack Router (which runs outside React, so it can't
189
+ * call `usePersistedState`). Combine with a URL-param check to create a search-param store
190
+ * backed by localStorage.
191
+ *
192
+ * @param key — un-namespaced key (the same one passed to `createPersistedState`)
193
+ * @param version — if provided, the envelope version must match; stale envelopes return `null`
194
+ *
195
+ * @example
196
+ * const stored = readPersistedValue('dashboard-range', 1)
197
+ * // stored is '1d' | '7d' | '30d' | null
198
+ */
199
+ export function readPersistedValue(key: string, version?: number): unknown | null {
200
+ const storageKey = `basalt:${key}`
201
+ try {
202
+ const raw = localStorage.getItem(storageKey)
203
+ if (raw === null) return null
204
+ const envelope: unknown = JSON.parse(raw)
205
+ if (!isEnvelope(envelope)) return null
206
+ if (version !== undefined && envelope.v !== version) return null
207
+ return envelope.value
208
+ } catch {
209
+ return null
210
+ }
211
+ }
package/src/state.ts CHANGED
@@ -1,246 +1,52 @@
1
1
  /**
2
- * createPersistedState — Mantine-free, React-only localStorage primitive.
2
+ * `basalt-ui/state` — the headless state surface: no Mantine, no router, no JSX.
3
3
  *
4
- * Versioned, Standard-Schema-validated state via useSyncExternalStore. SSR-safe.
5
- * Cross-tab via the 'storage' event. Keys are namespaced 'basalt:*' so they never
6
- * collide with the localstorage-theme guard pattern.
7
- */
8
- import { useSyncExternalStore } from 'react'
9
- import type { StandardSchemaV1 } from './register'
10
-
11
- // Shared noop subscribe — passed to useSyncExternalStore on the server so no window access occurs.
12
- const noopSubscribe =
13
- (_cb: () => void): (() => void) =>
14
- () => {}
15
-
16
- // ── useOnlineStatus ────────────────────────────────────────────────────────────────────────────────
17
-
18
- function subscribeOnline(cb: () => void): () => void {
19
- window.addEventListener('online', cb)
20
- window.addEventListener('offline', cb)
21
- return () => {
22
- window.removeEventListener('online', cb)
23
- window.removeEventListener('offline', cb)
24
- }
25
- }
26
-
27
- const getOnlineSnapshot = (): boolean => navigator.onLine
28
- const getOnlineServerSnapshot = (): boolean => true
29
-
30
- // Detect SSR once at module load — mirrors createPersistedState's isServer pattern.
31
- const isOnlineServer = typeof window === 'undefined'
32
-
33
- /**
34
- * Returns `true` when the browser reports an active network connection, `false` otherwise.
35
- * Backed by `useSyncExternalStore`: subscribes to window `online`/`offline` events.
36
- * SSR-safe — `getServerSnapshot` returns `true` (optimistic: assume online on the server).
4
+ * Two halves, one barrel:
37
5
  *
38
- * @deprecated Use `useConnectivity()` from `basalt-ui` instead — it aggregates browser,
39
- * React Query, SSE, and health-ping signals into a richer `{ status, details }` object.
40
- * `useOnlineStatus` remains for simple boolean needs without a provider dependency.
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.
41
11
  *
42
- * @example
43
- * const isOnline = useOnlineStatus()
44
- * if (!isOnline) return <OfflineBanner />
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`.
45
14
  */
46
- export function useOnlineStatus(): boolean {
47
- return useSyncExternalStore(
48
- isOnlineServer ? noopSubscribe : subscribeOnline,
49
- isOnlineServer ? getOnlineServerSnapshot : getOnlineSnapshot,
50
- getOnlineServerSnapshot,
51
- )
52
- }
53
-
54
- export type PersistedStateOptions<T> = {
55
- /** localStorage key (will be namespaced as `basalt:<key>`). */
56
- readonly key: string
57
- /** Envelope version — increment when the shape changes. */
58
- readonly version: number
59
- /** Value to use when nothing is persisted, after migration fails, or on SSR. */
60
- readonly initial: T
61
- /** Migrate a previous-version persisted value forward to the current shape. */
62
- readonly migrate?: (persisted: unknown, fromVersion: number) => T
63
- /**
64
- * Standard-Schema validate the (post-migrate) value. Invalid result OR a Promise (async schema)
65
- * falls back to `initial` — sync storage can't await, so async validators are treated as invalid.
66
- */
67
- readonly schema?: StandardSchemaV1<unknown, T>
68
- }
69
-
70
- /** The envelope stored in localStorage: `{ v: number, value: unknown }`. */
71
- type Envelope = { v: number; value: unknown }
72
15
 
73
- function isEnvelope(raw: unknown): raw is Envelope {
74
- return (
75
- typeof raw === 'object' &&
76
- raw !== null &&
77
- 'v' in raw &&
78
- typeof (raw as Record<string, unknown>)['v'] === 'number' &&
79
- 'value' in raw
80
- )
81
- }
82
-
83
- /** Parse a raw localStorage string (or null) into a value, falling back to `initial` on any miss. */
84
- function parseStorage<T>(raw: string | null, opts: PersistedStateOptions<T>): T {
85
- if (raw === null) return opts.initial
86
- try {
87
- const parsed: unknown = JSON.parse(raw)
88
- if (!isEnvelope(parsed)) return opts.initial
89
-
90
- let value: unknown = parsed.value
91
-
92
- if (parsed.v !== opts.version) {
93
- value = opts.migrate ? opts.migrate(parsed.value, parsed.v) : opts.initial
94
- }
95
-
96
- if (opts.schema) {
97
- const result = opts.schema['~standard'].validate(value)
98
- // Async schema — can't await in sync storage path; fall back to initial
99
- if (result instanceof Promise) return opts.initial
100
- if (result.issues !== undefined) return opts.initial
101
- // Narrowed to SuccessResult<T> — value is T
102
- return result.value
103
- }
104
-
105
- return value as T
106
- } catch {
107
- return opts.initial
108
- }
109
- }
110
-
111
- function writeStorage<T>(opts: PersistedStateOptions<T>, next: T): void {
112
- const storageKey = `basalt:${opts.key}`
113
- try {
114
- const envelope: Envelope = { v: opts.version, value: next }
115
- window.localStorage.setItem(storageKey, JSON.stringify(envelope))
116
- } catch {
117
- // Silently fail (storage full, private browsing, etc.)
118
- }
119
- }
120
-
121
- /**
122
- * Versioned, Standard-Schema-validated localStorage state via useSyncExternalStore (no zustand).
123
- * SSR-safe (getServerSnapshot returns `initial`). Cross-tab via the storage event.
124
- *
125
- * Returns a factory hook — call it once per module, then use the returned hook in components.
126
- * Common case is 3 lines.
127
- *
128
- * @example
129
- * export const useFilterDraft = createPersistedState({ key: 'filters', version: 1, initial: DEFAULT, schema: FilterSchema })
130
- * // in a component:
131
- * const [draft, setDraft] = useFilterDraft()
132
- */
133
- export function createPersistedState<T>(
134
- opts: PersistedStateOptions<T>,
135
- ): () => readonly [T, (next: T) => void] {
136
- const storageKey = `basalt:${opts.key}`
137
-
138
- // One Set of subscriber callbacks — mirrors notifications/store.ts single-listener-to-Set pattern.
139
- const listeners = new Set<() => void>()
140
-
141
- // ONE module-scoped 'storage' event listener per key, registered lazily on first subscriber.
142
- // Removed when the last subscriber unsubscribes (cleanup semantics preserved).
143
- let storageHandler: ((e: StorageEvent) => void) | null = null
144
-
145
- const subscribe = (cb: () => void): (() => void) => {
146
- listeners.add(cb)
147
-
148
- // Attach the shared window listener on the first subscriber.
149
- if (storageHandler === null) {
150
- storageHandler = (e: StorageEvent): void => {
151
- if (e.key === storageKey) {
152
- for (const listener of listeners) listener()
153
- }
154
- }
155
- window.addEventListener('storage', storageHandler)
156
- }
157
-
158
- return () => {
159
- listeners.delete(cb)
160
- // Detach the shared window listener when the last subscriber unsubscribes.
161
- if (listeners.size === 0 && storageHandler !== null) {
162
- window.removeEventListener('storage', storageHandler)
163
- storageHandler = null
164
- }
165
- }
166
- }
167
-
168
- // Snapshot cache — useSyncExternalStore requires getSnapshot to return a referentially STABLE
169
- // value while the store is unchanged. parseStorage() allocates a fresh object/array on every
170
- // call, so returning it raw makes React see an ever-changing snapshot for object/array state
171
- // and loop until "Maximum update depth exceeded". We cache the parsed value keyed on the raw
172
- // localStorage string: same string → same reference; a write (this tab or another) changes the
173
- // string, so the next read re-parses exactly once. Primitive state was unaffected — this fixes
174
- // the object/array case (chat history, form drafts).
175
- let cachedRaw: string | null = null
176
- let cachedValue: T = opts.initial
177
- let primed = false
178
-
179
- const getSnapshot = (): T => {
180
- let raw: string | null
181
- try {
182
- raw = window.localStorage.getItem(storageKey)
183
- } catch {
184
- raw = null
185
- }
186
- if (primed && raw === cachedRaw) return cachedValue
187
- cachedRaw = raw
188
- cachedValue = parseStorage(raw, opts)
189
- primed = true
190
- return cachedValue
191
- }
192
- const getServerSnapshot = (): T => opts.initial
193
-
194
- const setState = (next: T): void => {
195
- writeStorage(opts, next)
196
- // Notify in-tab listeners (the 'storage' event only fires in OTHER tabs)
197
- for (const cb of listeners) cb()
198
- }
199
-
200
- // Detect SSR once at creation time — the environment doesn't change between renders.
201
- const isServer = typeof window === 'undefined'
202
-
203
- return function usePersistedState(): readonly [T, (next: T) => void] {
204
- const value = useSyncExternalStore<T>(
205
- // SSR guard — subscribe must not reference `window` on the server.
206
- isServer ? noopSubscribe : subscribe,
207
- isServer ? getServerSnapshot : getSnapshot,
208
- getServerSnapshot,
209
- )
210
-
211
- return [value, setState] as const
212
- }
213
- }
214
-
215
- // ── readPersistedValue — plain function, no React required ─────────────────────────────────
216
-
217
- /**
218
- * Read a value previously written by `createPersistedState` under the namespaced key
219
- * `basalt:<key>`. Parses the versioned envelope shape `{ v, value }`. Returns `null` on
220
- * miss, corruption, version mismatch, or SSR — no fallback is applied (call sites supply
221
- * their own default).
222
- *
223
- * Primary use: `validateSearch` in TanStack Router (which runs outside React, so it can't
224
- * call `usePersistedState`). Combine with a URL-param check to create a search-param store
225
- * backed by localStorage.
226
- *
227
- * @param key — un-namespaced key (the same one passed to `createPersistedState`)
228
- * @param version — if provided, the envelope version must match; stale envelopes return `null`
229
- *
230
- * @example
231
- * const stored = readPersistedValue('dashboard-range', 1)
232
- * // stored is '1d' | '7d' | '30d' | null
233
- */
234
- export function readPersistedValue(key: string, version?: number): unknown | null {
235
- const storageKey = `basalt:${key}`
236
- try {
237
- const raw = localStorage.getItem(storageKey)
238
- if (raw === null) return null
239
- const envelope: unknown = JSON.parse(raw)
240
- if (!isEnvelope(envelope)) return null
241
- if (version !== undefined && envelope.v !== version) return null
242
- return envelope.value
243
- } catch {
244
- return null
245
- }
246
- }
16
+ export {
17
+ createPersistedState,
18
+ type PersistedStateOptions,
19
+ readPersistedValue,
20
+ } from './state/persisted'
21
+
22
+ export {
23
+ field,
24
+ createLocalStore,
25
+ type LocalStore,
26
+ // The field vocabulary
27
+ type FieldLane,
28
+ type ResolvedLane,
29
+ type ResolveLane,
30
+ type RangeValue,
31
+ type RangeParams,
32
+ type RangePresets,
33
+ type EnumField,
34
+ type MultiField,
35
+ type RangeField,
36
+ type NumberField,
37
+ type BooleanField,
38
+ type StringField,
39
+ type AnyField,
40
+ type FieldValue,
41
+ type FieldOption,
42
+ type FieldHandle,
43
+ type SearchValues,
44
+ type StoredValues,
45
+ // @internal — the seam `createSearchStore` is built on, not a consumer API.
46
+ type FieldCodec,
47
+ type StoreEntry,
48
+ type StoreCoreOptions,
49
+ type StoreCore,
50
+ resolveFieldCodec,
51
+ createStoreCore,
52
+ } from './state/fields'
package/src/styles.css CHANGED
@@ -68,6 +68,19 @@
68
68
  font-size: max(16px, var(--input-fz, 1rem)) !important;
69
69
  }
70
70
 
71
+ /* `data-numeric` — one typographic law, every element. A number read as a MEASUREMENT (a preset
72
+ like `7d`/`30d`, a KPI value, a table cell, a count tag) is mono with tabular figures, so
73
+ digits line up column-wise and a changing value does not reflow its neighbours. Declared here,
74
+ unscoped, because the attribute is the contract: `theme/segmented-control.module.css` and
75
+ `controls/controls.module.css` were each carrying their own copy, which is why a plain
76
+ `<Text data-numeric>` in a consumer silently rendered in the sans face. Those two module rules
77
+ STAY — they additionally set `font-size` for their own component, which is a component
78
+ decision, not a law. */
79
+ [data-numeric] {
80
+ font-family: var(--basalt-font-mono);
81
+ font-variant-numeric: tabular-nums;
82
+ }
83
+
71
84
  /* Focus ring = the accent as INK, not as a fill. Mantine hardcodes the ring to
72
85
  `var(--mantine-primary-color-filled)` — the SURFACE token, which is deliberately deep in both
73
86
  schemes so a white label sits on it (see ACCENT in tokens/palette.ts). A ring is read against