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
|
@@ -161,9 +161,9 @@ describe('Fix 4 — every interactive target clears the WCAG 2.5.8 24px floor at
|
|
|
161
161
|
}
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
-
test('the mobile header actions row
|
|
164
|
+
test('the page bar row (pageBarRowHeight — the mobile header actions row it replaced is gone)', () => {
|
|
165
165
|
for (const level of ALL_LEVELS) {
|
|
166
|
-
expect(deriveSpacing(level).step.
|
|
166
|
+
expect(deriveSpacing(level).step.pageBarRowHeight).toBeGreaterThanOrEqual(24)
|
|
167
167
|
}
|
|
168
168
|
})
|
|
169
169
|
|
|
@@ -183,6 +183,15 @@ describe('Fix 4 — every interactive target clears the WCAG 2.5.8 24px floor at
|
|
|
183
183
|
}
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
+
// `sheetRowHeight`'s own doc calls it "same floor family as `mobileNavRowHeight`" — same touch-only
|
|
187
|
+
// surface (the `Filters (n)` drawer, the mobile kebab sheet), same Apple HIG 44pt / WCAG 2.5.5 AAA
|
|
188
|
+
// floor, so it must clear it at every level too, not just coincidentally at level 0.
|
|
189
|
+
test('a Filters/kebab sheet row never drops below its own 44px floor', () => {
|
|
190
|
+
for (const level of ALL_LEVELS) {
|
|
191
|
+
expect(deriveSpacing(level).step.sheetRowHeight).toBeGreaterThanOrEqual(44)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
186
195
|
test('the resolved control/input height (size="md" Button/ActionIcon/Input) clears the floor too', () => {
|
|
187
196
|
for (const level of ALL_LEVELS) {
|
|
188
197
|
const { anchors } = deriveSpacing(level)
|
|
@@ -190,6 +199,25 @@ describe('Fix 4 — every interactive target clears the WCAG 2.5.8 24px floor at
|
|
|
190
199
|
expect(anchors.inputHeight).toBeGreaterThanOrEqual(24)
|
|
191
200
|
}
|
|
192
201
|
})
|
|
202
|
+
|
|
203
|
+
// C15 (`docs/CONTROLS-SPEC.md` §5): "every touch target inside a home is ≥36px below `sm` (floor
|
|
204
|
+
// 30 at density −3)". Without the floor the 1 + 0.1*level multiplier takes 36 -> 25 at level -3,
|
|
205
|
+
// well under even the 24px WCAG floor above.
|
|
206
|
+
test('touchControlHeight never drops below its own 30px floor (C15)', () => {
|
|
207
|
+
for (const level of ALL_LEVELS) {
|
|
208
|
+
expect(deriveSpacing(level).anchors.touchControlHeight).toBeGreaterThanOrEqual(30)
|
|
209
|
+
}
|
|
210
|
+
expect(deriveSpacing(-3).anchors.touchControlHeight).toBe(30)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('the remaining controls-tier anchors clear their own floors at every level', () => {
|
|
214
|
+
for (const level of ALL_LEVELS) {
|
|
215
|
+
const { anchors } = deriveSpacing(level)
|
|
216
|
+
expect(anchors.controlHeightTag).toBeGreaterThanOrEqual(18)
|
|
217
|
+
expect(anchors.controlHeightWidget).toBeGreaterThanOrEqual(22)
|
|
218
|
+
expect(anchors.controlHeightCtl).toBeGreaterThanOrEqual(28)
|
|
219
|
+
}
|
|
220
|
+
})
|
|
193
221
|
})
|
|
194
222
|
|
|
195
223
|
describe('Fix 5 — scaleSpace floors a sub-1 base at 1 (the inverted-clamp regression)', () => {
|
|
@@ -224,14 +252,14 @@ function oldLawFlatten(level: number): Record<string, number> {
|
|
|
224
252
|
for (const [k, v] of Object.entries(SPACE_SCALE)) scale[k] = scaleSpace(v, level, multiplier)
|
|
225
253
|
const step: Record<string, number> = {}
|
|
226
254
|
for (const [k, v] of Object.entries(SPACE_STEP)) {
|
|
227
|
-
if (k === 'stickyHeaderClearance'
|
|
255
|
+
if (k === 'stickyHeaderClearance') continue
|
|
228
256
|
step[k] = scaleSpace(v, level, multiplier)
|
|
229
257
|
}
|
|
230
258
|
step['sidebarSearchTriggerHeight'] = Math.max(24, step['sidebarSearchTriggerHeight']!)
|
|
231
259
|
step['mobileNavBarHeight'] = Math.max(48, step['mobileNavBarHeight']!)
|
|
232
260
|
step['mobileNavRowHeight'] = Math.max(44, step['mobileNavRowHeight']!)
|
|
261
|
+
step['sheetRowHeight'] = Math.max(44, step['sheetRowHeight']!)
|
|
233
262
|
step['stickyHeaderClearance'] = step['appShellHeaderHeight']! + anchors['stackMd']!
|
|
234
|
-
step['stickyHeaderClearanceMobile'] = step['appShellHeaderMobileHeight']! + anchors['stackMd']!
|
|
235
263
|
return { ...anchors, ...scale, ...step }
|
|
236
264
|
}
|
|
237
265
|
|
|
@@ -298,10 +326,10 @@ describe("Fix 7 — the knob's resolution is honest at every notch", () => {
|
|
|
298
326
|
expect(deriveSpacing(3).anchors.inputHeight).toBe(55)
|
|
299
327
|
expect(deriveSpacing(-3).anchors.rowInsetX).toBe(7)
|
|
300
328
|
expect(deriveSpacing(3).anchors.rowInsetX).toBe(13)
|
|
301
|
-
// `stickyHeaderClearance
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// begin with;
|
|
329
|
+
// `stickyHeaderClearance` is EXCLUDED here on purpose — since it now DERIVES from the AppShell
|
|
330
|
+
// header instead of scaling off an independent base (see `deriveSpacing`'s doc, third bullet),
|
|
331
|
+
// it never reproduced the old ±5 envelope to
|
|
332
|
+
// begin with; its own law is asserted by the dedicated describe block in `tokens/density.test.ts`
|
|
305
333
|
// (specific low/zero/high values) AND by the "Fix 8" describe block below in this same file
|
|
306
334
|
// (the generic `clearance = header + anchors.stackMd` relation, looped over every level).
|
|
307
335
|
expect(deriveSpacing(-3).rowLineHeight).toBe(1.25)
|
|
@@ -309,32 +337,18 @@ describe("Fix 7 — the knob's resolution is honest at every notch", () => {
|
|
|
309
337
|
})
|
|
310
338
|
})
|
|
311
339
|
|
|
312
|
-
describe('Fix 8 — the sticky-header clearance clears
|
|
313
|
-
test('
|
|
340
|
+
describe('Fix 8 — the sticky-header clearance clears the AppShell header, exactly', () => {
|
|
341
|
+
test('clearance = appShellHeaderHeight + stackMd, exactly, at every level', () => {
|
|
314
342
|
for (const level of ALL_LEVELS) {
|
|
315
343
|
const { step, anchors } = deriveSpacing(level)
|
|
316
344
|
expect(step.stickyHeaderClearance).toBe(step.appShellHeaderHeight + anchors.stackMd)
|
|
317
345
|
}
|
|
318
346
|
})
|
|
319
347
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
step.appShellHeaderMobileHeight + anchors.stackMd,
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
test('mobile clearance exceeds desktop clearance at every level — catches a swap of the two formulas', () => {
|
|
330
|
-
// Both formulas add the SAME `anchors.stackMd` breathing room, so this reduces to
|
|
331
|
-
// `mobileHeader > desktopHeader`, which holds at every level today — but a bug that swapped
|
|
332
|
-
// which header feeds which key would flip this comparison immediately.
|
|
333
|
-
for (const level of ALL_LEVELS) {
|
|
334
|
-
const { step } = deriveSpacing(level)
|
|
335
|
-
expect(step.stickyHeaderClearanceMobile).toBeGreaterThan(step.stickyHeaderClearance)
|
|
336
|
-
}
|
|
337
|
-
})
|
|
348
|
+
// The "mobile clearance = appShellHeaderMobileHeight + stackMd" and "mobile exceeds desktop" tests
|
|
349
|
+
// that used to sit here are DELETED, not renamed: since 1.26.0 the AppShell header is ONE 48px row
|
|
350
|
+
// at every viewport (law C14, `docs/CONTROLS-SPEC.md` §2.1), so `stickyHeaderClearanceMobile` and
|
|
351
|
+
// the `appShellHeaderMobileHeight` it derived from no longer exist to assert anything about.
|
|
338
352
|
|
|
339
353
|
// The "both breathing-room margins (clearance minus own header) are monotonically non-decreasing"
|
|
340
354
|
// check used to live here as its own test — deleted, not merely renamed: the two tests above this
|
|
@@ -362,34 +376,29 @@ describe('Fix 6 — BasaltShell AppShell dimensions track density', () => {
|
|
|
362
376
|
}
|
|
363
377
|
})
|
|
364
378
|
|
|
365
|
-
test(
|
|
366
|
-
// The
|
|
367
|
-
// (
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
379
|
+
test('the header is ONE row at every viewport, and it holds a touch target below sm', () => {
|
|
380
|
+
// The invariant this replaced was a BUDGET: `appShellHeaderMobileHeight` was a documented SUM
|
|
381
|
+
// (row 1 + `SPACE_SCALE.sm` + `appHeaderMobileActionsHeight`), so row 1 was whatever the total
|
|
382
|
+
// left over, and raising an addend without raising the total silently ate the target-size margin
|
|
383
|
+
// — which is exactly what the component-roominess retune did (`SPACE_SCALE.sm` 12 -> 13 drove
|
|
384
|
+
// level -3 to 22px, flush against Mantine's 22px `--ai-size-sm` floor).
|
|
371
385
|
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// it names this test as the thing that measures the drift, and computing the header from its
|
|
378
|
-
// addends as the fix that would make the drift impossible.
|
|
386
|
+
// 1.26.0 deletes the sum instead of re-tuning it: `PageBar` folds the page's overflow into a
|
|
387
|
+
// kebab and moves filters/tabs into the page flow, so there is no second mobile row to reserve
|
|
388
|
+
// (law C14) and the header is `appShellHeaderHeight` at EVERY width. Row 1's budget is therefore
|
|
389
|
+
// the whole header, and the thing worth asserting is what law C15 actually promises — that a
|
|
390
|
+
// `touchControlHeight` target fits inside it below `sm`.
|
|
379
391
|
for (const level of ALL_LEVELS) {
|
|
380
|
-
const { step,
|
|
381
|
-
|
|
382
|
-
expect(
|
|
392
|
+
const { step, anchors } = deriveSpacing(level)
|
|
393
|
+
expect(step.appShellHeaderHeight).toBeGreaterThanOrEqual(anchors.touchControlHeight)
|
|
394
|
+
expect(anchors.touchControlHeight).toBeGreaterThanOrEqual(anchors.controlHeightCtl)
|
|
383
395
|
}
|
|
384
396
|
})
|
|
385
397
|
|
|
386
|
-
test('the
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
})
|
|
391
|
-
for (let i = 1; i < budgets.length; i++) {
|
|
392
|
-
expect(budgets[i]).toBeGreaterThanOrEqual(budgets[i - 1]!)
|
|
398
|
+
test('the header row is monotonically non-decreasing across levels', () => {
|
|
399
|
+
const heights = ALL_LEVELS.map((level) => deriveSpacing(level).step.appShellHeaderHeight)
|
|
400
|
+
for (let i = 1; i < heights.length; i++) {
|
|
401
|
+
expect(heights[i]).toBeGreaterThanOrEqual(heights[i - 1]!)
|
|
393
402
|
}
|
|
394
403
|
})
|
|
395
404
|
|
|
@@ -449,3 +458,49 @@ describe('Fix 9 — the Mantine spacing scale keeps its strict xs < sm < md < lg
|
|
|
449
458
|
}
|
|
450
459
|
})
|
|
451
460
|
})
|
|
461
|
+
|
|
462
|
+
describe('Fix 10 — ctl SegmentedControl matches the ctl Button/Input box at every level, not a fixed 30px', () => {
|
|
463
|
+
// `SegmentedControl.extend`'s function-form `styles` only sets a label `minHeight` when
|
|
464
|
+
// `props.size === 'ctl'` — everything else (`sm`/`md` in a form) must fall through untouched.
|
|
465
|
+
const stylesFn = baseTheme.components?.['SegmentedControl']?.styles as
|
|
466
|
+
| ((t: MantineTheme, props: { size?: string }, ctx: never) => Record<string, unknown>)
|
|
467
|
+
| undefined
|
|
468
|
+
|
|
469
|
+
test('a non-ctl size (e.g. the form default) gets no label override', () => {
|
|
470
|
+
const result = stylesFn?.(theme, { size: 'md' }, {} as never)
|
|
471
|
+
expect(
|
|
472
|
+
(result?.['label'] as Record<string, unknown> | undefined)?.['minHeight'],
|
|
473
|
+
).toBeUndefined()
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test("size='ctl' gets an inline-flex, vertically centred label — Mantine's label is `display: block`, so a bare minHeight alone would leave the text top-aligned", () => {
|
|
477
|
+
const result = stylesFn?.(theme, { size: 'ctl' }, {} as never)
|
|
478
|
+
const label = result?.['label'] as Record<string, unknown> | undefined
|
|
479
|
+
expect(label?.['display']).toBe('inline-flex')
|
|
480
|
+
expect(label?.['alignItems']).toBe('center')
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('the label minHeight formula, plus the root track padding on both sides, reconstructs EXACTLY the same scaled expression as `--button-height-ctl` — the algebraic identity that makes the two controls the same height at every density level, not just level 0', () => {
|
|
484
|
+
const result = stylesFn?.(theme, { size: 'ctl' }, {} as never)
|
|
485
|
+
const label = result?.['label'] as Record<string, unknown> | undefined
|
|
486
|
+
const minHeight = label?.['minHeight']
|
|
487
|
+
expect(typeof minHeight).toBe('string')
|
|
488
|
+
const rootPadding = 2 * SPACE_FIXED.segmentedTrackInset
|
|
489
|
+
expect(minHeight).toBe(
|
|
490
|
+
`calc(var(--vx-space-control-height-ctl) * var(--mantine-scale) - ${rootPadding}px)`,
|
|
491
|
+
)
|
|
492
|
+
// `--button-height-ctl` (`ctlSizeVars` in theme/index.ts) is the un-subtracted form — adding the
|
|
493
|
+
// root's own `2 * segmentedTrackInset` padding back to the label's minHeight must reproduce it,
|
|
494
|
+
// for every level, since neither term is level-dependent (the level only changes the CSS var's
|
|
495
|
+
// RESOLVED value at paint time, never this formula).
|
|
496
|
+
for (const level of ALL_LEVELS) {
|
|
497
|
+
// `anchors.controlHeightCtl` is the JS-side value `--vx-space-control-height-ctl` resolves to
|
|
498
|
+
// at this level (scale = 1, the JS-derivation default) — substituting it into both formulas
|
|
499
|
+
// must land on the same total.
|
|
500
|
+
const { anchors } = deriveSpacing(level)
|
|
501
|
+
const labelHeight = anchors.controlHeightCtl - rootPadding
|
|
502
|
+
const totalHeight = labelHeight + rootPadding
|
|
503
|
+
expect(totalHeight).toBe(anchors.controlHeightCtl)
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
})
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The divider law — a mechanical inventory of every HORIZONTAL hairline basalt draws
|
|
3
|
+
* (`docs/DESIGN-SPEC.md` §8: depth is a shadow, and a border-as-border is reserved for a layout
|
|
4
|
+
* divider).
|
|
5
|
+
*
|
|
6
|
+
* The bug this file exists to prevent, measured on the playground's `Controls (mobile)` page at
|
|
7
|
+
* 390px: the page carried five rules in its first 320px — a table box whose top edge sat directly
|
|
8
|
+
* under the table's own `WidgetHeader`, its bottom edge on the last row's own hairline, and a
|
|
9
|
+
* section rule 8px from the next one. None of them divided anything. Two rules stack into a band,
|
|
10
|
+
* and a band under a heading says "the heading is not part of what follows", which is the opposite
|
|
11
|
+
* of what a section header is for.
|
|
12
|
+
*
|
|
13
|
+
* So the law has two halves and this file pins both:
|
|
14
|
+
*
|
|
15
|
+
* 1. **A header never draws a rule under itself** — in the CHROME lane (shell, controls, dashboard,
|
|
16
|
+
* data, widget-header, theme). The reader already has the heading's type scale, its weight and
|
|
17
|
+
* the space below it; a line adds a fourth channel saying the same thing and costs a band.
|
|
18
|
+
* 2. **A between-rows family always drops its LAST rule** — the line after the final row is an
|
|
19
|
+
* outer border pretending to be a separator, and it is the line that lands on top of whatever
|
|
20
|
+
* frame the container already has.
|
|
21
|
+
*
|
|
22
|
+
* The guard is an INVENTORY, not a pattern match: every horizontal hairline in every CSS module has
|
|
23
|
+
* to be listed below with a `kind` and a reason, so a seventh one added next month fails here and
|
|
24
|
+
* gets classified rather than shipping unexamined. That is the same idiom `theme/border-coverage.test.ts`
|
|
25
|
+
* uses for Mantine's stock borders, applied to the ones basalt writes itself.
|
|
26
|
+
*
|
|
27
|
+
* Not in scope, deliberately: the `border` SHORTHAND (a box around a control or a card — that is an
|
|
28
|
+
* edge, not a rule, and `border-coverage.test.ts` owns it), vertical hairlines (a column rule
|
|
29
|
+
* divides two regions by construction), and Mantine's own `inset 0 -1px 0` redraw of a STICKY table
|
|
30
|
+
* head's `border-bottom`, which is a row line drawn as a shadow because `border-collapse: collapse`
|
|
31
|
+
* drops borders on sticky cells — it appears and disappears with `withRowBorders`, so it is the same
|
|
32
|
+
* line every other row gets and not a header rule.
|
|
33
|
+
*/
|
|
34
|
+
import { describe, expect, test } from 'bun:test'
|
|
35
|
+
import { Glob } from 'bun'
|
|
36
|
+
import { readFileSync } from 'node:fs'
|
|
37
|
+
import { join } from 'node:path'
|
|
38
|
+
|
|
39
|
+
const SRC = join(import.meta.dir, '..')
|
|
40
|
+
|
|
41
|
+
type Hairline = { readonly file: string; readonly selector: string; readonly decl: string }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every `border-{top,bottom,block-start,block-end}` and every 1px inset `box-shadow` in a CSS
|
|
45
|
+
* module, with the selector it sits under. A hand-rolled scanner rather than a regex per file: the
|
|
46
|
+
* SELECTOR is half the classification (`.header` vs `.rows > *:not(:last-child)` are the two
|
|
47
|
+
* opposite verdicts), and a regex over declarations alone cannot see it.
|
|
48
|
+
*/
|
|
49
|
+
function scanHairlines(): { rules: Hairline[]; drops: Hairline[] } {
|
|
50
|
+
const rules: Hairline[] = []
|
|
51
|
+
const drops: Hairline[] = []
|
|
52
|
+
const files = [...new Glob('**/*.module.css').scanSync({ cwd: SRC })].sort()
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const css = readFileSync(join(SRC, file), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
55
|
+
const stack: string[] = []
|
|
56
|
+
let buf = ''
|
|
57
|
+
for (const ch of css) {
|
|
58
|
+
if (ch === '{') {
|
|
59
|
+
stack.push(buf.trim().replace(/\s+/g, ' '))
|
|
60
|
+
buf = ''
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (ch === '}') {
|
|
64
|
+
stack.pop()
|
|
65
|
+
buf = ''
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (ch !== ';') {
|
|
69
|
+
buf += ch
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
const decl = buf.trim().replace(/\s+/g, ' ')
|
|
73
|
+
buf = ''
|
|
74
|
+
const border = /^border-(?:top|bottom|block-start|block-end):\s*(.+)$/.exec(decl)
|
|
75
|
+
const selector = stack.join(' >> ')
|
|
76
|
+
if (border !== null) {
|
|
77
|
+
;(border[1] === 'none' ? drops : rules).push({ file, selector, decl })
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (decl.startsWith('box-shadow:') && decl.includes('inset') && /\b-?1px\b/.test(decl)) {
|
|
81
|
+
rules.push({ file, selector, decl })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { rules, drops }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `between-rows` — the line sits BETWEEN two members of a list and the family drops its last one.
|
|
90
|
+
* `region-boundary` — it separates two layout regions that are not a header and its content (the
|
|
91
|
+
* mobile bar and the page it floats over, an article's body and its footer).
|
|
92
|
+
* `prose-typography` — an AUTHORED document, not app chrome: an `<hr>` the author wrote, the rule
|
|
93
|
+
* under an article's `h2`, a code block's filename bar. A document's conventions are the reader's
|
|
94
|
+
* expectations there (`docs/CONTENT-SPEC.md`), and `content/**` is out of the chrome lane for
|
|
95
|
+
* exactly that reason. Nothing in this kind may appear outside `content/`.
|
|
96
|
+
*/
|
|
97
|
+
type Kind = 'between-rows' | 'region-boundary' | 'prose-typography'
|
|
98
|
+
|
|
99
|
+
const LEDGER: readonly { file: string; selector: string; kind: Kind; why: string }[] = [
|
|
100
|
+
{
|
|
101
|
+
file: 'shell/app-mobile-nav.module.css',
|
|
102
|
+
selector: '.bar',
|
|
103
|
+
kind: 'region-boundary',
|
|
104
|
+
why: 'The mobile bar is a fixed region floating over a scrolling page — the rule is the only thing that says the page ends there, and it is the boundary between two regions, not a header and its content.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
file: 'controls/controls.module.css',
|
|
108
|
+
selector:
|
|
109
|
+
'.sheetOption + .sheetOption, .sheetDisclosureBody + .sheetOption, .sheetOption + .sheetDisclosureBody',
|
|
110
|
+
kind: 'between-rows',
|
|
111
|
+
why: "The filter sheet's option rows. Written as an adjacent-sibling rule so the FIRST row has no top line — the group label above it is a header and gets no rule (C15).",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
file: 'controls/controls.module.css',
|
|
115
|
+
selector: '.sheetRow',
|
|
116
|
+
kind: 'between-rows',
|
|
117
|
+
why: 'A 44px sheet row. Paired with `.sheetRow:last-child { border-bottom: none }` below, which the last-rule assertion checks.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
file: 'dashboard/settings-section.module.css',
|
|
121
|
+
selector: '.rows > *:not(:last-child)',
|
|
122
|
+
kind: 'between-rows',
|
|
123
|
+
why: 'A settings list. The `:not(:last-child)` IS the last-rule drop, written into the selector rather than as a second rule.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
file: 'content/article-layout.module.css',
|
|
127
|
+
selector: '.header',
|
|
128
|
+
kind: 'prose-typography',
|
|
129
|
+
why: "An article's masthead rule — the title-block convention of a printed document, and the one place a reader expects a line under a heading.",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
file: 'content/article-layout.module.css',
|
|
133
|
+
selector: '.footer',
|
|
134
|
+
kind: 'region-boundary',
|
|
135
|
+
why: 'Prev/next navigation below the article body — two regions, and the rule is above the footer rather than under a heading.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
file: 'content/guide.module.css',
|
|
139
|
+
selector: '.footer',
|
|
140
|
+
kind: 'region-boundary',
|
|
141
|
+
why: 'Same shape as the article footer: the boundary between a document and the navigation after it.',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
file: 'content/prose.module.css',
|
|
145
|
+
selector: '.root hr',
|
|
146
|
+
kind: 'prose-typography',
|
|
147
|
+
why: 'An `<hr>` the AUTHOR wrote in their markdown. basalt does not get to drop a divider the document asked for.',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
file: 'content/prose.module.css',
|
|
151
|
+
selector: '.root.article h2',
|
|
152
|
+
kind: 'prose-typography',
|
|
153
|
+
why: "The `article` prose variant's section rule — a long-form document convention, scoped to that variant and never applied to app chrome.",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
file: 'content/code-block.module.css',
|
|
157
|
+
selector: '.header',
|
|
158
|
+
kind: 'prose-typography',
|
|
159
|
+
why: "A code block's filename/language bar. The block is a quoted artifact inside a document, and the bar is its frame's top row rather than a section heading.",
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
const CHROME_LANES = ['shell/', 'controls/', 'dashboard/', 'data/', 'widget-header/', 'theme/']
|
|
164
|
+
|
|
165
|
+
function isChrome(file: string): boolean {
|
|
166
|
+
return CHROME_LANES.some((lane) => file.startsWith(lane))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { rules, drops } = scanHairlines()
|
|
170
|
+
|
|
171
|
+
describe('every horizontal hairline is inventoried and classified', () => {
|
|
172
|
+
test('no hairline exists that the ledger does not name', () => {
|
|
173
|
+
const listed = new Set(LEDGER.map((e) => `${e.file}|${e.selector}`))
|
|
174
|
+
const unlisted = rules
|
|
175
|
+
.filter((r) => !listed.has(`${r.file}|${r.selector}`))
|
|
176
|
+
.map((r) => `${r.file} → ${r.selector} { ${r.decl} }`)
|
|
177
|
+
// Do NOT silence this by adding an entry with a hand-wave. Decide which `kind` the line is: a
|
|
178
|
+
// `between-rows` family (then add the last-rule drop too), a `region-boundary`, or — only inside
|
|
179
|
+
// `content/**` — `prose-typography`. A rule under a heading in the chrome lane is none of the
|
|
180
|
+
// three, and the fix is to delete it and let the type scale and the space do the work.
|
|
181
|
+
expect(unlisted).toEqual([])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('the ledger names nothing that no longer exists', () => {
|
|
185
|
+
const found = new Set(rules.map((r) => `${r.file}|${r.selector}`))
|
|
186
|
+
expect(LEDGER.filter((e) => !found.has(`${e.file}|${e.selector}`)).map((e) => e.file)).toEqual(
|
|
187
|
+
[],
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('`prose-typography` is confined to content/ — app chrome has no authored-document lane', () => {
|
|
192
|
+
expect(
|
|
193
|
+
LEDGER.filter((e) => e.kind === 'prose-typography' && !e.file.startsWith('content/')).map(
|
|
194
|
+
(e) => `${e.file} → ${e.selector}`,
|
|
195
|
+
),
|
|
196
|
+
).toEqual([])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('every ledger entry carries a real reason', () => {
|
|
200
|
+
expect(LEDGER.filter((e) => e.why.length < 40).map((e) => e.selector)).toEqual([])
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('a header never draws a rule under itself in the chrome lane', () => {
|
|
205
|
+
// The five class names the chrome lane uses for "the heading row of a thing" — `WidgetHeader`'s
|
|
206
|
+
// root/titleRow/subtitle, `Section`'s, the filter sheet's, the table toolbar's. A `border-bottom`
|
|
207
|
+
// on any of them is the exact regression this describe block exists for.
|
|
208
|
+
const HEADER_SELECTOR =
|
|
209
|
+
/^\.(?:root|header|titleRow|title|subtitle|sheetHeader|sheetTitle|toolbar|thead|head)\b/
|
|
210
|
+
|
|
211
|
+
test('no chrome-lane hairline sits under a header-shaped selector', () => {
|
|
212
|
+
const offenders = rules
|
|
213
|
+
.filter((r) => isChrome(r.file) && HEADER_SELECTOR.test(r.selector))
|
|
214
|
+
.map((r) => `${r.file} → ${r.selector} { ${r.decl} }`)
|
|
215
|
+
expect(offenders).toEqual([])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('the chrome lane holds ONLY between-rows and region-boundary lines', () => {
|
|
219
|
+
const kinds = LEDGER.filter((e) => isChrome(e.file)).map((e) => e.kind)
|
|
220
|
+
expect([...new Set(kinds)].sort()).toEqual(['between-rows', 'region-boundary'])
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('a between-rows family drops its last rule', () => {
|
|
225
|
+
for (const entry of LEDGER.filter((e) => e.kind === 'between-rows')) {
|
|
226
|
+
test(`${entry.file} → ${entry.selector}`, () => {
|
|
227
|
+
// Either the selector itself excludes the last member (`:not(:last-child)`, or an
|
|
228
|
+
// adjacent-sibling combinator, which draws the line as the SECOND row's top edge and so has no
|
|
229
|
+
// trailing rule by construction), or the module carries an explicit `border: none` drop.
|
|
230
|
+
const selfLimiting = /:not\(:last-child\)|\+/.test(entry.selector)
|
|
231
|
+
const dropped = drops.some(
|
|
232
|
+
(d) => d.file === entry.file && d.selector.startsWith(`${entry.selector}:last`),
|
|
233
|
+
)
|
|
234
|
+
expect(selfLimiting || dropped).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
test('the themed Table drops its last body row — the rule that used to land on the outer box', () => {
|
|
239
|
+
// `BasaltDataTable`'s `withTableBorder` went to `false` by default in the same minor (see
|
|
240
|
+
// `data/data-table.test.tsx`); while both shipped, the two lines sat on top of each other and the
|
|
241
|
+
// doubling hid which of them was drawing it. `tbody` scoped so a `tfoot` totals row keeps the
|
|
242
|
+
// BETWEEN rule above it.
|
|
243
|
+
const css = readFileSync(join(SRC, 'theme/controls.module.css'), 'utf8')
|
|
244
|
+
expect(css).toContain('html .tableRoot tbody tr:last-of-type')
|
|
245
|
+
expect(drops).toContainEqual({
|
|
246
|
+
file: 'theme/controls.module.css',
|
|
247
|
+
selector: 'html .tableRoot tbody tr:last-of-type',
|
|
248
|
+
decl: 'border-bottom: none',
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('the themed Accordion drops its last item — the same law, already shipped', () => {
|
|
253
|
+
expect(drops).toContainEqual({
|
|
254
|
+
file: 'theme/controls.module.css',
|
|
255
|
+
selector: '.accordionItem:last-of-type',
|
|
256
|
+
decl: 'border-bottom: none',
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The framework's ONE icon box (`docs/CONTROLS-SPEC.md` §3) — see `icon-slot.tsx` for why every
|
|
3
|
+
* `icon` prop in the public API renders through it.
|
|
4
|
+
*
|
|
5
|
+
* The box is a FIXED square, not a shrink-to-fit inline box: `width`/`height` are declared on the
|
|
6
|
+
* slot itself so the row's geometry is decided by the SLOT and never by the glyph the caller
|
|
7
|
+
* happened to pass. `--vx-space-icon-size` is the host's override hook (a `WidgetHeader` tier, a
|
|
8
|
+
* collapsed rail), defaulting to the 16px the control tier draws; it is deliberately NOT a
|
|
9
|
+
* `--vx-space-*` token declared by `tokens/index.ts` — no home needs it globally, and a token would
|
|
10
|
+
* invite a call site to set it.
|
|
11
|
+
*/
|
|
12
|
+
.slot {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
flex: none;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
width: var(--vx-space-icon-size, 1rem);
|
|
18
|
+
height: var(--vx-space-icon-size, 1rem);
|
|
19
|
+
/* The glyph is `display: block` below, so the slot has no text to lead — a residual line box is
|
|
20
|
+
* what pushed a 16px icon a fraction below the optical centre of a 30px button. */
|
|
21
|
+
line-height: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* `width`/`height: 100%` is what makes ANY consumer glyph land on the tier. An inline `<svg>`
|
|
26
|
+
* carries its own `width`/`height` attributes (14, 20, 24 — or none at all, which paints at the
|
|
27
|
+
* `viewBox`'s intrinsic size, or 300×150 with no `viewBox` either), and those attributes are
|
|
28
|
+
* PRESENTATION attributes: they lose to any CSS declaration. So the slot restates them from the
|
|
29
|
+
* box, and the caller's own numbers stop mattering.
|
|
30
|
+
*
|
|
31
|
+
* `display: block` removes the baseline the glyph would otherwise sit on. An inline `<svg>` aligns
|
|
32
|
+
* its own bottom edge to the parent's text baseline, which leaves the descender gap BELOW it inside
|
|
33
|
+
* the box — the icon then paints high or low of centre by a fraction of the line box, and by a
|
|
34
|
+
* DIFFERENT fraction per font stack. `object-fit: contain` keeps a non-square glyph whole rather
|
|
35
|
+
* than stretching it to the square box.
|
|
36
|
+
*
|
|
37
|
+
* `img` and `picture` are covered too: an icon slot filled from a sprite sheet or a remote asset is
|
|
38
|
+
* the same geometry problem, and a consumer's icon set is as likely to be one as the other.
|
|
39
|
+
*/
|
|
40
|
+
.slot > svg,
|
|
41
|
+
.slot > img,
|
|
42
|
+
.slot > picture {
|
|
43
|
+
display: block;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
object-fit: contain;
|
|
47
|
+
}
|