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,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IconSlot` — the one icon box (`docs/CONTROLS-SPEC.md` §3).
|
|
3
|
+
*
|
|
4
|
+
* The bug this file exists to prevent: a consumer's inline `<svg width="24" height="24">` handed to
|
|
5
|
+
* a basalt control's `icon` prop landed in Mantine's `leftSection`, which is a bare `flex` box with
|
|
6
|
+
* no size of its own. The glyph painted at 24px inside a 30px `ctl` button, sat on the parent's text
|
|
7
|
+
* baseline (so it hung low of the optical centre by the line box's descender), and set the row's
|
|
8
|
+
* height. Measured in Chrome on the playground's header: the `Accounts` secondary read visibly low
|
|
9
|
+
* and left of centre.
|
|
10
|
+
*
|
|
11
|
+
* Two halves, because neither alone would hold. The CSS-TEXT half pins the mechanism (a fixed square
|
|
12
|
+
* box, `100%` on the glyph, `display: block`) — CSS-module class hashes are unavailable under
|
|
13
|
+
* `bun test`, so the declarations themselves are read from the file, the same idiom
|
|
14
|
+
* `controls/filter-set.test.tsx` and `theme/layout-rhythm-css.test.ts` use. The RENDER half pins that
|
|
15
|
+
* every home named by the spec actually routes its `icon` through the slot, found by the
|
|
16
|
+
* `data-basalt-icon` marker attribute rather than by a class.
|
|
17
|
+
*/
|
|
18
|
+
import { MantineProvider } from '@mantine/core'
|
|
19
|
+
import { render } from '@testing-library/react'
|
|
20
|
+
import { describe, expect, test } from 'bun:test'
|
|
21
|
+
import { readFileSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import type { ReactNode } from 'react'
|
|
24
|
+
import { ActionGroup } from '../controls/actions'
|
|
25
|
+
import { FilterPill } from '../controls/filter-pill'
|
|
26
|
+
import { SyncButton } from '../controls/sync-button'
|
|
27
|
+
import { WidgetHeader } from '../widget-header/widget-header'
|
|
28
|
+
import { IconSlot } from './icon-slot'
|
|
29
|
+
|
|
30
|
+
const ICON_SLOT_CSS = readFileSync(join(import.meta.dir, 'icon-slot.module.css'), 'utf8')
|
|
31
|
+
const CONTROLS_CSS = readFileSync(join(import.meta.dir, '../controls/controls.module.css'), 'utf8')
|
|
32
|
+
const WIDGET_HEADER_CSS = readFileSync(
|
|
33
|
+
join(import.meta.dir, '../widget-header/widget-header.module.css'),
|
|
34
|
+
'utf8',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** A consumer glyph that declares its OWN geometry — the exact shape that used to leak through. */
|
|
38
|
+
function OversizedGlyph(): ReactNode {
|
|
39
|
+
return (
|
|
40
|
+
<svg width={24} height={24} viewBox="0 0 24 24" data-testid="glyph">
|
|
41
|
+
<circle cx={12} cy={12} r={10} />
|
|
42
|
+
</svg>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A consumer glyph with no `width`/`height` and no `viewBox` — paints 300×150 unstyled. */
|
|
47
|
+
function UnsizedGlyph(): ReactNode {
|
|
48
|
+
return (
|
|
49
|
+
<svg data-testid="glyph">
|
|
50
|
+
<circle cx={8} cy={8} r={8} />
|
|
51
|
+
</svg>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function slots(container: HTMLElement): HTMLElement[] {
|
|
56
|
+
return [...container.querySelectorAll<HTMLElement>('[data-basalt-icon]')]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The slot wrapping the marked glyph — asserts the glyph is a DIRECT child, which the `> svg`
|
|
60
|
+
* selector requires to bite. */
|
|
61
|
+
function slotAround(container: HTMLElement): HTMLElement | null {
|
|
62
|
+
const glyph = container.querySelector('[data-testid="glyph"]')
|
|
63
|
+
const parent = glyph?.parentElement ?? null
|
|
64
|
+
return parent !== null && parent.hasAttribute('data-basalt-icon') ? parent : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('the box is declared once, in CSS, and cannot be set at a call site', () => {
|
|
68
|
+
test('the slot is a fixed square off --vx-space-icon-size with a 16px fallback', () => {
|
|
69
|
+
expect(ICON_SLOT_CSS).toContain('width: var(--vx-space-icon-size, 1rem)')
|
|
70
|
+
expect(ICON_SLOT_CSS).toContain('height: var(--vx-space-icon-size, 1rem)')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('the slot never participates in its row’s flex sizing', () => {
|
|
74
|
+
expect(ICON_SLOT_CSS).toContain('flex: none')
|
|
75
|
+
expect(ICON_SLOT_CSS).toContain('display: inline-flex')
|
|
76
|
+
expect(ICON_SLOT_CSS).toContain('align-items: center')
|
|
77
|
+
expect(ICON_SLOT_CSS).toContain('justify-content: center')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('the glyph is restated at 100% and taken off the baseline — the two halves of "centred"', () => {
|
|
81
|
+
const rule = ICON_SLOT_CSS.slice(ICON_SLOT_CSS.indexOf('.slot > svg'))
|
|
82
|
+
expect(rule).toContain('width: 100%')
|
|
83
|
+
expect(rule).toContain('height: 100%')
|
|
84
|
+
expect(rule).toContain('display: block')
|
|
85
|
+
// Presentation attributes lose to CSS, which is the whole mechanism — a 24px, a 14px and an
|
|
86
|
+
// attribute-less SVG all resolve to the box.
|
|
87
|
+
expect(rule).toContain('object-fit: contain')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('`img` and `picture` icons get the same box — an icon set is as likely to be a sprite', () => {
|
|
91
|
+
expect(ICON_SLOT_CSS).toContain('.slot > img')
|
|
92
|
+
expect(ICON_SLOT_CSS).toContain('.slot > picture')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('IconSlot has no size prop — a call site cannot pick a geometry', () => {
|
|
96
|
+
const source = readFileSync(join(import.meta.dir, 'icon-slot.tsx'), 'utf8')
|
|
97
|
+
const props = source.slice(
|
|
98
|
+
source.indexOf('export type IconSlotProps'),
|
|
99
|
+
source.indexOf('export function IconSlot'),
|
|
100
|
+
)
|
|
101
|
+
expect(props).not.toContain('size')
|
|
102
|
+
// `children` + `className` and nothing else.
|
|
103
|
+
expect(props.match(/readonly \w+\??:/g)).toEqual(['readonly children:', 'readonly className?:'])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a home sizes the slot by overriding the box VAR, never by sizing the glyph', () => {
|
|
107
|
+
// The trailing pill affordance is 14px and the widget-tier heading icon is 14px. Both get there
|
|
108
|
+
// through `--vx-space-icon-size`; neither may reintroduce a per-component `> svg` size rule,
|
|
109
|
+
// which is the duplication that let `BarAction` ship with no box at all.
|
|
110
|
+
expect(CONTROLS_CSS).toContain('--vx-space-icon-size: 0.875rem')
|
|
111
|
+
expect(WIDGET_HEADER_CSS).toContain('--vx-space-icon-size: 0.875rem')
|
|
112
|
+
expect(WIDGET_HEADER_CSS).toContain('--vx-space-icon-size: 1rem')
|
|
113
|
+
expect(CONTROLS_CSS).not.toMatch(/\.pillIcon\s*>\s*svg/)
|
|
114
|
+
expect(WIDGET_HEADER_CSS).not.toMatch(/\.icon\s*>\s*svg/)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('every home named by the spec routes `icon` through the slot', () => {
|
|
119
|
+
test('a BarAction secondary — an oversized consumer SVG lands inside the slot', () => {
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<MantineProvider>
|
|
122
|
+
<ActionGroup
|
|
123
|
+
secondary={[{ key: 'accounts', label: 'Accounts', icon: <OversizedGlyph /> }]}
|
|
124
|
+
/>
|
|
125
|
+
</MantineProvider>,
|
|
126
|
+
)
|
|
127
|
+
// Both viewport variants mount (law C9), so the glyph appears twice; every occurrence is slotted.
|
|
128
|
+
const glyphs = [...container.querySelectorAll('[data-testid="glyph"]')]
|
|
129
|
+
expect(glyphs.length).toBeGreaterThan(0)
|
|
130
|
+
for (const glyph of glyphs) {
|
|
131
|
+
expect(glyph.parentElement?.hasAttribute('data-basalt-icon')).toBe(true)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('a BarAction PRIMARY too — the mobile icon-only form is the same box', () => {
|
|
136
|
+
const { container } = render(
|
|
137
|
+
<MantineProvider>
|
|
138
|
+
<ActionGroup primary={{ key: 'save', label: 'Save as report', icon: <UnsizedGlyph /> }} />
|
|
139
|
+
</MantineProvider>,
|
|
140
|
+
)
|
|
141
|
+
const glyphs = [...container.querySelectorAll('[data-testid="glyph"]')]
|
|
142
|
+
expect(glyphs.length).toBeGreaterThan(0)
|
|
143
|
+
for (const glyph of glyphs) {
|
|
144
|
+
expect(glyph.parentElement?.hasAttribute('data-basalt-icon')).toBe(true)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('an OverflowMenu row — the fold is a home too', () => {
|
|
149
|
+
const { container } = render(
|
|
150
|
+
<MantineProvider>
|
|
151
|
+
<ActionGroup
|
|
152
|
+
secondary={[
|
|
153
|
+
{ key: 'a', label: 'A' },
|
|
154
|
+
{ key: 'b', label: 'B' },
|
|
155
|
+
{ key: 'c', label: 'C' },
|
|
156
|
+
{ key: 'd', label: 'D', icon: <OversizedGlyph /> },
|
|
157
|
+
]}
|
|
158
|
+
/>
|
|
159
|
+
</MantineProvider>,
|
|
160
|
+
)
|
|
161
|
+
// The `More` dropdown mounts lazily, so assert the fold exists and its trigger is present; the
|
|
162
|
+
// ROW's slotting is covered by the source guard below, which reads the same code path.
|
|
163
|
+
expect(container.textContent).toContain('More')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('a FilterPill — the leading glyph is slotted (every enum filter renders through it)', () => {
|
|
167
|
+
const { container } = render(
|
|
168
|
+
<MantineProvider>
|
|
169
|
+
<FilterPill label="EUR" icon={<OversizedGlyph />} />
|
|
170
|
+
</MantineProvider>,
|
|
171
|
+
)
|
|
172
|
+
expect(slotAround(container)).not.toBeNull()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('a FilterPill’s trailing affordance is slotted too, at the 14px box', () => {
|
|
176
|
+
const { container } = render(
|
|
177
|
+
<MantineProvider>
|
|
178
|
+
<FilterPill label="EUR" />
|
|
179
|
+
</MantineProvider>,
|
|
180
|
+
)
|
|
181
|
+
// The `⇅` affordance is basalt's own glyph and still goes through the slot — the box is the
|
|
182
|
+
// slot's on every icon in the tier, framework-drawn or consumer-passed.
|
|
183
|
+
expect(slots(container).length).toBe(1)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('a WidgetHeader — tier="widget"', () => {
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<WidgetHeader tier="widget" title="Active users" icon={<OversizedGlyph />} />,
|
|
189
|
+
)
|
|
190
|
+
expect(slotAround(container)).not.toBeNull()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('a WidgetHeader — tier="section" (and StatCard, which forwards `icon` to it)', () => {
|
|
194
|
+
const { container } = render(
|
|
195
|
+
<WidgetHeader tier="section" title="Funnel & retention" icon={<UnsizedGlyph />} />,
|
|
196
|
+
)
|
|
197
|
+
expect(slotAround(container)).not.toBeNull()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('a SyncButton — basalt’s own glyph obeys the same box', () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<MantineProvider>
|
|
203
|
+
<SyncButton scope="page" syncing={false} onSync={() => {}} />
|
|
204
|
+
</MantineProvider>,
|
|
205
|
+
)
|
|
206
|
+
expect(slots(container).length).toBeGreaterThan(0)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('the slot is decorative and queryable, by construction', () => {
|
|
211
|
+
test('aria-hidden is unconditional — the control carries the name, never the icon', () => {
|
|
212
|
+
const { container } = render(
|
|
213
|
+
<IconSlot>
|
|
214
|
+
<OversizedGlyph />
|
|
215
|
+
</IconSlot>,
|
|
216
|
+
)
|
|
217
|
+
expect(container.querySelector('[data-basalt-icon]')?.getAttribute('aria-hidden')).toBe('true')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('a home’s className is composed ONTO the slot, never in place of it', () => {
|
|
221
|
+
const { container } = render(
|
|
222
|
+
<IconSlot className="home-colour">
|
|
223
|
+
<OversizedGlyph />
|
|
224
|
+
</IconSlot>,
|
|
225
|
+
)
|
|
226
|
+
// The module hash is '' under `bun test`, so the assertion is that the home's class survived
|
|
227
|
+
// alongside whatever the module resolves to — i.e. it is appended, not substituted.
|
|
228
|
+
expect(container.querySelector('[data-basalt-icon]')?.className).toContain('home-colour')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The recurrence guard. Every `leftSection`/`rightSection` in the control tier that carries a
|
|
234
|
+
* CALLER-supplied icon must route it through `IconSlot` — this is what makes a seventh control
|
|
235
|
+
* added next month fail here instead of shipping an un-boxed glyph, which is exactly how
|
|
236
|
+
* `BarAction` shipped.
|
|
237
|
+
*
|
|
238
|
+
* Scoped to the files that own an `icon` prop in the tier the spec governs. `src/shell/app-sidebar.tsx`
|
|
239
|
+
* and `src/shell/app-mobile-nav.tsx` are deliberately out of scope: a nav row's icon column is sized
|
|
240
|
+
* by the sidebar's own rhythm rules, not by the control tier, and folding them in is a separate
|
|
241
|
+
* change with its own visual budget.
|
|
242
|
+
*/
|
|
243
|
+
describe('no control in the tier may hand a caller’s icon straight to a section slot', () => {
|
|
244
|
+
const TIER_FILES = [
|
|
245
|
+
'../controls/actions.tsx',
|
|
246
|
+
'../controls/filter-pill.tsx',
|
|
247
|
+
'../controls/sync-button.tsx',
|
|
248
|
+
'../shell/sidebar-blocks.tsx',
|
|
249
|
+
] as const
|
|
250
|
+
|
|
251
|
+
for (const rel of TIER_FILES) {
|
|
252
|
+
test(`${rel} routes every icon-bearing section through IconSlot`, () => {
|
|
253
|
+
const source = readFileSync(join(import.meta.dir, rel), 'utf8')
|
|
254
|
+
const offenders: string[] = []
|
|
255
|
+
for (const match of source.matchAll(/(left|right)Section[:=]\s*([^,\n]*)/g)) {
|
|
256
|
+
const raw = match[2] ?? ''
|
|
257
|
+
// A bare identifier (`leftSection: lead`) is resolved one level to its `const` initialiser,
|
|
258
|
+
// so a slot built a line above still counts. One level only — anything deeper than that is
|
|
259
|
+
// indirection a reader could not follow either.
|
|
260
|
+
const bare = /^([A-Za-z_$][\w$]*)\s*\}?\)?,?$/.exec(raw.trim())
|
|
261
|
+
const declared =
|
|
262
|
+
bare === null
|
|
263
|
+
? undefined
|
|
264
|
+
: new RegExp(`\\bconst ${bare[1]}\\b[^\\n]*(?:\\n[^\\n]*){0,3}`).exec(source)?.[0]
|
|
265
|
+
const value = declared ?? raw
|
|
266
|
+
// Only the icon-bearing sites are in scope — a `rightSection` holding a count badge or a
|
|
267
|
+
// `<Text>` meta string is not an icon and has no box to pin.
|
|
268
|
+
if (!/icon|glyph|lead/i.test(value)) continue
|
|
269
|
+
if (value.includes('IconSlot')) continue
|
|
270
|
+
offenders.push(match[0].trim())
|
|
271
|
+
}
|
|
272
|
+
expect(offenders).toEqual([])
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IconSlot` — the ONE box every `icon` prop in the public API renders through
|
|
3
|
+
* (`docs/CONTROLS-SPEC.md` §3).
|
|
4
|
+
*
|
|
5
|
+
* basalt ships no icon dependency, so every `icon` slot takes a `ReactNode` the consumer fills —
|
|
6
|
+
* and a consumer's glyph arrives at whatever geometry its own icon set chose: `width="24"` from
|
|
7
|
+
* Tabler's defaults, no `width` at all from a hand-written `<svg viewBox="0 0 20 20">`, a
|
|
8
|
+
* `stroke-width` tuned for a 24px box, or an `<img>` off a sprite sheet. Passing that node straight
|
|
9
|
+
* into Mantine's `leftSection` (which is a bare `flex` box with no size of its own) lets the CALLER
|
|
10
|
+
* decide the height of a basalt control's row. That is the class of bug this component closes: the
|
|
11
|
+
* icon's box is the framework's, and there is no prop on it.
|
|
12
|
+
*
|
|
13
|
+
* **Derived, not configured.** There is no `size` prop, on purpose — a size at a call site is the
|
|
14
|
+
* same C5 violation a `size="xs"` on a Button is. The box reads `--vx-space-icon-size` (default
|
|
15
|
+
* 16px, the control tier's mark), and the only thing entitled to set that var is a HOME's own CSS:
|
|
16
|
+
* `WidgetHeader` sets it per `data-tier`, so a `tier="widget"` heading draws 14px icons and a
|
|
17
|
+
* `tier="section"` heading draws 16px, with every call site unchanged.
|
|
18
|
+
*
|
|
19
|
+
* Mantine-free and dependency-free (one CSS module, one `<span>`), so `src/widget-header/**` — which
|
|
20
|
+
* `ChartCard` composes from inside `src/charts/**` — can deep-import it without pulling
|
|
21
|
+
* `@mantine/*` into the charts graph (`scripts/check-dist-layering.mjs`). Deep-import it as
|
|
22
|
+
* `../theme/icon-slot`, never through the `./theme` barrel, for exactly that reason. INTERNAL: it is
|
|
23
|
+
* not on any barrel. A consumer never needs it — passing `icon` IS the contract, and this is what
|
|
24
|
+
* makes that contract hold.
|
|
25
|
+
*
|
|
26
|
+
* `data-basalt-icon` is the queryable marker, the same idiom `CtlSlot` uses for `data-basalt-tier`:
|
|
27
|
+
* a CSS-module class name is a build-time hash, so it is not something a test — or a consumer
|
|
28
|
+
* debugging a misaligned row — can select on.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // A control's icon slot — no size anywhere at the call site.
|
|
32
|
+
* <Button leftSection={<IconSlot>{icon}</IconSlot>}>{label}</Button>
|
|
33
|
+
*/
|
|
34
|
+
import type { ReactNode } from 'react'
|
|
35
|
+
import classes from './icon-slot.module.css'
|
|
36
|
+
|
|
37
|
+
export type IconSlotProps = {
|
|
38
|
+
readonly children: ReactNode
|
|
39
|
+
/**
|
|
40
|
+
* Composed ONTO the slot class, never in place of it — a home adds colour/state rules (the pill's
|
|
41
|
+
* muted-then-ink icon, a header's faint one) and the geometry stays the slot's.
|
|
42
|
+
*/
|
|
43
|
+
readonly className?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function IconSlot({ children, className }: IconSlotProps): ReactNode {
|
|
47
|
+
return (
|
|
48
|
+
// `aria-hidden` unconditionally: an icon in a basalt control is DECORATIVE by construction —
|
|
49
|
+
// every control that takes one also carries a label or an `aria-label`, so a named icon would
|
|
50
|
+
// double the announced name. A caller wanting a meaningful graphic names the control, not this.
|
|
51
|
+
<span
|
|
52
|
+
className={className === undefined ? classes.slot : `${classes.slot} ${className}`}
|
|
53
|
+
data-basalt-icon
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</span>
|
|
58
|
+
)
|
|
59
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -99,6 +99,10 @@ export type { BasaltFontsConfig } from '../tokens'
|
|
|
99
99
|
// snapshot) — see its own doc comment for why it lives here rather than in `tokens/`.
|
|
100
100
|
export { useBasaltSpacing } from './use-basalt-spacing'
|
|
101
101
|
|
|
102
|
+
// The `size="ctl"` slot theme (`docs/CONTROLS-SPEC.md` §5) — see `./ctl-theme`'s own doc for the
|
|
103
|
+
// merge/precedence contract.
|
|
104
|
+
export { CTL_THEME, CtlSlot, type CtlSlotProps } from './ctl-theme'
|
|
105
|
+
|
|
102
106
|
// Typed `theme.other.basaltDerive` / `theme.other.basaltFonts` / `theme.other.basaltRadius` /
|
|
103
107
|
// `theme.other.basaltDensity` reads (Mantine's `MantineThemeOther` ships an index signature, so
|
|
104
108
|
// this merge is additive — no widening of the existing `[key: string]: any`). Set by
|
|
@@ -745,7 +749,12 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
|
|
|
745
749
|
SegmentedControl: SegmentedControl.extend({
|
|
746
750
|
defaultProps: { radius: radius.card },
|
|
747
751
|
classNames: { label: segmentedControlClasses.label },
|
|
748
|
-
styles
|
|
752
|
+
// `label` gets a `ctl`-only min-height (function-form `styles`, same `props.size === …` gate
|
|
753
|
+
// as Button/ActionIcon/Input's `vars` overrides above) so a `size="ctl"` SegmentedControl
|
|
754
|
+
// matches the ctl Button/Input box exactly instead of following its own fixed padding — see
|
|
755
|
+
// `CTL_SEGMENT_PADDING_Y`'s doc below for why the label's own padding alone can't track
|
|
756
|
+
// density. `sm`/`md` SegmentedControls (forms) are untouched, keeping Mantine's own sizing.
|
|
757
|
+
styles: (_theme, props) => ({
|
|
749
758
|
root: {
|
|
750
759
|
backgroundColor: 'color-mix(in srgb, var(--vx-ink) 6%, transparent)',
|
|
751
760
|
padding: `${SPACE_FIXED.segmentedTrackInset}px`,
|
|
@@ -755,7 +764,19 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
|
|
|
755
764
|
boxShadow: 'var(--vx-shadow-ctrl)',
|
|
756
765
|
borderRadius: 'var(--vx-radius-tight)',
|
|
757
766
|
},
|
|
758
|
-
|
|
767
|
+
// Conditionally spread the WHOLE key (never a present `label: undefined`) — the package
|
|
768
|
+
// builds under `exactOptionalPropertyTypes`, which treats a key present-but-`undefined` as
|
|
769
|
+
// a type error distinct from the key being absent.
|
|
770
|
+
...(props.size === 'ctl'
|
|
771
|
+
? {
|
|
772
|
+
label: {
|
|
773
|
+
display: 'inline-flex',
|
|
774
|
+
alignItems: 'center',
|
|
775
|
+
minHeight: `calc(var(--vx-space-control-height-ctl) * var(--mantine-scale) - ${2 * SPACE_FIXED.segmentedTrackInset}px)`,
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
: {}),
|
|
779
|
+
}),
|
|
759
780
|
}),
|
|
760
781
|
// Track = ink-8%; leader/section fill colors are a per-usage `color` prop (left to consumers).
|
|
761
782
|
Progress: Progress.extend({
|
|
@@ -935,10 +956,16 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
|
|
|
935
956
|
// body cells is a per-table call-site concern, left to consumers. `--table-border-color`
|
|
936
957
|
// drives row separators AND the opt-in `withTableBorder` outer border — both drop to the
|
|
937
958
|
// hairline (Mantine's default reads gray-3/dark-4 = the STRONG line, too heavy in a card).
|
|
959
|
+
// Row heights are declared here rather than through `verticalSpacing`, because Mantine derives
|
|
960
|
+
// BOTH `th` and `td` padding from that one prop — a 36/40 pair (label band over roomier data
|
|
961
|
+
// rows) is not expressible by it at all. `height` on a table cell behaves as a MINIMUM, so a
|
|
962
|
+
// wrapping cell still grows; `verticalSpacing` keeps working on top of it for a consumer that
|
|
963
|
+
// wants a denser or looser table than the tier.
|
|
938
964
|
Table: Table.extend({
|
|
939
965
|
classNames: { table: controlsClasses.tableRoot },
|
|
940
966
|
styles: {
|
|
941
967
|
th: {
|
|
968
|
+
height: spacing.step.tableHeaderHeight,
|
|
942
969
|
fontFamily: 'var(--basalt-font-mono)',
|
|
943
970
|
fontSize: VX.text.micro,
|
|
944
971
|
fontWeight: 500,
|
|
@@ -946,6 +973,7 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
|
|
|
946
973
|
textTransform: 'uppercase',
|
|
947
974
|
color: 'var(--vx-faint)',
|
|
948
975
|
},
|
|
976
|
+
td: { height: spacing.step.tableRowHeight },
|
|
949
977
|
},
|
|
950
978
|
}),
|
|
951
979
|
// The v9 thumb indicator is a center dot painted in the TRACK color — over the remapped
|
|
@@ -1105,6 +1133,138 @@ function familyBridge(data: PaletteData): Record<string, string> {
|
|
|
1105
1133
|
return vars
|
|
1106
1134
|
}
|
|
1107
1135
|
|
|
1136
|
+
/**
|
|
1137
|
+
* The `ctl` tier's horizontal inset — one number for a Button, a `FilterPill`, a SegmentedControl
|
|
1138
|
+
* segment and a `kind: 'custom'` Badge, so a home's row reads as one control family
|
|
1139
|
+
* (`docs/CONTROLS-SPEC.md` §3: "the pill: 30px, padding 0 10px"). Not in `SPACE_STEP`: it is a
|
|
1140
|
+
* Mantine size-var value, consumed only by {@link ctlSizeVars}, and it happens to equal
|
|
1141
|
+
* `SPACE.rowInsetX` without being the same concept (that one is a nav/menu ROW's inset).
|
|
1142
|
+
*/
|
|
1143
|
+
const CTL_PADDING_X = 10
|
|
1144
|
+
/** The `ctl` tier's Input vertical inset — 30px box, ~20px line, so 4px a side leaves the border. */
|
|
1145
|
+
const CTL_INPUT_PADDING_Y = 4
|
|
1146
|
+
/**
|
|
1147
|
+
* A `ctl` SegmentedControl segment's vertical inset, in px. Fractional on purpose, and derived
|
|
1148
|
+
* rather than picked: the label's line box is `VX.text.sm` × `--mantine-line-height-sm` ≈ 21px and
|
|
1149
|
+
* the track adds `SPACE_FIXED.segmentedTrackInset` on each side (2px), so landing the CONTROL on the
|
|
1150
|
+
* 30px `ctl` tier needs (30 − 4 − 21) / 2. Mantine's own `--input-padding-y-xs` is 4.5px for the
|
|
1151
|
+
* same reason — a control tier is a total height, and the halves that make it up need not be whole.
|
|
1152
|
+
*/
|
|
1153
|
+
const CTL_SEGMENT_PADDING_Y = 2.5
|
|
1154
|
+
/** A `ctl` Combobox chevron. Mantine's own `xs` value; restated because it cannot be referenced. */
|
|
1155
|
+
const CTL_CHEVRON_SIZE = 14
|
|
1156
|
+
/**
|
|
1157
|
+
* The `ctl` tier's Radio/Checkbox indicator box, in px — 16, the SAME square
|
|
1158
|
+
* `theme/icon-slot.module.css` gives every icon in the tier, and for the same reason: a 13.5px
|
|
1159
|
+
* option label needs a mark it reads as part of the row rather than as a control beside it. Mantine's
|
|
1160
|
+
* default is `sm` (20px) and its `md` is 24px, both of which made the popover a column of controls
|
|
1161
|
+
* (`controls/filter-sheet.tsx`'s `SheetOptionList` doc records the sheet's version of the same
|
|
1162
|
+
* finding — there the fix was to remove the indicator entirely).
|
|
1163
|
+
*/
|
|
1164
|
+
const CTL_TOGGLE_SIZE = 16
|
|
1165
|
+
/**
|
|
1166
|
+
* A `ctl` Radio's inner dot. Mantine's own ratio is `size * 0.4` — the literal fallback its
|
|
1167
|
+
* `varsResolver` applies to a NUMERIC size (`Radio/RadioIndicator/RadioIndicator.mjs`), and what its
|
|
1168
|
+
* shipped pairs already encode (10px dot in a 24px `md` box). Derived rather than picked so the dot
|
|
1169
|
+
* stays proportional if the box moves.
|
|
1170
|
+
*/
|
|
1171
|
+
const CTL_RADIO_DOT_SIZE = CTL_TOGGLE_SIZE * 0.4
|
|
1172
|
+
/**
|
|
1173
|
+
* The `ctl` Switch: an 18×30 track with a 12px thumb. Mantine ships no tier between `xs` (16×32,
|
|
1174
|
+
* 12px thumb) and `sm` (20×38, 14px), and neither fits — `xs` is SHORTER than the tier's indicator
|
|
1175
|
+
* box while being twice as wide as it, which reads as a slider rather than a toggle. 18 is one step
|
|
1176
|
+
* over the 16px indicator (a switch must read as the taller control, it carries a moving part), and
|
|
1177
|
+
* 30 is the narrowest track that still shows travel: 30 − 12 thumb − 2×3 padding leaves 12px of it.
|
|
1178
|
+
*/
|
|
1179
|
+
const CTL_SWITCH_HEIGHT = 18
|
|
1180
|
+
const CTL_SWITCH_WIDTH = 30
|
|
1181
|
+
const CTL_SWITCH_THUMB_SIZE = 12
|
|
1182
|
+
/** The `on`/`off` labels INSIDE a `ctl` switch track, and their inset. Mantine's `xs` pair, which is
|
|
1183
|
+
* the last tier whose label still fits an 18px track. */
|
|
1184
|
+
const CTL_SWITCH_TRACK_LABEL_SIZE = 5
|
|
1185
|
+
const CTL_SWITCH_TRACK_LABEL_PADDING = 2
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* The `size="ctl"` / `size="icon"` Mantine size-tier var sets (`docs/CONTROLS-SPEC.md` §5, C5) —
|
|
1189
|
+
* scheme-independent, so they live in `cssVariablesResolver`'s `variables` block (`:root`, not
|
|
1190
|
+
* `light`/`dark`), declared exactly once regardless of color scheme. Mechanism, verified against
|
|
1191
|
+
* the installed `@mantine/core` 9.3.0 source (`core/utils/get-size/get-size.mjs`): a component's own
|
|
1192
|
+
* `varsResolver` calls `getSize(props.size, '<prefix>')`, which for a non-numeric `size` returns the
|
|
1193
|
+
* STRING `var(--<prefix>-<size>)` verbatim — so `size="ctl"` on `Button` resolves
|
|
1194
|
+
* `--button-height: var(--button-height-ctl)` with ZERO code in this framework's own `Button.extend`
|
|
1195
|
+
* `vars` callback; only the var these names point AT has to exist. `getFontSize(size)` is
|
|
1196
|
+
* `getSize(size, 'mantine-font-size')`, so every `-fz`/`-font-size` component var (Button's
|
|
1197
|
+
* `--button-fz`, Input's `--input-fz`, SegmentedControl's `--sc-font-size`, Combobox's
|
|
1198
|
+
* `--combobox-option-fz`) resolves through the ONE `--mantine-font-size-ctl` declared below — no
|
|
1199
|
+
* per-component `-ctl` font-size var exists to declare (verified against
|
|
1200
|
+
* `SegmentedControl.mjs`/`Combobox.mjs`; a per-component name would be dead weight no `getSize`
|
|
1201
|
+
* call ever reads).
|
|
1202
|
+
*
|
|
1203
|
+
* Heights read the density-tracking `--vx-space-control-height-ctl`/`-widget` anchors
|
|
1204
|
+
* (`tokens/index.ts`'s `spaceDecls`, `docs/CONTROLS-SPEC.md` §5) through the SAME rem-plus-
|
|
1205
|
+
* `--mantine-scale` reconstruction `--vx-space-input-height`/`--vx-space-control-height` already
|
|
1206
|
+
* use for `size="md"` (see `Input.extend`'s doc above) — so the ctl/icon tier tracks the density
|
|
1207
|
+
* knob exactly like every other control height. Paddings, the combobox chevron, and the
|
|
1208
|
+
* SegmentedControl/Combobox option padding have no density-tracking token of their own
|
|
1209
|
+
* (`docs/CONTROLS-SPEC.md` §5 specifies only the height ladder), so they are declared as literal
|
|
1210
|
+
* values here.
|
|
1211
|
+
*
|
|
1212
|
+
* **They may NOT be declared as `var(--<prefix>-xs)`, and that mistake shipped.** Mantine declares
|
|
1213
|
+
* its `-xs`/`-sm`/… size vars on the COMPONENT ROOT class (`.m_77c9d27d { --button-padding-x-xs:
|
|
1214
|
+
* 14px }` in `Button.css`, `.m_cf365364 { --sc-padding-xs: 2px 6px }` in `SegmentedControl.css`,
|
|
1215
|
+
* and likewise for Input/Combobox/Badge) — never on `:root`. A `-ctl` var declared HERE lives on
|
|
1216
|
+
* `:root`, where `var(--sc-padding-xs)` resolves to nothing, so the whole custom property became
|
|
1217
|
+
* guaranteed-invalid and every property reading it fell back to its INITIAL value: a pill with
|
|
1218
|
+
* `padding: 0`, a SegmentedControl whose segments touched
|
|
1219
|
+
* (`allnewreturningchurnedflagged`), a chevron-less Select. Literal values are the only form that
|
|
1220
|
+
* can resolve at `:root`. Verified live with `getComputedStyle(document.documentElement)
|
|
1221
|
+
* .getPropertyValue('--sc-padding-ctl')` returning `''`.
|
|
1222
|
+
*
|
|
1223
|
+
* `size="icon"` (24px) has no vertical-padding/chevron consumer (it is ActionIcon-only), so only
|
|
1224
|
+
* `--ai-size-icon` is declared for it. The Badge tier exists so a `kind: 'custom'` chip in a home
|
|
1225
|
+
* (`docs/CONTROLS-SPEC.md` §2.1) is the same 30px as the buttons beside it — Mantine's tallest
|
|
1226
|
+
* shipped Badge is 32px `xl`, and none of its tiers is 30.
|
|
1227
|
+
*/
|
|
1228
|
+
function ctlSizeVars(): Record<string, string> {
|
|
1229
|
+
const ctlHeight = 'calc(var(--vx-space-control-height-ctl) * var(--mantine-scale))'
|
|
1230
|
+
return {
|
|
1231
|
+
'--button-height-ctl': ctlHeight,
|
|
1232
|
+
'--button-padding-x-ctl': `calc(${pxRem(CTL_PADDING_X)} * var(--mantine-scale))`,
|
|
1233
|
+
'--input-height-ctl': ctlHeight,
|
|
1234
|
+
'--input-padding-y-ctl': `calc(${pxRem(CTL_INPUT_PADDING_Y)} * var(--mantine-scale))`,
|
|
1235
|
+
'--ai-size-ctl': ctlHeight,
|
|
1236
|
+
'--ai-size-icon': 'calc(var(--vx-space-control-height-widget) * var(--mantine-scale))',
|
|
1237
|
+
// A shorthand pair (`<y> <x>`), the shape SegmentedControl's own `--sc-padding-*` vars take.
|
|
1238
|
+
// `y` stays a bare literal (the label's own box height is now enforced by the `size === 'ctl'`
|
|
1239
|
+
// `minHeight` override in `SegmentedControl.extend` above, not by this padding), `x` scales like
|
|
1240
|
+
// every other ctl-tier horizontal inset (`--button-padding-x-ctl`, `--badge-padding-x-ctl`).
|
|
1241
|
+
'--sc-padding-ctl': `${pxRem(CTL_SEGMENT_PADDING_Y)} calc(${pxRem(CTL_PADDING_X)} * var(--mantine-scale))`,
|
|
1242
|
+
'--combobox-option-padding-ctl': `${pxRem(CTL_SEGMENT_PADDING_Y)} ${pxRem(CTL_PADDING_X)}`,
|
|
1243
|
+
'--combobox-chevron-size-ctl': pxRem(CTL_CHEVRON_SIZE),
|
|
1244
|
+
'--badge-height-ctl': ctlHeight,
|
|
1245
|
+
'--badge-padding-x-ctl': `calc(${pxRem(CTL_PADDING_X)} * var(--mantine-scale))`,
|
|
1246
|
+
'--badge-fz-ctl': `calc(${pxRem(VX.text.xs)} * var(--mantine-scale))`,
|
|
1247
|
+
// `= VX.text.sm` per `docs/CONTROLS-SPEC.md` §5 — same `calc(<rem> * var(--mantine-scale))`
|
|
1248
|
+
// shape Mantine's own `--mantine-font-size-*` vars use.
|
|
1249
|
+
'--mantine-font-size-ctl': `calc(${pxRem(VX.text.sm)} * var(--mantine-scale))`,
|
|
1250
|
+
'--mantine-line-height-ctl': 'var(--mantine-line-height-sm)',
|
|
1251
|
+
// Radio / Checkbox / Switch — the three components whose DEFAULT (`sm`, a 20px indicator) sat
|
|
1252
|
+
// beside a 13.5px option label in every popover and sheet basalt draws. Every prefix here was
|
|
1253
|
+
// read out of the installed `@mantine/core` source, and `ctl-tier-coverage.test.ts` now scans
|
|
1254
|
+
// those three components too, so a future minor that adds a `getSize` call fails the build
|
|
1255
|
+
// instead of resolving the new var to its initial value. Note there is NO
|
|
1256
|
+
// `--checkbox-icon-size`: Checkbox reads only `checkbox-size` and derives its tick in CSS.
|
|
1257
|
+
'--radio-size-ctl': `calc(${pxRem(CTL_TOGGLE_SIZE)} * var(--mantine-scale))`,
|
|
1258
|
+
'--radio-icon-size-ctl': `calc(${pxRem(CTL_RADIO_DOT_SIZE)} * var(--mantine-scale))`,
|
|
1259
|
+
'--checkbox-size-ctl': `calc(${pxRem(CTL_TOGGLE_SIZE)} * var(--mantine-scale))`,
|
|
1260
|
+
'--switch-height-ctl': `calc(${pxRem(CTL_SWITCH_HEIGHT)} * var(--mantine-scale))`,
|
|
1261
|
+
'--switch-width-ctl': `calc(${pxRem(CTL_SWITCH_WIDTH)} * var(--mantine-scale))`,
|
|
1262
|
+
'--switch-thumb-size-ctl': `calc(${pxRem(CTL_SWITCH_THUMB_SIZE)} * var(--mantine-scale))`,
|
|
1263
|
+
'--switch-label-font-size-ctl': pxRem(CTL_SWITCH_TRACK_LABEL_SIZE),
|
|
1264
|
+
'--switch-track-label-padding-ctl': pxRem(CTL_SWITCH_TRACK_LABEL_PADDING),
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1108
1268
|
/**
|
|
1109
1269
|
* Build the `cssVariablesResolver` bound to a given palette `data` — the derive-config-dependent
|
|
1110
1270
|
* half of the fallback hexes below (see `buildTheme` above for the same split on `theme.colors`).
|
|
@@ -1123,7 +1283,9 @@ function familyBridge(data: PaletteData): Record<string, string> {
|
|
|
1123
1283
|
function buildCssVariablesResolver(data: PaletteData): CSSVariablesResolver {
|
|
1124
1284
|
const { INK, NEUTRAL, SURFACE } = data
|
|
1125
1285
|
return (theme) => ({
|
|
1126
|
-
variables: {
|
|
1286
|
+
variables: {
|
|
1287
|
+
...ctlSizeVars(),
|
|
1288
|
+
},
|
|
1127
1289
|
light: {
|
|
1128
1290
|
...onColorVars(theme, 'light', data),
|
|
1129
1291
|
...familyBridge(data),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS-text regression tests for two shell rules that broke silently because neither is exercised
|
|
3
|
+
* by a render test: `.footerVersion`'s collapsed-rail hide (a corrupted selector left it unscoped,
|
|
4
|
+
* hiding the sidebar version label at every desktop viewport) and the sticky page-bar rows' z-index
|
|
5
|
+
* (a bare `1` let ordinary in-flow content — Mantine's Input section, Indicator, sticky table
|
|
6
|
+
* headers — paint over them while scrolling).
|
|
7
|
+
*
|
|
8
|
+
* Both are asserted against the raw `.module.css` source text rather than through jsdom: jsdom does
|
|
9
|
+
* not implement CSS cascade/specificity resolution or `getComputedStyle` for stacking, so a
|
|
10
|
+
* render-based test cannot see either regression. Reading the source text directly is what actually
|
|
11
|
+
* pins the rule shape.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { describe, expect, it } from 'bun:test'
|
|
16
|
+
|
|
17
|
+
const APP_SIDEBAR_CSS = readFileSync(
|
|
18
|
+
join(import.meta.dir, '../shell/app-sidebar.module.css'),
|
|
19
|
+
'utf8',
|
|
20
|
+
)
|
|
21
|
+
const PAGE_BAR_CSS = readFileSync(join(import.meta.dir, '../shell/page-bar.module.css'), 'utf8')
|
|
22
|
+
|
|
23
|
+
/** The `@media (min-width: 48em)` rail block — where `[data-collapsed]` rules live. */
|
|
24
|
+
function railBlock(css: string): string {
|
|
25
|
+
const start = css.indexOf('@media (min-width: 48em)')
|
|
26
|
+
expect(start).toBeGreaterThanOrEqual(0)
|
|
27
|
+
return css.slice(start)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('app-sidebar.module.css — collapsed-rail `.footerVersion` hide', () => {
|
|
31
|
+
const rail = railBlock(APP_SIDEBAR_CSS)
|
|
32
|
+
|
|
33
|
+
it('scopes the version-label hide to `.root[data-collapsed]`, not a bare `.footerVersion`', () => {
|
|
34
|
+
expect(rail).toContain('.root[data-collapsed] .footerVersion {\n display: none;\n }')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('never declares an unscoped `.footerVersion` rule inside the rail media query', () => {
|
|
38
|
+
// A bare `.footerVersion {` (no `[data-collapsed]` ancestor on the same rule) would hide the
|
|
39
|
+
// version label at every desktop viewport, expanded or collapsed — the exact regression.
|
|
40
|
+
for (const m of rail.matchAll(/([^{}\n]+)\{\s*display:\s*none;\s*\}/g)) {
|
|
41
|
+
const selector = m[1]!.trim()
|
|
42
|
+
if (!selector.endsWith('.footerVersion')) continue
|
|
43
|
+
expect(selector).toContain('[data-collapsed]')
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not duplicate the base `.footerIconSlot` rule inside the rail media query', () => {
|
|
48
|
+
// The real `.footerIconSlot` rule is declared once, outside the media query — a stray copy
|
|
49
|
+
// inside `@media (min-width: 48em)` is the corruption artifact this test guards against.
|
|
50
|
+
expect([...rail.matchAll(/\.footerIconSlot\s*\{/g)]).toHaveLength(0)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('page-bar.module.css — sticky rows sit under the AppShell header, above page content', () => {
|
|
55
|
+
it('`.bar` sits one layer below the AppShell header, not at the bare `z-index: 1` every sibling can equal', () => {
|
|
56
|
+
const bar = PAGE_BAR_CSS.slice(
|
|
57
|
+
PAGE_BAR_CSS.indexOf('.bar {'),
|
|
58
|
+
PAGE_BAR_CSS.indexOf('\n}', PAGE_BAR_CSS.indexOf('.bar {')),
|
|
59
|
+
)
|
|
60
|
+
expect(bar).toContain('z-index: calc(var(--app-shell-header-z-index, 100) - 1);')
|
|
61
|
+
expect(bar).not.toMatch(/z-index:\s*1\s*;/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('`.row2Sticky` uses the same header-relative z-index as `.bar`, so the two never fall out of sync', () => {
|
|
65
|
+
const row2 = PAGE_BAR_CSS.slice(
|
|
66
|
+
PAGE_BAR_CSS.indexOf('.row2Sticky {'),
|
|
67
|
+
PAGE_BAR_CSS.indexOf('\n}', PAGE_BAR_CSS.indexOf('.row2Sticky {')),
|
|
68
|
+
)
|
|
69
|
+
expect(row2).toContain('z-index: calc(var(--app-shell-header-z-index, 100) - 1);')
|
|
70
|
+
expect(row2).not.toMatch(/z-index:\s*1\s*;/)
|
|
71
|
+
})
|
|
72
|
+
})
|
package/src/theme/radius.test.ts
CHANGED
|
@@ -70,7 +70,17 @@ describe('every re-pointed component defaultProps/styles radius matches its ship
|
|
|
70
70
|
test('SegmentedControl root = 7, indicator = 5', () => {
|
|
71
71
|
const sc = baseTheme.components?.['SegmentedControl']
|
|
72
72
|
expect(sc?.defaultProps?.['radius']).toBe(7)
|
|
73
|
-
|
|
73
|
+
// `styles` is function-form (size='ctl' gates a label override — see `theme/index.ts`), not a
|
|
74
|
+
// flat object, so it has to be called rather than indexed directly.
|
|
75
|
+
const stylesFn = sc?.styles as
|
|
76
|
+
| ((
|
|
77
|
+
theme: unknown,
|
|
78
|
+
props: { size?: string },
|
|
79
|
+
ctx: unknown,
|
|
80
|
+
) => Record<string, Record<string, unknown>>)
|
|
81
|
+
| undefined
|
|
82
|
+
const resolved = stylesFn?.({}, {}, {})
|
|
83
|
+
expect(resolved?.['indicator']?.['borderRadius']).toBe('var(--vx-radius-tight)')
|
|
74
84
|
})
|
|
75
85
|
|
|
76
86
|
test('Progress = 4', () => {
|