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,91 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Radio, Stack } from "@mantine/core";
|
|
3
|
+
import classes from "./controls.module.css";
|
|
4
|
+
import { useFilterRegistration, useFilterSurface } from "./filter-context.js";
|
|
5
|
+
import { FilterPill } from "./filter-pill.js";
|
|
6
|
+
import { SheetField, SheetOptionList, useControlName } from "./filter-sheet.js";
|
|
7
|
+
function EnumFilter({
|
|
8
|
+
field,
|
|
9
|
+
label,
|
|
10
|
+
icon,
|
|
11
|
+
clearable,
|
|
12
|
+
options: optionsProp
|
|
13
|
+
}) {
|
|
14
|
+
const [value, setValue] = field.use();
|
|
15
|
+
const surface = useFilterSurface();
|
|
16
|
+
const isDefault = field.isDefault(value);
|
|
17
|
+
useFilterRegistration(!isDefault, () => {
|
|
18
|
+
setValue(field.fallback);
|
|
19
|
+
});
|
|
20
|
+
const options = optionsProp ?? field.options;
|
|
21
|
+
const selected = options.find((option) => option.value === value);
|
|
22
|
+
const inSheet = surface === "sheet";
|
|
23
|
+
const { labelId } = useControlName(label, inSheet);
|
|
24
|
+
if (inSheet) {
|
|
25
|
+
return /* @__PURE__ */ jsx(SheetField, { label, labelId, children: /* @__PURE__ */ jsx(
|
|
26
|
+
SheetOptionList,
|
|
27
|
+
{
|
|
28
|
+
mode: "single",
|
|
29
|
+
labelId,
|
|
30
|
+
selected: [value],
|
|
31
|
+
options,
|
|
32
|
+
onToggle: (next) => {
|
|
33
|
+
setValue(next);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
) });
|
|
37
|
+
}
|
|
38
|
+
const body = (
|
|
39
|
+
// Mantine's OWN `label` prop, not an `aria-label`: `Radio.Group` overwrites both `aria-label`
|
|
40
|
+
// and `aria-labelledby` with its own `aria-labelledby` pointing at the `Input.Wrapper` label it
|
|
41
|
+
// renders — which, with no `label` passed, points at an element that does not exist. So the only
|
|
42
|
+
// way to name this group is to give it the label. It doubles as the heading in both surfaces:
|
|
43
|
+
// the group needs the FILTER's name ('Currency'), which the pill cannot supply because the pill
|
|
44
|
+
// reads the VALUE ('EUR') once set.
|
|
45
|
+
/* @__PURE__ */ jsxs(
|
|
46
|
+
Radio.Group,
|
|
47
|
+
{
|
|
48
|
+
label,
|
|
49
|
+
classNames: { label: classes.groupLabel },
|
|
50
|
+
value,
|
|
51
|
+
onChange: (next) => {
|
|
52
|
+
setValue(next);
|
|
53
|
+
},
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx(Stack, { gap: 2, children: options.map((option) => /* @__PURE__ */ jsx(
|
|
56
|
+
Radio,
|
|
57
|
+
{
|
|
58
|
+
value: option.value,
|
|
59
|
+
label: option.label,
|
|
60
|
+
...option.disabled === true && { disabled: true }
|
|
61
|
+
},
|
|
62
|
+
option.value
|
|
63
|
+
)) }),
|
|
64
|
+
clearable === true && !isDefault && /* @__PURE__ */ jsx(
|
|
65
|
+
Button,
|
|
66
|
+
{
|
|
67
|
+
variant: "subtle",
|
|
68
|
+
size: "ctl",
|
|
69
|
+
onClick: () => {
|
|
70
|
+
setValue(field.fallback);
|
|
71
|
+
},
|
|
72
|
+
children: "Clear"
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
return /* @__PURE__ */ jsx(
|
|
80
|
+
FilterPill,
|
|
81
|
+
{
|
|
82
|
+
label: selected?.label ?? label,
|
|
83
|
+
active: !isDefault,
|
|
84
|
+
...icon !== void 0 && { icon },
|
|
85
|
+
children: /* @__PURE__ */ jsx("div", { className: classes.optionList, children: body })
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
EnumFilter
|
|
91
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** Which form a control renders. Provided by `FilterSet`; `'pill'` when there is no `FilterSet`. */
|
|
3
|
+
export type FilterSurface = 'pill' | 'sheet';
|
|
4
|
+
/**
|
|
5
|
+
* The form this control should render — the ONE thing a control branches on.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const surface = useFilterSurface()
|
|
9
|
+
* if (surface === 'sheet') return <SheetField label={label}>…</SheetField>
|
|
10
|
+
*/
|
|
11
|
+
export declare function useFilterSurface(): FilterSurface;
|
|
12
|
+
/** What one filter reports about itself, keyed internally by a `useId()` per mount. */
|
|
13
|
+
export type FilterRegistration = {
|
|
14
|
+
/** `!field.isDefault(value)` — the only input to `Filters (n)` and the pill's accent border. */
|
|
15
|
+
readonly isActive: boolean;
|
|
16
|
+
/** Writes the field back to its fallback. Called by `Reset all`. */
|
|
17
|
+
readonly reset: () => void;
|
|
18
|
+
};
|
|
19
|
+
type FilterRegistry = {
|
|
20
|
+
register(key: string, registration: FilterRegistration): void;
|
|
21
|
+
unregister(key: string): void;
|
|
22
|
+
};
|
|
23
|
+
export type FilterSetScopeProps = {
|
|
24
|
+
readonly surface: FilterSurface;
|
|
25
|
+
/** `null` on a second copy of the same children (the fold dropdown, the sheet). */
|
|
26
|
+
readonly registry: FilterRegistry | null;
|
|
27
|
+
readonly children: ReactNode;
|
|
28
|
+
};
|
|
29
|
+
/** Provides both contexts at once — every place FilterSet mounts children goes through this. */
|
|
30
|
+
export declare function FilterSetScope({ surface, registry, children }: FilterSetScopeProps): ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* Report this filter's active/reset state to the enclosing `FilterSet`. Call it unconditionally —
|
|
33
|
+
* it no-ops outside a registering scope (a bare filter with no `FilterSet`, the fold dropdown, the
|
|
34
|
+
* sheet), which is what keeps it hook-safe in every control.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const [value, setValue] = field.use()
|
|
38
|
+
* useFilterRegistration(!field.isDefault(value), () => setValue(field.fallback))
|
|
39
|
+
*/
|
|
40
|
+
export declare function useFilterRegistration(isActive: boolean, reset: () => void): void;
|
|
41
|
+
export type FilterCensus = {
|
|
42
|
+
readonly registry: FilterRegistry;
|
|
43
|
+
/** How many registered filters are non-default — `Filters (n)`. */
|
|
44
|
+
readonly activeCount: number;
|
|
45
|
+
/** Resets every registered filter. */
|
|
46
|
+
readonly resetAll: () => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The registry side, for `FilterSet` itself. The map is a ref (registration happens in children's
|
|
50
|
+
* effects, which must not re-render the parent mid-commit) and the COUNT is state, so only a real
|
|
51
|
+
* change in the count re-renders the bar.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useFilterCensus(): FilterCensus;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=filter-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-context.d.ts","sourceRoot":"","sources":["../../src/controls/filter-context.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,oGAAoG;AACpG,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAA;AAI5C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAED,uFAAuF;AACvF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC7D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAID,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,mBAAmB,GAAG,SAAS,CAM9F;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI,CAkBhF;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;IACjC,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAiC9C"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
const FilterSurfaceContext = createContext("pill");
|
|
13
|
+
function useFilterSurface() {
|
|
14
|
+
return useContext(FilterSurfaceContext);
|
|
15
|
+
}
|
|
16
|
+
const FilterRegistryContext = createContext(null);
|
|
17
|
+
function FilterSetScope({ surface, registry, children }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(FilterSurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(FilterRegistryContext.Provider, { value: registry, children }) });
|
|
19
|
+
}
|
|
20
|
+
function useFilterRegistration(isActive, reset) {
|
|
21
|
+
const registry = useContext(FilterRegistryContext);
|
|
22
|
+
const key = useId();
|
|
23
|
+
const resetRef = useRef(reset);
|
|
24
|
+
resetRef.current = reset;
|
|
25
|
+
const stableReset = useCallback(() => {
|
|
26
|
+
resetRef.current();
|
|
27
|
+
}, []);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (registry === null) return;
|
|
30
|
+
registry.register(key, { isActive, reset: stableReset });
|
|
31
|
+
return () => {
|
|
32
|
+
registry.unregister(key);
|
|
33
|
+
};
|
|
34
|
+
}, [registry, key, isActive, stableReset]);
|
|
35
|
+
}
|
|
36
|
+
function useFilterCensus() {
|
|
37
|
+
const entries = useRef(/* @__PURE__ */ new Map());
|
|
38
|
+
const [activeCount, setActiveCount] = useState(0);
|
|
39
|
+
const sync = useCallback(() => {
|
|
40
|
+
let next = 0;
|
|
41
|
+
for (const entry of entries.current.values()) {
|
|
42
|
+
if (entry.isActive) next += 1;
|
|
43
|
+
}
|
|
44
|
+
setActiveCount((current) => current === next ? current : next);
|
|
45
|
+
}, []);
|
|
46
|
+
const registry = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
register(key, registration) {
|
|
49
|
+
entries.current.set(key, registration);
|
|
50
|
+
sync();
|
|
51
|
+
},
|
|
52
|
+
unregister(key) {
|
|
53
|
+
entries.current.delete(key);
|
|
54
|
+
sync();
|
|
55
|
+
}
|
|
56
|
+
}),
|
|
57
|
+
[sync]
|
|
58
|
+
);
|
|
59
|
+
const resetAll = useCallback(() => {
|
|
60
|
+
for (const entry of entries.current.values()) entry.reset();
|
|
61
|
+
}, []);
|
|
62
|
+
return { registry, activeCount, resetAll };
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
FilterSetScope,
|
|
66
|
+
useFilterCensus,
|
|
67
|
+
useFilterRegistration,
|
|
68
|
+
useFilterSurface
|
|
69
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type FilterPillProps = {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly icon?: ReactNode;
|
|
5
|
+
/** Non-default → accent-tinted border. Derived from `field.isDefault(value)`, never hand-set. */
|
|
6
|
+
readonly active?: boolean;
|
|
7
|
+
/** Mono label, for a numeric preset set (`7d` / `30d`) — the same law `[data-numeric]` applies
|
|
8
|
+
* to a numeric SegmentedControl label (`theme/segmented-control.module.css`). */
|
|
9
|
+
readonly numeric?: boolean;
|
|
10
|
+
/** Popover body. Omit for a press-to-act pill. */
|
|
11
|
+
readonly children?: ReactNode;
|
|
12
|
+
readonly onClick?: () => void;
|
|
13
|
+
/** Accessible name when the visible label is a bare count (`+2`). */
|
|
14
|
+
readonly ariaLabel?: string;
|
|
15
|
+
/**
|
|
16
|
+
* `aria-pressed` for the press-to-act mode (`ToggleFilter`) — without it the on/off state is
|
|
17
|
+
* carried by a border colour and nothing else. Deliberately NOT set on a popover-target pill,
|
|
18
|
+
* where `aria-expanded` (which Mantine's `Popover.Target` supplies) is the correct state.
|
|
19
|
+
*/
|
|
20
|
+
readonly pressed?: boolean;
|
|
21
|
+
readonly className?: string;
|
|
22
|
+
/** Set by `FilterSet` on its own fold/sheet pills; a filter never passes either of these. */
|
|
23
|
+
readonly hideGlyph?: boolean;
|
|
24
|
+
/** `data-shown` — the CSS hook that reveals the `+N` fold pill once folding is needed. */
|
|
25
|
+
readonly shown?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare function FilterPill({ label, icon, active, numeric, children, onClick, ariaLabel, pressed, className, hideGlyph, shown, }: FilterPillProps): ReactNode;
|
|
28
|
+
//# sourceMappingURL=filter-pill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-pill.d.ts","sourceRoot":"","sources":["../../src/controls/filter-pill.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;sFACkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,SAAS,EACT,OAAO,EACP,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE,eAAe,GAAG,SAAS,CA2D7B"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Popover } from "@mantine/core";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { IconSlot } from "../theme/icon-slot.js";
|
|
5
|
+
import classes from "./controls.module.css";
|
|
6
|
+
import { UpDownGlyph } from "./glyphs.js";
|
|
7
|
+
function FilterPill({
|
|
8
|
+
label,
|
|
9
|
+
icon,
|
|
10
|
+
active,
|
|
11
|
+
numeric,
|
|
12
|
+
children,
|
|
13
|
+
onClick,
|
|
14
|
+
ariaLabel,
|
|
15
|
+
pressed,
|
|
16
|
+
className,
|
|
17
|
+
hideGlyph,
|
|
18
|
+
shown
|
|
19
|
+
}) {
|
|
20
|
+
const [opened, setOpened] = useState(false);
|
|
21
|
+
const target = /* @__PURE__ */ jsx(
|
|
22
|
+
Button,
|
|
23
|
+
{
|
|
24
|
+
variant: "default",
|
|
25
|
+
size: "ctl",
|
|
26
|
+
className: className === void 0 ? classes.pill : `${classes.pill} ${className}`,
|
|
27
|
+
classNames: { label: classes.pillLabel },
|
|
28
|
+
...active === true && { "data-active": true },
|
|
29
|
+
...numeric === true && { "data-numeric": true },
|
|
30
|
+
...ariaLabel !== void 0 && { "aria-label": ariaLabel },
|
|
31
|
+
...pressed !== void 0 && children === void 0 && { "aria-pressed": pressed },
|
|
32
|
+
...shown === true && { "data-shown": true },
|
|
33
|
+
...icon !== void 0 && {
|
|
34
|
+
// `IconSlot` owns the 16px box and the optical centring; `.pillIcon` adds only the pill's
|
|
35
|
+
// muted-then-ink colour law. A consumer's glyph can no longer set this row's height.
|
|
36
|
+
leftSection: /* @__PURE__ */ jsx(IconSlot, { className: classes.pillIcon, children: icon })
|
|
37
|
+
},
|
|
38
|
+
...hideGlyph !== true && {
|
|
39
|
+
rightSection: /* @__PURE__ */ jsx(IconSlot, { className: classes.glyph, children: /* @__PURE__ */ jsx(UpDownGlyph, {}) })
|
|
40
|
+
},
|
|
41
|
+
onClick: onClick ?? (() => {
|
|
42
|
+
setOpened((current) => !current);
|
|
43
|
+
}),
|
|
44
|
+
children: label
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
if (children === void 0) return target;
|
|
48
|
+
return /* @__PURE__ */ jsxs(
|
|
49
|
+
Popover,
|
|
50
|
+
{
|
|
51
|
+
opened,
|
|
52
|
+
onChange: setOpened,
|
|
53
|
+
onDismiss: () => {
|
|
54
|
+
setOpened(false);
|
|
55
|
+
},
|
|
56
|
+
position: "bottom-start",
|
|
57
|
+
withinPortal: true,
|
|
58
|
+
shadow: "md",
|
|
59
|
+
radius: "sm",
|
|
60
|
+
trapFocus: true,
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsx(Popover.Target, { children: target }),
|
|
63
|
+
/* @__PURE__ */ jsx(Popover.Dropdown, { className: classes.dropdown, children })
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
FilterPill
|
|
70
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type FilterSetProps = {
|
|
3
|
+
readonly children: ReactNode;
|
|
4
|
+
/**
|
|
5
|
+
* How many children stay inline pills below `sm`. The rest are reachable only through the
|
|
6
|
+
* `Filters (n)` sheet — which always holds every child, inline ones included.
|
|
7
|
+
*
|
|
8
|
+
* @default 1
|
|
9
|
+
*/
|
|
10
|
+
readonly inline?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function FilterSet({ children, inline }: FilterSetProps): ReactNode;
|
|
13
|
+
//# sourceMappingURL=filter-set.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-set.d.ts","sourceRoot":"","sources":["../../src/controls/filter-set.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,OAAO,CAAA;AAYjD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAU,EAAE,EAAE,cAAc,GAAG,SAAS,CAoF7E"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement, useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import classes from "./controls.module.css";
|
|
4
|
+
import { FilterSetScope, useFilterCensus } from "./filter-context.js";
|
|
5
|
+
import { FilterPill } from "./filter-pill.js";
|
|
6
|
+
import { FilterSheet } from "./filter-sheet.js";
|
|
7
|
+
import { FunnelGlyph } from "./glyphs.js";
|
|
8
|
+
const FOLD_PILL_WIDTH = 52;
|
|
9
|
+
const FALLBACK_GAP = 6;
|
|
10
|
+
function FilterSet({ children, inline = 1 }) {
|
|
11
|
+
const items = Children.toArray(children);
|
|
12
|
+
const { registry, activeCount, resetAll } = useFilterCensus();
|
|
13
|
+
const [sheetOpened, setSheetOpened] = useState(false);
|
|
14
|
+
const rowRef = useRef(null);
|
|
15
|
+
const fit = useOverflowFit(rowRef, items.length);
|
|
16
|
+
const openSheet = useCallback(() => {
|
|
17
|
+
setSheetOpened(true);
|
|
18
|
+
}, []);
|
|
19
|
+
const closeSheet = useCallback(() => {
|
|
20
|
+
setSheetOpened(false);
|
|
21
|
+
}, []);
|
|
22
|
+
if (items.length === 0) return null;
|
|
23
|
+
const folded = items.slice(fit);
|
|
24
|
+
const anyFoldedOnMobile = items.length > inline;
|
|
25
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
26
|
+
/* @__PURE__ */ jsxs("div", { className: classes.pillRow, ref: rowRef, "data-filter-row": true, children: [
|
|
27
|
+
/* @__PURE__ */ jsx(FilterSetScope, { surface: "pill", registry, children: items.map((child, index) => (
|
|
28
|
+
// `Children.toArray` already stamped a stable `.0`/`.1`-style key on every child, so
|
|
29
|
+
// the slot wrapper reuses it rather than inventing an array-index key.
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
className: classes.pillSlot,
|
|
34
|
+
"data-filter-slot": true,
|
|
35
|
+
...index < inline && { "data-inline": true },
|
|
36
|
+
...index >= fit && { "data-folded": true },
|
|
37
|
+
children: child
|
|
38
|
+
},
|
|
39
|
+
slotKey(child, index)
|
|
40
|
+
)
|
|
41
|
+
)) }),
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
FilterPill,
|
|
44
|
+
{
|
|
45
|
+
label: `+${folded.length}`,
|
|
46
|
+
ariaLabel: `${folded.length} more filters`,
|
|
47
|
+
className: classes.foldPill,
|
|
48
|
+
hideGlyph: true,
|
|
49
|
+
shown: folded.length > 0,
|
|
50
|
+
children: /* @__PURE__ */ jsx("div", { className: classes.optionList, children: /* @__PURE__ */ jsx(FilterSetScope, { surface: "sheet", registry: null, children: folded }) })
|
|
51
|
+
}
|
|
52
|
+
),
|
|
53
|
+
anyFoldedOnMobile && /* @__PURE__ */ jsx(
|
|
54
|
+
FilterPill,
|
|
55
|
+
{
|
|
56
|
+
label: activeCount > 0 ? `Filters (${activeCount})` : "Filters",
|
|
57
|
+
active: activeCount > 0,
|
|
58
|
+
icon: /* @__PURE__ */ jsx(FunnelGlyph, {}),
|
|
59
|
+
className: classes.sheetPill,
|
|
60
|
+
hideGlyph: true,
|
|
61
|
+
onClick: openSheet
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
] }),
|
|
65
|
+
anyFoldedOnMobile && /* @__PURE__ */ jsx(FilterSheet, { opened: sheetOpened, onClose: closeSheet, onResetAll: resetAll, children: /* @__PURE__ */ jsx(FilterSetScope, { surface: "sheet", registry: null, children: items }) })
|
|
66
|
+
] });
|
|
67
|
+
}
|
|
68
|
+
function useOverflowFit(rowRef, count) {
|
|
69
|
+
const [fit, setFit] = useState(count);
|
|
70
|
+
const widths = useRef([]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const row = rowRef.current;
|
|
73
|
+
if (row === null) return;
|
|
74
|
+
if (typeof ResizeObserver === "undefined") {
|
|
75
|
+
setFit(count);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const measure = () => {
|
|
79
|
+
const slots = [...row.querySelectorAll("[data-filter-slot]")];
|
|
80
|
+
slots.forEach((slot, index) => {
|
|
81
|
+
const width = slot.offsetWidth;
|
|
82
|
+
if (width > 0) widths.current[index] = width;
|
|
83
|
+
});
|
|
84
|
+
const available = row.clientWidth;
|
|
85
|
+
if (available <= 0) {
|
|
86
|
+
setFit(count);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const gap = readGap(row);
|
|
90
|
+
const bare = fitCount(widths.current, count, gap, available, 0);
|
|
91
|
+
setFit(
|
|
92
|
+
bare === count ? count : fitCount(widths.current, count, gap, available, FOLD_PILL_WIDTH)
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
measure();
|
|
96
|
+
const observer = new ResizeObserver(measure);
|
|
97
|
+
observer.observe(row);
|
|
98
|
+
for (const slot of row.querySelectorAll("[data-filter-slot]")) {
|
|
99
|
+
observer.observe(slot);
|
|
100
|
+
}
|
|
101
|
+
return () => {
|
|
102
|
+
observer.disconnect();
|
|
103
|
+
};
|
|
104
|
+
}, [rowRef, count]);
|
|
105
|
+
return fit;
|
|
106
|
+
}
|
|
107
|
+
function fitCount(widths, count, gap, available, reserve) {
|
|
108
|
+
let used = reserve === 0 ? 0 : reserve + gap;
|
|
109
|
+
for (let index = 0; index < count; index += 1) {
|
|
110
|
+
used += (index === 0 ? 0 : gap) + (widths[index] ?? 0);
|
|
111
|
+
if (used > available) return index;
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
function slotKey(child, index) {
|
|
116
|
+
if (isValidElement(child) && child.key !== null) return child.key;
|
|
117
|
+
return String(index);
|
|
118
|
+
}
|
|
119
|
+
function readGap(row) {
|
|
120
|
+
const parsed = Number.parseFloat(getComputedStyle(row).columnGap);
|
|
121
|
+
return Number.isFinite(parsed) ? parsed : FALLBACK_GAP;
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
FilterSet
|
|
125
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-set.test.d.ts","sourceRoot":"","sources":["../../src/controls/filter-set.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type FilterSheetProps = {
|
|
3
|
+
readonly opened: boolean;
|
|
4
|
+
readonly onClose: () => void;
|
|
5
|
+
readonly onResetAll: () => void;
|
|
6
|
+
readonly title?: string;
|
|
7
|
+
readonly children: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* `Reset all` rides the HEADER, beside the title and the close ×, not a footer.
|
|
11
|
+
*
|
|
12
|
+
* A bottom drawer's footer is the one region a phone cannot promise is on screen: the sheet grows
|
|
13
|
+
* with its content, so with six filters open the reset button sat below the fold and the only way to
|
|
14
|
+
* reach it was to scroll past every row it would have undone. The header is fixed by construction.
|
|
15
|
+
* It also removes the sheet's last horizontal rule — a footer needs separating from the rows above
|
|
16
|
+
* it, a header does not (`docs/CONTROLS-SPEC.md` §2.1: hairlines between OPTION rows only).
|
|
17
|
+
*/
|
|
18
|
+
export declare function FilterSheet({ opened, onClose, onResetAll, title, children, }: FilterSheetProps): ReactNode;
|
|
19
|
+
/** One row of a {@link SheetOptionList}. */
|
|
20
|
+
export type SheetOption = {
|
|
21
|
+
readonly value: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly disabled?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type SheetOptionListProps = {
|
|
26
|
+
/** `single` renders radio semantics, `multi` checkbox semantics. */
|
|
27
|
+
readonly mode: 'single' | 'multi';
|
|
28
|
+
/** The selected value(s) — a one-element array in `single` mode. */
|
|
29
|
+
readonly selected: readonly string[];
|
|
30
|
+
readonly options: readonly SheetOption[];
|
|
31
|
+
readonly onToggle: (value: string) => void;
|
|
32
|
+
/** The `SheetField` heading this list is named by. */
|
|
33
|
+
readonly labelId: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The sheet's ONE list form (`docs/CONTROLS-SPEC.md` §2.1): every option is a 44px full-width row,
|
|
37
|
+
* label left, a check at the trailing edge when selected, a 1px hairline between rows and nowhere
|
|
38
|
+
* else.
|
|
39
|
+
*
|
|
40
|
+
* **It replaced a `SegmentedControl` and a Mantine `Radio` list, for two different reasons.** The
|
|
41
|
+
* range presets rendered as a `orientation="vertical"` SegmentedControl, which in a full-width sheet
|
|
42
|
+
* is a stack of left-aligned labels inside a tinted track — it read as a broken control, not as a
|
|
43
|
+
* choice, and it carried the track's own inset so it aligned with nothing else in the sheet. The
|
|
44
|
+
* enum/multi filters rendered Mantine `Radio`/`Checkbox` rows, which put a 20px dot or box in the
|
|
45
|
+
* leading position of every row; at six filters the sheet was a column of controls rather than a
|
|
46
|
+
* column of options, and the leading marks pushed every label off the sheet's own text column.
|
|
47
|
+
*
|
|
48
|
+
* The semantics are a real native `<input>` inside the row's `<label>`, visually hidden — not
|
|
49
|
+
* `role="radio"` on a div. A hidden native input keeps the arrow-key group behaviour, the
|
|
50
|
+
* form-association and the announced state for free, and `<label>` wrapping it makes the whole 44px
|
|
51
|
+
* box the hit target (C15) without a single `onClick` on a non-interactive element.
|
|
52
|
+
*/
|
|
53
|
+
export declare function SheetOptionList({ mode, selected, options, onToggle, labelId, }: SheetOptionListProps): ReactNode;
|
|
54
|
+
export type SheetDisclosureProps = {
|
|
55
|
+
readonly label: string;
|
|
56
|
+
readonly expanded: boolean;
|
|
57
|
+
readonly onToggle: () => void;
|
|
58
|
+
readonly children: ReactNode;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A 44px row that expands its own body inline — the sheet's answer to a control too large to sit in
|
|
62
|
+
* the list unconditionally (`RangeFilter`'s custom date picker, which is a whole calendar). Same row
|
|
63
|
+
* geometry as a {@link SheetOptionList} option, so the disclosure reads as the list's last entry
|
|
64
|
+
* rather than as a different kind of thing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function SheetDisclosure({ label, expanded, onToggle, children, }: SheetDisclosureProps): ReactNode;
|
|
67
|
+
export type SheetFieldProps = {
|
|
68
|
+
readonly label: string;
|
|
69
|
+
/** Stamped on the heading so a control can point `aria-labelledby` at it — see `useControlName`. */
|
|
70
|
+
readonly labelId?: string;
|
|
71
|
+
readonly children: ReactNode;
|
|
72
|
+
};
|
|
73
|
+
/** A labelled block inside the sheet — the sheet form's outer shell for every control. */
|
|
74
|
+
export declare function SheetField({ label, labelId, children }: SheetFieldProps): ReactNode;
|
|
75
|
+
/**
|
|
76
|
+
* Names a control that has no VISIBLE Mantine `label`, and does it differently per surface — which
|
|
77
|
+
* is the whole reason this is one helper instead of an `aria-label` sprinkled per call site.
|
|
78
|
+
*
|
|
79
|
+
* In the SHEET the visible `SheetField` heading is right there, so the control points at it
|
|
80
|
+
* (`aria-labelledby`) and the heading stops being decoration. In the PILL surface there is no
|
|
81
|
+
* visible name to point at — the pill text is the VALUE (`EUR`, `30d`, `3 channels`) — so the
|
|
82
|
+
* control carries the filter's own name instead. Either way the announced name is what is being
|
|
83
|
+
* filtered, not what it currently holds.
|
|
84
|
+
*
|
|
85
|
+
* The id is generated by the CONTROL, not by `SheetField`, because the control renders its own
|
|
86
|
+
* `SheetField`: an id published downward through context could never reach the sibling that needs it.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const { labelId, nameProps } = useControlName(label, inSheet)
|
|
90
|
+
* <Radio.Group {...nameProps} …/>
|
|
91
|
+
* // and, in the sheet branch: <SheetField label={label} labelId={labelId}>…</SheetField>
|
|
92
|
+
*/
|
|
93
|
+
export declare function useControlName(label: string, inSheet: boolean): {
|
|
94
|
+
labelId: string;
|
|
95
|
+
nameProps: {
|
|
96
|
+
'aria-labelledby': string;
|
|
97
|
+
} | {
|
|
98
|
+
'aria-label': string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
export type SheetRowProps = {
|
|
102
|
+
readonly children: ReactNode;
|
|
103
|
+
};
|
|
104
|
+
/** One 44px full-width row (`--vx-space-sheet-row-height`). */
|
|
105
|
+
export declare function SheetRow({ children }: SheetRowProps): ReactNode;
|
|
106
|
+
/** The class every sheet-form list item shares, for controls that render their own row element. */
|
|
107
|
+
export declare const sheetRowClass: string;
|
|
108
|
+
/**
|
|
109
|
+
* `classNames` for a Radio/Checkbox/Switch rendered as a sheet row — stretches its `body` and
|
|
110
|
+
* `<label>` across the full 44px so the whole row is the touch target, not the middle 20px of it.
|
|
111
|
+
*/
|
|
112
|
+
export declare const sheetRowClassNames: {
|
|
113
|
+
readonly root: string;
|
|
114
|
+
readonly body: string;
|
|
115
|
+
readonly label: string;
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=filter-sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-sheet.d.ts","sourceRoot":"","sources":["../../src/controls/filter-sheet.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,IAAI,CAAA;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,UAAU,EACV,KAAiB,EACjB,QAAQ,GACT,EAAE,gBAAgB,GAAG,SAAS,CAoB9B;AAqBD,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAA;IACjC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,OAAO,GACR,EAAE,oBAAoB,GAAG,SAAS,CA+ClC;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,SAAS,CAoBlC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,oGAAoG;IACpG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,SAAS,CASnF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf;IACD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE;QAAE,iBAAiB,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAA;CACpE,CAMA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,CAAA;AAED,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,GAAG,SAAS,CAE/D;AAED,mGAAmG;AACnG,eAAO,MAAM,aAAa,QAAmB,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAIrB,CAAA"}
|