basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WidgetHeader — the one heading primitive behind every section/card/table title
|
|
3
|
+
* (docs/CONTROLS-SPEC.md §2.2). Plain elements only — no `@mantine/*` anywhere in this folder —
|
|
4
|
+
* so `ChartCard` (inside the `charts/` boundary) can compose it without tripping
|
|
5
|
+
* `basalt/token-layer-boundary` / `check-dist-layering.mjs`.
|
|
6
|
+
*
|
|
7
|
+
* `--vx-space-section-header-height` / `--vx-space-widget-header-height` / `--vx-space-control-gap`
|
|
8
|
+
* are density-tracking anchors landing alongside this wave (docs/CONTROLS-SPEC.md §5) — referenced
|
|
9
|
+
* by name with rem fallbacks at their level-0 px value (36/28/6), same idiom as ChartCard's
|
|
10
|
+
* `--mantine-spacing-*` fallbacks.
|
|
11
|
+
*
|
|
12
|
+
* Layout: the title row holds title/icon/info/count/actions only. `value` + `delta` render on
|
|
13
|
+
* their own hero-metric row directly beneath it — for BOTH tiers, never inline with the title.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.root {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.titleRow {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: var(--vx-space-control-gap, 0.375rem);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Below `sm` the title row WRAPS, which is the safety net for an `actions` slot too wide to sit
|
|
29
|
+
* beside the title. `flex-wrap` is the right tool precisely because it is conditional: a 30px kebab
|
|
30
|
+
* fits and stays on the row, while a control whose hypothetical size does not fit takes its own line
|
|
31
|
+
* instead of squeezing the heading to an ellipsis. No breakpoint arithmetic, no max-width fraction,
|
|
32
|
+
* and no prop — the row measures the actual content.
|
|
33
|
+
*
|
|
34
|
+
* `row-gap` is the 8px rhythm step, so a wrapped slot reads as the line under the title rather than
|
|
35
|
+
* as a second header. `.actions` keeps its `margin-inline-start: auto`, which on a wrapped line makes
|
|
36
|
+
* it right-aligned — the same edge it holds when it is on the row.
|
|
37
|
+
*/
|
|
38
|
+
@media (max-width: 47.99375em) {
|
|
39
|
+
.titleRow {
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
row-gap: var(--vx-space-stack-sm);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.root[data-tier='section'] .titleRow {
|
|
46
|
+
min-height: var(--vx-space-section-header-height, 2.25rem);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.root[data-tier='widget'] .titleRow {
|
|
50
|
+
min-height: var(--vx-space-widget-header-height, 1.75rem);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Section → h2, widget → h3, and the two tiers now read DIFFERENTLY on purpose — they did not, and
|
|
55
|
+
* that is what flattened the page.
|
|
56
|
+
*
|
|
57
|
+
* A `section` heading names a region of the page: 15px, weight 600, INK. A `widget` heading labels a
|
|
58
|
+
* number that is itself the thing being read (a KPI card's hero value, a chart card's total), so it
|
|
59
|
+
* is the quieter of the pair — one step down and MUTED, which is what lets the 24px mono value under
|
|
60
|
+
* it carry the card. Both keep the shared head-font 88% stretch.
|
|
61
|
+
*
|
|
62
|
+
* Rendered identically (15px ink both) the hero value competed with its own label, four cards in a
|
|
63
|
+
* row read as four headings, and the section that grouped them read as one more of the same.
|
|
64
|
+
*/
|
|
65
|
+
.heading {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 5px;
|
|
69
|
+
margin: 0;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
font-family: var(--basalt-font-head);
|
|
72
|
+
font-stretch: 88%;
|
|
73
|
+
font-weight: 550;
|
|
74
|
+
font-size: var(--vx-text-md);
|
|
75
|
+
color: var(--vx-ink);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.root[data-tier='section'] .heading {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.root[data-tier='widget'] .heading {
|
|
83
|
+
font-size: var(--vx-text-sm);
|
|
84
|
+
color: var(--vx-muted);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.titleText {
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* The icon slot PINS its glyph's box. A consumer's icon arrives at whatever size its own component
|
|
95
|
+
* chose (the playground's is 18px), and an 18px mark beside a 13.5px title is a bullet, not a label
|
|
96
|
+
* icon — so the tier states the size here rather than asking every call site to pass one.
|
|
97
|
+
*/
|
|
98
|
+
/*
|
|
99
|
+
* The heading's icon box carries COLOUR ONLY — geometry belongs to `theme/icon-slot.module.css`,
|
|
100
|
+
* which this class is composed onto (`IconSlot className={classes.icon}`). The per-tier SIZE is set
|
|
101
|
+
* as the slot's own box var below rather than as a `> svg` rule, so it survives whatever the caller's
|
|
102
|
+
* glyph declares for itself: a `width`/`height` attribute is a presentation attribute and loses to
|
|
103
|
+
* the slot's CSS, while a `> svg` rule scoped to one component only ever fixed that component.
|
|
104
|
+
*/
|
|
105
|
+
.icon {
|
|
106
|
+
color: var(--vx-faint);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.root[data-tier='widget'] .icon {
|
|
110
|
+
--vx-space-icon-size: 0.875rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.root[data-tier='section'] .icon {
|
|
114
|
+
--vx-space-icon-size: 1rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Mono count tag after the title (C11 — every table/list states its count in its header). Its box is
|
|
118
|
+
* the `tag` tier (20px), so it never grows the header row it sits in. */
|
|
119
|
+
.count {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
align-items: center;
|
|
123
|
+
/* Exactly the `tag` tier (20px), not a floor: the mono `VX.text.xs` line box plus
|
|
124
|
+
* `badgeInset`-y is 23px on its own, so a `min-height` changed nothing and the tag rendered 3px
|
|
125
|
+
* taller than the tier it is named for — enough to grow a 36px section-header row. `line-height: 1`
|
|
126
|
+
* is what lets the box be the stated height instead of the text's. */
|
|
127
|
+
/* `--mantine-scale`-aware, same reconstruction every other ctl/tag-tier box uses (`theme/index.ts`'s
|
|
128
|
+
* `ctlSizeVars`) — read raw, this box stayed fixed while the Badge/Button beside it in the same
|
|
129
|
+
* header scaled with `theme.scale`. */
|
|
130
|
+
height: calc(var(--vx-space-control-height-tag, 1.25rem) * var(--mantine-scale, 1));
|
|
131
|
+
line-height: 1;
|
|
132
|
+
border-radius: var(--vx-radius-ctrl);
|
|
133
|
+
background: var(--vx-surface-subtle);
|
|
134
|
+
padding: var(--vx-space-badge-inset-y) var(--vx-space-badge-inset-x);
|
|
135
|
+
font-family: var(--basalt-font-mono);
|
|
136
|
+
font-size: var(--vx-text-xs);
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
color: var(--vx-faint);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* The hero-metric row — value + delta, directly under the title row, for both tiers. The delta rides
|
|
143
|
+
* 4px off the value, not 8: it is a suffix ON the number (`$168,920 +47%`), and at 8px it read as a
|
|
144
|
+
* second, unrelated chip in the row. `baseline` alignment is what keeps the mono numeral and the
|
|
145
|
+
* badge's own smaller mono text sitting on one line.
|
|
146
|
+
*/
|
|
147
|
+
/*
|
|
148
|
+
* The hero-metric row: the value, then its `DeltaBadge`.
|
|
149
|
+
*
|
|
150
|
+
* The gap is the 8px vertical-rhythm step (`--vx-space-stack-sm`), not the 4px it shipped as. 4px is
|
|
151
|
+
* the rhythm's SMALLEST step — the distance between a label and the thing it labels — and at that
|
|
152
|
+
* distance the badge read as a superscript hanging off the numeral rather than as a second fact
|
|
153
|
+
* beside it. It is a token and not a literal so it tracks the density knob with every other spacing
|
|
154
|
+
* in the header (`deriveSpacing`, `tokens/palette.ts`); `theme/layout-rhythm-css.test.ts`'s sibling
|
|
155
|
+
* assertion in `widget-header.test.tsx` pins it against exactly the literal it used to be.
|
|
156
|
+
*
|
|
157
|
+
* `align-items: center`, not `baseline`. The badge is a BOX with its own padding and a mono 11.5px
|
|
158
|
+
* label, so aligning its text baseline to a 24px KPI numeral's baseline put the box itself low —
|
|
159
|
+
* its top edge sat below the numeral's x-height and its bottom edge hung under the numeral's
|
|
160
|
+
* baseline. A chip beside a numeral is centred on the numeral, the same law the `count` tag in the
|
|
161
|
+
* title row above already follows.
|
|
162
|
+
*/
|
|
163
|
+
.metrics {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: nowrap;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--vx-space-stack-sm);
|
|
168
|
+
margin-top: 2px;
|
|
169
|
+
min-width: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Below `sm` the metric row WRAPS, so the badge drops under the value instead of ellipsizing it.
|
|
174
|
+
* `nowrap` plus the `.value` clamp below is right on desktop — a `+18.9%` cannot ellipsize and read,
|
|
175
|
+
* a numeral can — but a KPI's numeral IS the content, and in a 2-up card grid at 390px
|
|
176
|
+
* `$1294.9k ▲18.9%` rendered as `$129… ▲18.9%`. Truncating the number to protect the delta about the
|
|
177
|
+
* number is the wrong way round. Conditional like the title row above it: a short value keeps the
|
|
178
|
+
* badge beside it, and only a value that would otherwise clip gives up the line.
|
|
179
|
+
*/
|
|
180
|
+
@media (max-width: 47.99375em) {
|
|
181
|
+
.metrics {
|
|
182
|
+
flex-wrap: wrap;
|
|
183
|
+
row-gap: 2px;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The value is the row's shrinkable half and the delta badge is not: a pre-formatted numeral can
|
|
188
|
+
* ellipsize and still read, a `+18.9%` cannot. Without the clamp the badge grew past the header box
|
|
189
|
+
* and painted over `StatCard`'s 72px sparkline slot beside it. */
|
|
190
|
+
.value {
|
|
191
|
+
min-width: 0;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
text-overflow: ellipsis;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
font-family: var(--basalt-font-mono);
|
|
196
|
+
font-size: var(--vx-text-kpi);
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
letter-spacing: -0.02em;
|
|
199
|
+
color: var(--vx-ink);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* The C1/C5 home for header actions — always pinned right, regardless of what else is present. */
|
|
203
|
+
.actions {
|
|
204
|
+
display: inline-flex;
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: var(--vx-space-control-gap, 0.375rem);
|
|
208
|
+
margin-inline-start: auto;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Directly under the title row, 4px off it — 1px put it inside the title's own line box, so a
|
|
212
|
+
* section with both read as a two-line heading rather than a heading plus a note. */
|
|
213
|
+
.subtitle {
|
|
214
|
+
margin-top: 4px;
|
|
215
|
+
font-size: var(--vx-text-sm);
|
|
216
|
+
font-weight: 400;
|
|
217
|
+
color: var(--vx-muted);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* `data-placement` is read by wave-3 composers wanting a `bleed` layout (docs/CONTROLS-SPEC.md
|
|
221
|
+
* §2.2's `sparklinePlacement` composer mapping) — this primitive only ever sets `right`. */
|
|
222
|
+
.sparkline {
|
|
223
|
+
margin-top: 4px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ── info glyph ─────────────────────────────────────────────────────────────────────────────── */
|
|
227
|
+
|
|
228
|
+
.info {
|
|
229
|
+
position: relative;
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
flex-shrink: 0;
|
|
232
|
+
align-items: center;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.infoTrigger {
|
|
236
|
+
display: inline-flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
appearance: none;
|
|
239
|
+
margin: 0;
|
|
240
|
+
padding: 0;
|
|
241
|
+
border: none;
|
|
242
|
+
background: none;
|
|
243
|
+
color: var(--vx-faint);
|
|
244
|
+
cursor: help;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.infoTrigger:hover,
|
|
248
|
+
.infoTrigger:focus-visible,
|
|
249
|
+
.infoTrigger[data-open='true'] {
|
|
250
|
+
color: var(--vx-muted);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* The tooltip bubble reads like every other floating surface — panel bg + `shadow-card` + card
|
|
255
|
+
* radius (docs/DESIGN-SPEC.md §5). It opens DOWNWARD by convention, into whatever sits below the
|
|
256
|
+
* header. `pointer-events: none` so it can never swallow the pointer on its way back to the
|
|
257
|
+
* trigger's `mouseleave`. Mantine-free by design (this whole folder is), so it is a plain
|
|
258
|
+
* state-driven bubble rather than a Mantine Tooltip — which is also what makes it composable from
|
|
259
|
+
* inside the `charts/` boundary.
|
|
260
|
+
*/
|
|
261
|
+
.infoBubble {
|
|
262
|
+
position: absolute;
|
|
263
|
+
top: calc(100% + 7px);
|
|
264
|
+
left: 0;
|
|
265
|
+
z-index: 10;
|
|
266
|
+
width: max-content;
|
|
267
|
+
max-width: 260px;
|
|
268
|
+
padding: 7px 10px;
|
|
269
|
+
border-radius: var(--vx-radius-card);
|
|
270
|
+
background-color: var(--vx-surface-panel);
|
|
271
|
+
box-shadow: var(--vx-shadow-card);
|
|
272
|
+
color: var(--vx-muted);
|
|
273
|
+
font-size: var(--vx-text-sm);
|
|
274
|
+
font-weight: 400;
|
|
275
|
+
line-height: 1.4;
|
|
276
|
+
white-space: normal;
|
|
277
|
+
pointer-events: none;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ── DeltaBadge ─────────────────────────────────────────────────────────────────────────────── */
|
|
281
|
+
|
|
282
|
+
.deltaBadge {
|
|
283
|
+
display: inline-flex;
|
|
284
|
+
flex: none;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 3px;
|
|
288
|
+
border-radius: var(--vx-radius-ctrl);
|
|
289
|
+
padding: var(--vx-space-badge-inset-y) var(--vx-space-badge-inset-x);
|
|
290
|
+
font-family: var(--basalt-font-mono);
|
|
291
|
+
font-size: var(--vx-text-xs);
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
line-height: 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.deltaGlyph {
|
|
297
|
+
font-size: 0.8em;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.deltaPeriod {
|
|
301
|
+
margin-inline-start: 4px;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
opacity: 0.6;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* Law C15 — the `actions` slot is the tier-2/tier-3 home, and its controls are 30px (`ctl`) or 24px
|
|
308
|
+
* (`icon`). Below `sm` an `::after` overlay lifts the HIT area to
|
|
309
|
+
* `--vx-space-touch-control-height` (36px at level 0) without moving the visible box, the same
|
|
310
|
+
* overlay `shell/page-bar.module.css` draws for the bar. `overflow: visible` is load-bearing:
|
|
311
|
+
* Mantine's Button/ActionIcon roots are `overflow: hidden`, which clips an overlay that by
|
|
312
|
+
* definition extends past the box — the clipping only exists for their loading overlay, painted
|
|
313
|
+
* under `[data-loading]`, which is excluded here.
|
|
314
|
+
*/
|
|
315
|
+
@media (max-width: 47.99375em) {
|
|
316
|
+
.actions :is(button, a) {
|
|
317
|
+
position: relative;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.actions :is(button, a):not([data-loading]) {
|
|
321
|
+
overflow: visible;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.actions :is(button, a)::after {
|
|
325
|
+
content: '';
|
|
326
|
+
position: absolute;
|
|
327
|
+
top: 50%;
|
|
328
|
+
left: 50%;
|
|
329
|
+
translate: -50% -50%;
|
|
330
|
+
width: 100%;
|
|
331
|
+
height: 100%;
|
|
332
|
+
min-width: var(--vx-space-touch-control-height, 2.25rem);
|
|
333
|
+
min-height: var(--vx-space-touch-control-height, 2.25rem);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
|
+
* signature — so dot access stays verbatim under the package's strict flags.
|
|
4
|
+
*/
|
|
5
|
+
declare const classes: {
|
|
6
|
+
readonly root: string
|
|
7
|
+
readonly titleRow: string
|
|
8
|
+
readonly heading: string
|
|
9
|
+
readonly titleText: string
|
|
10
|
+
readonly icon: string
|
|
11
|
+
readonly count: string
|
|
12
|
+
readonly metrics: string
|
|
13
|
+
readonly value: string
|
|
14
|
+
readonly actions: string
|
|
15
|
+
readonly subtitle: string
|
|
16
|
+
readonly sparkline: string
|
|
17
|
+
readonly info: string
|
|
18
|
+
readonly infoTrigger: string
|
|
19
|
+
readonly infoBubble: string
|
|
20
|
+
readonly deltaBadge: string
|
|
21
|
+
readonly deltaGlyph: string
|
|
22
|
+
readonly deltaPeriod: string
|
|
23
|
+
}
|
|
24
|
+
export default classes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WidgetHeader` — the tier/heading contract, the count tag, the actions slot, and the info glyph's
|
|
3
|
+
* accessibility contract (docs/CONTROLS-SPEC.md §2.2). Mantine-free, so components mount with no
|
|
4
|
+
* `MantineProvider` at all (unlike `stat-card.test.tsx`'s `renderToStaticMarkup` harness).
|
|
5
|
+
*
|
|
6
|
+
* CSS-module class hashes are unavailable under `bun test` (see `app-mobile-nav.test.tsx`'s doc) —
|
|
7
|
+
* nothing here selects on `classes.*`; every query goes through role/accessible-name/text content.
|
|
8
|
+
*/
|
|
9
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
10
|
+
import { describe, expect, test } from 'bun:test'
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { WidgetHeader } from './widget-header'
|
|
14
|
+
|
|
15
|
+
const CSS = readFileSync(join(import.meta.dir, 'widget-header.module.css'), 'utf8')
|
|
16
|
+
|
|
17
|
+
/** The declaration block of one class in this module — CSS-module hashes are unavailable under
|
|
18
|
+
* `bun test`, so the rules themselves are read from the file (same idiom as
|
|
19
|
+
* `theme/layout-rhythm-css.test.ts` and `controls/filter-set.test.tsx`). */
|
|
20
|
+
function block(selector: string): string {
|
|
21
|
+
const start = CSS.indexOf(`${selector} {`)
|
|
22
|
+
expect(start).toBeGreaterThan(-1)
|
|
23
|
+
return CSS.slice(start, CSS.indexOf('}', start))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('the hero-metric row — value → delta spacing and alignment', () => {
|
|
27
|
+
test('the value→badge gap is the 8px rhythm step, and a TOKEN so density tracks it', () => {
|
|
28
|
+
// It shipped as `gap: 4px`. 4px is the rhythm's smallest step — label-to-thing distance — and at
|
|
29
|
+
// that distance the DeltaBadge read as a superscript hanging off the numeral. A literal would
|
|
30
|
+
// also have frozen the gap while every other spacing in the header moved with the density knob.
|
|
31
|
+
const metrics = block('.metrics')
|
|
32
|
+
expect(metrics).toContain('gap: var(--vx-space-stack-sm)')
|
|
33
|
+
expect(metrics).not.toContain('gap: 4px')
|
|
34
|
+
expect(metrics).not.toMatch(/gap:\s*\d/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('the badge is CENTRED on the value, not baseline-aligned to it', () => {
|
|
38
|
+
// A DeltaBadge is a padded box with an 11.5px mono label; aligning its TEXT baseline to a 24px
|
|
39
|
+
// KPI numeral's baseline put the box itself low — top edge under the numeral's x-height, bottom
|
|
40
|
+
// edge hanging below its baseline. The `count` tag in the title row already centres for the
|
|
41
|
+
// same reason.
|
|
42
|
+
const metrics = block('.metrics')
|
|
43
|
+
expect(metrics).toContain('align-items: center')
|
|
44
|
+
expect(metrics).not.toContain('align-items: baseline')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('tier picks the heading level', () => {
|
|
49
|
+
test('section renders an h2', () => {
|
|
50
|
+
render(<WidgetHeader tier="section" title="Overview" />)
|
|
51
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Overview' })).toBeDefined()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('widget renders an h3', () => {
|
|
55
|
+
render(<WidgetHeader tier="widget" title="Active Users" />)
|
|
56
|
+
expect(screen.getByRole('heading', { level: 3, name: 'Active Users' })).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('count renders a mono tag after the title', () => {
|
|
61
|
+
render(<WidgetHeader tier="section" title="Recent orders" count={42} />)
|
|
62
|
+
expect(screen.getByText('42')).toBeDefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('count={0} renders "0" — a real count, distinct from an omitted one', () => {
|
|
66
|
+
render(<WidgetHeader tier="section" title="Recent orders" count={0} />)
|
|
67
|
+
expect(screen.getByText('0')).toBeDefined()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('omitting count renders no tag', () => {
|
|
71
|
+
render(<WidgetHeader tier="section" title="Recent orders" />)
|
|
72
|
+
expect(screen.queryByText('0')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('the actions slot renders only when passed', () => {
|
|
76
|
+
test('absent by default', () => {
|
|
77
|
+
render(<WidgetHeader tier="widget" title="Active Users" />)
|
|
78
|
+
expect(screen.queryByRole('button', { name: 'Menu' })).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('present when given', () => {
|
|
82
|
+
render(
|
|
83
|
+
<WidgetHeader
|
|
84
|
+
tier="widget"
|
|
85
|
+
title="Active Users"
|
|
86
|
+
actions={<button type="button">Menu</button>}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
expect(screen.getByRole('button', { name: 'Menu' })).toBeDefined()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('the info glyph', () => {
|
|
94
|
+
const INFO = 'Measured over the trailing 30 days.'
|
|
95
|
+
|
|
96
|
+
test("is named `More information` — the text is NOT the button's accessible name", () => {
|
|
97
|
+
render(<WidgetHeader tier="widget" title="Uptime" info={INFO} />)
|
|
98
|
+
expect(screen.getByRole('button', { name: 'More information' })).toBeDefined()
|
|
99
|
+
expect(screen.queryByRole('button', { name: INFO })).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("leaves the heading's accessible name as the title ALONE", () => {
|
|
103
|
+
// The glyph used to live inside the heading, so an `info` paragraph was read out with the title
|
|
104
|
+
// in every headings list. This is the assertion that keeps it outside.
|
|
105
|
+
render(<WidgetHeader tier="widget" title="Uptime" info={INFO} />)
|
|
106
|
+
const heading = screen.getByRole('heading', { level: 3 })
|
|
107
|
+
expect(heading.textContent).toBe('Uptime')
|
|
108
|
+
expect(heading.querySelector('button')).toBeNull()
|
|
109
|
+
expect(screen.getByRole('heading', { level: 3, name: 'Uptime' })).toBeDefined()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('the bubble is closed until asked, and describes the trigger while open', () => {
|
|
113
|
+
render(<WidgetHeader tier="section" title="Uptime" info={INFO} />)
|
|
114
|
+
const trigger = screen.getByRole('button', { name: 'More information' })
|
|
115
|
+
expect(trigger.getAttribute('aria-describedby')).toBeNull()
|
|
116
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
117
|
+
|
|
118
|
+
fireEvent.focus(trigger)
|
|
119
|
+
const bubble = screen.getByRole('tooltip')
|
|
120
|
+
expect(bubble.textContent).toBe(INFO)
|
|
121
|
+
expect(trigger.getAttribute('aria-describedby')).toBe(bubble.id)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('KEYBOARD reaches it — focus opens, Escape and blur close', () => {
|
|
125
|
+
// A `title` attribute alone rendered on hover only, which is the whole reason this bubble exists.
|
|
126
|
+
render(<WidgetHeader tier="widget" title="Uptime" info={INFO} />)
|
|
127
|
+
const trigger = screen.getByRole('button', { name: 'More information' })
|
|
128
|
+
|
|
129
|
+
fireEvent.focus(trigger)
|
|
130
|
+
expect(screen.getByRole('tooltip')).toBeDefined()
|
|
131
|
+
|
|
132
|
+
fireEvent.keyDown(trigger, { key: 'Escape' })
|
|
133
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
134
|
+
|
|
135
|
+
fireEvent.focus(trigger)
|
|
136
|
+
fireEvent.blur(trigger)
|
|
137
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('a pointer-down outside closes it', () => {
|
|
141
|
+
render(<WidgetHeader tier="widget" title="Uptime" info={INFO} />)
|
|
142
|
+
fireEvent.click(screen.getByRole('button', { name: 'More information' }))
|
|
143
|
+
expect(screen.getByRole('tooltip')).toBeDefined()
|
|
144
|
+
|
|
145
|
+
fireEvent.pointerDown(document.body)
|
|
146
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('omitting info renders no glyph', () => {
|
|
151
|
+
render(<WidgetHeader tier="widget" title="Uptime" />)
|
|
152
|
+
expect(screen.queryByRole('button')).toBeNull()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('value + delta render together, delta via DeltaBadge', () => {
|
|
156
|
+
render(<WidgetHeader tier="widget" title="Active Users" value="12,483" delta={4.2} />)
|
|
157
|
+
expect(screen.getByText('12,483')).toBeDefined()
|
|
158
|
+
expect(screen.getByText('4.2%')).toBeDefined()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('value + delta render on their own row under the title, for both tiers', () => {
|
|
162
|
+
for (const tier of ['section', 'widget'] as const) {
|
|
163
|
+
test(tier, () => {
|
|
164
|
+
render(<WidgetHeader tier={tier} title="Active Users" value="12,483" delta={4.2} />)
|
|
165
|
+
const valueNode = screen.getByText('12,483')
|
|
166
|
+
// Never inline with the title — the heading holds title/icon/info/count/actions only.
|
|
167
|
+
expect(valueNode.closest('h2, h3')).toBeNull()
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('subtitle renders below the title row', () => {
|
|
173
|
+
render(<WidgetHeader tier="section" title="Profile" subtitle="Your public identity." />)
|
|
174
|
+
expect(screen.getByText('Your public identity.')).toBeDefined()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('the sparkline slot carries a data-placement for a future bleed layout', () => {
|
|
178
|
+
render(
|
|
179
|
+
<WidgetHeader
|
|
180
|
+
tier="widget"
|
|
181
|
+
title="Active Users"
|
|
182
|
+
sparkline={<span data-testid="spark">trend</span>}
|
|
183
|
+
/>,
|
|
184
|
+
)
|
|
185
|
+
const spark = screen.getByTestId('spark')
|
|
186
|
+
expect(spark.closest('[data-placement]')?.getAttribute('data-placement')).toBe('right')
|
|
187
|
+
})
|