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
|
@@ -1,449 +1,156 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: basalt-ui
|
|
3
|
-
description: Mantine v9
|
|
3
|
+
description: Mantine v9 conventions for basalt-ui apps — provider and overlay mount order, the shell's props, surfaces and depth, scroll regions, motion. Mostly enforced by construction via createBasaltTheme.
|
|
4
4
|
paths:
|
|
5
5
|
- 'src/**'
|
|
6
6
|
- 'apps/**/src/**'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- **
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- **
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
banned inside `charts/**`), and never raw `@visx/responsive` outside `charts/**`.
|
|
123
|
-
|
|
124
|
-
## Strict surfaces & layout primitives
|
|
125
|
-
|
|
126
|
-
The theme runs a **strict surface system**: Mantine components don't chain through
|
|
127
|
-
`--mantine-color-default-*` — each reads a **raw ramp step** directly — so `cssVariablesResolver`
|
|
128
|
-
**collapses the ramp steps onto the basalt tokens**. Light borders (`--mantine-color-gray-2/3/4`)
|
|
129
|
-
and dark borders (`--mantine-color-dark-4`) both resolve to `--vx-surface-border`; light
|
|
130
|
-
hover/subtle steps (`--mantine-color-gray-0/1`) resolve to the `--vx-surface-subtle` token;
|
|
131
|
-
`Card`/`Paper` backgrounds are forced to `--vx-surface-panel` via theme `styles` (Mantine otherwise
|
|
132
|
-
defaults card bg to the page body color, so cards blend into the page); `--app-shell-border-color`
|
|
133
|
-
is pinned to `--vx-surface-border`. Net: **one border shade, one card background, one radius
|
|
134
|
-
(`--vx-radius-card`) across every surface** — AppShell, Table, Input, Divider, Tabs, Popover,
|
|
135
|
-
Accordion, cards. The agent cannot reintroduce a divergent border/bg (no more off-system
|
|
136
|
-
`#aba59c`). Surfaces must come from `VX.surface.*` + the radius token — **never** inline
|
|
137
|
-
`border`/`borderRadius`/`backgroundColor`/`boxShadow`, and never a raw Mantine ramp-step var
|
|
138
|
-
(`var(--mantine-color-gray-N)`).
|
|
139
|
-
|
|
140
|
-
**Never pass `withBorder` to a `Card`/`Paper`.** Card depth is `--vx-shadow-card`, which bakes its
|
|
141
|
-
1px ring into the shadow value itself; the theme's `styles.root` pins bg/shadow/radius but does not
|
|
142
|
-
clear `border`, so `withBorder` draws a _second_, real edge on top of the ring and the card reads
|
|
143
|
-
heavy and boxed. This is the most common way a migrated app silently keeps its old hairline
|
|
144
|
-
identity — the prop is on-token (its color resolves to `--vx-surface-border`), so only the
|
|
145
|
-
`card-with-border` guard kind catches it. `withBorder={false}` and `<Card.Section withBorder>` (a
|
|
146
|
-
section divider, not card depth) are both fine.
|
|
147
|
-
|
|
148
|
-
- **Use Mantine primitives, not raw HTML.** To keep padding/spacing/radius/colors consistent,
|
|
149
|
-
consumer code must compose Mantine layout/surface primitives — `Box`, `Flex`, `Grid`,
|
|
150
|
-
`SimpleGrid`, `Stack`, `Group`, `Paper`, `Card` — instead of raw `<div>`/`<span>` with inline
|
|
151
|
-
`style`. Raw HTML with inline layout/surface styling defeats the token system.
|
|
152
|
-
- **The second fix is a CSS Module, and it is the right one in list code.** What
|
|
153
|
-
`raw-html-layout` actually objects to is the inline `style` literal, not the `<div>`. Moving the
|
|
154
|
-
rule into a `.module.css` class that reads `var(--vx-*)` satisfies the guard, keeps the values
|
|
155
|
-
in the token system, and costs zero runtime — where a Mantine primitive costs one component
|
|
156
|
-
instance per row. In a row renderer that paints hundreds of items, or in a downstream library
|
|
157
|
-
that must not force `@mantine/core` on its consumers, take the CSS Module. `check-theme` scans
|
|
158
|
-
`.css` and `.module.css`, so the tokens stay policed there. The guard's fix text names only the
|
|
159
|
-
Mantine route; both are sanctioned.
|
|
160
|
-
- **Mechanical enforcement.** `basalt-ui check-theme` adds eight surface guard kinds (each a config
|
|
161
|
-
knob, default ON; scoped `theme-allow <rule-id> — <reason>` escape): `off-system-surface-var` (raw
|
|
162
|
-
ramp-step vars), `card-with-border` (`withBorder` on a `Card`/`Paper`), `mantine-shade-index` (a
|
|
163
|
-
shade-pinned color — see below), `raw-html-layout` (raw `<div>`/`<span>` with inline layout/surface
|
|
164
|
-
styling), `inline-spacing` (inline spacing literals), `inline-display` (inline `display` literals),
|
|
165
|
-
and since 1.20.0 `css-raw-surface` (the same surface kinds in their kebab CSS dialect, so
|
|
166
|
-
`border-radius: 4px` in a CSS module is no longer invisible beside a flagged `borderRadius: 2`) and
|
|
167
|
-
`hidden-inline-style` (`raw-html-layout`'s formatting-independent half — a multi-line opening tag,
|
|
168
|
-
or a style object hoisted to a const). The Mantine-free `src/charts/**` is the only place raw
|
|
169
|
-
`<div>` is allowed — and it must still use `VX.*` tokens. `oxlint` adds `basalt/card-inset`, which
|
|
170
|
-
catches the other half of the same card drift: an explicit `p`/`padding`/`radius` on a
|
|
171
|
-
`Card`/`Paper` instead of the xs/sm inset and the theme radius.
|
|
172
|
-
|
|
173
|
-
**Never pin a shade index on a color.** `c="yellow.7"`, `bg="blue.4"`, `var(--mantine-color-red-6)` —
|
|
174
|
-
each names one fixed swatch that is identical in light and dark, so a shade legible on the dark page
|
|
175
|
-
is the one that fails contrast on the light one. Two correct forms: `VX.status.*` /
|
|
176
|
-
`--vx-status-warn|bad|good` for a verdict color (emitted per scheme, contrast guaranteed by the
|
|
177
|
-
derivation), or the bare hue name (`c="red"`, `color="green"`) to let the theme pick the step. The
|
|
178
|
-
`mantine-shade-index` guard kind catches the pinned form; `gray`/`dark` steps in `var()` belong to
|
|
179
|
-
`off-system-surface-var` instead. Note the distinction from `off-identity-accent`, which polices
|
|
180
|
-
_which hue_ you reach for and permits the bare status names on purpose — this kind polices the index.
|
|
9
|
+
<!-- basalt:coverage -->
|
|
10
|
+
<!-- GENERATED from src/surfaces.ts — `basalt-ui check-coverage --write`. Do not hand-edit. -->
|
|
11
|
+
<!-- backed by: guard kinds — card-with-border, hidden-inline-style, in-body-page-title, inline-display, inline-spacing, mantine-shade-index, raw-form-control, raw-html-layout, raw-motion-value, raw-spacing, sub-16-input-font · oxlint rules — basalt/card-inset, basalt/hand-rolled-shell, basalt/in-body-page-title, basalt/page-bar-budget, basalt/raw-scroll-container, basalt/shadow-basalt-export -->
|
|
12
|
+
<!-- not guarded: — -->
|
|
13
|
+
<!-- /basalt:coverage -->
|
|
14
|
+
|
|
15
|
+
# Basalt Mantine — provider, shell, surfaces
|
|
16
|
+
|
|
17
|
+
`createBasaltTheme` + `BasaltProvider` encode the theming opinion **by construction**; this rule is
|
|
18
|
+
the part the framework cannot enforce in code. The identity itself and every color/spacing/type law
|
|
19
|
+
live in basalt-tokens.md — not restated here.
|
|
20
|
+
|
|
21
|
+
## Provider and overlay mount
|
|
22
|
+
|
|
23
|
+
Mount `BasaltProvider` at the top of the tree, **above the router** (Mantine's context must exist
|
|
24
|
+
before any route renders), with `theme={createBasaltTheme(overrides)}`. Then the data layer, then
|
|
25
|
+
`RouterProvider`. Don't hand-build `createTheme` and don't add a second `cssVariablesResolver`.
|
|
26
|
+
|
|
27
|
+
**Overlays mount exactly once, through `BasaltOverlays`** (`basalt-ui/commands`), inside
|
|
28
|
+
`BasaltProvider`: it composes `ModalsProvider`, Spotlight (against basalt's own store), the command
|
|
29
|
+
hotkeys and `<Notifications>` in one place, each disableable with `false`. A standalone
|
|
30
|
+
`<BasaltNotifications />` (`basalt-ui/notifications`) is the alternative for an app with no commands
|
|
31
|
+
layer — **never both in one tree**, which double-mounts `<Notifications>`. Import the **layered**
|
|
32
|
+
style bundles for every `@mantine/*` battery, then `basalt-ui/styles.css` last; an unlayered Mantine
|
|
33
|
+
import outranks `@layer basalt` regardless of specificity.
|
|
34
|
+
|
|
35
|
+
Color scheme is read and written through `useMantineColorScheme()` only — never a client store,
|
|
36
|
+
never `localStorage.getItem('theme')` (guard-enforced; see basalt-state.md).
|
|
37
|
+
|
|
38
|
+
## The shell — one mount, declared data
|
|
39
|
+
|
|
40
|
+
`BasaltShell` composes `AppSidebar` / `MobileNav` / `AppBreadcrumbs` / `PageBar` and is the canonical
|
|
41
|
+
single mount (`basalt/hand-rolled-shell` fires on `AppShell.*` parts or a `Burger` in a file that
|
|
42
|
+
does not render it). The sub-components are exported for a genuinely divergent layout; prefer the
|
|
43
|
+
shell, which already wires collapse persistence, the mobile breakpoint and the breadcrumb.
|
|
44
|
+
|
|
45
|
+
Every extension point is **declared data, not a `ReactNode` slot**, so basalt owns the mobile
|
|
46
|
+
projection:
|
|
47
|
+
|
|
48
|
+
| Prop | Shape | What basalt then owns |
|
|
49
|
+
| ------------------- | ------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
50
|
+
| `sections` | `SidebarSection[]` | the desktop sidebar AND the mobile bar (`projectMobileNav`) — one definition |
|
|
51
|
+
| `globalActions` | `GlobalAction[]` | header placement; the first two ride the mobile bar, the rest fold into ONE kebab |
|
|
52
|
+
| `sidebarBlocks` | `SidebarBlock[]` | rail projection (dot / ring), fold persistence, one More-sheet row per block (law C13) |
|
|
53
|
+
| `brand` | `BrandConfig & { menu? }` | a `Name ▾` workspace switcher when `menu` is present |
|
|
54
|
+
| `search` | `SidebarSearchConfig & { actions? }` | the ⌘K row plus one or two icon buttons (a tuple by type) |
|
|
55
|
+
| `settingsMenuItems` | `SettingsMenuItem[]` | flat footer rows at three or fewer, one gear menu above that |
|
|
56
|
+
| `account` | `BasaltAccountProps` | the footer row, below the settings menu |
|
|
57
|
+
|
|
58
|
+
- **The router seam is ONE component, not a render callback.** `SidebarItem.Anchor` (a `NavAnchor`)
|
|
59
|
+
is the consumer's `<Link>`; basalt renders every pixel around it — desktop row, mobile slot, sheet
|
|
60
|
+
row. Absent, the row falls back to `<a href>` + `onClick`. The breadcrumb's equivalent is
|
|
61
|
+
`parentAnchor`. There is no `renderNavLink` / `renderBreadcrumbLink` surface.
|
|
62
|
+
- **`AppBreadcrumbs` is prop-driven** (`section` / `parent` / `parentAnchor` / `parentHref` /
|
|
63
|
+
`page`) and renders `null` without `page`. `BasaltShell` derives those props from the active item;
|
|
64
|
+
`useRouterBreadcrumbs` (basalt-state.md) is the router-side source for a custom trail.
|
|
65
|
+
- **Collapse persists through basalt's own `createPersistedState`**, keyed `basalt:<storageKey>` —
|
|
66
|
+
read it with `readPersistedValue`, never a bare `localStorage.getItem`. Pass `collapsed` +
|
|
67
|
+
`onCollapsedChange` to own it instead (e.g. from your own hotkey — basalt binds none).
|
|
68
|
+
- **The `.` entry is router-agnostic; a router adapter DOES ship**, at `./router-tanstack`. Badge /
|
|
69
|
+
active / navigate wiring stays consumer-side either way; `NavCountBadge` is the count pattern.
|
|
70
|
+
- **The account row is presentational over a provider-agnostic contract** — basalt has no auth
|
|
71
|
+
dependency and ships no `./auth`. The authenticated row leads with initials derived from
|
|
72
|
+
`identity.name` (never an avatar image); the unauthenticated row falls back to a person glyph.
|
|
73
|
+
No separating hairline: the row's own top padding is the separation. The email is hidden unless
|
|
74
|
+
`showEmail` is passed.
|
|
75
|
+
|
|
76
|
+
**Mobile is a tab bar, not a menu.** A slot is a destination, so a tap navigates with nothing to
|
|
77
|
+
dismiss; there is no full-height mobile drawer, and everything it used to hold reaches the trailing
|
|
78
|
+
More slot. `projectMobileNav` is a PURE projection and infers the surface from row count — 0 drops
|
|
79
|
+
the slot, 1 collapses to a plain link (a group of one IS a destination), up to `menuMax` is a menu,
|
|
80
|
+
more is a bottom sheet. `SidebarItem.mobile` (`'tab'` / `'more'` / `'hidden'`) is the per-destination
|
|
81
|
+
placement; `MobileNavConfig` is the escape hatch, not the interface. Two bar rules that look like
|
|
82
|
+
bugs and are not: the active indicator is a **neutral** ink tint behind the ICON only (never the
|
|
83
|
+
identity blue, never a full-tab fill — the desktop sidebar's accent-icon rule does not apply here),
|
|
84
|
+
and the bar carries **no** `env(safe-area-inset-bottom)` padding, because Mantine's own
|
|
85
|
+
`AppShell.Footer` rule already grows the box by the inset. Search lives in the SIDEBAR, not the
|
|
86
|
+
header; the header carries no bottom rule. "Zero horizontal rules" is not true of the mobile bar,
|
|
87
|
+
which has a top border by design.
|
|
88
|
+
|
|
89
|
+
## Surfaces, depth and shape
|
|
90
|
+
|
|
91
|
+
- **Never pass `withBorder` to a `Card`/`Paper`.** Card depth is `VX.shadowCard`, which bakes its 1px
|
|
92
|
+
ring into the shadow value; the theme pins bg/shadow/radius but does not clear `border`, so
|
|
93
|
+
`withBorder` draws a second real edge and the card reads heavy. It is on-token, so only the
|
|
94
|
+
`card-with-border` guard kind catches it. `<Card.Section withBorder>` is a section divider and fine.
|
|
95
|
+
- **Three depth tiers, split control-vs-panel-vs-floating**, and the dividing line is that split —
|
|
96
|
+
never component-by-component. `shadowCard` for surfaces that HOLD content (Card, Paper,
|
|
97
|
+
Notification, ChartCard, SettingsSection): an outset ring on light, a deeper drop + inset rim on
|
|
98
|
+
dark. `shadowRaised` for anything you click or type into: **never an outset ring**, because an
|
|
99
|
+
outset ring paints in the color of whatever it is drawn over, so one value can ride a saturated
|
|
100
|
+
fill, a tint and a panel alike. `shadowOverlay` for detached floating surfaces (Tooltip, Popover,
|
|
101
|
+
Menu, Combobox, Modal, Drawer). Focus is a fourth tier and LAYERS over the resting depth.
|
|
102
|
+
- **Emphasis is fill weight, not depth.** Every box-owning Button/ActionIcon variant takes the same
|
|
103
|
+
resting depth; `outline` takes the ring-free `shadowCtrl` (its border is its edge) and `subtle` is
|
|
104
|
+
**flat in both states** — a text affordance, with a neutral ink tint as its hover and nothing more.
|
|
105
|
+
**Depth is static**: no variant lifts or gains a shadow on hover or press. A hover lift and a
|
|
106
|
+
materializing `subtle` shadow were both tried and reverted.
|
|
107
|
+
- **The ring must land on the box that carries the surface's `border-radius`** — it is drawn by that
|
|
108
|
+
box's own corners. `ChartCard` legitimately puts the shadow on a box with no background (an inner
|
|
109
|
+
box paints the fill, because `overflow: hidden` on the shadowed box would clip the shadow).
|
|
110
|
+
- **Use Mantine primitives, not raw HTML** — `Box`/`Flex`/`Grid`/`SimpleGrid`/`Stack`/`Group`/
|
|
111
|
+
`Paper`/`Card` instead of a `<div>` with an inline `style`. **The second sanctioned fix is a CSS
|
|
112
|
+
module**, and in row code it is the right one: what the guard objects to is the inline literal, not
|
|
113
|
+
the `<div>`, and a class reading `var(--vx-*)` costs no component instance per row. `check-theme`
|
|
114
|
+
scans `.css`, so the tokens stay policed there. Only the Mantine-free `src/charts/**` may use raw
|
|
115
|
+
`<div>` freely — still with `VX.*`.
|
|
116
|
+
- **Inputs come from `@mantine/core`**, never a native `<select>`; the theme defaults them to the
|
|
117
|
+
form tier, whose font size clears the iOS zoom threshold, and the floor in `styles.css` is
|
|
118
|
+
`!important` (`sub-16-input-font` flags a smaller one as dead code against it).
|
|
119
|
+
- **Charts measure themselves** — pass `height` / `aspectRatio` / `fill` and nothing else. Never
|
|
120
|
+
`useElementSize` inside a chart file (Mantine is banned there), never raw `@visx/responsive`
|
|
121
|
+
outside `charts/**`.
|
|
181
122
|
|
|
182
123
|
## Scroll regions
|
|
183
124
|
|
|
184
|
-
**A scroll region inside app chrome is a Mantine `ScrollArea`, not a raw `overflow: auto` box
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
the
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
a
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
basalt-ui
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- **
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
- **
|
|
215
|
-
|
|
216
|
-
**one depth token** (`shadow-card` — a whisper shadow + 1px ring, the ring lives IN the shadow, no
|
|
217
|
-
separate `border` property). Cards must never diverge: same radius, same shadow. Never
|
|
218
|
-
inline-override `border`/`borderRadius`/`boxShadow`/`backgroundColor` on a surface — use the
|
|
219
|
-
radius token (`radius="md"` / `var(--vx-radius-card)`) + `VX.shadowCard` + `VX.surface.*`.
|
|
220
|
-
Mechanically enforced by `basalt-ui check-theme`'s `raw-surface` guard. Outer spacing comes from the
|
|
221
|
-
parent `Stack`/`SimpleGrid` gap, not an intrinsic card margin.
|
|
222
|
-
- **Depth = a whisper shadow with a 1px ring baked in, not a plain hairline** (see
|
|
223
|
-
<https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, doctrine inversion #1 — this supersedes the old
|
|
224
|
-
"never a drop shadow" rule). **Three depth tiers, split control-vs-panel-vs-floating:**
|
|
225
|
-
- **`shadow-card` — panels.** A surface that HOLDS content: `Card`, `Paper`, `Notification`,
|
|
226
|
-
`ChartCard`, `SettingsSection`. Its light-mode ring is an **outset** 1px hairline; dark is a
|
|
227
|
-
deeper drop + an inset 4%-white rim.
|
|
228
|
-
- **`shadow-raised` — interactive controls.** Anything you click or type into: Button,
|
|
229
|
-
ActionIcon, the field idiom (Input/TextInput/PillsInput/…), the sidebar search trigger, header
|
|
230
|
-
icon buttons, `Chip`, `CheckboxCard`/`RadioCard`, the agent composer. **Never an outset ring** —
|
|
231
|
-
dark is drop + an inset 8%-white rim, light is drop only. That is what lets ONE value ride a
|
|
232
|
-
saturated fill, a 13% tint and a neutral panel alike: an outset ring paints in the color of
|
|
233
|
-
whatever it is drawn over, so `shadow-card`'s pale hairline would read as a grey outline around
|
|
234
|
-
a colored button, while an inset edge is drawn over the control's own background. The
|
|
235
|
-
light/dark asymmetry is deliberate — a light-mode edge would have to be a _darkening_, which
|
|
236
|
-
reads as a border (uniform) or a double bottom rule (bottom-only); both were tried and
|
|
237
|
-
rejected. Don't tidy the two schemes into one shape.
|
|
238
|
-
- **`shadow-overlay` — detached floating surfaces.** Tooltip, Popover, Menu, Combobox, Modal,
|
|
239
|
-
Drawer — they sit _above_ the page rather than on it. Resolved in `theme/index.ts`; not
|
|
240
|
-
Mantine's own shadow scale.
|
|
241
|
-
|
|
242
|
-
Focus is a fourth tier and **layers**: a 2px accent ring drawn OVER the resting depth, never
|
|
243
|
-
replacing it.
|
|
244
|
-
|
|
245
|
-
**The dividing line is control-vs-panel, not component-by-component.** Two controls sitting side
|
|
246
|
-
by side must never land on different tokens — a `size="md"` Input beside a `size="md"` Button
|
|
247
|
-
(pinned to the same height by `--vx-space-control-height`), or the header's search trigger and
|
|
248
|
-
icon button. Closing exactly that split is why `shadow-raised` exists.
|
|
249
|
-
|
|
250
|
-
**Emphasis is fill weight, not depth.** Every Button/ActionIcon variant that owns a box takes the
|
|
251
|
-
SAME resting depth: `default` / `filled` / `light` → `shadow-raised` unmodified; `outline` →
|
|
252
|
-
ring-free `shadow-ctrl` (its real 1px accent border already is its edge); `subtle` → **flat in
|
|
253
|
-
both states** (a text affordance, not an object — its hover is a neutral ink-6% tint and nothing
|
|
254
|
-
more). **Depth is static — no variant moves or changes depth on hover or press.** A hover lift
|
|
255
|
-
and a materializing `subtle` shadow were both tried and reverted; don't reintroduce either.
|
|
256
|
-
Disabled is flat and grounded. Reduced motion drops the transition, never the resting depth.
|
|
257
|
-
|
|
258
|
-
Mechanics: every default-variant control renders its bg + depth with a _transparent_ 1px border
|
|
259
|
-
box (never `border: none` — the box stays so focus/error can recolor it with no layout shift).
|
|
260
|
-
`--mantine-color-default-border` is pinned to `transparent` in `cssVariablesResolver`, the ONE
|
|
261
|
-
lever that kills the stock hairline for every default-variant component at once
|
|
262
|
-
(`defaultVariantColorsResolver`'s `variant === 'default'` branch reads that single var); the
|
|
263
|
-
shadow itself is added per-component in `controls.module.css`, since Mantine never declares one
|
|
264
|
-
for `default`. `PillsInput` reuses the Input field idiom's `classNames` wiring directly (it
|
|
265
|
-
renders `InputBase` under its own `__staticSelector`, so the base `Input` theme entry never
|
|
266
|
-
reached it). Layout dividers (header bottom border, sidebar section separators) still use plain
|
|
267
|
-
borders — only card/control depth moved to shadow. A dedicated test,
|
|
268
|
-
`src/theme/border-coverage.test.ts`, mechanically enumerates every `@mantine/core` component
|
|
269
|
-
whose shipped CSS declares a border and asserts it's either a themed `baseTheme.components` key
|
|
270
|
-
or a reasoned `BORDER_ALLOWLIST` entry — this is what catches the NEXT unthemed bordered
|
|
271
|
-
component (Button/ActionIcon shipped with no `.extend()` block at all, which no consumer-source
|
|
272
|
-
guard can ever see).
|
|
273
|
-
|
|
274
|
-
- **The ring must land on the box that carries the surface's `border-radius`.**
|
|
275
|
-
`shadow-card`/`shadow-raised` bakes a 1px ring into the shadow value itself, and the ring is drawn
|
|
276
|
-
by the shadowed box's OWN corners — it never reads correctly against a different element's radius
|
|
277
|
-
(the Input wrapper bug: a `position: relative` box with `border-radius: 0` drew a square ring
|
|
278
|
-
around the 8px-rounded `<input>` inside it). Background usually co-locates with the radius since
|
|
279
|
-
most surfaces declare both on the same box, but it's the radius the ring is contractually bound
|
|
280
|
-
to, not the background — `ChartCard.tsx` legitimately carries the shadow on a box with NO
|
|
281
|
-
background (an inner box, sharing its exact radius, paints the fill instead, because `overflow:
|
|
282
|
-
hidden` on the same box as the shadow would clip the shadow itself) and is correct. `src/theme/
|
|
283
|
-
shadow-surfaces.test.ts` mechanically enumerates every site that applies either token and requires
|
|
284
|
-
a written `roundedBy` reason naming where that box's radius comes from.
|
|
285
|
-
- **Tight radii read precise/technical** (Linear): `sm` for controls, `md` for cards, `pill` for
|
|
286
|
-
badges. Consume the token, never a raw number (basalt-tokens.md).
|
|
287
|
-
- **Type is carried by the three-font system** (sans body / head condensed headings / mono
|
|
288
|
-
numerals — basalt-tokens.md), numbers in the mono tabular stack so metric columns align (a
|
|
289
|
-
Coinbase pattern).
|
|
290
|
-
|
|
291
|
-
## Composing the shell from sub-components
|
|
292
|
-
|
|
293
|
-
`BasaltShell` is the canonical single-mount, and since 1.20.0 `basalt/hand-rolled-shell` (`warn`)
|
|
294
|
-
says so mechanically: `AppShell.*` parts or a `Burger` in a file that does not render `BasaltShell`
|
|
295
|
-
is the layer the 1.19 nav contract replaces. It is file-scoped and reported once — which shell a
|
|
296
|
-
file uses is one decision. When a fully custom layout is genuinely needed, the shell sub-components
|
|
297
|
-
are available individually from `basalt-ui`:
|
|
298
|
-
|
|
299
|
-
- `AppSidebar` — desktop sidebar (sections, collapse, brand, settings/account extras)
|
|
300
|
-
- `MobileNav` — the bottom tab bar (takes a `MobileNavModel` from `projectMobileNav`)
|
|
301
|
-
- `AppBreadcrumbs` — breadcrumb bar (reads from `useRouterBreadcrumbs` or a manual trail)
|
|
302
|
-
- `PageHeaderProvider` + `PageActions` + `PageActionsOutlet` — portal-based page action slots
|
|
303
|
-
- `NavCountBadge` — count badge for sidebar nav items
|
|
304
|
-
|
|
305
|
-
```tsx
|
|
306
|
-
import {
|
|
307
|
-
AppSidebar,
|
|
308
|
-
MobileNav,
|
|
309
|
-
AppBreadcrumbs,
|
|
310
|
-
PageHeaderProvider,
|
|
311
|
-
PageActionsOutlet,
|
|
312
|
-
NavCountBadge,
|
|
313
|
-
projectMobileNav,
|
|
314
|
-
} from 'basalt-ui'
|
|
315
|
-
|
|
316
|
-
function CustomShell({ children }: { children: React.ReactNode }) {
|
|
317
|
-
return (
|
|
318
|
-
<PageHeaderProvider>
|
|
319
|
-
<AppSidebar sections={sections} brand={brand} />
|
|
320
|
-
<MobileNav model={projectMobileNav(sections)} />
|
|
321
|
-
<main>
|
|
322
|
-
<AppBreadcrumbs />
|
|
323
|
-
<PageActionsOutlet />
|
|
324
|
-
{children}
|
|
325
|
-
</main>
|
|
326
|
-
</PageHeaderProvider>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Prefer `BasaltShell` unless the layout genuinely diverges — it already composes all of the above
|
|
332
|
-
with the correct wiring (collapse persistence, mobile breakpoint, breadcrumb integration).
|
|
333
|
-
|
|
334
|
-
## Mobile bar
|
|
335
|
-
|
|
336
|
-
The bottom bar is a **tab bar, not a menu**: a slot is a DESTINATION, so a tap navigates instantly
|
|
337
|
-
with nothing to dismiss. There is no full-height mobile sidebar drawer — everything it used to hold
|
|
338
|
-
(nav, account, settings, theme switcher) is reachable from the trailing More slot instead.
|
|
339
|
-
|
|
340
|
-
`BasaltShell` builds the whole thing from the same `SidebarSection[]` the desktop sidebar renders:
|
|
341
|
-
**`projectMobileNav`** projects those sections onto the bar and **`MobileNav`** paints the result.
|
|
342
|
-
There is no second nav model to maintain, and in the common case no configuration at all.
|
|
343
|
-
|
|
344
|
-
**Surface inference — the part you do not configure.** For any slot holding `n` rows:
|
|
345
|
-
|
|
346
|
-
| `n` | surface |
|
|
347
|
-
| ----------------------- | ------------------------------------------------------------------------- |
|
|
348
|
-
| 0 | the slot is dropped |
|
|
349
|
-
| 1 | a plain navigation link — a group of one IS a destination |
|
|
350
|
-
| ≤ `menuMax` (default 6) | a Mantine `Menu` that pops upward out of the tab, content-sized, no scrim |
|
|
351
|
-
| > `menuMax` | a bottom `Drawer` sheet with a grabber and an internal `ScrollArea` |
|
|
352
|
-
|
|
353
|
-
Six is arithmetic, not taste: 6 × 44px + 8px padding = 272px against 415px of headroom on the
|
|
354
|
-
smallest supported viewport, which is why the menu is configured `flip: false` and can never render
|
|
355
|
-
below the fold. Move a destination in or out of the More slot and the surface changes itself.
|
|
356
|
-
|
|
357
|
-
**Placement** is one field per destination — `SidebarItem`'s `mobile`: `'tab'` gives it a slot of
|
|
358
|
-
its own, `'more'` (the default) makes it an overflow row, `'hidden'` drops it from mobile entirely.
|
|
359
|
-
A whole section claims one slot with `mobile: { tab: true }` (its destinations then open per the
|
|
360
|
-
table above) or leaves mobile with `mobile: false`. Configure nothing and the first four non-disabled
|
|
361
|
-
top-level destinations take slots — a nav that works before anyone has thought about mobile.
|
|
362
|
-
|
|
363
|
-
**`MobileNavConfig`** (the `mobileNav` prop) is the escape hatch, not the interface: `tabs` for an
|
|
364
|
-
explicit slot order by key when the bar order must differ from the sidebar's, `maxTabs` (5),
|
|
365
|
-
`menuMax` (6), `moreLabel`, `moreExtra`, and `getScrollElement` for re-tap-to-top when the page
|
|
366
|
-
scrolls inside a container rather than the document.
|
|
367
|
-
|
|
368
|
-
Design rules specific to the bar, each one load-bearing:
|
|
369
|
-
|
|
370
|
-
- **The active indicator is a neutral pill behind the ICON only** — `--vx-ink` at 10% plus
|
|
371
|
-
`--vx-radius-ctrl`, the Material-3 idiom. Never the identity blue, and never a full-tab fill: a
|
|
372
|
-
filled tab reads as chrome instead of as a tab bar. This is the exception to "active nav = accent
|
|
373
|
-
icon" in the palette rules above; the desktop sidebar keeps that rule unchanged.
|
|
374
|
-
- **A count renders as an 8px accent dot** on the slot icon, not a number — a count glyph is
|
|
375
|
-
unreadable at 56px. `SidebarItem.count` drives it; `SidebarItem.badge` (a rich node) stays
|
|
376
|
-
desktop-only.
|
|
377
|
-
- **Press feedback is the icon, not the hit box**: `.tab:active` scales the icon to 0.9 over 140ms.
|
|
378
|
-
`touch-action: manipulation` + `-webkit-tap-highlight-color: transparent` kill the 300ms
|
|
379
|
-
double-tap delay and the blue flash.
|
|
380
|
-
- **Every `:hover` rule sits behind `@media (hover: hover)`.** Ungated, a tapped row stays lit until
|
|
381
|
-
something else is tapped.
|
|
382
|
-
- **Re-tapping the ACTIVE slot scrolls to top** instead of pushing a redundant history entry.
|
|
383
|
-
- **Never add `padding-bottom: env(safe-area-inset-bottom)` to the bar.** Mantine's own
|
|
384
|
-
`AppShell.Footer` rule already grows the footer box by the inset and pads its content; adding it
|
|
385
|
-
again double-counts. The one real gap is `AppShell.Main`, whose padding is computed from an offset
|
|
386
|
-
var that excludes the inset — the shell closes that itself.
|
|
387
|
-
- **Reduced motion** is read explicitly via `useReducedMotion` (Mantine's
|
|
388
|
-
`respectReducedMotion` defaults to `false` and basalt does not change it) and zeroes both the menu
|
|
389
|
-
and sheet transitions.
|
|
390
|
-
|
|
391
|
-
Accessibility, all handled by the shell: the bar is `<nav aria-label="Primary">`, the active slot
|
|
392
|
-
carries `aria-current="page"`, every slot carries `aria-label` (the visible text is the short
|
|
393
|
-
label), menus get `menuItemTabIndex={0}` for the WAI-ARIA disclosure pattern, and the row height
|
|
394
|
-
floors at 44px with the bar at 48px regardless of the `density` level. **Do not hand-set
|
|
395
|
-
`aria-haspopup` / `aria-expanded`** on a `Menu.Target` child — Mantine clones them on.
|
|
396
|
-
|
|
397
|
-
## Icons
|
|
398
|
-
|
|
399
|
-
basalt-ui takes icons as `ReactNode` (no icon dependency shipped). Use a single icon library across the
|
|
400
|
-
app — `@tabler/icons-react` pairs cleanly with Mantine. Size via the icon's `size` prop (`size={16}`),
|
|
401
|
-
not CSS.
|
|
402
|
-
|
|
403
|
-
## Interaction feedback (micro-animations)
|
|
404
|
-
|
|
405
|
-
Every action button (Save, Log, Add, Delete, Toggle) must give immediate visual confirmation — silent
|
|
406
|
-
success feels broken. Three layers, applied together:
|
|
407
|
-
|
|
408
|
-
1. **In-flight state** — `loading` prop on `<Button>` (built-in spinner). Disables clicks automatically.
|
|
409
|
-
2. **Post-success state on the trigger itself** — flip the button briefly: swap label `Save → Saved`,
|
|
410
|
-
set `color="green"` (the success status hue — never `teal`/`violet`, which the theme guard rejects as
|
|
411
|
-
off-identity), reveal an `IconCheck` via `<Transition mounted={justSaved} transition="pop"
|
|
412
|
-
duration={180}>`. Hold ~1.2–1.5s, then reset. The user's eye is on the thing they clicked — feedback
|
|
413
|
-
lands there, not only in a corner toast.
|
|
414
|
-
3. **Toast (`notifications.show`)** — only for outcomes the user might miss (writes that succeed
|
|
415
|
-
off-screen, errors, async background work). Don't toast trivial UI toggles. `autoClose` ≤ 2000ms for
|
|
416
|
-
success; errors stay until dismissed.
|
|
417
|
-
|
|
418
|
-
Guidelines:
|
|
419
|
-
|
|
420
|
-
- Durations: 120–200ms for state flips, 180–250ms for `Transition`, never above 300ms (feels laggy).
|
|
421
|
-
- Never animate layout-shifting properties (height, width) on buttons — animate `transform`, `opacity`,
|
|
422
|
-
`color`, `background`.
|
|
423
|
-
- A button that turns into a check **is** the confirmation; the toast is for context the user can't see.
|
|
424
|
-
- Destructive actions: `modals.openConfirmModal` first, then the same in-button success pattern after.
|
|
425
|
-
- Form errors: prefer inline field errors over toast; toast only for submit-level failures.
|
|
426
|
-
|
|
427
|
-
## Motion (`motion`)
|
|
428
|
-
|
|
429
|
-
basalt-ui ships `motion` (motion.dev, formerly framer-motion) as a bundled dependency — the same
|
|
430
|
-
precedent as `@visx/*`: an internal implementation detail, not a peer the consumer opts into.
|
|
431
|
-
`ThemeToggle` is the first consumer (icon morph + direct-select popover reveal).
|
|
432
|
-
|
|
433
|
-
- **Reach for Mantine's own `<Transition>`** (above) for simple mount/unmount fades/pops — it's
|
|
434
|
-
already there, needs no import. **Reach for `motion`** when the interaction needs something
|
|
435
|
-
`Transition` can't do: crossfade between two different elements (not just show/hide one),
|
|
436
|
-
spring physics, or a gesture-driven reveal.
|
|
437
|
-
- **Import from `motion/react`, never the raw `framer-motion` package** — `motion` re-exports it,
|
|
438
|
-
but a direct `framer-motion` import bypasses the pinned/vetted specifier. Enforced by oxlint
|
|
439
|
-
`no-restricted-imports` (repo-local and the shipped consumer preset).
|
|
440
|
-
- **Never a hardcoded duration/spring/ease literal in a `transition={{...}}` prop.** Import
|
|
441
|
-
`MOTION_DURATION` / `MOTION_SPRING` / `MOTION_EASE_STANDARD` from `basalt-ui` instead — the
|
|
442
|
-
motion analog of routing color through `VX.*`. `MOTION_DURATION` is capped at 0.3s (300ms),
|
|
443
|
-
matching the interaction-feedback ceiling above. Mechanically enforced by `basalt-ui check-theme`'s
|
|
444
|
-
`raw-motion-value` guard (default ON; `theme-allow` line-comment escape).
|
|
445
|
-
- **Always branch on `useReducedMotion`** (`@mantine/hooks`) — render a real unanimated code path
|
|
446
|
-
(no `motion.*` wrapper at all), not just `transition={{ duration: 0 }}`. See `ThemeToggle`'s
|
|
447
|
-
`SchemeGlyph` for the pattern.
|
|
448
|
-
- **Restraint applies to motion too.** Subtle and purposeful — a state change earns a transition,
|
|
449
|
-
idle chrome does not. Never a looping/pulsing idle animation.
|
|
125
|
+
**A scroll region inside app chrome is a Mantine `ScrollArea`, not a raw `overflow: auto` box** — it
|
|
126
|
+
draws its own bar inside a `position: relative; overflow: hidden` root, so the bar floats instead of
|
|
127
|
+
reserving gutter width and reflowing the column. `AppSidebar`'s nav is the reference (`type="hover"`,
|
|
128
|
+
`scrollbars="y"`, the flex fill on the ScrollArea root). `styles.css` re-hides the native bar on the
|
|
129
|
+
viewport, so **one overlay bar** is the framework-wide outcome — do not re-theme it.
|
|
130
|
+
|
|
131
|
+
Raw `overflow: auto` stays correct where a library owns the scroll node (`BasaltStickToBottom`,
|
|
132
|
+
`BasaltVirtualList`) and for a table body, which is `Table.ScrollContainer type="native"` — a
|
|
133
|
+
`ScrollArea` viewport is the positioning context a sticky `<thead>` resolves against, so the default
|
|
134
|
+
type pins the header to the page viewport instead of the table's box.
|
|
135
|
+
|
|
136
|
+
## Interaction feedback and motion
|
|
137
|
+
|
|
138
|
+
Every action button must confirm itself — silent success reads as broken. Three layers, together:
|
|
139
|
+
the `loading` prop while in flight; a brief flip on the trigger itself (label `Save → Saved`,
|
|
140
|
+
`color="green"`, a check revealed through Mantine's `<Transition>`), held about a second; and a toast
|
|
141
|
+
ONLY for an outcome the user might miss (an off-screen write, an error, background work). The user's
|
|
142
|
+
eye is on the thing they clicked. Destructive actions confirm through `modals.openConfirmModal`
|
|
143
|
+
first; form errors are inline, and a toast covers only submit-level failure.
|
|
144
|
+
|
|
145
|
+
- **Timing is a token, never a literal.** `MOTION_DURATION` / `MOTION_SPRING` /
|
|
146
|
+
`MOTION_EASE_STANDARD` from `basalt-ui`; the duration cap is the same interaction-feedback ceiling
|
|
147
|
+
above (`raw-motion-value` enforces it). Never animate a layout-shifting property — move
|
|
148
|
+
`transform`, `opacity`, `color`, `background`.
|
|
149
|
+
- **Import from `motion/react`, never `framer-motion`** (`no-restricted-imports`, repo and shipped).
|
|
150
|
+
`motion` is an exact-pinned optional PEER, not a bundled dependency. Reach for Mantine's own
|
|
151
|
+
`<Transition>` for a simple mount fade; reach for `motion` when the interaction needs a crossfade
|
|
152
|
+
between two elements, spring physics or a gesture.
|
|
153
|
+
- **Always branch on `useReducedMotion`** (`@mantine/hooks`) with a real unanimated code path — no
|
|
154
|
+
`motion.*` wrapper at all, not just `duration: 0`. `ThemeToggle` is the pattern.
|
|
155
|
+
- **Restraint applies to motion too.** A state change earns a transition; idle chrome does not.
|
|
156
|
+
Never a looping or pulsing idle animation.
|