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
|
+
* `CtlSlot`/`CTL_THEME` — proves the `size="ctl"` default reaches a bare `<Button>` dropped into
|
|
3
|
+
* the slot with NO prop at the call site, and that the tier stays scoped to the slot (a sibling
|
|
4
|
+
* `<Button>` outside `<CtlSlot>` never resolves the `-ctl` var).
|
|
5
|
+
*
|
|
6
|
+
* `data-size`/the inline `style` custom-property string are Mantine's own render output — verified
|
|
7
|
+
* directly against a real render (`@testing-library/react`, `tests/setup/dom.ts`'s jsdom harness),
|
|
8
|
+
* not asserted from memory. jsdom does not compute the CSS cascade, so this can only prove the
|
|
9
|
+
* INLINE var REFERENCE Mantine's `varsResolver` emits (`--button-height: var(--button-height-ctl)`)
|
|
10
|
+
* — not the resolved pixel value, which is `cssVariablesResolver`'s and `spaceDecls`'s job
|
|
11
|
+
* (covered by `spacing.test.ts`/`density.test.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Outside `<CtlSlot>`, `<Button>` resolves to Mantine's own native default, NOT basalt's `md` —
|
|
14
|
+
* verified against `theme/index.ts`'s `Button.extend`: unlike Input/TextInput/NumberInput/
|
|
15
|
+
* PasswordInput/Select/Textarea, basalt sets no `defaultProps.size` for Button/ActionIcon at all,
|
|
16
|
+
* so an un-sized Button carries no `data-size` attribute and no `--button-height` inline var
|
|
17
|
+
* (Mantine's own CSS stylesheet default takes over). The test below asserts the accurate,
|
|
18
|
+
* verified claim — the tier does not leak past the slot — rather than a `size="md"` claim Button
|
|
19
|
+
* was never given.
|
|
20
|
+
*/
|
|
21
|
+
import { ActionIcon, Button, Checkbox, MantineProvider, Radio, Switch } from '@mantine/core'
|
|
22
|
+
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
23
|
+
import { describe, expect, test } from 'bun:test'
|
|
24
|
+
import { FilterPill } from '../controls/filter-pill'
|
|
25
|
+
import { StatCard } from '../dashboard/stat-card'
|
|
26
|
+
import { baseTheme } from './index'
|
|
27
|
+
import { CTL_THEME, CtlSlot } from './ctl-theme'
|
|
28
|
+
|
|
29
|
+
describe('CtlSlot', () => {
|
|
30
|
+
test('a bare Button inside CtlSlot resolves the ctl tier with no size prop', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<MantineProvider theme={baseTheme}>
|
|
33
|
+
<CtlSlot>
|
|
34
|
+
<Button>Save</Button>
|
|
35
|
+
</CtlSlot>
|
|
36
|
+
</MantineProvider>,
|
|
37
|
+
)
|
|
38
|
+
const button = container.querySelector('button')
|
|
39
|
+
expect(button?.getAttribute('data-size')).toBe('ctl')
|
|
40
|
+
expect(button?.style.getPropertyValue('--button-height')).toBe('var(--button-height-ctl)')
|
|
41
|
+
expect(button?.style.getPropertyValue('--button-padding-x')).toBe('var(--button-padding-x-ctl)')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('the wrapper carries the data-basalt-tier marker and stays out of flow', () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<MantineProvider theme={baseTheme}>
|
|
47
|
+
<CtlSlot>
|
|
48
|
+
<Button>Save</Button>
|
|
49
|
+
</CtlSlot>
|
|
50
|
+
</MantineProvider>,
|
|
51
|
+
)
|
|
52
|
+
const wrapper = container.querySelector('[data-basalt-tier]')
|
|
53
|
+
expect(wrapper?.getAttribute('data-basalt-tier')).toBe('ctl')
|
|
54
|
+
expect((wrapper as HTMLElement | null)?.style.display).toBe('contents')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a Button outside CtlSlot never resolves the ctl tier', () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<MantineProvider theme={baseTheme}>
|
|
60
|
+
<Button>Save</Button>
|
|
61
|
+
</MantineProvider>,
|
|
62
|
+
)
|
|
63
|
+
const button = container.querySelector('button')
|
|
64
|
+
expect(button?.getAttribute('data-size')).not.toBe('ctl')
|
|
65
|
+
expect(button?.style.getPropertyValue('--button-height')).not.toBe('var(--button-height-ctl)')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('CTL_THEME sets defaultProps.size "ctl" for every documented component', () => {
|
|
69
|
+
const names = [
|
|
70
|
+
'Button',
|
|
71
|
+
'ActionIcon',
|
|
72
|
+
'Input',
|
|
73
|
+
'TextInput',
|
|
74
|
+
'Select',
|
|
75
|
+
'MultiSelect',
|
|
76
|
+
'SegmentedControl',
|
|
77
|
+
'NativeSelect',
|
|
78
|
+
'Radio',
|
|
79
|
+
'RadioGroup',
|
|
80
|
+
'Checkbox',
|
|
81
|
+
'CheckboxGroup',
|
|
82
|
+
'Switch',
|
|
83
|
+
'SwitchGroup',
|
|
84
|
+
] as const
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
expect(CTL_THEME.components?.[name]?.defaultProps?.['size']).toBe('ctl')
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Radio/Checkbox/Switch at the tier, and the portal question that had to be settled before the
|
|
93
|
+
* defaults could be trusted: a filter's popover body and the mobile sheet are rendered
|
|
94
|
+
* `withinPortal`, so they are NOT DOM descendants of the `<CtlSlot>` wrapper — but a React portal
|
|
95
|
+
* preserves the React tree, and `MantineThemeProvider` is React context. So the tier DOES reach them,
|
|
96
|
+
* and no second `CtlSlot` inside `Popover.Dropdown`/`Drawer` is needed. These tests are what makes
|
|
97
|
+
* that a checked fact rather than a reasoned one.
|
|
98
|
+
*/
|
|
99
|
+
describe('the toggle family at the ctl tier', () => {
|
|
100
|
+
test('a bare Radio inside CtlSlot resolves the ctl indicator box', () => {
|
|
101
|
+
const { container } = render(
|
|
102
|
+
<MantineProvider theme={baseTheme}>
|
|
103
|
+
<CtlSlot>
|
|
104
|
+
<Radio value="a" label="Previous period" />
|
|
105
|
+
</CtlSlot>
|
|
106
|
+
</MantineProvider>,
|
|
107
|
+
)
|
|
108
|
+
const root = container.querySelector('.mantine-Radio-root')
|
|
109
|
+
expect(root?.getAttribute('style')).toContain('--radio-size: var(--radio-size-ctl)')
|
|
110
|
+
expect(root?.getAttribute('style')).toContain('--radio-icon-size: var(--radio-icon-size-ctl)')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('a bare Checkbox inside CtlSlot resolves the ctl indicator box', () => {
|
|
114
|
+
const { container } = render(
|
|
115
|
+
<MantineProvider theme={baseTheme}>
|
|
116
|
+
<CtlSlot>
|
|
117
|
+
<Checkbox value="a" label="Web" />
|
|
118
|
+
</CtlSlot>
|
|
119
|
+
</MantineProvider>,
|
|
120
|
+
)
|
|
121
|
+
expect(container.querySelector('.mantine-Checkbox-root')?.getAttribute('style')).toContain(
|
|
122
|
+
'--checkbox-size: var(--checkbox-size-ctl)',
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('a bare Switch inside CtlSlot resolves all five of its ctl vars', () => {
|
|
127
|
+
const { container } = render(
|
|
128
|
+
<MantineProvider theme={baseTheme}>
|
|
129
|
+
<CtlSlot>
|
|
130
|
+
<Switch label="Errors only" />
|
|
131
|
+
</CtlSlot>
|
|
132
|
+
</MantineProvider>,
|
|
133
|
+
)
|
|
134
|
+
const style = container.querySelector('.mantine-Switch-root')?.getAttribute('style') ?? ''
|
|
135
|
+
for (const prefix of [
|
|
136
|
+
'switch-height',
|
|
137
|
+
'switch-width',
|
|
138
|
+
'switch-thumb-size',
|
|
139
|
+
'switch-label-font-size',
|
|
140
|
+
'switch-track-label-padding',
|
|
141
|
+
]) {
|
|
142
|
+
expect(style).toContain(`--${prefix}: var(--${prefix}-ctl)`)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('a Radio outside CtlSlot keeps Mantine’s own default — the tier does not leak', () => {
|
|
147
|
+
const { container } = render(
|
|
148
|
+
<MantineProvider theme={baseTheme}>
|
|
149
|
+
<Radio value="a" label="Previous period" />
|
|
150
|
+
</MantineProvider>,
|
|
151
|
+
)
|
|
152
|
+
expect(
|
|
153
|
+
container.querySelector('.mantine-Radio-root')?.getAttribute('style') ?? '',
|
|
154
|
+
).not.toContain('var(--radio-size-ctl)')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('a PORTALED popover body inherits the tier — React context crosses a portal', async () => {
|
|
158
|
+
render(
|
|
159
|
+
<MantineProvider theme={baseTheme}>
|
|
160
|
+
<CtlSlot>
|
|
161
|
+
<FilterPill label="No comparison">
|
|
162
|
+
<Radio.Group value="previous">
|
|
163
|
+
<Radio value="previous" label="Previous period" />
|
|
164
|
+
</Radio.Group>
|
|
165
|
+
</FilterPill>
|
|
166
|
+
</CtlSlot>
|
|
167
|
+
</MantineProvider>,
|
|
168
|
+
)
|
|
169
|
+
await act(async () => {
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'No comparison' }))
|
|
171
|
+
})
|
|
172
|
+
// The dropdown is a portal at document.body — deliberately queried off `document`, not the
|
|
173
|
+
// render container, because being outside the container is the whole point of the test.
|
|
174
|
+
const root = document.querySelector('.mantine-Popover-dropdown .mantine-Radio-root')
|
|
175
|
+
expect(root).not.toBeNull()
|
|
176
|
+
expect(root?.getAttribute('style')).toContain('--radio-size: var(--radio-size-ctl)')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* `tier="widget"` — the 24px ActionIcon step a `WidgetHeader tier="widget"` header row can hold.
|
|
182
|
+
*
|
|
183
|
+
* The bug: `StatCard` wrapped its `actions` slot in a plain `<CtlSlot>`, so a bare `ActionIcon`
|
|
184
|
+
* resolved to the 30px `ctl` tier inside a 28px header row (`--vx-space-widget-header-height`) and
|
|
185
|
+
* grew it to 30. Measured in Chrome on the playground's dashboard at 390px, two KPI cards in the
|
|
186
|
+
* same grid row: the one WITH a kebab put its hero value at y=164, the one without at y=162 — the
|
|
187
|
+
* whole card, and every number in it, 2px out of line because of a slot's tier.
|
|
188
|
+
*/
|
|
189
|
+
describe('CtlSlot tier="widget"', () => {
|
|
190
|
+
test('a bare ActionIcon resolves the 24px icon step, not the 30px ctl one', () => {
|
|
191
|
+
const { container } = render(
|
|
192
|
+
<MantineProvider theme={baseTheme}>
|
|
193
|
+
<CtlSlot tier="widget">
|
|
194
|
+
<ActionIcon variant="subtle" aria-label="Card actions">
|
|
195
|
+
<span />
|
|
196
|
+
</ActionIcon>
|
|
197
|
+
</CtlSlot>
|
|
198
|
+
</MantineProvider>,
|
|
199
|
+
)
|
|
200
|
+
const button = container.querySelector('button')
|
|
201
|
+
expect(button?.getAttribute('data-size')).toBe('icon')
|
|
202
|
+
// `--ai-size-icon` reads `--vx-space-control-height-widget` (24px) in `ctlSizeVars()`, so the
|
|
203
|
+
// tier tracks the density knob like every other control height.
|
|
204
|
+
expect(button?.style.getPropertyValue('--ai-size')).toBe('var(--ai-size-icon)')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('the marker is the TIER name, the same one ChartCard writes by hand', () => {
|
|
208
|
+
const { container } = render(
|
|
209
|
+
<MantineProvider theme={baseTheme}>
|
|
210
|
+
<CtlSlot tier="widget">
|
|
211
|
+
<span />
|
|
212
|
+
</CtlSlot>
|
|
213
|
+
</MantineProvider>,
|
|
214
|
+
)
|
|
215
|
+
expect(container.querySelector('[data-basalt-tier]')?.getAttribute('data-basalt-tier')).toBe(
|
|
216
|
+
'widget',
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('the default is still `ctl` — no call site had to change', () => {
|
|
221
|
+
const { container } = render(
|
|
222
|
+
<MantineProvider theme={baseTheme}>
|
|
223
|
+
<CtlSlot>
|
|
224
|
+
<ActionIcon variant="subtle" aria-label="Sync">
|
|
225
|
+
<span />
|
|
226
|
+
</ActionIcon>
|
|
227
|
+
</CtlSlot>
|
|
228
|
+
</MantineProvider>,
|
|
229
|
+
)
|
|
230
|
+
expect(container.querySelector('button')?.getAttribute('data-size')).toBe('ctl')
|
|
231
|
+
expect(container.querySelector('[data-basalt-tier]')?.getAttribute('data-basalt-tier')).toBe(
|
|
232
|
+
'ctl',
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('the widget tier is ActionIcon-only — a Button in it keeps whatever it had', () => {
|
|
237
|
+
// Only `--ai-size-icon` exists at this step (`theme/index.ts`'s `ctlSizeVars`). Defaulting a
|
|
238
|
+
// Button to `size="icon"` would resolve `--button-height: var(--button-height-icon)` to nothing
|
|
239
|
+
// and paint a zero-height button — the exact `-xs`-var failure `ctlSizeVars`' doc records.
|
|
240
|
+
const { container } = render(
|
|
241
|
+
<MantineProvider theme={baseTheme}>
|
|
242
|
+
<CtlSlot tier="widget">
|
|
243
|
+
<Button>Save</Button>
|
|
244
|
+
</CtlSlot>
|
|
245
|
+
</MantineProvider>,
|
|
246
|
+
)
|
|
247
|
+
expect(container.querySelector('button')?.getAttribute('data-size')).not.toBe('icon')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `StatCard`'s own slot — the regression guard on the call site, not just on the mechanism. A
|
|
253
|
+
* `<CtlSlot>` here (the default tier) is what shipped, and nothing else in the suite would have
|
|
254
|
+
* caught it.
|
|
255
|
+
*/
|
|
256
|
+
describe('StatCard mounts its actions slot at the widget tier', () => {
|
|
257
|
+
test('a raw ActionIcon in `actions` lands on the 24px step', () => {
|
|
258
|
+
const { container } = render(
|
|
259
|
+
<MantineProvider theme={baseTheme}>
|
|
260
|
+
<StatCard
|
|
261
|
+
title="Total sales"
|
|
262
|
+
value="$1294.9k"
|
|
263
|
+
actions={
|
|
264
|
+
<ActionIcon variant="subtle" aria-label="Card actions">
|
|
265
|
+
<span />
|
|
266
|
+
</ActionIcon>
|
|
267
|
+
}
|
|
268
|
+
/>
|
|
269
|
+
</MantineProvider>,
|
|
270
|
+
)
|
|
271
|
+
const slot = container.querySelector('[data-basalt-tier]')
|
|
272
|
+
expect(slot?.getAttribute('data-basalt-tier')).toBe('widget')
|
|
273
|
+
expect(container.querySelector('button')?.getAttribute('data-size')).toBe('icon')
|
|
274
|
+
})
|
|
275
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `size="ctl"` slot theme (`docs/CONTROLS-SPEC.md` §5) — every home (`PageBar`, `Section`,
|
|
3
|
+
* table toolbar, sidebar blocks, wave 3/4) wraps its **slot**, never its body, in `<CtlSlot>` so a
|
|
4
|
+
* raw `Button`/`ActionIcon`/`Input`/… dropped into that slot renders at the `ctl` tier with NO prop
|
|
5
|
+
* at the call site — a `size="xs"` typed there is C5 (`basalt/control-size-literal`).
|
|
6
|
+
*
|
|
7
|
+
* `CTL_THEME.components[*].defaultProps.size = 'ctl'` only sets the DEFAULT — an explicit
|
|
8
|
+
* `size="md"` (or anything else) at the call site still wins, same override precedence as every
|
|
9
|
+
* other Mantine `defaultProps`. Mantine's own `mergeMantineTheme` DEEP-merges `components` (not a
|
|
10
|
+
* shallow replace), so nesting `<CtlSlot>` under `BasaltProvider` never drops the base theme's
|
|
11
|
+
* `Button.extend`/`ActionIcon.extend`/… `vars`/`classNames` (the shadow/depth idiom,
|
|
12
|
+
* `docs/DESIGN-SPEC.md` §5) — only `defaultProps.size` is added on top.
|
|
13
|
+
*
|
|
14
|
+
* `Menu` is deliberately absent — verified against the installed `@mantine/core` 9.3.0 source
|
|
15
|
+
* (`Menu.d.ts`, `MenuItem.d.ts`): neither accepts a `size` prop at all, so "Menu/Menu.Item where
|
|
16
|
+
* applicable" (the spec's own hedge) resolves to "not applicable" here.
|
|
17
|
+
*
|
|
18
|
+
* **Radio / Checkbox / Switch and their `.Group`s, and the one consequence worth stating.** These
|
|
19
|
+
* three default to Mantine's `sm` — a 20px indicator beside the tier's 13.5px option label — which
|
|
20
|
+
* is what every filter popover and the mobile sheet rendered. They are here rather than fixed in a
|
|
21
|
+
* CSS module because their geometry is a Mantine SIZE (five vars for a Switch alone), so the tier
|
|
22
|
+
* reaches them the same way it reaches Button and Input: through `-ctl` vars in
|
|
23
|
+
* `cssVariablesResolver`. The `.Group`s are listed too, so the group's `Input.Wrapper` label lands
|
|
24
|
+
* on the tier's font step as well.
|
|
25
|
+
*
|
|
26
|
+
* The consequence: Mantine resolves a Radio's size as `props.size ? props.size : ctx.size`
|
|
27
|
+
* (`Radio.mjs`), and a theme `defaultProps.size` is indistinguishable from an explicit prop to that
|
|
28
|
+
* check — so inside a `<CtlSlot>` a `<Radio.Group size="lg">` no longer sizes its children, they stay
|
|
29
|
+
* `ctl`. That is the correct trade in a home's SLOT (the whole contract there is "everything is the
|
|
30
|
+
* tier"), and it is stated here because it is the kind of thing that otherwise gets rediscovered as
|
|
31
|
+
* a bug. A consumer wanting a `lg` radio group puts it in a BODY, which no `CtlSlot` wraps.
|
|
32
|
+
*/
|
|
33
|
+
import {
|
|
34
|
+
ActionIcon,
|
|
35
|
+
Box,
|
|
36
|
+
Button,
|
|
37
|
+
Checkbox,
|
|
38
|
+
Input,
|
|
39
|
+
MantineThemeProvider,
|
|
40
|
+
MultiSelect,
|
|
41
|
+
NativeSelect,
|
|
42
|
+
Radio,
|
|
43
|
+
SegmentedControl,
|
|
44
|
+
Select,
|
|
45
|
+
Switch,
|
|
46
|
+
TextInput,
|
|
47
|
+
} from '@mantine/core'
|
|
48
|
+
import type { MantineSize, MantineThemeOverride } from '@mantine/core'
|
|
49
|
+
import type { ReactNode } from 'react'
|
|
50
|
+
|
|
51
|
+
/** The `MantineThemeOverride` a `<CtlSlot>` provides — see this module's doc for the merge/
|
|
52
|
+
* precedence contract. Exported so a consumer composing its own slot (a bespoke home not covered
|
|
53
|
+
* by `PageBar`/`Section`/the sidebar blocks) can reach the SAME tier without re-declaring it. */
|
|
54
|
+
export const CTL_THEME: MantineThemeOverride = {
|
|
55
|
+
components: {
|
|
56
|
+
Button: Button.extend({ defaultProps: { size: 'ctl' } }),
|
|
57
|
+
ActionIcon: ActionIcon.extend({ defaultProps: { size: 'ctl' } }),
|
|
58
|
+
Input: Input.extend({ defaultProps: { size: 'ctl' } }),
|
|
59
|
+
TextInput: TextInput.extend({ defaultProps: { size: 'ctl' } }),
|
|
60
|
+
Select: Select.extend({ defaultProps: { size: 'ctl' } }),
|
|
61
|
+
MultiSelect: MultiSelect.extend({ defaultProps: { size: 'ctl' } }),
|
|
62
|
+
SegmentedControl: SegmentedControl.extend({ defaultProps: { size: 'ctl' } }),
|
|
63
|
+
NativeSelect: NativeSelect.extend({ defaultProps: { size: 'ctl' } }),
|
|
64
|
+
// The theme keys are the component names Mantine's own `useProps` looks up — `RadioGroup`, not
|
|
65
|
+
// `Radio.Group` (`RadioGroup.mjs`: `useProps("RadioGroup", …)`).
|
|
66
|
+
Radio: Radio.extend({ defaultProps: { size: 'ctl' } }),
|
|
67
|
+
// Mantine types `RadioGroup.size` as a CLOSED `MantineSize`, while `Radio`, `Checkbox`,
|
|
68
|
+
// `Checkbox.Group`, `Switch` and `Switch.Group` all type theirs OPEN
|
|
69
|
+
// (`MantineSize | (string & {})`) — verified in the installed 9.3.0 `.d.ts` files, so this is an
|
|
70
|
+
// upstream inconsistency, not a statement that `RadioGroup` cannot take a custom tier. At
|
|
71
|
+
// runtime `getSize` interpolates the string identically for all six. The cast restores what
|
|
72
|
+
// every sibling type already allows; it widens nothing.
|
|
73
|
+
RadioGroup: Radio.Group.extend({ defaultProps: { size: 'ctl' as MantineSize } }),
|
|
74
|
+
Checkbox: Checkbox.extend({ defaultProps: { size: 'ctl' } }),
|
|
75
|
+
CheckboxGroup: Checkbox.Group.extend({ defaultProps: { size: 'ctl' } }),
|
|
76
|
+
Switch: Switch.extend({ defaultProps: { size: 'ctl' } }),
|
|
77
|
+
SwitchGroup: Switch.Group.extend({ defaultProps: { size: 'ctl' } }),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The `tier="widget"` slot theme — ActionIcon-ONLY, at the 24px `size="icon"` step
|
|
83
|
+
* (`--ai-size-icon` → `--vx-space-control-height-widget`, `docs/CONTROLS-SPEC.md` §5).
|
|
84
|
+
*
|
|
85
|
+
* It exists because a `WidgetHeader tier="widget"` header row is 28px
|
|
86
|
+
* (`--vx-space-widget-header-height`) and a 30px `ctl` control does not fit inside it. `StatCard`
|
|
87
|
+
* wrapped its `actions` slot in a plain `<CtlSlot>`, so a card WITH a kebab measured a 30px title
|
|
88
|
+
* row and a card WITHOUT one measured 28 — two KPI cards side by side in the same grid row were
|
|
89
|
+
* 2px out of alignment, and every number under them with it. Measured on the playground's dashboard
|
|
90
|
+
* at 390px: `Total sales` at 164 against `Orders` at 162.
|
|
91
|
+
*
|
|
92
|
+
* The tier is a HEIGHT statement, so a slot can only fix it by owning the size — which is exactly
|
|
93
|
+
* what {@link controlHeightWidget}'s own doc already said this step was for ("the `size="icon"`
|
|
94
|
+
* ActionIcon height — `WidgetHeader tier="widget"` actions"). It is ActionIcon-only because the
|
|
95
|
+
* tier has one var (`--ai-size-icon`): a 24px Button or Input has no vars declared and would
|
|
96
|
+
* silently resolve to its initial value, which is why widening this set is a
|
|
97
|
+
* `theme/index.ts`-first change, never a line added here.
|
|
98
|
+
*/
|
|
99
|
+
const WIDGET_THEME: MantineThemeOverride = {
|
|
100
|
+
components: {
|
|
101
|
+
ActionIcon: ActionIcon.extend({ defaultProps: { size: 'icon' } }),
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type CtlSlotProps = {
|
|
106
|
+
children: ReactNode
|
|
107
|
+
/**
|
|
108
|
+
* Which tier the slot imposes. `ctl` (the default) is the 30px control tier every home's control
|
|
109
|
+
* row uses. `widget` is the 24px ActionIcon step a `WidgetHeader tier="widget"` header row can
|
|
110
|
+
* hold — see {@link WIDGET_THEME} for the misalignment that makes it a separate tier rather than
|
|
111
|
+
* a caller's `size` prop.
|
|
112
|
+
*
|
|
113
|
+
* The values are the TIER names, matching the `data-basalt-tier` attribute the slot emits and the
|
|
114
|
+
* one `ChartCard` already writes by hand (it lives inside the Mantine-free `charts/` boundary and
|
|
115
|
+
* so cannot mount this component). `widget` maps to Mantine's `size="icon"`; the two vocabularies
|
|
116
|
+
* differ and that is upstream's, not basalt's.
|
|
117
|
+
*
|
|
118
|
+
* @default 'ctl'
|
|
119
|
+
*/
|
|
120
|
+
tier?: 'ctl' | 'widget'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wrap a home's SLOT (never its body) to render every Mantine control inside it at the `ctl` tier.
|
|
125
|
+
* The marker is a Mantine `Box` (not a raw `<div>` — `basalt/raw-html-layout` flags inline
|
|
126
|
+
* layout/surface styling on a bare HTML tag), `display: contents` keeps it out of the slot's own
|
|
127
|
+
* flex/grid layout while leaving the `data-basalt-tier="ctl"` attribute queryable/testable
|
|
128
|
+
* (`docs/CONTROLS-SPEC.md` §5) without the wrapper participating in flow.
|
|
129
|
+
*/
|
|
130
|
+
export function CtlSlot({ children, tier = 'ctl' }: CtlSlotProps): ReactNode {
|
|
131
|
+
return (
|
|
132
|
+
<MantineThemeProvider inherit theme={tier === 'widget' ? WIDGET_THEME : CTL_THEME}>
|
|
133
|
+
<Box data-basalt-tier={tier} style={{ display: 'contents' }}>
|
|
134
|
+
{children}
|
|
135
|
+
</Box>
|
|
136
|
+
</MantineThemeProvider>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ctl-tier coverage gate (`docs/CONTROLS-SPEC.md` §5) — greps every `getSize(size, '<prefix>')`
|
|
3
|
+
* / `getFontSize(size)` call in the INSTALLED `@mantine/core` source for the TWELVE components the
|
|
4
|
+
* tier covers (Button, ActionIcon, Input, SegmentedControl, Combobox, Select, MultiSelect,
|
|
5
|
+
* TextInput, Menu — the nine the spec names — plus Radio, Checkbox and Switch) and asserts
|
|
6
|
+
* `cssVariablesResolver`'s `variables` block declares a `-ctl` var for every distinct prefix found —
|
|
7
|
+
* so a missing var (the `--button-padding-x-ctl` the spec's own text calls out as "every draft
|
|
8
|
+
* omitted") fails the build instead of silently rendering at 0px/undefined.
|
|
9
|
+
*
|
|
10
|
+
* Radio/Checkbox/Switch joined the scan when they joined `CTL_THEME`: they had defaulted to
|
|
11
|
+
* Mantine's `sm`, a 20px indicator beside the tier's 13.5px option label, in every filter popover and
|
|
12
|
+
* in the mobile sheet. Adding them to `defaultProps` without declaring their vars would have been
|
|
13
|
+
* strictly worse than leaving them alone — an undeclared `--radio-size-ctl` resolves to the
|
|
14
|
+
* property's INITIAL value, which is how a Select once shipped with no chevron (see
|
|
15
|
+
* `theme/index.ts`'s `ctlSizeVars` doc). This scan is what makes the two halves inseparable.
|
|
16
|
+
*
|
|
17
|
+
* Reads the REAL installed package (not a hand-typed list of prefixes) so a future `@mantine/core`
|
|
18
|
+
* minor that adds a new `getSize`/`getFontSize` call to one of these nine components fails this
|
|
19
|
+
* test until the matching `-ctl` var is declared — the same "verify against source, not memory"
|
|
20
|
+
* discipline `ctl-theme.tsx`'s own doc comment describes.
|
|
21
|
+
*
|
|
22
|
+
* `getFontSize(size)` is `getSize(size, 'mantine-font-size')` (`core/utils/get-size/get-size.mjs`),
|
|
23
|
+
* so every `-fz`/`-font-size` call site (Button's `--button-fz`, Input's `--input-fz`,
|
|
24
|
+
* SegmentedControl's `--sc-font-size`, Combobox's `--combobox-option-fz`) collapses onto the ONE
|
|
25
|
+
* `mantine-font-size` prefix — verified by proving the prove-it-would-fail step below actually red-
|
|
26
|
+
* lines when `--mantine-font-size-ctl` is removed, not merely when a component-named font var is
|
|
27
|
+
* missing (no such var is ever read).
|
|
28
|
+
*
|
|
29
|
+
* `*Group`/`*GroupSection` subdirectories (`Button.Group`, `ActionIcon.Group`) are excluded — they
|
|
30
|
+
* are separate, undocumented-for-`ctl` components (`section-height`/`section-padding-x`/
|
|
31
|
+
* `section-fz`), out of scope for `docs/CONTROLS-SPEC.md` §5's nine-component list.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
34
|
+
import { dirname, join } from 'node:path'
|
|
35
|
+
import { DEFAULT_THEME, mergeMantineTheme } from '@mantine/core'
|
|
36
|
+
import type { MantineTheme } from '@mantine/core'
|
|
37
|
+
import { describe, expect, test } from 'bun:test'
|
|
38
|
+
import { CTL_THEME, baseTheme, cssVariablesResolver } from './index'
|
|
39
|
+
|
|
40
|
+
const theme: MantineTheme = mergeMantineTheme(DEFAULT_THEME, baseTheme)
|
|
41
|
+
|
|
42
|
+
const MANTINE_CORE_ESM_COMPONENTS = join(
|
|
43
|
+
dirname(require.resolve('@mantine/core/package.json')),
|
|
44
|
+
'esm/components',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/** Every component `CTL_THEME` sets a `ctl` default on, plus `Input`/`Combobox`/`Menu` which the
|
|
48
|
+
* others resolve through. Keep this list and `CTL_THEME.components` in step — the last test in this
|
|
49
|
+
* file asserts exactly that, so they cannot drift apart silently. */
|
|
50
|
+
const CTL_TIER_COMPONENTS = [
|
|
51
|
+
'Button',
|
|
52
|
+
'ActionIcon',
|
|
53
|
+
'Input',
|
|
54
|
+
'SegmentedControl',
|
|
55
|
+
'Combobox',
|
|
56
|
+
'Select',
|
|
57
|
+
'MultiSelect',
|
|
58
|
+
'TextInput',
|
|
59
|
+
'Menu',
|
|
60
|
+
'Radio',
|
|
61
|
+
'Checkbox',
|
|
62
|
+
'Switch',
|
|
63
|
+
// In `CTL_THEME` since the tier shipped, and never scanned until the drift test below was added —
|
|
64
|
+
// which is the whole argument for that test. It renders a native `<select>` through
|
|
65
|
+
// `Input`/`InputBase`, so it contributes no prefix of its own; being scanned is what proves that
|
|
66
|
+
// rather than assuming it.
|
|
67
|
+
'NativeSelect',
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
/** Every `.mjs` file (not `.mjs.map`) under a component directory, recursing into subcomponents but
|
|
71
|
+
* skipping `*Group`/`*GroupSection` (see this file's doc). */
|
|
72
|
+
function collectMjsFiles(dir: string): string[] {
|
|
73
|
+
if (!existsSync(dir)) return []
|
|
74
|
+
const out: string[] = []
|
|
75
|
+
for (const entry of readdirSync(dir)) {
|
|
76
|
+
if (/Group/.test(entry)) continue
|
|
77
|
+
const full = join(dir, entry)
|
|
78
|
+
const stat = statSync(full)
|
|
79
|
+
if (stat.isDirectory()) out.push(...collectMjsFiles(full))
|
|
80
|
+
else if (entry.endsWith('.mjs')) out.push(full)
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every distinct `getSize`/`getFontSize` prefix the nine components actually read, extracted from
|
|
86
|
+
* the installed source — `getFontSize(...)` always collapses to `mantine-font-size` (see this
|
|
87
|
+
* file's doc). */
|
|
88
|
+
function collectSizePrefixes(): Set<string> {
|
|
89
|
+
const prefixes = new Set<string>()
|
|
90
|
+
for (const component of CTL_TIER_COMPONENTS) {
|
|
91
|
+
for (const file of collectMjsFiles(join(MANTINE_CORE_ESM_COMPONENTS, component))) {
|
|
92
|
+
const src = readFileSync(file, 'utf8')
|
|
93
|
+
for (const m of src.matchAll(/get(?:Size|Spacing)\(\s*[\w.?]+\s*,\s*["']([\w-]+)["']/g)) {
|
|
94
|
+
prefixes.add(m[1]!)
|
|
95
|
+
}
|
|
96
|
+
if (/getFontSize\(/.test(src)) prefixes.add('mantine-font-size')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return prefixes
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('ctl-tier var coverage', () => {
|
|
103
|
+
const prefixes = collectSizePrefixes()
|
|
104
|
+
|
|
105
|
+
test('the scan finds the prefixes this spec is grounded in (sanity check on the scan itself)', () => {
|
|
106
|
+
expect(prefixes.has('button-height')).toBe(true)
|
|
107
|
+
expect(prefixes.has('button-padding-x')).toBe(true)
|
|
108
|
+
expect(prefixes.has('ai-size')).toBe(true)
|
|
109
|
+
expect(prefixes.has('input-height')).toBe(true)
|
|
110
|
+
expect(prefixes.has('sc-padding')).toBe(true)
|
|
111
|
+
expect(prefixes.has('combobox-option-padding')).toBe(true)
|
|
112
|
+
expect(prefixes.has('combobox-chevron-size')).toBe(true)
|
|
113
|
+
expect(prefixes.has('mantine-font-size')).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('the scan reaches Radio/Checkbox/Switch — the three the popovers and the sheet render', () => {
|
|
117
|
+
expect(prefixes.has('radio-size')).toBe(true)
|
|
118
|
+
expect(prefixes.has('radio-icon-size')).toBe(true)
|
|
119
|
+
expect(prefixes.has('checkbox-size')).toBe(true)
|
|
120
|
+
expect(prefixes.has('switch-height')).toBe(true)
|
|
121
|
+
expect(prefixes.has('switch-width')).toBe(true)
|
|
122
|
+
expect(prefixes.has('switch-thumb-size')).toBe(true)
|
|
123
|
+
expect(prefixes.has('switch-label-font-size')).toBe(true)
|
|
124
|
+
expect(prefixes.has('switch-track-label-padding')).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('there is NO --checkbox-icon-size to declare — Checkbox derives its tick in CSS', () => {
|
|
128
|
+
// Worth pinning: the obvious symmetry with `radio-icon-size` does not exist in 9.3.0, and
|
|
129
|
+
// declaring a var no `getSize` call reads would be dead weight that reads like coverage.
|
|
130
|
+
expect(prefixes.has('checkbox-icon-size')).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('every scanned prefix has a declared -ctl var', () => {
|
|
134
|
+
const declared = cssVariablesResolver(theme).variables
|
|
135
|
+
const missing = [...prefixes].filter((prefix) => !(`--${prefix}-ctl` in declared))
|
|
136
|
+
expect(missing).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('ActionIcon additionally gets an -icon var (size="icon", WidgetHeader tier="widget")', () => {
|
|
140
|
+
const declared = cssVariablesResolver(theme).variables
|
|
141
|
+
expect(declared['--ai-size-icon']).toBeDefined()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('--mantine-line-height-ctl is declared (read by every -ctl Input/Button label via the line-height cascade)', () => {
|
|
145
|
+
const declared = cssVariablesResolver(theme).variables
|
|
146
|
+
expect(declared['--mantine-line-height-ctl']).toBeDefined()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('every component CTL_THEME defaults to `ctl` is in the scanned set', () => {
|
|
150
|
+
// The two halves of the tier — `defaultProps.size = 'ctl'` and the `-ctl` vars — must move
|
|
151
|
+
// together, and this is what makes adding a component to one without the other fail. `*Group`
|
|
152
|
+
// keys resolve to their base component's prefixes (a Group reads `Input.Wrapper`'s vars, and the
|
|
153
|
+
// scan already covers `Input`), so they are stripped before the comparison.
|
|
154
|
+
const scanned = new Set<string>(CTL_TIER_COMPONENTS)
|
|
155
|
+
const themed = Object.keys(CTL_THEME.components ?? {}).map((name) =>
|
|
156
|
+
name.endsWith('Group') ? name.slice(0, -'Group'.length) : name,
|
|
157
|
+
)
|
|
158
|
+
expect(themed.filter((name) => !scanned.has(name))).toEqual([])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Proves the gate actually bites — the exact regression the spec calls out
|
|
162
|
+
// ("--button-padding-x-ctl every draft omitted").
|
|
163
|
+
test('regression proof: a var missing from cssVariablesResolver would fail the coverage assertion above', () => {
|
|
164
|
+
const declared = cssVariablesResolver(theme).variables
|
|
165
|
+
const withoutOne = { ...declared }
|
|
166
|
+
delete withoutOne['--button-padding-x-ctl']
|
|
167
|
+
const missing = [...prefixes].filter((prefix) => !(`--${prefix}-ctl` in withoutOne))
|
|
168
|
+
expect(missing).toEqual(['button-padding-x'])
|
|
169
|
+
})
|
|
170
|
+
})
|