basalt-ui 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +18 -0
- package/MIGRATING.md +546 -20
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +88 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +220 -84
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared body behind `SelectFilter` and `CompareFilter` — one closed-enum filter over
|
|
3
|
+
* `field.enum` (`docs/CONTROLS-SPEC.md` §3). INTERNAL: the two exported wrappers differ only in
|
|
4
|
+
* their default label and in how narrowly they type `field`, so there is one implementation and no
|
|
5
|
+
* third public spelling of it.
|
|
6
|
+
*
|
|
7
|
+
* Both surfaces render the SAME `Radio.Group`, differing only in row height: a radio list states
|
|
8
|
+
* "one of these" without a second interaction (a nested Select inside a popover would), and it is
|
|
9
|
+
* the sheet form the spec prescribes — so it is the popover form too rather than inventing a second
|
|
10
|
+
* idiom for the same choice.
|
|
11
|
+
*
|
|
12
|
+
* **The pill label is ALWAYS the selected option's label, including at the field's default value.**
|
|
13
|
+
* It used to fall back to the FILTER's name while `isDefault(value)` held, and that is the bug this
|
|
14
|
+
* comment exists to keep fixed: a bar reading `Compare` over a field holding `'previous'` states
|
|
15
|
+
* something false — the popover said `Previous period` was selected and the pill said the filter had
|
|
16
|
+
* not been touched. A pill is a readout, so the value is the only thing it may print. `label` is the
|
|
17
|
+
* popover/sheet heading and the accessible name, nothing else; the ACTIVE state (touched vs at its
|
|
18
|
+
* default) is carried by the accent border, which is a second channel and does not need the text.
|
|
19
|
+
*
|
|
20
|
+
* `?? label` survives as a last resort for a value that is in the field but not in the rendered
|
|
21
|
+
* `options` — a runtime catalogue that has dropped a row while the URL still points at it. That is a
|
|
22
|
+
* data gap, not a default, and printing the filter's name is the least-wrong thing to do with it.
|
|
23
|
+
*
|
|
24
|
+
* `field` is typed as `ChoiceHandle` rather than a `FieldHandle<EnumField>`, and `options` may
|
|
25
|
+
* override the field's own rows — the two halves of `SelectFilter`'s runtime-catalogue shape (see
|
|
26
|
+
* its doc). Both public wrappers still type `field` exactly, so a wrong field kind is still a type
|
|
27
|
+
* error at the call site; the widening lives here, where nothing consumer-facing points at it.
|
|
28
|
+
*/
|
|
29
|
+
import { Button, Radio, Stack } from '@mantine/core'
|
|
30
|
+
import type { ReactNode } from 'react'
|
|
31
|
+
import type { FieldOption } from '../state'
|
|
32
|
+
import classes from './controls.module.css'
|
|
33
|
+
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
34
|
+
import { FilterPill } from './filter-pill'
|
|
35
|
+
import { SheetField, SheetOptionList, useControlName } from './filter-sheet'
|
|
36
|
+
import type { FilterOption } from './select-filter'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The slice of a `FieldHandle` this body actually binds to, generic over the VALUE rather than over
|
|
40
|
+
* the field kind. `FieldHandle<EnumField<T>>` satisfies it at `T` and `FieldHandle<StringField>` at
|
|
41
|
+
* `string`, which is what lets `SelectFilter` take either without this file knowing about kinds —
|
|
42
|
+
* and it stays a structural subset, so it can never widen into a second, hand-constructible handle.
|
|
43
|
+
*/
|
|
44
|
+
export type ChoiceHandle<T extends string> = {
|
|
45
|
+
readonly fallback: T
|
|
46
|
+
readonly options: readonly FieldOption[]
|
|
47
|
+
use(): readonly [T, (next: T) => void]
|
|
48
|
+
isDefault(v: T): boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type EnumFilterProps<T extends string> = {
|
|
52
|
+
readonly field: ChoiceHandle<T>
|
|
53
|
+
readonly label: string
|
|
54
|
+
readonly icon?: ReactNode
|
|
55
|
+
/** Adds a `Clear` action that writes the field back to its fallback. */
|
|
56
|
+
readonly clearable?: boolean
|
|
57
|
+
/**
|
|
58
|
+
* Overrides `field.options` at render — a runtime catalogue whose labels carry live data, or the
|
|
59
|
+
* only row source a `StringField` has. `| undefined` explicitly: an internal component is spread
|
|
60
|
+
* into, and `exactOptionalPropertyTypes` rejects a possibly-undefined member on a bare optional.
|
|
61
|
+
*/
|
|
62
|
+
readonly options?: readonly FilterOption[] | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function EnumFilter<T extends string>({
|
|
66
|
+
field,
|
|
67
|
+
label,
|
|
68
|
+
icon,
|
|
69
|
+
clearable,
|
|
70
|
+
options: optionsProp,
|
|
71
|
+
}: EnumFilterProps<T>): ReactNode {
|
|
72
|
+
const [value, setValue] = field.use()
|
|
73
|
+
const surface = useFilterSurface()
|
|
74
|
+
const isDefault = field.isDefault(value)
|
|
75
|
+
useFilterRegistration(!isDefault, () => {
|
|
76
|
+
setValue(field.fallback)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// The prop wins whole, never merged: a catalogue that has dropped a value must be able to drop
|
|
80
|
+
// its row, which a merge with `field.options` would silently put back.
|
|
81
|
+
const options: readonly FilterOption[] = optionsProp ?? field.options
|
|
82
|
+
const selected = options.find((option) => option.value === value)
|
|
83
|
+
const inSheet = surface === 'sheet'
|
|
84
|
+
const { labelId } = useControlName(label, inSheet)
|
|
85
|
+
|
|
86
|
+
// The SHEET form is a `SheetOptionList` (44px rows, trailing check, hairline between rows), not
|
|
87
|
+
// the popover's `Radio.Group` — see `SheetOptionList`'s doc for why the leading radio dot had to
|
|
88
|
+
// go. Both still write through the same setter, so there is one behaviour and two surfaces.
|
|
89
|
+
if (inSheet) {
|
|
90
|
+
return (
|
|
91
|
+
<SheetField label={label} labelId={labelId}>
|
|
92
|
+
<SheetOptionList
|
|
93
|
+
mode="single"
|
|
94
|
+
labelId={labelId}
|
|
95
|
+
selected={[value]}
|
|
96
|
+
options={options}
|
|
97
|
+
onToggle={(next) => {
|
|
98
|
+
// Rendered from `options` — the field's declared values, or the runtime catalogue
|
|
99
|
+
// standing in for them; the cast restores what the codec already guarantees.
|
|
100
|
+
setValue(next as T)
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
</SheetField>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const body = (
|
|
108
|
+
// Mantine's OWN `label` prop, not an `aria-label`: `Radio.Group` overwrites both `aria-label`
|
|
109
|
+
// and `aria-labelledby` with its own `aria-labelledby` pointing at the `Input.Wrapper` label it
|
|
110
|
+
// renders — which, with no `label` passed, points at an element that does not exist. So the only
|
|
111
|
+
// way to name this group is to give it the label. It doubles as the heading in both surfaces:
|
|
112
|
+
// the group needs the FILTER's name ('Currency'), which the pill cannot supply because the pill
|
|
113
|
+
// reads the VALUE ('EUR') once set.
|
|
114
|
+
<Radio.Group
|
|
115
|
+
label={label}
|
|
116
|
+
classNames={{ label: classes.groupLabel }}
|
|
117
|
+
value={value}
|
|
118
|
+
onChange={(next) => {
|
|
119
|
+
// `next` is a DOM string, and the radios are rendered from `options` — the field's own
|
|
120
|
+
// declared values, or the runtime catalogue standing in for them. Over a closed enum the
|
|
121
|
+
// cast restores what the codec already guarantees; over a `StringField` there is nothing to
|
|
122
|
+
// guarantee and nothing to widen, since every string is a legal value there.
|
|
123
|
+
setValue(next as T)
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<Stack gap={2}>
|
|
127
|
+
{options.map((option) => (
|
|
128
|
+
<Radio
|
|
129
|
+
key={option.value}
|
|
130
|
+
value={option.value}
|
|
131
|
+
label={option.label}
|
|
132
|
+
{...(option.disabled === true && { disabled: true })}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
</Stack>
|
|
136
|
+
{clearable === true && !isDefault && (
|
|
137
|
+
<Button
|
|
138
|
+
variant="subtle"
|
|
139
|
+
size="ctl"
|
|
140
|
+
onClick={() => {
|
|
141
|
+
setValue(field.fallback)
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
Clear
|
|
145
|
+
</Button>
|
|
146
|
+
)}
|
|
147
|
+
</Radio.Group>
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<FilterPill
|
|
152
|
+
label={selected?.label ?? label}
|
|
153
|
+
active={!isDefault}
|
|
154
|
+
{...(icon !== undefined && { icon })}
|
|
155
|
+
>
|
|
156
|
+
<div className={classes.optionList}>{body}</div>
|
|
157
|
+
</FilterPill>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two contexts `FilterSet` publishes and every control in this folder consumes
|
|
3
|
+
* (`docs/CONTROLS-SPEC.md` §3). Its own module so `filter-set.tsx` and `filter-sheet.tsx` can both
|
|
4
|
+
* read it without importing each other.
|
|
5
|
+
*
|
|
6
|
+
* **Surface** — `useFilterSurface()` is how a control decides its FORM, not a media query: `'pill'`
|
|
7
|
+
* inside the bar row (and inside the desktop `+N` fold), `'sheet'` inside the mobile `Filters (n)`
|
|
8
|
+
* Drawer. A control renders exactly one form per mount; the desktop/mobile swap WITHIN the pill
|
|
9
|
+
* form is CSS (`visibleFrom`/`hiddenFrom`), never JS (C9).
|
|
10
|
+
*
|
|
11
|
+
* **Registry** — `useFilterRegistration()` is what makes `Filters (n)` and `Reset all` derived
|
|
12
|
+
* rather than hand-passed: each filter reports `{ isActive, reset }` for itself, so `FilterSet`
|
|
13
|
+
* needs no list of its children's fields. Registration is deliberately scoped to ONE surface: the
|
|
14
|
+
* bar row keeps every child mounted on every viewport (hidden slots are `display: none`, never
|
|
15
|
+
* unmounted — see `controls.module.css`), so the row alone is a complete and stable census. The
|
|
16
|
+
* fold dropdown and the sheet mount a SECOND copy of the same children and therefore provide a
|
|
17
|
+
* `null` registry, which is what keeps one filter from counting twice.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
createContext,
|
|
21
|
+
useCallback,
|
|
22
|
+
useContext,
|
|
23
|
+
useEffect,
|
|
24
|
+
useId,
|
|
25
|
+
useMemo,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from 'react'
|
|
29
|
+
import type { ReactNode } from 'react'
|
|
30
|
+
|
|
31
|
+
/** Which form a control renders. Provided by `FilterSet`; `'pill'` when there is no `FilterSet`. */
|
|
32
|
+
export type FilterSurface = 'pill' | 'sheet'
|
|
33
|
+
|
|
34
|
+
const FilterSurfaceContext = createContext<FilterSurface>('pill')
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The form this control should render — the ONE thing a control branches on.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const surface = useFilterSurface()
|
|
41
|
+
* if (surface === 'sheet') return <SheetField label={label}>…</SheetField>
|
|
42
|
+
*/
|
|
43
|
+
export function useFilterSurface(): FilterSurface {
|
|
44
|
+
return useContext(FilterSurfaceContext)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What one filter reports about itself, keyed internally by a `useId()` per mount. */
|
|
48
|
+
export type FilterRegistration = {
|
|
49
|
+
/** `!field.isDefault(value)` — the only input to `Filters (n)` and the pill's accent border. */
|
|
50
|
+
readonly isActive: boolean
|
|
51
|
+
/** Writes the field back to its fallback. Called by `Reset all`. */
|
|
52
|
+
readonly reset: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type FilterRegistry = {
|
|
56
|
+
register(key: string, registration: FilterRegistration): void
|
|
57
|
+
unregister(key: string): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FilterRegistryContext = createContext<FilterRegistry | null>(null)
|
|
61
|
+
|
|
62
|
+
export type FilterSetScopeProps = {
|
|
63
|
+
readonly surface: FilterSurface
|
|
64
|
+
/** `null` on a second copy of the same children (the fold dropdown, the sheet). */
|
|
65
|
+
readonly registry: FilterRegistry | null
|
|
66
|
+
readonly children: ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Provides both contexts at once — every place FilterSet mounts children goes through this. */
|
|
70
|
+
export function FilterSetScope({ surface, registry, children }: FilterSetScopeProps): ReactNode {
|
|
71
|
+
return (
|
|
72
|
+
<FilterSurfaceContext.Provider value={surface}>
|
|
73
|
+
<FilterRegistryContext.Provider value={registry}>{children}</FilterRegistryContext.Provider>
|
|
74
|
+
</FilterSurfaceContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Report this filter's active/reset state to the enclosing `FilterSet`. Call it unconditionally —
|
|
80
|
+
* it no-ops outside a registering scope (a bare filter with no `FilterSet`, the fold dropdown, the
|
|
81
|
+
* sheet), which is what keeps it hook-safe in every control.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const [value, setValue] = field.use()
|
|
85
|
+
* useFilterRegistration(!field.isDefault(value), () => setValue(field.fallback))
|
|
86
|
+
*/
|
|
87
|
+
export function useFilterRegistration(isActive: boolean, reset: () => void): void {
|
|
88
|
+
const registry = useContext(FilterRegistryContext)
|
|
89
|
+
const key = useId()
|
|
90
|
+
// A ref, so a `reset` closure that is fresh every render does not re-run the effect (and so
|
|
91
|
+
// `Reset all` always calls the LATEST setter, not the one captured at registration).
|
|
92
|
+
const resetRef = useRef(reset)
|
|
93
|
+
resetRef.current = reset
|
|
94
|
+
const stableReset = useCallback(() => {
|
|
95
|
+
resetRef.current()
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (registry === null) return
|
|
100
|
+
registry.register(key, { isActive, reset: stableReset })
|
|
101
|
+
return () => {
|
|
102
|
+
registry.unregister(key)
|
|
103
|
+
}
|
|
104
|
+
}, [registry, key, isActive, stableReset])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type FilterCensus = {
|
|
108
|
+
readonly registry: FilterRegistry
|
|
109
|
+
/** How many registered filters are non-default — `Filters (n)`. */
|
|
110
|
+
readonly activeCount: number
|
|
111
|
+
/** Resets every registered filter. */
|
|
112
|
+
readonly resetAll: () => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The registry side, for `FilterSet` itself. The map is a ref (registration happens in children's
|
|
117
|
+
* effects, which must not re-render the parent mid-commit) and the COUNT is state, so only a real
|
|
118
|
+
* change in the count re-renders the bar.
|
|
119
|
+
*/
|
|
120
|
+
export function useFilterCensus(): FilterCensus {
|
|
121
|
+
const entries = useRef(new Map<string, FilterRegistration>())
|
|
122
|
+
const [activeCount, setActiveCount] = useState(0)
|
|
123
|
+
|
|
124
|
+
// The functional form, so React's own bail-out drops a write of the count already held — a
|
|
125
|
+
// register/unregister pair that leaves the count unchanged must not re-render the bar.
|
|
126
|
+
const sync = useCallback(() => {
|
|
127
|
+
let next = 0
|
|
128
|
+
for (const entry of entries.current.values()) {
|
|
129
|
+
if (entry.isActive) next += 1
|
|
130
|
+
}
|
|
131
|
+
setActiveCount((current) => (current === next ? current : next))
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
134
|
+
const registry = useMemo<FilterRegistry>(
|
|
135
|
+
() => ({
|
|
136
|
+
register(key, registration) {
|
|
137
|
+
entries.current.set(key, registration)
|
|
138
|
+
sync()
|
|
139
|
+
},
|
|
140
|
+
unregister(key) {
|
|
141
|
+
entries.current.delete(key)
|
|
142
|
+
sync()
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
[sync],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const resetAll = useCallback(() => {
|
|
149
|
+
for (const entry of entries.current.values()) entry.reset()
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
return { registry, activeCount, resetAll }
|
|
153
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FilterPill` — the one chip every filter in the bar row renders (`docs/CONTROLS-SPEC.md` §3):
|
|
3
|
+
* bordered, 30px, leading icon · label · trailing `⇅`. INTERNAL: it is not on the `./controls`
|
|
4
|
+
* barrel, because a consumer reaching for it directly is hand-rolling a filter
|
|
5
|
+
* (`basalt/hand-rolled-filter`) — the pill is what `RangeFilter`/`SelectFilter`/… ARE, not a
|
|
6
|
+
* building block to assemble one from.
|
|
7
|
+
*
|
|
8
|
+
* The 30px box is `variant="default" size="ctl"` and nothing else — the tier comes from the theme's
|
|
9
|
+
* `--button-height-ctl` var set (`docs/CONTROLS-SPEC.md` §5), so the pill is the same height whether
|
|
10
|
+
* or not the home wrapped its slot in `CtlSlot`, and it tracks the density knob for free.
|
|
11
|
+
*
|
|
12
|
+
* Two modes, one component: with `children` the pill is a `Popover` target (every enum-ish filter);
|
|
13
|
+
* with only `onClick` it is a plain button that acts on press (`ToggleFilter`, the `+N` fold's
|
|
14
|
+
* sibling, the `Filters (n)` sheet opener).
|
|
15
|
+
*/
|
|
16
|
+
import { Button, Popover } from '@mantine/core'
|
|
17
|
+
import { useState } from 'react'
|
|
18
|
+
import type { ReactNode } from 'react'
|
|
19
|
+
import { IconSlot } from '../theme/icon-slot'
|
|
20
|
+
import classes from './controls.module.css'
|
|
21
|
+
import { UpDownGlyph } from './glyphs'
|
|
22
|
+
|
|
23
|
+
export type FilterPillProps = {
|
|
24
|
+
readonly label: string
|
|
25
|
+
readonly icon?: ReactNode
|
|
26
|
+
/** Non-default → accent-tinted border. Derived from `field.isDefault(value)`, never hand-set. */
|
|
27
|
+
readonly active?: boolean
|
|
28
|
+
/** Mono label, for a numeric preset set (`7d` / `30d`) — the same law `[data-numeric]` applies
|
|
29
|
+
* to a numeric SegmentedControl label (`theme/segmented-control.module.css`). */
|
|
30
|
+
readonly numeric?: boolean
|
|
31
|
+
/** Popover body. Omit for a press-to-act pill. */
|
|
32
|
+
readonly children?: ReactNode
|
|
33
|
+
readonly onClick?: () => void
|
|
34
|
+
/** Accessible name when the visible label is a bare count (`+2`). */
|
|
35
|
+
readonly ariaLabel?: string
|
|
36
|
+
/**
|
|
37
|
+
* `aria-pressed` for the press-to-act mode (`ToggleFilter`) — without it the on/off state is
|
|
38
|
+
* carried by a border colour and nothing else. Deliberately NOT set on a popover-target pill,
|
|
39
|
+
* where `aria-expanded` (which Mantine's `Popover.Target` supplies) is the correct state.
|
|
40
|
+
*/
|
|
41
|
+
readonly pressed?: boolean
|
|
42
|
+
readonly className?: string
|
|
43
|
+
/** Set by `FilterSet` on its own fold/sheet pills; a filter never passes either of these. */
|
|
44
|
+
readonly hideGlyph?: boolean
|
|
45
|
+
/** `data-shown` — the CSS hook that reveals the `+N` fold pill once folding is needed. */
|
|
46
|
+
readonly shown?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function FilterPill({
|
|
50
|
+
label,
|
|
51
|
+
icon,
|
|
52
|
+
active,
|
|
53
|
+
numeric,
|
|
54
|
+
children,
|
|
55
|
+
onClick,
|
|
56
|
+
ariaLabel,
|
|
57
|
+
pressed,
|
|
58
|
+
className,
|
|
59
|
+
hideGlyph,
|
|
60
|
+
shown,
|
|
61
|
+
}: FilterPillProps): ReactNode {
|
|
62
|
+
// Mantine's `Popover` does not open on target click — it is a controlled surface, unlike `Menu`.
|
|
63
|
+
// This is the only `useState` in the folder, and it holds an OVERLAY's open flag, not a filter's
|
|
64
|
+
// value (C3 is about the value; a popover that survives a reload would be a bug, not a feature).
|
|
65
|
+
const [opened, setOpened] = useState(false)
|
|
66
|
+
|
|
67
|
+
const target = (
|
|
68
|
+
<Button
|
|
69
|
+
variant="default"
|
|
70
|
+
size="ctl"
|
|
71
|
+
className={className === undefined ? classes.pill : `${classes.pill} ${className}`}
|
|
72
|
+
classNames={{ label: classes.pillLabel }}
|
|
73
|
+
{...(active === true && { 'data-active': true })}
|
|
74
|
+
{...(numeric === true && { 'data-numeric': true })}
|
|
75
|
+
{...(ariaLabel !== undefined && { 'aria-label': ariaLabel })}
|
|
76
|
+
{...(pressed !== undefined && children === undefined && { 'aria-pressed': pressed })}
|
|
77
|
+
{...(shown === true && { 'data-shown': true })}
|
|
78
|
+
{...(icon !== undefined && {
|
|
79
|
+
// `IconSlot` owns the 16px box and the optical centring; `.pillIcon` adds only the pill's
|
|
80
|
+
// muted-then-ink colour law. A consumer's glyph can no longer set this row's height.
|
|
81
|
+
leftSection: <IconSlot className={classes.pillIcon}>{icon}</IconSlot>,
|
|
82
|
+
})}
|
|
83
|
+
{...(hideGlyph !== true && {
|
|
84
|
+
rightSection: (
|
|
85
|
+
<IconSlot className={classes.glyph}>
|
|
86
|
+
<UpDownGlyph />
|
|
87
|
+
</IconSlot>
|
|
88
|
+
),
|
|
89
|
+
})}
|
|
90
|
+
onClick={
|
|
91
|
+
onClick ??
|
|
92
|
+
(() => {
|
|
93
|
+
setOpened((current) => !current)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
{label}
|
|
98
|
+
</Button>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (children === undefined) return target
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Popover
|
|
105
|
+
opened={opened}
|
|
106
|
+
onChange={setOpened}
|
|
107
|
+
onDismiss={() => {
|
|
108
|
+
setOpened(false)
|
|
109
|
+
}}
|
|
110
|
+
position="bottom-start"
|
|
111
|
+
withinPortal
|
|
112
|
+
shadow="md"
|
|
113
|
+
radius="sm"
|
|
114
|
+
trapFocus
|
|
115
|
+
>
|
|
116
|
+
<Popover.Target>{target}</Popover.Target>
|
|
117
|
+
<Popover.Dropdown className={classes.dropdown}>{children}</Popover.Dropdown>
|
|
118
|
+
</Popover>
|
|
119
|
+
)
|
|
120
|
+
}
|