basalt-ui 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile `Filters (n)` sheet and the two row primitives every control's sheet form composes
|
|
3
|
+
* (`docs/CONTROLS-SPEC.md` §2.1/§3). A bottom `Drawer`: every child of the `FilterSet` renders its
|
|
4
|
+
* full-width sheet form, edits APPLY IMMEDIATELY (there is no Apply button — the URL is the truth
|
|
5
|
+
* and a filter write is already `history: 'replace'`), and one `Reset all` footer clears every
|
|
6
|
+
* registered filter.
|
|
7
|
+
*
|
|
8
|
+
* 44px rows are the one sheet dimension C15 pins, so they come from
|
|
9
|
+
* `--vx-space-sheet-row-height` — the density-tracking token, not a literal.
|
|
10
|
+
*
|
|
11
|
+
* INTERNAL, like `FilterPill`: `FilterSet` owns when the sheet opens, so a consumer mounting it by
|
|
12
|
+
* hand would own a second, unregistered filter home (C1).
|
|
13
|
+
*/
|
|
14
|
+
import { Button, Drawer } from '@mantine/core'
|
|
15
|
+
import { useId } from 'react'
|
|
16
|
+
import type { ReactNode } from 'react'
|
|
17
|
+
import classes from './controls.module.css'
|
|
18
|
+
|
|
19
|
+
export type FilterSheetProps = {
|
|
20
|
+
readonly opened: boolean
|
|
21
|
+
readonly onClose: () => void
|
|
22
|
+
readonly onResetAll: () => void
|
|
23
|
+
readonly title?: string
|
|
24
|
+
readonly children: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `Reset all` rides the HEADER, beside the title and the close ×, not a footer.
|
|
29
|
+
*
|
|
30
|
+
* A bottom drawer's footer is the one region a phone cannot promise is on screen: the sheet grows
|
|
31
|
+
* with its content, so with six filters open the reset button sat below the fold and the only way to
|
|
32
|
+
* reach it was to scroll past every row it would have undone. The header is fixed by construction.
|
|
33
|
+
* It also removes the sheet's last horizontal rule — a footer needs separating from the rows above
|
|
34
|
+
* it, a header does not (`docs/CONTROLS-SPEC.md` §2.1: hairlines between OPTION rows only).
|
|
35
|
+
*/
|
|
36
|
+
export function FilterSheet({
|
|
37
|
+
opened,
|
|
38
|
+
onClose,
|
|
39
|
+
onResetAll,
|
|
40
|
+
title = 'Filters',
|
|
41
|
+
children,
|
|
42
|
+
}: FilterSheetProps): ReactNode {
|
|
43
|
+
return (
|
|
44
|
+
<Drawer
|
|
45
|
+
opened={opened}
|
|
46
|
+
onClose={onClose}
|
|
47
|
+
position="bottom"
|
|
48
|
+
radius="sm"
|
|
49
|
+
classNames={{ header: classes.sheetHeader, title: classes.sheetTitle }}
|
|
50
|
+
title={
|
|
51
|
+
<>
|
|
52
|
+
<span className={classes.sheetTitleText}>{title}</span>
|
|
53
|
+
<Button variant="subtle" size="ctl" onClick={onResetAll}>
|
|
54
|
+
Reset all
|
|
55
|
+
</Button>
|
|
56
|
+
</>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<div className={classes.sheetBody}>{children}</div>
|
|
60
|
+
</Drawer>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The selected-row mark — 16px, drawn at the row's trailing edge (`docs/CONTROLS-SPEC.md` §2.1). */
|
|
65
|
+
function CheckGlyph(): ReactNode {
|
|
66
|
+
return (
|
|
67
|
+
<svg
|
|
68
|
+
width={16}
|
|
69
|
+
height={16}
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth={2.25}
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
aria-hidden
|
|
77
|
+
>
|
|
78
|
+
<path d="M5 12.5l5 5l9 -11" />
|
|
79
|
+
</svg>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One row of a {@link SheetOptionList}. */
|
|
84
|
+
export type SheetOption = {
|
|
85
|
+
readonly value: string
|
|
86
|
+
readonly label: string
|
|
87
|
+
readonly disabled?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type SheetOptionListProps = {
|
|
91
|
+
/** `single` renders radio semantics, `multi` checkbox semantics. */
|
|
92
|
+
readonly mode: 'single' | 'multi'
|
|
93
|
+
/** The selected value(s) — a one-element array in `single` mode. */
|
|
94
|
+
readonly selected: readonly string[]
|
|
95
|
+
readonly options: readonly SheetOption[]
|
|
96
|
+
readonly onToggle: (value: string) => void
|
|
97
|
+
/** The `SheetField` heading this list is named by. */
|
|
98
|
+
readonly labelId: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The sheet's ONE list form (`docs/CONTROLS-SPEC.md` §2.1): every option is a 44px full-width row,
|
|
103
|
+
* label left, a check at the trailing edge when selected, a 1px hairline between rows and nowhere
|
|
104
|
+
* else.
|
|
105
|
+
*
|
|
106
|
+
* **It replaced a `SegmentedControl` and a Mantine `Radio` list, for two different reasons.** The
|
|
107
|
+
* range presets rendered as a `orientation="vertical"` SegmentedControl, which in a full-width sheet
|
|
108
|
+
* is a stack of left-aligned labels inside a tinted track — it read as a broken control, not as a
|
|
109
|
+
* choice, and it carried the track's own inset so it aligned with nothing else in the sheet. The
|
|
110
|
+
* enum/multi filters rendered Mantine `Radio`/`Checkbox` rows, which put a 20px dot or box in the
|
|
111
|
+
* leading position of every row; at six filters the sheet was a column of controls rather than a
|
|
112
|
+
* column of options, and the leading marks pushed every label off the sheet's own text column.
|
|
113
|
+
*
|
|
114
|
+
* The semantics are a real native `<input>` inside the row's `<label>`, visually hidden — not
|
|
115
|
+
* `role="radio"` on a div. A hidden native input keeps the arrow-key group behaviour, the
|
|
116
|
+
* form-association and the announced state for free, and `<label>` wrapping it makes the whole 44px
|
|
117
|
+
* box the hit target (C15) without a single `onClick` on a non-interactive element.
|
|
118
|
+
*/
|
|
119
|
+
export function SheetOptionList({
|
|
120
|
+
mode,
|
|
121
|
+
selected,
|
|
122
|
+
options,
|
|
123
|
+
onToggle,
|
|
124
|
+
labelId,
|
|
125
|
+
}: SheetOptionListProps): ReactNode {
|
|
126
|
+
// ONE name per rendered list, so a sheet holding two range filters keeps two radio groups.
|
|
127
|
+
const name = useId()
|
|
128
|
+
return (
|
|
129
|
+
// A real `<fieldset>`, not `role="group"` on a div — the native element carries the grouping
|
|
130
|
+
// semantics and gets its own box reset in the module (`.sheetList`). `aria-labelledby` points at
|
|
131
|
+
// the `SheetField` heading rather than a `<legend>`: the heading is already visible above the
|
|
132
|
+
// list, and a legend would render it a second time.
|
|
133
|
+
<fieldset className={classes.sheetList} aria-labelledby={labelId}>
|
|
134
|
+
{options.map((option) => {
|
|
135
|
+
const isSelected = selected.includes(option.value)
|
|
136
|
+
return (
|
|
137
|
+
<label
|
|
138
|
+
key={option.value}
|
|
139
|
+
className={classes.sheetOption}
|
|
140
|
+
{...(isSelected && { 'data-selected': true })}
|
|
141
|
+
{...(option.disabled === true && { 'data-disabled': true })}
|
|
142
|
+
>
|
|
143
|
+
{/* theme-allow raw-form-control — the input is NEVER PAINTED. It is visually hidden
|
|
144
|
+
(`.sheetOptionInput`) and exists only to carry the row's semantics: the checked state,
|
|
145
|
+
the radio-group arrow keys, the announced name. The surface the reader sees and taps
|
|
146
|
+
is the `<label>` around it, which the theme's tokens style directly. A Mantine
|
|
147
|
+
`Radio` here would paint the 20px indicator this row form exists to remove — see
|
|
148
|
+
`SheetOptionList`'s doc. */}
|
|
149
|
+
<input
|
|
150
|
+
className={classes.sheetOptionInput}
|
|
151
|
+
type={mode === 'single' ? 'radio' : 'checkbox'}
|
|
152
|
+
name={name}
|
|
153
|
+
value={option.value}
|
|
154
|
+
checked={isSelected}
|
|
155
|
+
disabled={option.disabled === true}
|
|
156
|
+
aria-label={option.label}
|
|
157
|
+
onChange={() => {
|
|
158
|
+
onToggle(option.value)
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
<span className={classes.sheetOptionLabel}>{option.label}</span>
|
|
162
|
+
{isSelected && (
|
|
163
|
+
<span className={classes.sheetOptionCheck} aria-hidden>
|
|
164
|
+
<CheckGlyph />
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</label>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</fieldset>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type SheetDisclosureProps = {
|
|
175
|
+
readonly label: string
|
|
176
|
+
readonly expanded: boolean
|
|
177
|
+
readonly onToggle: () => void
|
|
178
|
+
readonly children: ReactNode
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A 44px row that expands its own body inline — the sheet's answer to a control too large to sit in
|
|
183
|
+
* the list unconditionally (`RangeFilter`'s custom date picker, which is a whole calendar). Same row
|
|
184
|
+
* geometry as a {@link SheetOptionList} option, so the disclosure reads as the list's last entry
|
|
185
|
+
* rather than as a different kind of thing.
|
|
186
|
+
*/
|
|
187
|
+
export function SheetDisclosure({
|
|
188
|
+
label,
|
|
189
|
+
expanded,
|
|
190
|
+
onToggle,
|
|
191
|
+
children,
|
|
192
|
+
}: SheetDisclosureProps): ReactNode {
|
|
193
|
+
return (
|
|
194
|
+
<div className={classes.sheetList}>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
className={classes.sheetOption}
|
|
198
|
+
aria-expanded={expanded}
|
|
199
|
+
{...(expanded && { 'data-selected': true })}
|
|
200
|
+
onClick={onToggle}
|
|
201
|
+
>
|
|
202
|
+
<span className={classes.sheetOptionLabel}>{label}</span>
|
|
203
|
+
{expanded && (
|
|
204
|
+
<span className={classes.sheetOptionCheck} aria-hidden>
|
|
205
|
+
<CheckGlyph />
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</button>
|
|
209
|
+
{expanded && <div className={classes.sheetDisclosureBody}>{children}</div>}
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type SheetFieldProps = {
|
|
215
|
+
readonly label: string
|
|
216
|
+
/** Stamped on the heading so a control can point `aria-labelledby` at it — see `useControlName`. */
|
|
217
|
+
readonly labelId?: string
|
|
218
|
+
readonly children: ReactNode
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A labelled block inside the sheet — the sheet form's outer shell for every control. */
|
|
222
|
+
export function SheetField({ label, labelId, children }: SheetFieldProps): ReactNode {
|
|
223
|
+
return (
|
|
224
|
+
<div className={classes.sheetField}>
|
|
225
|
+
<span className={classes.sheetLabel} {...(labelId !== undefined && { id: labelId })}>
|
|
226
|
+
{label}
|
|
227
|
+
</span>
|
|
228
|
+
{children}
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Names a control that has no VISIBLE Mantine `label`, and does it differently per surface — which
|
|
235
|
+
* is the whole reason this is one helper instead of an `aria-label` sprinkled per call site.
|
|
236
|
+
*
|
|
237
|
+
* In the SHEET the visible `SheetField` heading is right there, so the control points at it
|
|
238
|
+
* (`aria-labelledby`) and the heading stops being decoration. In the PILL surface there is no
|
|
239
|
+
* visible name to point at — the pill text is the VALUE (`EUR`, `30d`, `3 channels`) — so the
|
|
240
|
+
* control carries the filter's own name instead. Either way the announced name is what is being
|
|
241
|
+
* filtered, not what it currently holds.
|
|
242
|
+
*
|
|
243
|
+
* The id is generated by the CONTROL, not by `SheetField`, because the control renders its own
|
|
244
|
+
* `SheetField`: an id published downward through context could never reach the sibling that needs it.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* const { labelId, nameProps } = useControlName(label, inSheet)
|
|
248
|
+
* <Radio.Group {...nameProps} …/>
|
|
249
|
+
* // and, in the sheet branch: <SheetField label={label} labelId={labelId}>…</SheetField>
|
|
250
|
+
*/
|
|
251
|
+
export function useControlName(
|
|
252
|
+
label: string,
|
|
253
|
+
inSheet: boolean,
|
|
254
|
+
): {
|
|
255
|
+
labelId: string
|
|
256
|
+
nameProps: { 'aria-labelledby': string } | { 'aria-label': string }
|
|
257
|
+
} {
|
|
258
|
+
const labelId = useId()
|
|
259
|
+
return {
|
|
260
|
+
labelId,
|
|
261
|
+
nameProps: inSheet ? { 'aria-labelledby': labelId } : { 'aria-label': label },
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export type SheetRowProps = {
|
|
266
|
+
readonly children: ReactNode
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** One 44px full-width row (`--vx-space-sheet-row-height`). */
|
|
270
|
+
export function SheetRow({ children }: SheetRowProps): ReactNode {
|
|
271
|
+
return <div className={classes.sheetRow}>{children}</div>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The class every sheet-form list item shares, for controls that render their own row element. */
|
|
275
|
+
export const sheetRowClass = classes.sheetRow
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `classNames` for a Radio/Checkbox/Switch rendered as a sheet row — stretches its `body` and
|
|
279
|
+
* `<label>` across the full 44px so the whole row is the touch target, not the middle 20px of it.
|
|
280
|
+
*/
|
|
281
|
+
export const sheetRowClassNames = {
|
|
282
|
+
root: classes.sheetRow,
|
|
283
|
+
body: classes.sheetRowBody,
|
|
284
|
+
label: classes.sheetRowLabel,
|
|
285
|
+
} as const
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control tier's own inline glyphs (`docs/CONTROLS-SPEC.md` §3).
|
|
3
|
+
*
|
|
4
|
+
* basalt ships NO icon dependency — every `icon` slot in the public API takes a `ReactNode` the
|
|
5
|
+
* consumer fills. These are the few glyphs the FRAMEWORK draws for itself, where a default icon is
|
|
6
|
+
* part of the control's identity rather than a caller's decision: a range filter reads as a date
|
|
7
|
+
* picker because it carries a calendar, a compare filter because it carries the swap arrows, and
|
|
8
|
+
* the mobile `Filters (n)` pill because it carries a funnel. Same convention the shell already uses
|
|
9
|
+
* for its own chrome (`shell/sidebar-search.tsx`, `shell/app-sidebar.tsx`): a 24×24 `viewBox`,
|
|
10
|
+
* `stroke="currentColor"`, sized down by the `size` prop.
|
|
11
|
+
*
|
|
12
|
+
* INTERNAL — not on the `./controls` barrel. A consumer wanting a different glyph passes `icon`.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from 'react'
|
|
15
|
+
|
|
16
|
+
/** The tier's icon box: 16px inside a button, 14px for a trailing affordance. */
|
|
17
|
+
const ICON_SIZE = 16
|
|
18
|
+
const AFFORDANCE_SIZE = 14
|
|
19
|
+
|
|
20
|
+
function Stroke({ size = ICON_SIZE, children }: { size?: number; children: ReactNode }): ReactNode {
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
width={size}
|
|
24
|
+
height={size}
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
strokeWidth={1.75}
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
aria-hidden
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `RangeFilter`'s default leading icon. */
|
|
39
|
+
export function CalendarGlyph(): ReactNode {
|
|
40
|
+
return (
|
|
41
|
+
<Stroke>
|
|
42
|
+
<rect x={3} y={5} width={18} height={16} rx={2.5} />
|
|
43
|
+
<path d="M3 10h18" />
|
|
44
|
+
<path d="M8 3v4" />
|
|
45
|
+
<path d="M16 3v4" />
|
|
46
|
+
</Stroke>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `CompareFilter`'s default leading icon — the arrows-swap glyph. */
|
|
51
|
+
export function SwapGlyph(): ReactNode {
|
|
52
|
+
return (
|
|
53
|
+
<Stroke>
|
|
54
|
+
<path d="M4 8h13l-3.5 -3.5" />
|
|
55
|
+
<path d="M20 16h-13l3.5 3.5" />
|
|
56
|
+
</Stroke>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The mobile `Filters (n)` pill's leading icon. */
|
|
61
|
+
export function FunnelGlyph(): ReactNode {
|
|
62
|
+
return (
|
|
63
|
+
<Stroke>
|
|
64
|
+
<path d="M4 4h16l-6.5 8v7l-3 -1.5v-5.5z" />
|
|
65
|
+
</Stroke>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every pill's trailing affordance — 14px, muted, and an SVG rather than the `⇅` character it
|
|
71
|
+
* replaced. A text glyph rendered at the label's own font metrics, so it sat off the optical centre
|
|
72
|
+
* and shifted with the font stack; the path is centred in its own box.
|
|
73
|
+
*/
|
|
74
|
+
export function UpDownGlyph(): ReactNode {
|
|
75
|
+
return (
|
|
76
|
+
<Stroke size={AFFORDANCE_SIZE}>
|
|
77
|
+
<path d="M8 9l4 -4l4 4" />
|
|
78
|
+
<path d="M8 15l4 4l4 -4" />
|
|
79
|
+
</Stroke>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `basalt-ui/controls` — the Mantine-coupled control tier (`docs/CONTROLS-SPEC.md` §3).
|
|
3
|
+
*
|
|
4
|
+
* Every control here renders at the `ctl` tier from the home that hosts it and owns its own
|
|
5
|
+
* desktop/mobile swap in CSS (law C9). Actions and sync land in wave 3; the filter/tab family
|
|
6
|
+
* follows in wave 4.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
ActionGroup,
|
|
10
|
+
OverflowMenu,
|
|
11
|
+
DESKTOP_SECONDARY_MAX,
|
|
12
|
+
MOBILE_GLOBAL_BAR_MAX,
|
|
13
|
+
barActionMobile,
|
|
14
|
+
globalActionMobile,
|
|
15
|
+
type BarAction,
|
|
16
|
+
type BarActionItem,
|
|
17
|
+
type BarActionMenu,
|
|
18
|
+
type BarActionCustom,
|
|
19
|
+
type ActionGroupProps,
|
|
20
|
+
type GlobalAction,
|
|
21
|
+
type OverflowMenuProps,
|
|
22
|
+
} from './actions'
|
|
23
|
+
export { SyncButton, formatAge, AGE_REFRESH_MS, type SyncButtonProps } from './sync-button'
|
|
24
|
+
export { ControlGroup, type ControlGroupProps } from './control-group'
|
|
25
|
+
|
|
26
|
+
// ── Filters + tabs (wave 4) ───────────────────────────────────────────────────────────────────
|
|
27
|
+
// `FilterPill`, `FilterSheet` and the `EnumFilter` body are deliberately NOT here: reaching for
|
|
28
|
+
// them is hand-rolling a filter (`basalt/hand-rolled-filter`), and `useFilterSurface` is only
|
|
29
|
+
// meaningful to a control living inside a `FilterSet` — i.e. to this folder.
|
|
30
|
+
export { FilterSet, type FilterSetProps } from './filter-set'
|
|
31
|
+
export { RangeFilter, type RangeCustomPickerProps, type RangeFilterProps } from './range-filter'
|
|
32
|
+
export {
|
|
33
|
+
CompareFilter,
|
|
34
|
+
COMPARE_VALUES,
|
|
35
|
+
type CompareFilterProps,
|
|
36
|
+
type CompareValue,
|
|
37
|
+
} from './compare-filter'
|
|
38
|
+
export {
|
|
39
|
+
SelectFilter,
|
|
40
|
+
type FilterOption,
|
|
41
|
+
type SelectFilterEnumProps,
|
|
42
|
+
type SelectFilterProps,
|
|
43
|
+
type SelectFilterStringProps,
|
|
44
|
+
} from './select-filter'
|
|
45
|
+
export { MultiSelectFilter, type MultiSelectFilterProps } from './multi-select-filter'
|
|
46
|
+
export { NumberFilter, type NumberFilterOption, type NumberFilterProps } from './number-filter'
|
|
47
|
+
export { SearchFilter, type SearchFilterProps } from './search-filter'
|
|
48
|
+
export { ToggleFilter, type ToggleFilterProps } from './toggle-filter'
|
|
49
|
+
export { ViewTabs, type ViewTabsOption, type ViewTabsProps } from './view-tabs'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MultiSelectFilter` — an any-of set over one closed enum (`docs/CONTROLS-SPEC.md` §3).
|
|
3
|
+
*
|
|
4
|
+
* The pill reads the filter's name while the selection carries no information — empty (no
|
|
5
|
+
* constraint) or every option (the same thing said the long way) — and switches to a count once it
|
|
6
|
+
* does: `All channels` → `3 channels`. `noun` is the plural read in that count and defaults to the
|
|
7
|
+
* label lowercased, so `label="Channels"` needs no second prop.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <MultiSelectFilter field={analytics.field.channels} label="All channels" noun="channels" />
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // A runtime catalogue over the same closed field — live labels, same values.
|
|
14
|
+
* <MultiSelectFilter
|
|
15
|
+
* field={analytics.field.channels}
|
|
16
|
+
* label="All channels"
|
|
17
|
+
* options={CHANNELS.map((c) => ({ value: c, label: `${c} · ${counts[c]}` }))}
|
|
18
|
+
* />
|
|
19
|
+
*/
|
|
20
|
+
import { Button, Checkbox, Stack } from '@mantine/core'
|
|
21
|
+
import type { ReactNode } from 'react'
|
|
22
|
+
import type { FieldHandle, MultiField } from '../state'
|
|
23
|
+
import type { FilterOption } from './select-filter'
|
|
24
|
+
import classes from './controls.module.css'
|
|
25
|
+
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
26
|
+
import { FilterPill } from './filter-pill'
|
|
27
|
+
import { SheetField, SheetOptionList, useControlName } from './filter-sheet'
|
|
28
|
+
|
|
29
|
+
export type MultiSelectFilterProps<T extends string> = {
|
|
30
|
+
readonly field: FieldHandle<MultiField<T>>
|
|
31
|
+
readonly label: string
|
|
32
|
+
readonly icon?: ReactNode
|
|
33
|
+
/**
|
|
34
|
+
* The plural noun in the `3 channels` count.
|
|
35
|
+
*
|
|
36
|
+
* @default the label, lowercased
|
|
37
|
+
*/
|
|
38
|
+
readonly noun?: string
|
|
39
|
+
/**
|
|
40
|
+
* Overrides `field.options` at render — a runtime catalogue whose labels carry live data
|
|
41
|
+
* (`web · 1.2k`). The VALUES still belong to the field: a multi field is a closed set, so this
|
|
42
|
+
* relabels and reorders rows, it does not open new ones. Unlike `SelectFilter` there is no
|
|
43
|
+
* string-field shape to make it required.
|
|
44
|
+
*/
|
|
45
|
+
readonly options?: readonly FilterOption[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function MultiSelectFilter<T extends string>({
|
|
49
|
+
field,
|
|
50
|
+
label,
|
|
51
|
+
icon,
|
|
52
|
+
noun,
|
|
53
|
+
options: optionsProp,
|
|
54
|
+
}: MultiSelectFilterProps<T>): ReactNode {
|
|
55
|
+
const [value, setValue] = field.use()
|
|
56
|
+
const surface = useFilterSurface()
|
|
57
|
+
const isDefault = field.isDefault(value)
|
|
58
|
+
useFilterRegistration(!isDefault, () => {
|
|
59
|
+
field.clear()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// The prop wins whole, never merged — same rule as `EnumFilter`: a catalogue that dropped a row
|
|
63
|
+
// must be able to drop it, and `All channels` is counted against the rows actually shown.
|
|
64
|
+
const options: readonly FilterOption[] = optionsProp ?? field.options
|
|
65
|
+
const inSheet = surface === 'sheet'
|
|
66
|
+
const carriesInformation = value.length > 0 && value.length < options.length
|
|
67
|
+
const { labelId } = useControlName(label, inSheet)
|
|
68
|
+
|
|
69
|
+
// The SHEET form is a `SheetOptionList` in `multi` mode — 44px rows, a trailing check on each
|
|
70
|
+
// selected one, hairlines between rows only. Same reasoning as `EnumFilter`: the popover keeps
|
|
71
|
+
// Mantine's `Checkbox.Group`, the sheet reads as a list of options rather than a stack of boxes.
|
|
72
|
+
if (inSheet) {
|
|
73
|
+
return (
|
|
74
|
+
<SheetField label={label} labelId={labelId}>
|
|
75
|
+
<SheetOptionList
|
|
76
|
+
mode="multi"
|
|
77
|
+
labelId={labelId}
|
|
78
|
+
selected={value}
|
|
79
|
+
options={options}
|
|
80
|
+
onToggle={(next) => {
|
|
81
|
+
// The rows are rendered from `options`, whose values belong to the field either way (the
|
|
82
|
+
// prop relabels a closed set, it cannot open it) — the cast restores what the codec
|
|
83
|
+
// already guarantees.
|
|
84
|
+
const has = value.includes(next as T)
|
|
85
|
+
setValue(has ? value.filter((v) => v !== next) : [...value, next as T])
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</SheetField>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const body = (
|
|
93
|
+
// Mantine's own `label`, for the same reason `enum-filter.tsx` uses it — `Checkbox.Group`
|
|
94
|
+
// overwrites any `aria-label`/`aria-labelledby` with its own. Named from `label`, never from the
|
|
95
|
+
// pill text, which reads `3 channels` and describes the selection rather than the filter.
|
|
96
|
+
<Checkbox.Group
|
|
97
|
+
label={label}
|
|
98
|
+
classNames={{ label: classes.groupLabel }}
|
|
99
|
+
value={[...value]}
|
|
100
|
+
onChange={(next) => {
|
|
101
|
+
// The boxes are rendered from `options`, whose values belong to the field either way (the
|
|
102
|
+
// prop relabels a closed set, it cannot open it) — the cast restores what the codec
|
|
103
|
+
// already guarantees.
|
|
104
|
+
setValue(next as readonly T[])
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<Stack gap={2}>
|
|
108
|
+
{options.map((option) => (
|
|
109
|
+
<Checkbox
|
|
110
|
+
key={option.value}
|
|
111
|
+
value={option.value}
|
|
112
|
+
label={option.label}
|
|
113
|
+
{...(option.disabled === true && { disabled: true })}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</Stack>
|
|
117
|
+
{!isDefault && (
|
|
118
|
+
<Button
|
|
119
|
+
variant="subtle"
|
|
120
|
+
size="ctl"
|
|
121
|
+
onClick={() => {
|
|
122
|
+
field.clear()
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
Clear
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
128
|
+
</Checkbox.Group>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<FilterPill
|
|
133
|
+
label={carriesInformation ? `${value.length} ${noun ?? label.toLowerCase()}` : label}
|
|
134
|
+
active={!isDefault}
|
|
135
|
+
{...(icon !== undefined && { icon })}
|
|
136
|
+
>
|
|
137
|
+
<div className={classes.optionList}>{body}</div>
|
|
138
|
+
</FilterPill>
|
|
139
|
+
)
|
|
140
|
+
}
|