basalt-ui 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +18 -0
- package/MIGRATING.md +546 -20
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +88 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +220 -84
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { createPersistedState, readPersistedValue } from "./persisted.js";
|
|
3
|
+
const field = {
|
|
4
|
+
/** A closed string enum — a tab, a compare mode, a single-select filter. */
|
|
5
|
+
enum(values, fallback, lane) {
|
|
6
|
+
return { kind: "enum", values, fallback, lane: resolveLane(lane) };
|
|
7
|
+
},
|
|
8
|
+
/** An any-of set over the same closed enum. Decoding is deduped and canonically re-sorted. */
|
|
9
|
+
multi(values, fallback = [], lane) {
|
|
10
|
+
return { kind: "multi", values, fallback, lane: resolveLane(lane) };
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* A time window: THREE URL params (preset + `from` + `to`, each renamable through `params`), so a
|
|
14
|
+
* consumer's existing deep links and loaders keep their shape. `custom: true` additionally
|
|
15
|
+
* allows the preset `'custom'` with two ISO dates.
|
|
16
|
+
*
|
|
17
|
+
* A second `field.range` in the same store — or a second range store composed into the same
|
|
18
|
+
* route's `validateSearch` — MUST rename `from`/`to` via `params`: every range defaults to the
|
|
19
|
+
* literal param names `'from'`/`'to'`, and two fields silently sharing them makes the later one's
|
|
20
|
+
* `toSearch` overwrite the earlier one's dates. `createStoreCore` throws at definition for the
|
|
21
|
+
* in-store case; the cross-store case is not detectable by either store alone.
|
|
22
|
+
*/
|
|
23
|
+
range(o, lane) {
|
|
24
|
+
return {
|
|
25
|
+
kind: "range",
|
|
26
|
+
presets: o.presets,
|
|
27
|
+
fallback: { preset: o.fallback },
|
|
28
|
+
// The only cast left, and it is narrow: `C` is inferred from this very literal, so the
|
|
29
|
+
// runtime boolean IS `C` — `?? false` is what the compiler cannot see through.
|
|
30
|
+
custom: o.custom ?? false,
|
|
31
|
+
params: o.params ?? {},
|
|
32
|
+
lane: resolveLane(lane)
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
/** A number — pagination, a threshold. Out-of-range input clamps to `min`/`max`. */
|
|
36
|
+
number(o, lane) {
|
|
37
|
+
return {
|
|
38
|
+
kind: "number",
|
|
39
|
+
fallback: o.fallback,
|
|
40
|
+
min: o.min,
|
|
41
|
+
max: o.max,
|
|
42
|
+
int: o.int === true,
|
|
43
|
+
lane: resolveLane(lane)
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
/** A toggle. Accepts `true`/`false` and the strings `'true'`/`'false'` from a hand-typed URL. */
|
|
47
|
+
boolean(fallback, lane) {
|
|
48
|
+
return { kind: "boolean", fallback, lane: resolveLane(lane) };
|
|
49
|
+
},
|
|
50
|
+
/** Free text — a search box. Defaults to `history: 'replace'` like every other field. */
|
|
51
|
+
string(o, lane) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "string",
|
|
54
|
+
fallback: o?.fallback ?? "",
|
|
55
|
+
max: o?.max,
|
|
56
|
+
lane: resolveLane(lane)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function resolveLane(lane) {
|
|
61
|
+
return {
|
|
62
|
+
url: lane?.url !== false,
|
|
63
|
+
persist: lane?.persist !== false,
|
|
64
|
+
history: lane?.history === "push" ? "push" : "replace"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T[\d:.]+(?:Z|[+-]\d{2}:\d{2})?)?$/;
|
|
68
|
+
function isIsoDate(raw) {
|
|
69
|
+
return typeof raw === "string" && ISO_DATE_RE.test(raw) && !Number.isNaN(Date.parse(raw));
|
|
70
|
+
}
|
|
71
|
+
const NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
72
|
+
function decodeBoolean(raw) {
|
|
73
|
+
if (typeof raw === "boolean") return raw;
|
|
74
|
+
if (raw === "true") return true;
|
|
75
|
+
if (raw === "false") return false;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function sameList(a, b) {
|
|
79
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
80
|
+
return a.length === b.length && a.every((entry, i) => entry === b[i]);
|
|
81
|
+
}
|
|
82
|
+
function resolveFieldCodec(name, f, opts) {
|
|
83
|
+
switch (f.kind) {
|
|
84
|
+
case "enum": {
|
|
85
|
+
const allowed = new Set(f.values);
|
|
86
|
+
const decode = (raw) => typeof raw === "string" && allowed.has(raw) ? raw : null;
|
|
87
|
+
return single(name, f, decode, { optionValues: f.values });
|
|
88
|
+
}
|
|
89
|
+
case "multi": {
|
|
90
|
+
const present = new Set(f.values);
|
|
91
|
+
const decode = (raw) => {
|
|
92
|
+
if (!Array.isArray(raw)) return null;
|
|
93
|
+
const picked = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const entry of raw) {
|
|
95
|
+
if (typeof entry === "string" && present.has(entry)) picked.add(entry);
|
|
96
|
+
}
|
|
97
|
+
return f.values.filter((value) => picked.has(value));
|
|
98
|
+
};
|
|
99
|
+
const decodeUrl = opts?.legacyMultiEmpty === true ? (raw) => {
|
|
100
|
+
const value = decode(raw);
|
|
101
|
+
return Array.isArray(value) && value.length === 0 ? null : value;
|
|
102
|
+
} : decode;
|
|
103
|
+
return single(name, f, decode, {
|
|
104
|
+
optionValues: f.values,
|
|
105
|
+
equals: sameList,
|
|
106
|
+
decodeUrl
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
case "range": {
|
|
110
|
+
const presetParam = f.params.preset ?? name;
|
|
111
|
+
const fromParam = f.params.from ?? "from";
|
|
112
|
+
const toParam = f.params.to ?? "to";
|
|
113
|
+
const allowed = new Set(f.presets);
|
|
114
|
+
const decode = (raw) => {
|
|
115
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
116
|
+
const value = raw;
|
|
117
|
+
if (typeof value.preset !== "string") return null;
|
|
118
|
+
if (value.preset === "custom") {
|
|
119
|
+
if (!f.custom || !isIsoDate(value.from) || !isIsoDate(value.to)) return null;
|
|
120
|
+
return { preset: "custom", from: value.from, to: value.to };
|
|
121
|
+
}
|
|
122
|
+
return allowed.has(value.preset) ? { preset: value.preset } : null;
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
kind: "range",
|
|
126
|
+
lane: f.lane,
|
|
127
|
+
params: [presetParam, fromParam, toParam],
|
|
128
|
+
primary: presetParam,
|
|
129
|
+
fallback: f.fallback,
|
|
130
|
+
decode,
|
|
131
|
+
fromSearch: (search) => decode({
|
|
132
|
+
preset: search[presetParam],
|
|
133
|
+
from: search[fromParam],
|
|
134
|
+
to: search[toParam]
|
|
135
|
+
}),
|
|
136
|
+
toSearch: (value) => {
|
|
137
|
+
const range = value;
|
|
138
|
+
return {
|
|
139
|
+
[presetParam]: range.preset,
|
|
140
|
+
[fromParam]: range.from,
|
|
141
|
+
[toParam]: range.to
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
equals: (a, b) => {
|
|
145
|
+
const x = a;
|
|
146
|
+
const y = b;
|
|
147
|
+
if (x === null || y === null) return x === y;
|
|
148
|
+
return x.preset === y.preset && x.from === y.from && x.to === y.to;
|
|
149
|
+
},
|
|
150
|
+
optionValues: f.custom ? [...f.presets, "custom"] : f.presets
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
case "number": {
|
|
154
|
+
const decode = (raw) => {
|
|
155
|
+
const parsed = typeof raw === "number" ? raw : typeof raw === "string" && NUMERIC_RE.test(raw) ? Number(raw) : NaN;
|
|
156
|
+
if (!Number.isFinite(parsed)) return null;
|
|
157
|
+
if (f.int && !Number.isInteger(parsed)) return null;
|
|
158
|
+
let value = parsed;
|
|
159
|
+
if (f.min !== void 0 && value < f.min) value = f.min;
|
|
160
|
+
if (f.max !== void 0 && value > f.max) value = f.max;
|
|
161
|
+
return value;
|
|
162
|
+
};
|
|
163
|
+
return single(name, f, decode);
|
|
164
|
+
}
|
|
165
|
+
case "boolean": {
|
|
166
|
+
return single(name, f, decodeBoolean);
|
|
167
|
+
}
|
|
168
|
+
case "string": {
|
|
169
|
+
const decode = (raw) => {
|
|
170
|
+
if (typeof raw !== "string") return null;
|
|
171
|
+
return f.max !== void 0 && raw.length > f.max ? raw.slice(0, f.max) : raw;
|
|
172
|
+
};
|
|
173
|
+
return single(name, f, decode);
|
|
174
|
+
}
|
|
175
|
+
default: {
|
|
176
|
+
throw new Error("basalt-ui: createSearchStore received a field of an unknown kind");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function single(name, f, decode, extra) {
|
|
181
|
+
const decodeUrl = extra?.decodeUrl ?? decode;
|
|
182
|
+
return {
|
|
183
|
+
kind: f.kind,
|
|
184
|
+
lane: f.lane,
|
|
185
|
+
params: [name],
|
|
186
|
+
primary: name,
|
|
187
|
+
fallback: f.fallback,
|
|
188
|
+
decode,
|
|
189
|
+
fromSearch: (search) => decodeUrl(search[name]),
|
|
190
|
+
toSearch: (value) => ({ [name]: value }),
|
|
191
|
+
equals: extra?.equals ?? ((a, b) => a === b),
|
|
192
|
+
optionValues: extra?.optionValues ?? []
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function isRecord(raw) {
|
|
196
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
197
|
+
}
|
|
198
|
+
function assignDefined(into, from) {
|
|
199
|
+
for (const [key, value] of Object.entries(from)) {
|
|
200
|
+
if (value !== void 0) into[key] = value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function resolveField(entry, search, record) {
|
|
204
|
+
const fromUrl = search === null ? null : entry.codec.fromSearch(search);
|
|
205
|
+
if (fromUrl !== null) return fromUrl;
|
|
206
|
+
if (entry.codec.lane.persist) {
|
|
207
|
+
const stored = entry.codec.decode(record[entry.name]);
|
|
208
|
+
if (stored !== null) return stored;
|
|
209
|
+
}
|
|
210
|
+
return entry.codec.fallback;
|
|
211
|
+
}
|
|
212
|
+
function createMemoryLane() {
|
|
213
|
+
const values = /* @__PURE__ */ new Map();
|
|
214
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
215
|
+
let version = 0;
|
|
216
|
+
const bump = () => {
|
|
217
|
+
version += 1;
|
|
218
|
+
for (const cb of listeners) cb();
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
subscribe: (cb) => {
|
|
222
|
+
listeners.add(cb);
|
|
223
|
+
return () => {
|
|
224
|
+
listeners.delete(cb);
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
getVersion: () => version,
|
|
228
|
+
// `undefined` is the only "never written" marker the lane needs: no field kind has `undefined`
|
|
229
|
+
// as a legal value, so it can never collide with a written one.
|
|
230
|
+
resolve: (entry) => {
|
|
231
|
+
const stored = values.get(entry.name);
|
|
232
|
+
return stored === void 0 ? entry.codec.fallback : stored;
|
|
233
|
+
},
|
|
234
|
+
write: (name, value) => {
|
|
235
|
+
values.set(name, value);
|
|
236
|
+
bump();
|
|
237
|
+
},
|
|
238
|
+
clear: () => {
|
|
239
|
+
if (values.size === 0) return;
|
|
240
|
+
values.clear();
|
|
241
|
+
bump();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function assertNoParamCollision(key, entries) {
|
|
246
|
+
const owner = /* @__PURE__ */ new Map();
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
for (const param of entry.codec.params) {
|
|
249
|
+
const existing = owner.get(param);
|
|
250
|
+
if (existing !== void 0 && existing !== entry.name) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`basalt-ui: createSearchStore('${key}'): fields '${existing}' and '${entry.name}' both own the URL param '${param}' \u2014 rename one via field.range({ params: { ... } }).`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
owner.set(param, entry.name);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function createStoreCore(o) {
|
|
260
|
+
const version = o.version ?? 1;
|
|
261
|
+
const legacy = o.legacyValueField;
|
|
262
|
+
const entries = Object.entries(o.fields).map(([name, f]) => ({
|
|
263
|
+
name,
|
|
264
|
+
codec: resolveFieldCodec(name, f, { legacyMultiEmpty: legacy !== void 0 })
|
|
265
|
+
}));
|
|
266
|
+
assertNoParamCollision(o.key, entries);
|
|
267
|
+
const urlEntries = entries.filter((entry) => entry.codec.lane.url);
|
|
268
|
+
const anyPersisted = entries.some((entry) => entry.codec.lane.persist);
|
|
269
|
+
const labels = /* @__PURE__ */ new Map();
|
|
270
|
+
const usePersistedRaw = createPersistedState({ key: o.key, version, initial: null });
|
|
271
|
+
const memory = createMemoryLane();
|
|
272
|
+
const toRecord = (raw) => {
|
|
273
|
+
if (legacy !== void 0) return raw === null || raw === void 0 ? {} : { [legacy]: raw };
|
|
274
|
+
return isRecord(raw) ? raw : {};
|
|
275
|
+
};
|
|
276
|
+
const fromRecord = (record) => legacy !== void 0 ? record[legacy] ?? null : record;
|
|
277
|
+
const readRecord = () => toRecord(readPersistedValue(o.key, version));
|
|
278
|
+
const optionsFor = (entry) => {
|
|
279
|
+
const map = labels.get(entry.name);
|
|
280
|
+
return entry.codec.optionValues.map((value) => ({ value, label: map?.[value] ?? value }));
|
|
281
|
+
};
|
|
282
|
+
const writeField = (setRecord, name, value) => {
|
|
283
|
+
setRecord({ ...readRecord(), [name]: value });
|
|
284
|
+
};
|
|
285
|
+
const handle = (entry, use) => {
|
|
286
|
+
const { codec } = entry;
|
|
287
|
+
const built = {
|
|
288
|
+
kind: codec.kind,
|
|
289
|
+
fallback: codec.fallback,
|
|
290
|
+
use,
|
|
291
|
+
isDefault: (v) => codec.equals(v, codec.fallback)
|
|
292
|
+
};
|
|
293
|
+
if (codec.kind === "range") {
|
|
294
|
+
built["toWindow"] = (v) => v.preset === "custom" && v.from !== void 0 && v.to !== void 0 ? { from: v.from, to: v.to } : { window: v.preset };
|
|
295
|
+
}
|
|
296
|
+
Object.defineProperty(built, "options", {
|
|
297
|
+
enumerable: true,
|
|
298
|
+
get: () => optionsFor(entry)
|
|
299
|
+
});
|
|
300
|
+
return built;
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
entries,
|
|
304
|
+
urlEntries,
|
|
305
|
+
anyPersisted,
|
|
306
|
+
usePersistedRecord() {
|
|
307
|
+
const [raw, setRaw] = usePersistedRaw();
|
|
308
|
+
return [toRecord(raw), (next) => setRaw(fromRecord(next))];
|
|
309
|
+
},
|
|
310
|
+
readRecord,
|
|
311
|
+
memoryUse(entry) {
|
|
312
|
+
return () => {
|
|
313
|
+
useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion);
|
|
314
|
+
return [memory.resolve(entry), (next) => memory.write(entry.name, next)];
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
readMemoryValue: (entry) => memory.resolve(entry),
|
|
318
|
+
useMemoryVersion: () => useSyncExternalStore(memory.subscribe, memory.getVersion, memory.getVersion),
|
|
319
|
+
resetMemory: () => memory.clear(),
|
|
320
|
+
setLabels(map) {
|
|
321
|
+
for (const [name, entry] of Object.entries(map)) {
|
|
322
|
+
if (entry !== void 0) labels.set(name, entry);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
optionsFor,
|
|
326
|
+
resolve: resolveField,
|
|
327
|
+
writeField,
|
|
328
|
+
readStoredFlat() {
|
|
329
|
+
const record = readRecord();
|
|
330
|
+
const out = {};
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const stored = entry.codec.decode(record[entry.name]);
|
|
333
|
+
if (stored !== null) assignDefined(out, entry.codec.toSearch(stored));
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
},
|
|
337
|
+
handle
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function createLocalStore(o) {
|
|
341
|
+
const core = createStoreCore({ key: o.key, fields: o.fields, version: o.version });
|
|
342
|
+
const persistedUse = (entry) => () => {
|
|
343
|
+
const [record, setRecord] = core.usePersistedRecord();
|
|
344
|
+
return [
|
|
345
|
+
core.resolve(entry, null, record),
|
|
346
|
+
(next) => core.writeField(setRecord, entry.name, next)
|
|
347
|
+
];
|
|
348
|
+
};
|
|
349
|
+
const field2 = {};
|
|
350
|
+
for (const entry of core.entries) {
|
|
351
|
+
const use = entry.codec.lane.persist ? persistedUse(entry) : core.memoryUse(entry);
|
|
352
|
+
field2[entry.name] = core.handle(entry, use);
|
|
353
|
+
}
|
|
354
|
+
const store = {
|
|
355
|
+
field: field2,
|
|
356
|
+
readStored: () => core.readStoredFlat(),
|
|
357
|
+
labels: (map) => {
|
|
358
|
+
core.setLabels(map);
|
|
359
|
+
return store;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
return store;
|
|
363
|
+
}
|
|
364
|
+
export {
|
|
365
|
+
createLocalStore,
|
|
366
|
+
createStoreCore,
|
|
367
|
+
field,
|
|
368
|
+
resolveFieldCodec
|
|
369
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.test.d.ts","sourceRoot":"","sources":["../../src/state/fields.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
//# 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;AAqDD;;;;;;;;;;;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,CA8EvC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAYhF"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
const noopSubscribe = (_cb) => () => {
|
|
3
|
+
};
|
|
4
|
+
function isEnvelope(raw) {
|
|
5
|
+
return typeof raw === "object" && raw !== null && "v" in raw && typeof raw["v"] === "number" && "value" in raw;
|
|
6
|
+
}
|
|
7
|
+
function parseStorage(raw, opts) {
|
|
8
|
+
if (raw === null) return opts.initial;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (!isEnvelope(parsed)) return opts.initial;
|
|
12
|
+
let value = parsed.value;
|
|
13
|
+
if (parsed.v !== opts.version) {
|
|
14
|
+
value = opts.migrate ? opts.migrate(parsed.value, parsed.v) : opts.initial;
|
|
15
|
+
}
|
|
16
|
+
if (opts.schema) {
|
|
17
|
+
const result = opts.schema["~standard"].validate(value);
|
|
18
|
+
if (result instanceof Promise) return opts.initial;
|
|
19
|
+
if (result.issues !== void 0) return opts.initial;
|
|
20
|
+
return result.value;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
} catch {
|
|
24
|
+
return opts.initial;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeStorage(opts, next) {
|
|
28
|
+
const storageKey = `basalt:${opts.key}`;
|
|
29
|
+
try {
|
|
30
|
+
const envelope = { v: opts.version, value: next };
|
|
31
|
+
window.localStorage.setItem(storageKey, JSON.stringify(envelope));
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createPersistedState(opts) {
|
|
36
|
+
const storageKey = `basalt:${opts.key}`;
|
|
37
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
38
|
+
let storageHandler = null;
|
|
39
|
+
const subscribe = (cb) => {
|
|
40
|
+
listeners.add(cb);
|
|
41
|
+
if (storageHandler === null) {
|
|
42
|
+
storageHandler = (e) => {
|
|
43
|
+
if (e.key === storageKey) {
|
|
44
|
+
for (const listener of listeners) listener();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
window.addEventListener("storage", storageHandler);
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
listeners.delete(cb);
|
|
51
|
+
if (listeners.size === 0 && storageHandler !== null) {
|
|
52
|
+
window.removeEventListener("storage", storageHandler);
|
|
53
|
+
storageHandler = null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
let cachedRaw = null;
|
|
58
|
+
let cachedValue = opts.initial;
|
|
59
|
+
let primed = false;
|
|
60
|
+
const getSnapshot = () => {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = window.localStorage.getItem(storageKey);
|
|
64
|
+
} catch {
|
|
65
|
+
raw = null;
|
|
66
|
+
}
|
|
67
|
+
if (primed && raw === cachedRaw) return cachedValue;
|
|
68
|
+
cachedRaw = raw;
|
|
69
|
+
cachedValue = parseStorage(raw, opts);
|
|
70
|
+
primed = true;
|
|
71
|
+
return cachedValue;
|
|
72
|
+
};
|
|
73
|
+
const getServerSnapshot = () => opts.initial;
|
|
74
|
+
const setState = (next) => {
|
|
75
|
+
writeStorage(opts, next);
|
|
76
|
+
for (const cb of listeners) cb();
|
|
77
|
+
};
|
|
78
|
+
const isServer = typeof window === "undefined";
|
|
79
|
+
return function usePersistedState() {
|
|
80
|
+
const value = useSyncExternalStore(
|
|
81
|
+
// SSR guard — subscribe must not reference `window` on the server.
|
|
82
|
+
isServer ? noopSubscribe : subscribe,
|
|
83
|
+
isServer ? getServerSnapshot : getSnapshot,
|
|
84
|
+
getServerSnapshot
|
|
85
|
+
);
|
|
86
|
+
return [value, setState];
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function readPersistedValue(key, version) {
|
|
90
|
+
const storageKey = `basalt:${key}`;
|
|
91
|
+
try {
|
|
92
|
+
const raw = localStorage.getItem(storageKey);
|
|
93
|
+
if (raw === null) return null;
|
|
94
|
+
const envelope = JSON.parse(raw);
|
|
95
|
+
if (!isEnvelope(envelope)) return null;
|
|
96
|
+
if (version !== void 0 && envelope.v !== version) return null;
|
|
97
|
+
return envelope.value;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
createPersistedState,
|
|
104
|
+
readPersistedValue
|
|
105
|
+
};
|
package/dist/state.d.ts
CHANGED
|
@@ -1,62 +1,17 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from './register.js';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
*
|
|
58
|
-
*
|
|
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
|
|
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 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, resolveFieldCodec, createStoreCore, } from './state/fields.js';
|
|
62
17
|
//# sourceMappingURL=state.d.ts.map
|
package/dist/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,kBAAkB,GACnB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,iBAAiB,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAA"}
|