basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time proof that law C6's "exactly one primary" is a TYPE fact, not a runtime check.
|
|
3
|
+
*
|
|
4
|
+
* `ActionGroupProps.primary` is singular, so a second one is unwritable: an array is rejected, and
|
|
5
|
+
* a duplicate object key is a tsc error in its own right. This mirrors the
|
|
6
|
+
* `apps/playground/src/*.type-guard.ts` convention (one `@ts-expect-error` per bad line, proven by
|
|
7
|
+
* `tsc --noEmit`) but lives beside the type, exactly as `agent-chat/virtualize.type-guard.test.ts`
|
|
8
|
+
* does — and for the same packaging reason: a bare `*.type-guard.ts` under `src/` would be picked
|
|
9
|
+
* up by tsup's build glob and ship an unused fixture, while `*.test.ts` is excluded from it.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, test } from 'bun:test'
|
|
12
|
+
import type { ActionGroupProps, BarAction, GlobalAction } from './actions'
|
|
13
|
+
|
|
14
|
+
function accept(props: ActionGroupProps): ActionGroupProps {
|
|
15
|
+
return props
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function acceptAction(action: BarAction): BarAction {
|
|
19
|
+
return action
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function acceptGlobal(action: GlobalAction): GlobalAction {
|
|
23
|
+
return action
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Valid — must type-check with no error ─────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
accept({})
|
|
29
|
+
accept({ primary: { key: 'a', label: 'A' } })
|
|
30
|
+
accept({ primary: { key: 'a', label: 'A' }, secondary: [{ key: 'b', label: 'B' }] })
|
|
31
|
+
accept({ secondary: [{ key: 'm', kind: 'menu', label: 'Export as', items: [] }] })
|
|
32
|
+
accept({ primary: { key: 'c', kind: 'custom', node: null } })
|
|
33
|
+
acceptAction({ key: 'a', label: 'A', mobile: 'hidden', danger: true })
|
|
34
|
+
acceptGlobal({ key: 'g', node: null, mobile: 'bar' })
|
|
35
|
+
|
|
36
|
+
// ── Invalid — each MUST be a tsc error, one directive per bad line ────────────
|
|
37
|
+
|
|
38
|
+
const TWO: BarAction[] = [
|
|
39
|
+
{ key: 'a', label: 'A' },
|
|
40
|
+
{ key: 'b', label: 'B' },
|
|
41
|
+
]
|
|
42
|
+
// @ts-expect-error a SECOND primary is unwritable: `primary` is one action, never a list
|
|
43
|
+
accept({ primary: TWO })
|
|
44
|
+
// @ts-expect-error `secondary` is the list; a single action is not one
|
|
45
|
+
accept({ secondary: { key: 'b', label: 'B' } })
|
|
46
|
+
// @ts-expect-error a `kind: 'menu'` group has no mobile placement — it always folds
|
|
47
|
+
acceptAction({ key: 'm', kind: 'menu', label: 'M', items: [], mobile: 'bar' })
|
|
48
|
+
// @ts-expect-error a `kind: 'custom'` action carries a node, never a label
|
|
49
|
+
acceptAction({ key: 'c', kind: 'custom', node: null, label: 'C' })
|
|
50
|
+
// @ts-expect-error `label` is required on a button-shaped action
|
|
51
|
+
acceptAction({ key: 'a' })
|
|
52
|
+
// @ts-expect-error 'sheet' is not a mobile placement
|
|
53
|
+
acceptAction({ key: 'a', label: 'A', mobile: 'sheet' })
|
|
54
|
+
// @ts-expect-error a GlobalAction is data, not a bare node
|
|
55
|
+
acceptGlobal({ node: null })
|
|
56
|
+
|
|
57
|
+
describe('ActionGroupProps (type-guard)', () => {
|
|
58
|
+
test('is a compile-time-only fixture — see the @ts-expect-error directives above', () => {
|
|
59
|
+
// No runtime behavior to assert: the proof is that this file type-checks at all. A single
|
|
60
|
+
// trivial assertion keeps this a normal bun:test file rather than an empty-suite oddity.
|
|
61
|
+
expect(true).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CompareFilter` — the period-comparison enum every dashboard grows
|
|
3
|
+
* (`docs/CONTROLS-SPEC.md` §3). A `SelectFilter` narrowed to the three values basalt names, so the
|
|
4
|
+
* label, the option order and the fallback are the same on every page instead of three per app.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // compare: field.enum(['none', 'previous', 'year'], 'none')
|
|
8
|
+
* <CompareFilter field={analytics.field.compare} />
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
import type { EnumField, FieldHandle } from '../state'
|
|
12
|
+
import { EnumFilter } from './enum-filter'
|
|
13
|
+
import { SwapGlyph } from './glyphs'
|
|
14
|
+
|
|
15
|
+
/** The three comparison bases — declare the field as `field.enum(COMPARE_VALUES, 'none')`. */
|
|
16
|
+
export type CompareValue = 'none' | 'previous' | 'year'
|
|
17
|
+
|
|
18
|
+
/** The declared value order, so a consumer's `field.enum` and this control cannot disagree. */
|
|
19
|
+
export const COMPARE_VALUES: readonly CompareValue[] = ['none', 'previous', 'year']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The three option labels basalt names — the other half of "the same on every page instead of three
|
|
23
|
+
* per app", which this control promised and did not deliver.
|
|
24
|
+
*
|
|
25
|
+
* `field.enum` labels an option with the raw VALUE until a consumer calls `store.labels()`, so a
|
|
26
|
+
* `CompareFilter` over an unlabelled field printed `none` / `previous` / `year` in its popover and,
|
|
27
|
+
* now that the pill reads the value (see `EnumFilter`'s doc), in the bar too. Every app then wrote
|
|
28
|
+
* the same three strings into its own `labels()` call, which is three chances to disagree about what
|
|
29
|
+
* `year` means.
|
|
30
|
+
*
|
|
31
|
+
* These are DEFAULTS, not overrides: a field whose label differs from its value has been labelled
|
|
32
|
+
* deliberately, and that label wins. Detecting it as `label !== value` is exactly the signal
|
|
33
|
+
* `optionsFor` leaves behind (`state/fields.ts` — `label: map?.[value] ?? value`), so nothing new
|
|
34
|
+
* has to be threaded through the store to ask the question.
|
|
35
|
+
*/
|
|
36
|
+
export const COMPARE_LABELS: Readonly<Record<CompareValue, string>> = {
|
|
37
|
+
none: 'No comparison',
|
|
38
|
+
previous: 'Previous period',
|
|
39
|
+
year: 'Same period last year',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CompareFilterProps = {
|
|
43
|
+
readonly field: FieldHandle<EnumField<CompareValue>>
|
|
44
|
+
/** @default 'Compare' */
|
|
45
|
+
readonly label?: string
|
|
46
|
+
/** Leading pill icon. Defaults to an arrows-swap glyph — like `RangeFilter`'s calendar, the mark
|
|
47
|
+
* is part of what makes the pill legible as a period comparison. */
|
|
48
|
+
readonly icon?: ReactNode
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function CompareFilter({ field, label = 'Compare', icon }: CompareFilterProps): ReactNode {
|
|
52
|
+
// Read through the handle's own options so a consumer's `labels()` and its option ORDER both
|
|
53
|
+
// survive; only an UNLABELLED row (label === value) takes basalt's default string.
|
|
54
|
+
const options = field.options.map((option) =>
|
|
55
|
+
option.label === option.value && isCompareValue(option.value)
|
|
56
|
+
? { value: option.value, label: COMPARE_LABELS[option.value] }
|
|
57
|
+
: option,
|
|
58
|
+
)
|
|
59
|
+
return <EnumFilter field={field} label={label} icon={icon ?? <SwapGlyph />} options={options} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isCompareValue(value: string): value is CompareValue {
|
|
63
|
+
return (COMPARE_VALUES as readonly string[]).includes(value)
|
|
64
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ControlGroup` — the joined-control primitive (`docs/CONTROLS-SPEC.md` §3), and `ActionGroup`'s
|
|
3
|
+
* derived use of it.
|
|
4
|
+
*
|
|
5
|
+
* The gap it closes: every consumer that wanted a `‹ Today ›` stepper hand-built one — a `Group
|
|
6
|
+
* gap={0}` plus per-child `radius` and `style={{ marginLeft: -1 }}`, restated on each page, each
|
|
7
|
+
* getting the focus-ring z-index wrong in its own way. `basalt/shadow-basalt-export` now names the
|
|
8
|
+
* three words that fork tends to arrive under (`ButtonGroup`, `ButtonRow`, `JoinedButtons`).
|
|
9
|
+
*
|
|
10
|
+
* The GEOMETRY is CSS, and CSS-module hashes are unavailable under `bun test`, so it is asserted
|
|
11
|
+
* from the module text (the idiom `filter-set.test.tsx` and `theme/layout-rhythm-css.test.ts` use).
|
|
12
|
+
* The PARTITION — which adjacent actions get joined — is real logic and is rendered.
|
|
13
|
+
*/
|
|
14
|
+
import { ActionIcon, Button, MantineProvider } from '@mantine/core'
|
|
15
|
+
import { render } from '@testing-library/react'
|
|
16
|
+
import { describe, expect, test } from 'bun:test'
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import type { ReactNode } from 'react'
|
|
20
|
+
import { ActionGroup } from './actions'
|
|
21
|
+
import type { BarAction } from './actions'
|
|
22
|
+
import { ControlGroup } from './control-group'
|
|
23
|
+
|
|
24
|
+
const CSS = readFileSync(join(import.meta.dir, 'controls.module.css'), 'utf8')
|
|
25
|
+
const JOINED = CSS.slice(CSS.indexOf('/* ── ControlGroup'))
|
|
26
|
+
|
|
27
|
+
function mount(node: ReactNode) {
|
|
28
|
+
return render(<MantineProvider>{node}</MantineProvider>)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const desktop = () => document.querySelector('.mantine-visible-from-sm')
|
|
32
|
+
const mobile = () => document.querySelector('.mantine-hidden-from-sm')
|
|
33
|
+
|
|
34
|
+
/** The joined boxes inside one variant of the row. */
|
|
35
|
+
function groups(root: Element | null): Element[] {
|
|
36
|
+
return [...(root?.querySelectorAll('[data-gap]') ?? [])]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Glyph(): ReactNode {
|
|
40
|
+
return <svg viewBox="0 0 24 24" />
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('the joined box', () => {
|
|
44
|
+
test('renders one element carrying the gap mode, defaulting to "none"', () => {
|
|
45
|
+
const { container } = mount(
|
|
46
|
+
<ControlGroup>
|
|
47
|
+
<Button variant="default">Today</Button>
|
|
48
|
+
</ControlGroup>,
|
|
49
|
+
)
|
|
50
|
+
expect(container.querySelector('[data-gap]')?.getAttribute('data-gap')).toBe('none')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('gap="tight" is the un-joined mode, for a set that must stay separately pressable', () => {
|
|
54
|
+
const { container } = mount(
|
|
55
|
+
<ControlGroup gap="tight">
|
|
56
|
+
<Button variant="default">A</Button>
|
|
57
|
+
<Button variant="default">B</Button>
|
|
58
|
+
</ControlGroup>,
|
|
59
|
+
)
|
|
60
|
+
expect(container.querySelector('[data-gap]')?.getAttribute('data-gap')).toBe('tight')
|
|
61
|
+
expect(JOINED).toContain("[data-gap='tight'] {\n gap: var(--vx-space-control-gap")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('joined children collapse their two adjacent borders into one', () => {
|
|
65
|
+
// 2px down the middle reads as a gap someone forgot to close, not as a divider.
|
|
66
|
+
expect(JOINED).toContain("[data-gap='none'] > * + * {\n margin-inline-start: -1px;")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('the radius lives on the outer ends only — that is what makes three boxes one box', () => {
|
|
70
|
+
expect(JOINED).toContain("[data-gap='none'] > *:not(:first-child):not(:last-child) {")
|
|
71
|
+
expect(JOINED).toContain('border-radius: 0;')
|
|
72
|
+
expect(JOINED).toContain("[data-gap='none'] > *:first-child:not(:only-child) {")
|
|
73
|
+
expect(JOINED).toContain("[data-gap='none'] > *:last-child:not(:only-child) {")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('a one-child group keeps its normal shape — :only-child is excluded on both ends', () => {
|
|
77
|
+
// A conditional render that dropped two of three children must not lose half its corners.
|
|
78
|
+
expect(JOINED).toContain(':first-child:not(:only-child)')
|
|
79
|
+
expect(JOINED).toContain(':last-child:not(:only-child)')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('the internal hairline is drawn here, because the default border is transparent', () => {
|
|
83
|
+
// `cssVariablesResolver` points `--mantine-color-default-border` at transparent on purpose
|
|
84
|
+
// (DESIGN-SPEC §8). The 12%-ink value is `.pill`'s, so a joined pill row and a joined button row
|
|
85
|
+
// read as one family.
|
|
86
|
+
expect(JOINED).toContain("[data-gap='none'] > [data-variant='default'] {")
|
|
87
|
+
expect(JOINED).toContain('color-mix(in srgb, var(--vx-ink) 12%, transparent)')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('a filled child keeps its fill edge — the border rule is scoped to the default variant', () => {
|
|
91
|
+
expect(JOINED).not.toMatch(/\[data-gap='none'\] > \* \{\s*border-color/)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('hover/focus/active raise the child so its shared edge paints whole', () => {
|
|
95
|
+
expect(JOINED).toContain("[data-gap='none'] > *:focus-visible,")
|
|
96
|
+
expect(JOINED).toContain('z-index: 1;')
|
|
97
|
+
expect(JOINED).toContain('position: relative;')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('the group never wraps — a joined set that broke mid-box is worse than an overflow', () => {
|
|
101
|
+
const rule = JOINED.slice(
|
|
102
|
+
JOINED.indexOf('.group {'),
|
|
103
|
+
JOINED.indexOf('}', JOINED.indexOf('.group {')),
|
|
104
|
+
)
|
|
105
|
+
expect(rule).toContain('flex-wrap: nowrap')
|
|
106
|
+
expect(rule).toContain('flex: none')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('it adds no role and no label — a joined LOOK is not a semantic group', () => {
|
|
110
|
+
const { container } = mount(
|
|
111
|
+
<ControlGroup>
|
|
112
|
+
<ActionIcon variant="default" aria-label="Previous">
|
|
113
|
+
<Glyph />
|
|
114
|
+
</ActionIcon>
|
|
115
|
+
<Button variant="default">Today</Button>
|
|
116
|
+
</ControlGroup>,
|
|
117
|
+
)
|
|
118
|
+
const box = container.querySelector('[data-gap]')
|
|
119
|
+
expect(box?.hasAttribute('role')).toBe(false)
|
|
120
|
+
expect(box?.hasAttribute('aria-label')).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('ActionGroup joins what belongs together, and nothing else', () => {
|
|
125
|
+
const stepper: BarAction[] = [
|
|
126
|
+
{ key: 'prev', label: 'Previous period', icon: <Glyph />, group: true },
|
|
127
|
+
{ key: 'today', label: 'Today', group: true },
|
|
128
|
+
{ key: 'next', label: 'Next period', icon: <Glyph />, group: true },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
test('a run of group:true secondaries becomes ONE joined box on desktop', () => {
|
|
132
|
+
mount(<ActionGroup secondary={stepper} />)
|
|
133
|
+
const joined = groups(desktop())
|
|
134
|
+
expect(joined.length).toBe(1)
|
|
135
|
+
expect(joined[0]?.querySelectorAll('button').length).toBe(3)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('a joined member with an icon renders icon-only, its label demoted to the a11y name', () => {
|
|
139
|
+
// `‹ Today ›` is ONE control whose middle segment names it; the arrows' content is their
|
|
140
|
+
// direction. Labelled, the same three actions measured 304px against 118px in Chrome.
|
|
141
|
+
mount(<ActionGroup secondary={stepper} />)
|
|
142
|
+
const row = desktop()
|
|
143
|
+
const prev = row?.querySelector('[aria-label="Previous period"]')
|
|
144
|
+
expect(prev).not.toBeNull()
|
|
145
|
+
expect(prev?.textContent).toBe('')
|
|
146
|
+
// The one member with no icon keeps its label — the rule reads what the caller supplied.
|
|
147
|
+
expect(row?.textContent).toBe('Today')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('a joined set with NO icons keeps every label — the icon-only rule is not a width policy', () => {
|
|
151
|
+
mount(
|
|
152
|
+
<ActionGroup
|
|
153
|
+
secondary={[
|
|
154
|
+
{ key: 'abs', label: 'Absolute', group: true },
|
|
155
|
+
{ key: 'rate', label: 'Rate', group: true },
|
|
156
|
+
]}
|
|
157
|
+
/>,
|
|
158
|
+
)
|
|
159
|
+
expect(groups(desktop()).length).toBe(1)
|
|
160
|
+
expect(desktop()?.textContent).toBe('AbsoluteRate')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('independent secondaries are NOT joined — the default row is unchanged', () => {
|
|
164
|
+
mount(
|
|
165
|
+
<ActionGroup
|
|
166
|
+
secondary={[
|
|
167
|
+
{ key: 'export', label: 'Export CSV' },
|
|
168
|
+
{ key: 'accounts', label: 'Accounts', icon: <Glyph /> },
|
|
169
|
+
]}
|
|
170
|
+
/>,
|
|
171
|
+
)
|
|
172
|
+
expect(groups(desktop()).length).toBe(0)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('a non-group action between two group ones splits the run in two', () => {
|
|
176
|
+
// Two runs of one is the same row it was before — the group flag is per-member on purpose, so a
|
|
177
|
+
// set that forgets one member degrades to the old row instead of joining the wrong things.
|
|
178
|
+
mount(
|
|
179
|
+
<ActionGroup
|
|
180
|
+
secondary={[
|
|
181
|
+
{ key: 'prev', label: 'Prev', group: true },
|
|
182
|
+
{ key: 'loose', label: 'Loose' },
|
|
183
|
+
{ key: 'next', label: 'Next', group: true },
|
|
184
|
+
]}
|
|
185
|
+
/>,
|
|
186
|
+
)
|
|
187
|
+
expect(groups(desktop()).length).toBe(0)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('adjacent ICON-ONLY entries join on the MOBILE bar without any flag', () => {
|
|
191
|
+
// Below `sm` an icon-bearing action renders as an `ActionIcon` and nothing else, so two in a row
|
|
192
|
+
// are two 30px squares with a gap — three boxes' worth of border for two actions, exactly where
|
|
193
|
+
// width is scarcest.
|
|
194
|
+
mount(
|
|
195
|
+
<ActionGroup
|
|
196
|
+
secondary={[
|
|
197
|
+
{ key: 'a', label: 'Filter', icon: <Glyph />, mobile: 'bar' },
|
|
198
|
+
{ key: 'b', label: 'Sort', icon: <Glyph />, mobile: 'bar' },
|
|
199
|
+
]}
|
|
200
|
+
/>,
|
|
201
|
+
)
|
|
202
|
+
expect(groups(mobile()).length).toBe(1)
|
|
203
|
+
// …and NOT on desktop, where the same two actions are labelled buttons that are not related.
|
|
204
|
+
expect(groups(desktop()).length).toBe(0)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('the mobile PRIMARY is never joined to a sibling — a fill edge against a border edge', () => {
|
|
208
|
+
mount(
|
|
209
|
+
<ActionGroup
|
|
210
|
+
primary={{ key: 'save', label: 'Save', icon: <Glyph /> }}
|
|
211
|
+
secondary={[{ key: 'a', label: 'Filter', icon: <Glyph />, mobile: 'bar' }]}
|
|
212
|
+
/>,
|
|
213
|
+
)
|
|
214
|
+
expect(groups(mobile()).length).toBe(0)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('a kind:"custom" entry breaks the run — basalt does not draw that node', () => {
|
|
218
|
+
mount(
|
|
219
|
+
<ActionGroup
|
|
220
|
+
secondary={[
|
|
221
|
+
{ key: 'prev', label: 'Prev', group: true },
|
|
222
|
+
{ key: 'chip', kind: 'custom', node: <span>live</span> },
|
|
223
|
+
{ key: 'next', label: 'Next', group: true },
|
|
224
|
+
]}
|
|
225
|
+
/>,
|
|
226
|
+
)
|
|
227
|
+
expect(groups(desktop()).length).toBe(0)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ControlGroup` — adjacent controls that act on ONE thing, joined into one box
|
|
3
|
+
* (`docs/CONTROLS-SPEC.md` §3). Blueprint's `ControlGroup` and Mantine's own `Button.Group` are the
|
|
4
|
+
* same primitive; neither works here, because basalt's tier holds `FilterPill`s, `Button`s,
|
|
5
|
+
* `ActionIcon`s and `TextInput`s in the same row and Mantine's `Button.Group` joins Buttons only
|
|
6
|
+
* (it declares its own `section-*` size vars, which the `ctl` tier does not define — see
|
|
7
|
+
* `theme/ctl-tier-coverage.test.ts`'s note on why `*Group` directories are excluded from the scan).
|
|
8
|
+
*
|
|
9
|
+
* **What it is for.** A `‹ Today ›` date stepper, a `− 1 +` quantity, an `Absolute | Rate` pair — a
|
|
10
|
+
* set of controls a reader should read as ONE control with several affordances. The joined box is the
|
|
11
|
+
* statement: these three act on the same value, and the two dividers inside it are the only place a
|
|
12
|
+
* hairline is a divider rather than a border (`docs/DESIGN-SPEC.md` §8 — depth is a shadow). A row of
|
|
13
|
+
* INDEPENDENT actions is not this; that is `ActionGroup`, and joining it would claim a relationship
|
|
14
|
+
* that is not there.
|
|
15
|
+
*
|
|
16
|
+
* **Presentational by design — no `role`, no `aria-label`.** A joined LOOK is not a semantic group:
|
|
17
|
+
* each child is already a button or an input with its own accessible name, and wrapping them in a
|
|
18
|
+
* `role="group"` adds a level to the a11y tree that announces nothing a reader needs. Where the set
|
|
19
|
+
* genuinely IS one control with one name — a range picker, a tab set — basalt already ships that
|
|
20
|
+
* control (`RangeFilter`, `ViewTabs`) with the right semantics, and reaching for `ControlGroup`
|
|
21
|
+
* instead is `basalt/hand-rolled-filter`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // The one shape this exists for: three affordances, one value.
|
|
25
|
+
* <ControlGroup>
|
|
26
|
+
* <ActionIcon variant="default" aria-label="Previous period" onClick={back}><IconLeft /></ActionIcon>
|
|
27
|
+
* <Button variant="default" onClick={today}>Today</Button>
|
|
28
|
+
* <ActionIcon variant="default" aria-label="Next period" onClick={next}><IconRight /></ActionIcon>
|
|
29
|
+
* </ControlGroup>
|
|
30
|
+
*/
|
|
31
|
+
import { Box } from '@mantine/core'
|
|
32
|
+
import type { ReactNode } from 'react'
|
|
33
|
+
import classes from './controls.module.css'
|
|
34
|
+
|
|
35
|
+
export type ControlGroupProps = {
|
|
36
|
+
readonly children: ReactNode
|
|
37
|
+
/**
|
|
38
|
+
* `none` (the default) JOINS the children: no gap, one shared hairline between each pair, and the
|
|
39
|
+
* corner radius only on the outer ends, so the set reads as one box.
|
|
40
|
+
*
|
|
41
|
+
* `tight` keeps the tier's own control gap and only guarantees the alignment and the no-wrap. It
|
|
42
|
+
* is for a set that belongs together but must stay separately pressable — three destructive
|
|
43
|
+
* actions, or a pair where one child is `filled` and joining it would put a fill edge against a
|
|
44
|
+
* border edge.
|
|
45
|
+
*
|
|
46
|
+
* @default 'none'
|
|
47
|
+
*/
|
|
48
|
+
readonly gap?: 'none' | 'tight'
|
|
49
|
+
readonly className?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ControlGroup({ children, gap = 'none', className }: ControlGroupProps): ReactNode {
|
|
53
|
+
return (
|
|
54
|
+
// A Mantine `Box`, not a raw `<div>` — `basalt/raw-html-layout` flags layout styling on a bare
|
|
55
|
+
// HTML tag, the same reason `CtlSlot` uses one.
|
|
56
|
+
<Box
|
|
57
|
+
className={className === undefined ? classes.group : `${classes.group} ${className}`}
|
|
58
|
+
data-gap={gap}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</Box>
|
|
62
|
+
)
|
|
63
|
+
}
|