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