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
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The deprecated multi wrapper's contract, unchanged across the rewrite onto
|
|
3
|
+
* `createSearchStore` — plus the storage path two of these tests used to claim was unreachable.
|
|
4
|
+
*
|
|
5
|
+
* `bun test` runs under happy-dom (root `bunfig.toml` preload), so `localStorage` has always been
|
|
6
|
+
* REAL here: "no localStorage in this environment" was false, and it meant the fallback-to-storage
|
|
7
|
+
* half of `validateSearch` — the entire reason the store exists — was asserted only in the
|
|
8
|
+
* single-value store's tests (D12).
|
|
9
|
+
*/
|
|
10
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
11
|
import { createMultiSearchParamStore } from './multi-search-param-store'
|
|
3
12
|
|
|
4
13
|
const VALUES = ['api', 'design', 'guide'] as const
|
|
5
14
|
|
|
15
|
+
/** Writes the wrapper's single-value envelope — the layout it has used since 1.0.0. */
|
|
16
|
+
function persist(key: string, value: readonly string[], version = 1): void {
|
|
17
|
+
localStorage.setItem(`basalt:${key}`, JSON.stringify({ v: version, value }))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
localStorage.clear()
|
|
22
|
+
})
|
|
23
|
+
|
|
6
24
|
describe('createMultiSearchParamStore', () => {
|
|
7
25
|
describe('validateSearch', () => {
|
|
8
26
|
test('URL value wins when it decodes to a non-empty array', () => {
|
|
@@ -30,10 +48,17 @@ describe('createMultiSearchParamStore', () => {
|
|
|
30
48
|
expect(a.tags).toEqual(['api', 'design'])
|
|
31
49
|
})
|
|
32
50
|
|
|
33
|
-
test('a non-array URL value falls through to
|
|
51
|
+
test('a non-array URL value falls through to storage, then to the fallback', () => {
|
|
34
52
|
const store = createMultiSearchParamStore({ key: 'tags-e', param: 'tags', values: VALUES })
|
|
35
53
|
expect(store.validateSearch({ tags: 'api' })).toEqual({ tags: [] })
|
|
36
54
|
expect(store.validateSearch({})).toEqual({ tags: [] })
|
|
55
|
+
|
|
56
|
+
persist('tags-e', ['guide', 'api'])
|
|
57
|
+
// Storage is read, and normalized on the way out like any other source.
|
|
58
|
+
expect(store.validateSearch({ tags: 'api' })).toEqual({ tags: ['api', 'guide'] })
|
|
59
|
+
expect(store.validateSearch({})).toEqual({ tags: ['api', 'guide'] })
|
|
60
|
+
// A valid URL array still wins over it.
|
|
61
|
+
expect(store.validateSearch({ tags: ['design'] })).toEqual({ tags: ['design'] })
|
|
37
62
|
})
|
|
38
63
|
|
|
39
64
|
test('an empty URL array falls through to the fallback', () => {
|
|
@@ -48,9 +73,52 @@ describe('createMultiSearchParamStore', () => {
|
|
|
48
73
|
})
|
|
49
74
|
|
|
50
75
|
describe('readStored', () => {
|
|
51
|
-
test('returns null when nothing is persisted
|
|
76
|
+
test('returns null when nothing is persisted', () => {
|
|
52
77
|
const store = createMultiSearchParamStore({ key: 'tags-g', param: 'tags', values: VALUES })
|
|
53
78
|
expect(store.readStored()).toBeNull()
|
|
54
79
|
})
|
|
80
|
+
|
|
81
|
+
test('returns the normalized stored selection, and null for a stale envelope version', () => {
|
|
82
|
+
const store = createMultiSearchParamStore({ key: 'tags-h', param: 'tags', values: VALUES })
|
|
83
|
+
persist('tags-h', ['guide', 'nope', 'api'])
|
|
84
|
+
expect(store.readStored()).toEqual(['api', 'guide'])
|
|
85
|
+
|
|
86
|
+
const versioned = createMultiSearchParamStore({
|
|
87
|
+
key: 'tags-i',
|
|
88
|
+
param: 'tags',
|
|
89
|
+
values: VALUES,
|
|
90
|
+
version: 1,
|
|
91
|
+
})
|
|
92
|
+
persist('tags-i', ['api'], 2)
|
|
93
|
+
expect(versioned.readStored()).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('a stored EMPTY selection reads back as empty — a cleared filter stays cleared', () => {
|
|
97
|
+
// `{ v: 1, value: [] }` is exactly what `useStore()`'s setter writes when a user clears every
|
|
98
|
+
// filter. The empty-array-means-absent rule is a URL rule; applying it to storage too would
|
|
99
|
+
// resurrect the fallback on upgrade, which is not what "byte-compatible" means.
|
|
100
|
+
const store = createMultiSearchParamStore({
|
|
101
|
+
key: 'tags-empty',
|
|
102
|
+
param: 'tags',
|
|
103
|
+
values: VALUES,
|
|
104
|
+
fallback: ['guide'],
|
|
105
|
+
})
|
|
106
|
+
persist('tags-empty', [])
|
|
107
|
+
|
|
108
|
+
expect(store.readStored()).toEqual([])
|
|
109
|
+
expect(store.validateSearch({})).toEqual({ tags: [] })
|
|
110
|
+
expect(store.linkSearch()).toEqual({ tags: [] })
|
|
111
|
+
// …while an empty URL array still falls through to storage, as it always has.
|
|
112
|
+
persist('tags-empty', ['api'])
|
|
113
|
+
expect(store.validateSearch({ tags: [] })).toEqual({ tags: ['api'] })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('linkSearch carries the stored selection at CALL time', () => {
|
|
117
|
+
const store = createMultiSearchParamStore({ key: 'tags-j', param: 'tags', values: VALUES })
|
|
118
|
+
expect(store.linkSearch()).toEqual({ tags: [] })
|
|
119
|
+
|
|
120
|
+
persist('tags-j', ['design'])
|
|
121
|
+
expect(store.linkSearch()).toEqual({ tags: ['design'] })
|
|
122
|
+
})
|
|
55
123
|
})
|
|
56
124
|
})
|
|
@@ -1,54 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* createMultiSearchParamStore
|
|
3
|
-
*
|
|
2
|
+
* @deprecated `createMultiSearchParamStore` is a thin wrapper over `createSearchStore`
|
|
3
|
+
* and is removed in 1.29.0. Replace it with `field.multi`:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Call `persist(next)` alongside `navigate` so the selection survives navigation.
|
|
10
|
-
* 3. `readStored` — plain function for non-React contexts (tests, guards).
|
|
11
|
-
* 4. `linkSearch` — the click-time `search:` thunk for a nav link, ready-made.
|
|
5
|
+
* const store = createSearchStore({
|
|
6
|
+
* key: 'article-tags',
|
|
7
|
+
* fields: { tags: field.multi(['api', 'design', 'guide'], []) },
|
|
8
|
+
* })
|
|
12
9
|
*
|
|
13
|
-
*
|
|
10
|
+
* Two differences to know when you migrate:
|
|
14
11
|
*
|
|
15
|
-
*
|
|
12
|
+
* - **An empty URL array is a VALUE in the new store**, not "absent". This wrapper keeps the old
|
|
13
|
+
* rule (`?tags=[]` falls through to localStorage, then to `fallback`) so an existing consumer's
|
|
14
|
+
* links behave identically; `field.multi` treats a cleared selection as a cleared selection.
|
|
15
|
+
* - `useStore` is localStorage-only. `store.field.tags.use()` reads the URL first (A8).
|
|
16
16
|
*
|
|
17
|
-
* TanStack Router's default
|
|
18
|
-
* JSON.parse)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* reachable from here — there is no string-building step to intercept. A consumer who wants
|
|
23
|
-
* prettier URLs (e.g. `?tags=a,b`) configures router-global `parseSearch`/`stringifySearch` (see
|
|
24
|
-
* TanStack Router's documented custom-search-param-serialization recipe); this store keeps working
|
|
25
|
-
* untouched either way, because it hands the router an array, not an encoding.
|
|
17
|
+
* Encoding is unchanged and array-native: TanStack Router's default
|
|
18
|
+
* `stringifySearchWith(JSON.stringify, JSON.parse)` round-trips `{ tags: ['a', 'b'] }` losslessly,
|
|
19
|
+
* so there is no CSV step to intercept — a consumer wanting `?tags=a,b` configures router-global
|
|
20
|
+
* `parseSearch`/`stringifySearch` and this store keeps working untouched, because it hands the
|
|
21
|
+
* router an array rather than an encoding.
|
|
26
22
|
*
|
|
27
|
-
*
|
|
23
|
+
* Decoding is unchanged too: array-only input, allowlist-filtered, deduped, then re-sorted into
|
|
24
|
+
* `values` DECLARATION order — so `['api', 'design']` and `['design', 'api']` normalize to the same
|
|
25
|
+
* array, and one logical selection has one URL.
|
|
28
26
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 2. Keep only entries present in `opts.values` (drops tampered values and values dropped from the
|
|
32
|
-
* vocabulary since the URL/localStorage entry was written).
|
|
33
|
-
* 3. Dedupe.
|
|
34
|
-
* 4. Re-sort into `opts.values` DECLARATION order.
|
|
35
|
-
*
|
|
36
|
-
* Canonical ordering means `['api', 'design']` and `['design', 'api']` normalize to the SAME
|
|
37
|
-
* array — one URL for one logical selection, so history entries and any loader cache keyed on the
|
|
38
|
-
* search stay stable regardless of the order a user toggled filters in.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* export const articleTags = createMultiSearchParamStore({
|
|
42
|
-
* key: 'article-tags', param: 'tags',
|
|
43
|
-
* values: ['api', 'design', 'guide'] as const,
|
|
44
|
-
* })
|
|
45
|
-
*
|
|
46
|
-
* export const Route = createFileRoute('/content')({
|
|
47
|
-
* validateSearch: articleTags.validateSearch,
|
|
48
|
-
* })
|
|
27
|
+
* The four returned members (`validateSearch`, `useStore`, `readStored`, `linkSearch`), their
|
|
28
|
+
* signatures and the single-value localStorage envelope are preserved byte-for-byte.
|
|
49
29
|
*/
|
|
50
|
-
|
|
51
|
-
import {
|
|
30
|
+
import { createPersistedState, field } from '../state'
|
|
31
|
+
import { buildSearchStore } from './search-store'
|
|
52
32
|
|
|
53
33
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
54
34
|
|
|
@@ -73,7 +53,7 @@ export type MultiSearchParamStore<T extends string, P extends string = string> =
|
|
|
73
53
|
validateSearch: (search: Record<string, unknown>) => { [K in P]: readonly T[] }
|
|
74
54
|
/**
|
|
75
55
|
* React hook: `const [current, persist] = store.useStore()`.
|
|
76
|
-
*
|
|
56
|
+
* localStorage only — it does NOT read or write the URL.
|
|
77
57
|
*/
|
|
78
58
|
useStore: () => readonly [readonly T[], (next: readonly T[]) => void]
|
|
79
59
|
/** Plain read — for use outside React (tests, guards, fallback reads). */
|
|
@@ -84,54 +64,43 @@ export type MultiSearchParamStore<T extends string, P extends string = string> =
|
|
|
84
64
|
* for why a module-scope literal silently defeats the store.
|
|
85
65
|
*
|
|
86
66
|
* No dev warning rides on this one, unlike the single-value store: an empty array in the URL is
|
|
87
|
-
* indistinguishable from an absent param here
|
|
88
|
-
*
|
|
67
|
+
* indistinguishable from an absent param here, so a link declaring `{ tags: [] }` restores
|
|
68
|
+
* correctly and there is no broken state to detect.
|
|
89
69
|
*/
|
|
90
70
|
linkSearch: () => { [K in P]: readonly T[] }
|
|
91
71
|
}
|
|
92
72
|
|
|
93
73
|
// ── Implementation ─────────────────────────────────────────────────────────
|
|
94
74
|
|
|
75
|
+
/** @deprecated Use `createSearchStore` + `field.multi`. Removed in 1.29.0. */
|
|
95
76
|
export function createMultiSearchParamStore<const T extends string, const P extends string>(
|
|
96
77
|
opts: MultiSearchParamStoreOptions<T, P>,
|
|
97
78
|
): MultiSearchParamStore<T, P> {
|
|
98
79
|
const version = opts.version ?? 1
|
|
99
80
|
const fallback = opts.fallback ?? []
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (const entry of raw) {
|
|
107
|
-
if (typeof entry === 'string' && allowed.has(entry)) present.add(entry as T)
|
|
108
|
-
}
|
|
109
|
-
return opts.values.filter((value) => present.has(value))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const readStored = (): readonly T[] | null => {
|
|
113
|
-
const raw = readPersistedValue(opts.key, version)
|
|
114
|
-
if (!Array.isArray(raw)) return null
|
|
115
|
-
return normalize(raw)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const linkSearch = (): { [K in P]: readonly T[] } => {
|
|
119
|
-
return { [opts.param]: readStored() ?? fallback } as { [K in P]: readonly T[] }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const validateSearch = (search: Record<string, unknown>): { [K in P]: readonly T[] } => {
|
|
123
|
-
const fromUrl = normalize(search[opts.param])
|
|
124
|
-
if (fromUrl.length > 0) {
|
|
125
|
-
return { [opts.param]: fromUrl } as { [K in P]: readonly T[] }
|
|
126
|
-
}
|
|
127
|
-
return { [opts.param]: readStored() ?? fallback } as { [K in P]: readonly T[] }
|
|
128
|
-
}
|
|
81
|
+
const store = buildSearchStore({
|
|
82
|
+
key: opts.key,
|
|
83
|
+
version,
|
|
84
|
+
fields: { [opts.param]: field.multi(opts.values, fallback) },
|
|
85
|
+
legacyValueField: opts.param,
|
|
86
|
+
})
|
|
129
87
|
|
|
130
|
-
const
|
|
88
|
+
const persisted = createPersistedState<readonly T[]>({
|
|
131
89
|
key: opts.key,
|
|
132
90
|
version,
|
|
133
91
|
initial: fallback,
|
|
134
92
|
})
|
|
93
|
+
// A reader, like the single-value wrapper's — see the note there.
|
|
94
|
+
const useStore = (): readonly [readonly T[], (next: readonly T[]) => void] => {
|
|
95
|
+
store.markReaderWired()
|
|
96
|
+
return persisted()
|
|
97
|
+
}
|
|
135
98
|
|
|
136
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
validateSearch: (search) => store.validateSearch(search) as { [K in P]: readonly T[] },
|
|
101
|
+
useStore,
|
|
102
|
+
readStored: () =>
|
|
103
|
+
(store.readStored() as Record<string, readonly T[] | undefined>)[opts.param] ?? null,
|
|
104
|
+
linkSearch: () => store.linkSearch() as { [K in P]: readonly T[] },
|
|
105
|
+
}
|
|
137
106
|
}
|
|
@@ -1,91 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* createSearchParamStore
|
|
3
|
-
*
|
|
2
|
+
* @deprecated `createSearchParamStore` is a thin wrapper over `createSearchStore` and is
|
|
3
|
+
* removed in 1.29.0. It can express exactly one closed string enum on one param; the store it
|
|
4
|
+
* delegates to expresses six field kinds, per-field lanes, and a range's three params.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1. `validateSearch` — pass directly to `createFileRoute({ validateSearch })`.
|
|
7
|
-
* Falls back to localStorage when the URL param is absent or invalid.
|
|
8
|
-
* 2. `useStore` — React hook: `const [value, persist] = store.useStore()`.
|
|
9
|
-
* Call `persist(next)` alongside `navigate` so the selection survives
|
|
10
|
-
* navigation away from and back to the route.
|
|
11
|
-
* 3. `readStored` — plain function for non-React contexts (tests, guards).
|
|
12
|
-
* 4. `linkSearch` — the click-time `search:` thunk a nav link needs, ready-made.
|
|
6
|
+
* Migration is mechanical:
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* anything about a nav link that declares the param itself — and a link written as
|
|
18
|
-
* `search: { range: '30d' }` at module scope pins the fallback on every click, which silently
|
|
19
|
-
* defeats the whole store. The reference consumer shipped exactly that: three features adopted the
|
|
20
|
-
* persistence, the reader had ZERO call sites, and "remember my window" had never once worked.
|
|
21
|
-
*
|
|
22
|
-
* `linkSearch` exists so that failure is unreachable from the API rather than only warned about in
|
|
23
|
-
* this comment — the mistake is made while typing `search:`, so the remedy has to be something
|
|
24
|
-
* autocomplete offers at that keystroke. The JSDoc was already here when it happened.
|
|
25
|
-
*
|
|
26
|
-
* Headless — no Mantine, no JSX. Same tier as `useBasaltNav` and
|
|
27
|
-
* `useRouterBreadcrumbs`.
|
|
28
|
-
*
|
|
29
|
-
* ## Full integration recipe (5 steps)
|
|
30
|
-
*
|
|
31
|
-
* **1. Define** — one call per search param, e.g. in a shared lib/ module:
|
|
32
|
-
*
|
|
33
|
-
* export const dashboardRange = createSearchParamStore({
|
|
34
|
-
* key: 'dashboard-range', param: 'range',
|
|
35
|
-
* values: ['1d', '7d', '30d'] as const, fallback: '30d',
|
|
8
|
+
* // before
|
|
9
|
+
* const range = createSearchParamStore({
|
|
10
|
+
* key: 'dashboard-range', param: 'range', values: ['1d', '7d', '30d'] as const, fallback: '30d',
|
|
36
11
|
* })
|
|
12
|
+
* const [, persist] = range.useStore() // localStorage only — never the URL
|
|
37
13
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* component: DashboardLayout, // renders <DateFilter/> + <Outlet/>
|
|
14
|
+
* // after
|
|
15
|
+
* const dash = createSearchStore({
|
|
16
|
+
* key: 'dashboard-range',
|
|
17
|
+
* fields: { range: field.enum(['1d', '7d', '30d'], '30d') },
|
|
43
18
|
* })
|
|
19
|
+
* const [value, set] = dash.field.range.use() // reads the URL, writes both lanes
|
|
44
20
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* const { range } = useSearch({ from: '/dashboard' })
|
|
49
|
-
* const [, persist] = dashboardRange.useStore()
|
|
50
|
-
* return <SegmentedControl value={range}
|
|
51
|
-
* onChange={(v) => { persist(v); navigate({ to: '.',
|
|
52
|
-
* search: (prev) => ({ ...prev, range: v }) }) }} />
|
|
53
|
-
* }
|
|
54
|
-
*
|
|
55
|
-
* **4. Page** — accepts the param as a **prop**, NOT from useSearch:
|
|
21
|
+
* The two are NOT equivalent in the one way that matters: `useStore` reads localStorage, so a deep
|
|
22
|
+
* link `?range=7d` rendered `30d` (A8). `field.range.use()` reads the URL first. See
|
|
23
|
+
* `docs/CONTROLS-SPEC.md` §4 and `MIGRATING.md`.
|
|
56
24
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* function DashboardPage({ range = '30d' }: { range?: DateRange }) {
|
|
61
|
-
* const data = useMemo(() => generateData(range), [range])
|
|
62
|
-
* return <Charts data={data} range={range} />
|
|
63
|
-
* }
|
|
64
|
-
*
|
|
65
|
-
* **5. Nav links** — carry the param across sub-pages, set per destination in
|
|
66
|
-
* the nav definition so only the dashboard sub-tree inherits it and every
|
|
67
|
-
* other link stays clean. Pass `linkSearch` BY REFERENCE — it is the click-time thunk:
|
|
68
|
-
*
|
|
69
|
-
* navGroup({ id: 'dash', label: 'Dashboard' }, [
|
|
70
|
-
* { id: 'dash-overview', label: 'Overview', icon: <IconHome />,
|
|
71
|
-
* link: linkOptions({ to: '/dashboard', search: dashboardRange.linkSearch }) },
|
|
72
|
-
* { id: 'dash-activity', label: 'Activity', icon: <IconPulse />,
|
|
73
|
-
* link: linkOptions({ to: '/dashboard/activity', search: dashboardRange.linkSearch }) },
|
|
74
|
-
* ])
|
|
75
|
-
*
|
|
76
|
-
* `linkSearch` IS that thunk (`() => ({ [param]: readStored() ?? fallback })`) — hand-rolling it
|
|
77
|
-
* over `readStored` still works and is what you need when the link also carries other params.
|
|
78
|
-
*
|
|
79
|
-
* NOT `search: true`. That flag means "keep the current search" and TanStack
|
|
80
|
-
* only offers it where the target's search is OPTIONAL — a route wired to a
|
|
81
|
-
* store's `validateSearch` always returns the param, so the router requires a
|
|
82
|
-
* value and `search: true` is a type error. The thunk is also the better
|
|
83
|
-
* behaviour: `<Link>` re-evaluates it at click time, so arriving from OUTSIDE
|
|
84
|
-
* the sub-tree restores the last selection instead of landing on the factory
|
|
85
|
-
* default, and a definition evaluated once at module scope never goes stale.
|
|
25
|
+
* What the wrapper preserves byte-for-byte, because `basalt-ui-obsidian` consumes it two hops
|
|
26
|
+
* downstream: the four returned members (`validateSearch`, `useStore`, `readStored`, `linkSearch`),
|
|
27
|
+
* their signatures, and the single-value localStorage envelope an existing browser already holds.
|
|
86
28
|
*/
|
|
87
|
-
|
|
88
|
-
import {
|
|
29
|
+
import { createPersistedState, field } from '../state'
|
|
30
|
+
import { buildSearchStore } from './search-store'
|
|
89
31
|
|
|
90
32
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
91
33
|
|
|
@@ -110,7 +52,8 @@ export type SearchParamStore<T extends string, P extends string = string> = {
|
|
|
110
52
|
validateSearch: (search: Record<string, unknown>) => { [K in P]: T }
|
|
111
53
|
/**
|
|
112
54
|
* React hook: `const [current, persist] = store.useStore()`.
|
|
113
|
-
*
|
|
55
|
+
* localStorage only — it does NOT read or write the URL. `createSearchStore`'s
|
|
56
|
+
* `field.<name>.use()` is the replacement that does both.
|
|
114
57
|
*/
|
|
115
58
|
useStore: () => readonly [T, (next: T) => void]
|
|
116
59
|
/** Plain read — for use outside React (tests, guards, fallback reads). */
|
|
@@ -125,93 +68,33 @@ export type SearchParamStore<T extends string, P extends string = string> = {
|
|
|
125
68
|
linkSearch: () => { [K in P]: T }
|
|
126
69
|
}
|
|
127
70
|
|
|
128
|
-
/**
|
|
129
|
-
* Dev-only, once per store: the URL is pinning `fallback` while something else is persisted, and
|
|
130
|
-
* neither reader has ever been called. That combination has exactly one realistic cause — a nav
|
|
131
|
-
* link declaring the param as a literal — and it is invisible otherwise, because every individual
|
|
132
|
-
* piece looks correct and the feature merely never remembers anything.
|
|
133
|
-
*
|
|
134
|
-
* Deliberately NOT a timer over "was this store ever read": that fires in every test that builds a
|
|
135
|
-
* store, and it cannot say what to do about it. This fires only in the broken state, and names the
|
|
136
|
-
* fix.
|
|
137
|
-
*/
|
|
138
|
-
function warnLinkPinsFallback(input: {
|
|
139
|
-
key: string
|
|
140
|
-
param: string
|
|
141
|
-
urlValue: string
|
|
142
|
-
storedValue: string
|
|
143
|
-
}): void {
|
|
144
|
-
const { key, param, urlValue, storedValue } = input
|
|
145
|
-
// oxlint-disable-next-line no-console -- a dev-time wiring warning has no other channel
|
|
146
|
-
console.warn(
|
|
147
|
-
`[basalt-ui] createSearchParamStore('${key}'): the URL pinned \`${param}=${urlValue}\` ` +
|
|
148
|
-
'(the fallback) ' +
|
|
149
|
-
`while '${storedValue}' was persisted, and nothing has read this store back. A nav link is ` +
|
|
150
|
-
`almost certainly declaring \`search: { ${param}: '${urlValue}' }\` at module scope, which ` +
|
|
151
|
-
"overrides the stored value on every click — pass the store's own reader instead: " +
|
|
152
|
-
'`search: <store>.linkSearch`. (dev only)',
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
71
|
// ── Implementation ─────────────────────────────────────────────────────────
|
|
157
72
|
|
|
73
|
+
/** @deprecated Use `createSearchStore` + `field.enum`. Removed in 1.29.0. */
|
|
158
74
|
export function createSearchParamStore<const T extends string, const P extends string>(
|
|
159
75
|
opts: SearchParamStoreOptions<T, P>,
|
|
160
76
|
): SearchParamStore<T, P> {
|
|
161
77
|
const version = opts.version ?? 1
|
|
162
|
-
|
|
163
|
-
// `validateSearch` reads the store internally on every navigation, which says nothing about
|
|
164
|
-
// whether the CONSUMER wired a reader — only the two public readers below set this.
|
|
165
|
-
let readerWired = false
|
|
166
|
-
let warned = false
|
|
167
|
-
|
|
168
|
-
const read = (): T | null => {
|
|
169
|
-
const raw = readPersistedValue(opts.key, version)
|
|
170
|
-
if (typeof raw !== 'string') return null
|
|
171
|
-
if ((opts.values as readonly string[]).includes(raw)) return raw as T
|
|
172
|
-
return null
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const readStored = (): T | null => {
|
|
176
|
-
readerWired = true
|
|
177
|
-
return read()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const linkSearch = (): { [K in P]: T } => {
|
|
181
|
-
readerWired = true
|
|
182
|
-
return { [opts.param]: (read() ?? opts.fallback) as T } as { [K in P]: T }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const validateSearch = (search: Record<string, unknown>): { [K in P]: T } => {
|
|
186
|
-
const raw = search[opts.param]
|
|
187
|
-
if (typeof raw === 'string' && (opts.values as readonly string[]).includes(raw)) {
|
|
188
|
-
if (
|
|
189
|
-
!warned &&
|
|
190
|
-
!readerWired &&
|
|
191
|
-
raw === opts.fallback &&
|
|
192
|
-
process.env['NODE_ENV'] !== 'production'
|
|
193
|
-
) {
|
|
194
|
-
const stored = read()
|
|
195
|
-
if (stored !== null && stored !== raw) {
|
|
196
|
-
warned = true
|
|
197
|
-
warnLinkPinsFallback({
|
|
198
|
-
key: opts.key,
|
|
199
|
-
param: opts.param,
|
|
200
|
-
urlValue: raw,
|
|
201
|
-
storedValue: stored,
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return { [opts.param]: raw as T } as { [K in P]: T }
|
|
206
|
-
}
|
|
207
|
-
return { [opts.param]: (read() ?? opts.fallback) as T } as { [K in P]: T }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const useStore = createPersistedState<T>({
|
|
78
|
+
const store = buildSearchStore({
|
|
211
79
|
key: opts.key,
|
|
212
80
|
version,
|
|
213
|
-
|
|
81
|
+
fields: { [opts.param]: field.enum(opts.values, opts.fallback) },
|
|
82
|
+
legacyValueField: opts.param,
|
|
214
83
|
})
|
|
215
84
|
|
|
216
|
-
|
|
85
|
+
const persisted = createPersistedState<T>({ key: opts.key, version, initial: opts.fallback })
|
|
86
|
+
// `useStore` is a reader too. Without this, a consumer whose ONLY reader is `useStore()` — the
|
|
87
|
+
// documented wiring for this factory — would start seeing the pinned-link warning it never saw
|
|
88
|
+
// before, purely because the wrapper now delegates.
|
|
89
|
+
const useStore = (): readonly [T, (next: T) => void] => {
|
|
90
|
+
store.markReaderWired()
|
|
91
|
+
return persisted()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
validateSearch: (search) => store.validateSearch(search) as { [K in P]: T },
|
|
96
|
+
useStore,
|
|
97
|
+
readStored: () => (store.readStored() as Record<string, T | undefined>)[opts.param] ?? null,
|
|
98
|
+
linkSearch: () => store.linkSearch() as { [K in P]: T },
|
|
99
|
+
}
|
|
217
100
|
}
|