basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -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 +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -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 +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- 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
package/src/guard/types.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
|
|
3
3
|
*
|
|
4
|
-
* GuardKind is the closed set of
|
|
4
|
+
* GuardKind is the closed set of 27 violation kinds the theme guard can emit.
|
|
5
5
|
* Finding is the structured result per violation, replacing the old `Violation` shape.
|
|
6
6
|
* GuardConfig is the per-run configuration that drives checkSource.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/** The
|
|
9
|
+
/** The 27 theme-guard violation kinds. */
|
|
10
10
|
export type GuardKind =
|
|
11
11
|
| 'raw-hex'
|
|
12
12
|
| 'raw-color-fn'
|
|
@@ -33,6 +33,8 @@ export type GuardKind =
|
|
|
33
33
|
| 'css-raw-surface'
|
|
34
34
|
| 'inline-font-size'
|
|
35
35
|
| 'hidden-inline-style'
|
|
36
|
+
| 'in-body-page-title'
|
|
37
|
+
| 'raw-selection-control'
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* How hard a finding lands. `error` fails the build; `warn` reports and passes.
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,14 @@ export {
|
|
|
14
14
|
} from './provider'
|
|
15
15
|
export { createBasaltTheme, baseTheme, cssVariablesResolver } from './theme'
|
|
16
16
|
export type { BasaltFontsConfig, CreateBasaltThemeOptions } from './theme'
|
|
17
|
+
export { CTL_THEME, CtlSlot } from './theme'
|
|
18
|
+
export type { CtlSlotProps } from './theme'
|
|
19
|
+
/**
|
|
20
|
+
* The action vocabulary every home's `actions` slot takes (`docs/CONTROLS-SPEC.md` §2.1). The
|
|
21
|
+
* COMPONENTS that project it (`ActionGroup`, `OverflowMenu`, `SyncButton`, and the filter family)
|
|
22
|
+
* live on `basalt-ui/controls` — only the types a `PageBar`/`BasaltShell` prop mentions are here.
|
|
23
|
+
*/
|
|
24
|
+
export type { BarAction, ActionGroupProps, GlobalAction } from './controls/actions'
|
|
17
25
|
export { ThemeToggle, type ThemeToggleProps } from './theme-toggle'
|
|
18
26
|
export { MOTION_DURATION, MOTION_SPRING, MOTION_EASE_STANDARD } from './motion'
|
|
19
27
|
export {
|
|
@@ -22,6 +30,7 @@ export {
|
|
|
22
30
|
AppSidebar,
|
|
23
31
|
SidebarSearch,
|
|
24
32
|
type SidebarSearchConfig,
|
|
33
|
+
type SidebarSearchActions,
|
|
25
34
|
SidebarAccount,
|
|
26
35
|
MobileNav,
|
|
27
36
|
type MobileNavProps,
|
|
@@ -31,11 +40,16 @@ export {
|
|
|
31
40
|
MOBILE_MENU_MAX_DEFAULT,
|
|
32
41
|
MOBILE_MORE_KEY,
|
|
33
42
|
AppBreadcrumbs,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
PageActionsOutlet,
|
|
43
|
+
PageBar,
|
|
44
|
+
type PageBarProps,
|
|
37
45
|
type SidebarSection,
|
|
38
46
|
type SidebarItem,
|
|
47
|
+
type SidebarBlock,
|
|
48
|
+
type SidebarBlockItem,
|
|
49
|
+
type SidebarBlockTone,
|
|
50
|
+
type SidebarListBlock,
|
|
51
|
+
type SidebarProgressBlock,
|
|
52
|
+
type SidebarCustomBlock,
|
|
39
53
|
type NavAnchor,
|
|
40
54
|
type NavAnchorProps,
|
|
41
55
|
type NavMobilePlacement,
|
|
@@ -72,14 +86,41 @@ export {
|
|
|
72
86
|
type StandardSchemaV1,
|
|
73
87
|
} from './register'
|
|
74
88
|
|
|
75
|
-
// ── Persisted state +
|
|
89
|
+
// ── Persisted state + the headless store kernel (state.ts) ───────────────────────────────────────
|
|
90
|
+
// The `field.*` VALUE is deliberately not re-exported here — `basalt-ui/forms` already owns the
|
|
91
|
+
// name `field` on its own barrel. Import it from `basalt-ui/router-tanstack` (beside
|
|
92
|
+
// `createSearchStore`) or from `basalt-ui/state` (beside `createLocalStore`).
|
|
76
93
|
export {
|
|
77
94
|
createPersistedState,
|
|
78
95
|
type PersistedStateOptions,
|
|
79
|
-
useOnlineStatus,
|
|
80
96
|
readPersistedValue,
|
|
97
|
+
createLocalStore,
|
|
98
|
+
type LocalStore,
|
|
99
|
+
type AnyField,
|
|
100
|
+
type BooleanField,
|
|
101
|
+
type EnumField,
|
|
102
|
+
type FieldHandle,
|
|
103
|
+
type FieldLane,
|
|
104
|
+
type FieldOption,
|
|
105
|
+
type FieldValue,
|
|
106
|
+
type MultiField,
|
|
107
|
+
type NumberField,
|
|
108
|
+
type RangeField,
|
|
109
|
+
type RangeParams,
|
|
110
|
+
type RangePresets,
|
|
111
|
+
type RangeValue,
|
|
112
|
+
type ResolvedLane,
|
|
113
|
+
type SearchValues,
|
|
114
|
+
type StoredValues,
|
|
115
|
+
type StringField,
|
|
81
116
|
} from './state'
|
|
82
117
|
|
|
118
|
+
// ── WidgetHeader (unified section/widget/card heading primitive, docs/CONTROLS-SPEC.md §2.2) ─────
|
|
119
|
+
export { WidgetHeader, type WidgetHeaderProps } from './widget-header'
|
|
120
|
+
|
|
121
|
+
// ── Section (tier-2 heading composer over WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ───────────────
|
|
122
|
+
export { Section, type SectionProps } from './section'
|
|
123
|
+
|
|
83
124
|
// ── Dashboard composites (KPI atoms + settings building blocks) ──────────────────────────────────
|
|
84
125
|
export {
|
|
85
126
|
DeltaBadge,
|
package/src/nav/types.ts
CHANGED
|
@@ -40,6 +40,8 @@ export type SidebarItem = {
|
|
|
40
40
|
/** Plain href — the no-router fallback, and the breadcrumb parent target. */
|
|
41
41
|
href?: string
|
|
42
42
|
active?: boolean
|
|
43
|
+
/** A parent on the path to the active row. NOT active itself — never carries `aria-current`. */
|
|
44
|
+
ancestor?: boolean
|
|
43
45
|
disabled?: boolean
|
|
44
46
|
onClick?: (e: MouseEvent) => void
|
|
45
47
|
/** Rich desktop badge (e.g. `NavCountBadge`). Desktop only. */
|
|
@@ -70,6 +72,88 @@ export type SidebarSection = {
|
|
|
70
72
|
mobile?: false | NavSectionMobile
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Status tone on a sidebar-block item — the same three-member vocabulary as `StatCardTone`, whose
|
|
77
|
+
* members are `VX.status` keys by construction. Declared here rather than imported from
|
|
78
|
+
* `../dashboard/stat-card`: this module is the one `./router-tanstack` reads, and even a
|
|
79
|
+
* type-only edge into the Mantine layer would make `nav/types.d.ts` unresolvable without
|
|
80
|
+
* `@mantine/core` installed.
|
|
81
|
+
*/
|
|
82
|
+
export type SidebarBlockTone = 'good' | 'warn' | 'bad'
|
|
83
|
+
|
|
84
|
+
/** One row inside a `kind: 'list'` block. Same router seam as `SidebarItem` — `Anchor`, then href. */
|
|
85
|
+
export type SidebarBlockItem = {
|
|
86
|
+
key: string
|
|
87
|
+
label: string
|
|
88
|
+
/** Trailing muted text (a timestamp, a count, an owner). */
|
|
89
|
+
meta?: string
|
|
90
|
+
icon?: ReactNode
|
|
91
|
+
/** Renders a status dot in place of a missing `icon`; reads `--vx-status-*`, never a hex. */
|
|
92
|
+
tone?: SidebarBlockTone
|
|
93
|
+
Anchor?: NavAnchor
|
|
94
|
+
href?: string
|
|
95
|
+
onClick?: () => void
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A list of non-destination rows under a micro-label — "Awaiting action", "Recents", a project
|
|
100
|
+
* list. `count` is the header badge (and what earns a rail dot); `max` shows the first N with a
|
|
101
|
+
* "Show more" toggle past it.
|
|
102
|
+
*/
|
|
103
|
+
export type SidebarListBlock = {
|
|
104
|
+
kind: 'list'
|
|
105
|
+
key: string
|
|
106
|
+
label: string
|
|
107
|
+
icon?: ReactNode
|
|
108
|
+
count?: number
|
|
109
|
+
/** Rows shown before the "Show more" toggle. Omit to show every row. */
|
|
110
|
+
max?: number
|
|
111
|
+
items: SidebarBlockItem[]
|
|
112
|
+
/** @default 'nav' */
|
|
113
|
+
placement?: 'nav' | 'bottom'
|
|
114
|
+
collapsible?: boolean
|
|
115
|
+
/** Collapsed-rail projection. @default 'dot' when `count` is set, `'hidden'` otherwise */
|
|
116
|
+
rail?: 'dot' | 'hidden'
|
|
117
|
+
/** @default 'more' */
|
|
118
|
+
mobile?: 'more' | 'hidden'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A "Getting started 1 of 5" progress row. Pinned above the settings footer; never in the nav. */
|
|
122
|
+
export type SidebarProgressBlock = {
|
|
123
|
+
kind: 'progress'
|
|
124
|
+
key: string
|
|
125
|
+
label: string
|
|
126
|
+
value: number
|
|
127
|
+
total: number
|
|
128
|
+
onClick?: () => void
|
|
129
|
+
/** @default 'bottom' — the only placement a progress row has. */
|
|
130
|
+
placement?: 'bottom'
|
|
131
|
+
/** Collapsed-rail projection: a ring on the settings row. @default 'ring' */
|
|
132
|
+
rail?: 'ring' | 'hidden'
|
|
133
|
+
/** @default 'hidden' */
|
|
134
|
+
mobile?: 'more' | 'hidden'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* DESKTOP ONLY — arbitrary consumer content (a tree, a filter panel) that no set of rows can
|
|
139
|
+
* express. Replaced `BasaltShellProps.sidebarNavExtra`; hidden on the collapsed rail and absent
|
|
140
|
+
* from mobile for the same reason that prop was.
|
|
141
|
+
*/
|
|
142
|
+
export type SidebarCustomBlock = {
|
|
143
|
+
kind: 'custom'
|
|
144
|
+
key: string
|
|
145
|
+
node: ReactNode
|
|
146
|
+
/** @default 'nav' */
|
|
147
|
+
placement?: 'nav' | 'bottom'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A sidebar block — DECLARED DATA, never a `ReactNode` slot (law C13, `docs/CONTROLS-SPEC.md`
|
|
152
|
+
* §2.3), which is what lets basalt own the collapsed-rail badge and the mobile More-sheet
|
|
153
|
+
* projection instead of each consumer hand-rolling both.
|
|
154
|
+
*/
|
|
155
|
+
export type SidebarBlock = SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock
|
|
156
|
+
|
|
73
157
|
export type MobileNavSurface = 'link' | 'menu' | 'sheet'
|
|
74
158
|
|
|
75
159
|
/** One row inside a `menu`/`sheet` slot, grouped under its source section. */
|
|
@@ -108,8 +192,6 @@ export type MobileNavConfig = {
|
|
|
108
192
|
menuMax?: number
|
|
109
193
|
/** @default 'More' */
|
|
110
194
|
moreLabel?: string
|
|
111
|
-
/** Extra content pinned to the bottom of the More surface. */
|
|
112
|
-
moreExtra?: ReactNode
|
|
113
195
|
/** Scroll container for active-slot re-tap. @default document.scrollingElement */
|
|
114
196
|
getScrollElement?: () => HTMLElement | null
|
|
115
197
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NotificationBell — ActionIcon + Indicator showing the unread Inbox count, opens a
|
|
3
|
-
* NotificationCenter in a Popover. Designed for the shell `globalActions` slot (
|
|
3
|
+
* NotificationCenter in a Popover. Designed for the shell `globalActions` slot (`GlobalAction[]`).
|
|
4
4
|
*
|
|
5
5
|
* The badge counts unread Inbox items (errors / warnings / actionable notifications you haven't
|
|
6
6
|
* seen) — not every toast that fired. Opening the bell does NOT mark anything read; items are
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* // In the shell's globalActions prop:
|
|
13
13
|
* <BasaltShell
|
|
14
|
-
* globalActions={<NotificationBell
|
|
14
|
+
* globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]}
|
|
15
15
|
* {...rest}
|
|
16
16
|
* />
|
|
17
17
|
*/
|
|
@@ -33,7 +33,10 @@ export type NotificationBellProps = {
|
|
|
33
33
|
* Bell icon button with an unread-Inbox indicator. Opens a NotificationCenter popover.
|
|
34
34
|
*
|
|
35
35
|
* @example
|
|
36
|
-
* <BasaltShell
|
|
36
|
+
* <BasaltShell
|
|
37
|
+
* globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]}
|
|
38
|
+
* {...rest}
|
|
39
|
+
* />
|
|
37
40
|
*/
|
|
38
41
|
export function NotificationBell({ label = 'Notifications' }: NotificationBellProps) {
|
|
39
42
|
const [opened, { open, toggle, close }] = useDisclosure(false)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* notifyPromise(save(), { loading: 'Saving…', success: 'Saved', error: 'Failed' })
|
|
21
21
|
*
|
|
22
22
|
* // Shell bell (globalActions slot):
|
|
23
|
-
* <BasaltShell globalActions={<NotificationBell
|
|
23
|
+
* <BasaltShell globalActions={[{ key: 'notifications', node: <NotificationBell />, mobile: 'bar' }]} ... />
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
// ── notify core ───────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `field.*` — the field vocabulary `createSearchStore` takes, re-exported at the surface a consumer
|
|
3
|
+
* imports it from (`basalt-ui/router-tanstack`, beside the store factory).
|
|
4
|
+
*
|
|
5
|
+
* The builders and their codecs LIVE in `../state` on purpose: a field is declarative data with no
|
|
6
|
+
* router in it, and `createLocalStore` — the router-free lane, published on `basalt-ui/state` —
|
|
7
|
+
* has to build the same fields without pulling in `@tanstack/react-router`. One definition, two
|
|
8
|
+
* surfaces, no second description of the same param.
|
|
9
|
+
*
|
|
10
|
+
* Full field reference: the JSDoc on `field` itself, and `docs/CONTROLS-SPEC.md` §4.
|
|
11
|
+
*/
|
|
12
|
+
export { field } from '../state'
|
|
13
|
+
export type {
|
|
14
|
+
AnyField,
|
|
15
|
+
BooleanField,
|
|
16
|
+
EnumField,
|
|
17
|
+
FieldHandle,
|
|
18
|
+
FieldLane,
|
|
19
|
+
FieldOption,
|
|
20
|
+
FieldValue,
|
|
21
|
+
MultiField,
|
|
22
|
+
NumberField,
|
|
23
|
+
RangeField,
|
|
24
|
+
RangeParams,
|
|
25
|
+
RangePresets,
|
|
26
|
+
RangeValue,
|
|
27
|
+
ResolvedLane,
|
|
28
|
+
SearchValues,
|
|
29
|
+
StoredValues,
|
|
30
|
+
StringField,
|
|
31
|
+
} from '../state'
|
|
@@ -3,10 +3,32 @@
|
|
|
3
3
|
* Optional peer: @tanstack/react-router.
|
|
4
4
|
* Carries the typed navigation definition (`defineNav`/`navGroup`/`navTarget`/`flattenNav`) and
|
|
5
5
|
* `useNav`, which resolves it into `BasaltShell`'s `sections` + `mobileNav` props; plus the
|
|
6
|
-
* StaticDataRouteOption augmentation (title/icon/navSection) for router-derived breadcrumbs
|
|
7
|
-
* the
|
|
6
|
+
* StaticDataRouteOption augmentation (title/icon/navSection) for router-derived breadcrumbs, plus
|
|
7
|
+
* `createSearchStore` + the `field.*` vocabulary (and the two deprecated enum-only wrappers it
|
|
8
|
+
* replaced) — no Mantine, no JSX.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
export { createSearchStore, type CreateSearchStoreOptions, type SearchStore } from './search-store'
|
|
12
|
+
export { field } from './field'
|
|
13
|
+
export type {
|
|
14
|
+
AnyField,
|
|
15
|
+
BooleanField,
|
|
16
|
+
EnumField,
|
|
17
|
+
FieldHandle,
|
|
18
|
+
FieldLane,
|
|
19
|
+
FieldOption,
|
|
20
|
+
FieldValue,
|
|
21
|
+
MultiField,
|
|
22
|
+
NumberField,
|
|
23
|
+
RangeField,
|
|
24
|
+
RangeParams,
|
|
25
|
+
RangePresets,
|
|
26
|
+
RangeValue,
|
|
27
|
+
ResolvedLane,
|
|
28
|
+
SearchValues,
|
|
29
|
+
StoredValues,
|
|
30
|
+
StringField,
|
|
31
|
+
} from './field'
|
|
10
32
|
export {
|
|
11
33
|
createSearchParamStore,
|
|
12
34
|
type SearchParamStore,
|
|
@@ -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
|
}
|