basalt-ui 1.30.2 → 1.31.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 +1 -1
- package/CHANGELOG.md +31 -0
- package/MIGRATING.md +172 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +1 -1
- package/configs/oxlint-plugin.js +254 -4
- package/configs/oxlint-plugin.test.ts +145 -2
- package/configs/oxlint.json +2 -1
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +41 -43
- package/dist/agent-chat/thread-workspace.d.ts +2 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +3 -4
- package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
- package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
- package/dist/agent-chat/use-keyboard-inset.js +34 -0
- package/dist/charts/cursor/touch-pin.d.ts +45 -0
- package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
- package/dist/charts/cursor/touch-pin.js +79 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
- package/dist/charts/hooks/use-discrete-cursor.js +198 -0
- package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
- package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
- package/dist/charts/hooks/use-hysteresis.js +12 -0
- package/dist/charts/hooks/useBandPlot.d.ts +7 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +62 -18
- package/dist/charts/hooks/useChartCursor.d.ts +40 -11
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +102 -11
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +0 -10
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +54 -41
- package/dist/charts/kinds/Donut.d.ts +9 -3
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +110 -48
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +145 -131
- package/dist/charts/kinds/Heatmap.d.ts +7 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +37 -16
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +83 -68
- package/dist/charts/kinds/donut-layout.d.ts +47 -0
- package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
- package/dist/charts/kinds/donut-layout.js +52 -0
- package/dist/charts/layout/auto-margin.d.ts +7 -2
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +12 -3
- package/dist/charts/layout/end-labels.d.ts +64 -0
- package/dist/charts/layout/end-labels.d.ts.map +1 -0
- package/dist/charts/layout/end-labels.js +76 -0
- package/dist/charts/primitives/Axes.d.ts +32 -4
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +62 -16
- package/dist/charts/primitives/CartesianChart.d.ts +7 -6
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +271 -125
- package/dist/charts/primitives/ChartCard.d.ts +0 -14
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +74 -21
- package/dist/charts/primitives/ChartFrame.d.ts +28 -22
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +133 -34
- package/dist/charts/primitives/ChartLegend.d.ts +19 -15
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +212 -71
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +4 -3
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +37 -26
- package/dist/charts/primitives/HoverOverlay.module.css +13 -0
- package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/dist/charts/primitives/chart-card-context.d.ts +29 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +13 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +76 -29
- package/dist/charts/primitives/chart-layout.d.ts +112 -0
- package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-layout.js +175 -0
- package/dist/charts/primitives/chart-tier.d.ts +13 -11
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
- package/dist/charts/primitives/chart-tier.js +42 -13
- package/dist/charts/series.d.ts +4 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/utils/ticks.d.ts +27 -8
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +35 -11
- package/dist/common/errors.d.ts +11 -1
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +16 -2
- package/dist/common/hit-floor.d.ts +10 -0
- package/dist/common/hit-floor.d.ts.map +1 -0
- package/dist/common/hit-floor.js +6 -0
- package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
- package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/common/use-measured-widths.d.ts +37 -0
- package/dist/common/use-measured-widths.d.ts.map +1 -0
- package/dist/common/use-measured-widths.js +51 -0
- package/dist/common/use-media-query.d.ts +6 -0
- package/dist/common/use-media-query.d.ts.map +1 -0
- package/dist/common/use-media-query.js +40 -0
- package/dist/common/validate.d.ts +9 -0
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +9 -4
- package/dist/content/article-layout.d.ts +4 -2
- package/dist/content/article-layout.d.ts.map +1 -1
- package/dist/content/article-layout.js +43 -3
- package/dist/content/article-layout.module.css +45 -4
- package/dist/content/article-layout.module.css.d.ts +3 -0
- package/dist/content/code-block.d.ts.map +1 -1
- package/dist/content/code-block.js +40 -2
- package/dist/content/code-block.module.css +32 -2
- package/dist/content/prose.module.css +13 -8
- package/dist/controls/actions.d.ts +29 -6
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +158 -27
- package/dist/controls/actions.module.css +14 -1
- package/dist/controls/actions.module.css.d.ts +1 -0
- package/dist/controls/controls.module.css +8 -31
- package/dist/controls/enum-filter.js +2 -2
- package/dist/controls/filter-context.d.ts +10 -0
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-context.js +2 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +1 -0
- package/dist/controls/index.d.ts +1 -1
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +0 -2
- package/dist/controls/multi-select-filter.js +2 -2
- package/dist/controls/sync-button.module.css +1 -1
- package/dist/dashboard/section.d.ts.map +1 -1
- package/dist/dashboard/section.js +1 -0
- package/dist/dashboard/section.module.css +1 -26
- package/dist/dashboard/stat-card.d.ts +1 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +4 -1
- package/dist/dashboard/stat-card.module.css +6 -9
- package/dist/dashboard/stat-group.module.css +16 -4
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-header.d.ts +14 -4
- package/dist/dashboard/widget-header.d.ts.map +1 -1
- package/dist/dashboard/widget-header.js +23 -6
- package/dist/dashboard/widget-header.module.css +145 -133
- package/dist/dashboard/widget-header.module.css.d.ts +2 -0
- package/dist/data/data-table.d.ts +23 -0
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +339 -141
- package/dist/data/data-table.module.css +94 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +10 -0
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.js +3 -3
- package/dist/forms/form-layout.module.css +30 -5
- package/dist/forms/form-layout.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +8 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -2
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +22 -2
- package/dist/shell/app-brand.d.ts.map +1 -1
- package/dist/shell/app-brand.js +1 -0
- package/dist/shell/app-brand.module.css +2 -2
- package/dist/shell/app-header.module.css +18 -8
- package/dist/shell/app-main.module.css +54 -3
- package/dist/shell/app-main.module.css.d.ts +1 -0
- package/dist/shell/app-mobile-nav.module.css +16 -4
- package/dist/shell/app-sidebar.module.css +4 -6
- package/dist/shell/index.d.ts +12 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +57 -16
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/page-aside.d.ts +22 -3
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +27 -13
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +10 -5
- package/dist/shell/page-bar.module.css +10 -34
- package/dist/shell/use-breakpoint.d.ts +3 -4
- package/dist/shell/use-breakpoint.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.js +3 -25
- package/dist/shell/use-size-class.d.ts +6 -0
- package/dist/shell/use-size-class.d.ts.map +1 -0
- package/dist/shell/use-size-class.js +19 -0
- package/dist/state/persisted-or-local.d.ts +4 -1
- package/dist/state/persisted-or-local.d.ts.map +1 -1
- package/dist/state/persisted-or-local.js +4 -3
- package/dist/state/persisted.d.ts +10 -0
- package/dist/state/persisted.d.ts.map +1 -1
- package/dist/state/persisted.js +44 -14
- package/dist/styles.css +41 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +11 -3
- package/dist/theme/controls.module.css +7 -0
- package/dist/theme/ctl-theme.d.ts +1 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/ctl-theme.js +12 -4
- package/dist/theme/floating.module.css +36 -0
- package/dist/theme/floating.module.css.d.ts +3 -0
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +34 -5
- package/dist/theme/nav-link.module.css +7 -0
- package/dist/tokens/index.d.ts +7 -16
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +23 -28
- package/dist/tokens/palette.d.ts +5 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +8 -30
- package/dist/tokens/size-classes.d.ts +40 -0
- package/dist/tokens/size-classes.d.ts.map +1 -0
- package/dist/tokens/size-classes.js +29 -0
- package/dist/tokens.css +8 -4
- package/llms.txt +3 -3
- package/package.json +17 -17
- package/src/agent-chat/composer.tsx +74 -47
- package/src/agent-chat/thread-workspace.tsx +8 -11
- package/src/agent-chat/use-keyboard-inset.ts +68 -0
- package/src/charts/cursor/touch-pin.ts +138 -0
- package/src/charts/hooks/use-discrete-cursor.ts +335 -0
- package/src/charts/hooks/use-hysteresis.ts +26 -0
- package/src/charts/hooks/useBandPlot.ts +90 -27
- package/src/charts/hooks/useChartCursor.ts +211 -26
- package/src/charts/index.ts +0 -8
- package/src/charts/kinds/BandStrip.tsx +11 -1
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +19 -4
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +15 -3
- package/src/charts/kinds/donut-layout.ts +82 -0
- package/src/charts/layout/auto-margin.ts +24 -5
- package/src/charts/layout/end-labels.ts +135 -0
- package/src/charts/primitives/Axes.tsx +112 -21
- package/src/charts/primitives/CartesianChart.tsx +282 -68
- package/src/charts/primitives/ChartCard.tsx +141 -48
- package/src/charts/primitives/ChartFrame.tsx +243 -60
- package/src/charts/primitives/ChartLegend.tsx +257 -73
- package/src/charts/primitives/ChartTooltip.tsx +8 -3
- package/src/charts/primitives/Crosshair.tsx +2 -2
- package/src/charts/primitives/HoverOverlay.module.css +13 -0
- package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/src/charts/primitives/HoverOverlay.tsx +15 -4
- package/src/charts/primitives/chart-card-context.ts +37 -0
- package/src/charts/primitives/chart-frame-layout.ts +214 -177
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/series.ts +4 -1
- package/src/charts/utils/ticks.ts +95 -19
- package/src/common/errors.ts +25 -1
- package/src/common/hit-floor.ts +9 -0
- package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
- package/src/common/use-measured-widths.ts +88 -0
- package/src/common/use-media-query.ts +56 -0
- package/src/common/validate.ts +17 -4
- package/src/content/article-layout.module.css +45 -4
- package/src/content/article-layout.module.css.d.ts +3 -0
- package/src/content/article-layout.tsx +60 -5
- package/src/content/code-block.module.css +32 -2
- package/src/content/code-block.tsx +47 -3
- package/src/content/prose.module.css +13 -8
- package/src/controls/actions.module.css +14 -1
- package/src/controls/actions.module.css.d.ts +1 -0
- package/src/controls/actions.tsx +223 -75
- package/src/controls/controls.module.css +8 -31
- package/src/controls/enum-filter.tsx +2 -2
- package/src/controls/filter-context.tsx +11 -0
- package/src/controls/filter-pill.tsx +1 -0
- package/src/controls/index.ts +0 -1
- package/src/controls/multi-select-filter.tsx +2 -2
- package/src/controls/sync-button.module.css +1 -1
- package/src/dashboard/section.module.css +1 -26
- package/src/dashboard/section.tsx +1 -0
- package/src/dashboard/stat-card.module.css +6 -9
- package/src/dashboard/stat-card.tsx +6 -3
- package/src/dashboard/stat-group.module.css +16 -4
- package/src/dashboard/widget-grid.tsx +7 -6
- package/src/dashboard/widget-header.module.css +145 -133
- package/src/dashboard/widget-header.module.css.d.ts +2 -0
- package/src/dashboard/widget-header.tsx +45 -11
- package/src/data/data-table.module.css +94 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +503 -164
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +30 -5
- package/src/forms/form-layout.module.css.d.ts +1 -0
- package/src/forms/form-layout.tsx +12 -10
- package/src/guard/index.ts +183 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/provider/index.tsx +53 -1
- package/src/shell/app-brand.module.css +2 -2
- package/src/shell/app-brand.tsx +1 -0
- package/src/shell/app-header.module.css +18 -8
- package/src/shell/app-main.module.css +54 -3
- package/src/shell/app-main.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.module.css +16 -4
- package/src/shell/app-sidebar.module.css +4 -6
- package/src/shell/index.tsx +200 -129
- package/src/shell/mobile-nav-model.ts +1 -1
- package/src/shell/page-aside.tsx +70 -21
- package/src/shell/page-bar.module.css +10 -34
- package/src/shell/page-bar.tsx +10 -5
- package/src/shell/use-breakpoint.ts +9 -45
- package/src/shell/use-size-class.ts +31 -0
- package/src/state/persisted-or-local.ts +10 -4
- package/src/state/persisted.ts +76 -17
- package/src/styles.css +41 -0
- package/src/surfaces.ts +9 -1
- package/src/theme/controls.module.css +7 -0
- package/src/theme/ctl-theme.tsx +15 -3
- package/src/theme/floating.module.css +36 -0
- package/src/theme/floating.module.css.d.ts +3 -0
- package/src/theme/index.ts +46 -8
- package/src/theme/nav-link.module.css +7 -0
- package/src/tokens/index.ts +20 -30
- package/src/tokens/palette.ts +6 -28
- package/src/tokens/size-classes.ts +46 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
- /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* StatCard's sparkline placement (docs/CONTROLS-SPEC.md §2.2). `bleed` (default) keeps the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* below `sm` — CSS only for the sparkline's own box, `.metricsRow` below for the row it sits in
|
|
7
|
-
* (`47.99375em` is Mantine's own complement of `min-width: 48em`, same value
|
|
8
|
-
* `content/prose.module.css` derives it from).
|
|
2
|
+
* StatCard's sparkline placement (docs/CONTROLS-SPEC.md §2.2). `bleed` (default) keeps the full-width
|
|
3
|
+
* row beneath the hero value, bled past the card's inset padding. `right` sits the sparkline beside
|
|
4
|
+
* the BREAKDOWN row and collapses back to `bleed`'s stacked layout when the card itself is narrower
|
|
5
|
+
* than 480px (`container: basalt-card`, set on the Card root in `stat-card.tsx`).
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
.body {
|
|
@@ -131,7 +128,7 @@
|
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
/* Asks for `StatCard`'s `SPARKLINE_RIGHT_WIDTH`/`_HEIGHT` (handed down as custom properties, so this
|
|
134
|
-
* rule and the
|
|
131
|
+
* rule and the narrow-card reset below can both reach them). `align-self: flex-end` is the one alignment
|
|
135
132
|
* this restructure keeps at the SPARKLINE's own level rather than the row's: `.metricsRow` does not
|
|
136
133
|
* set `align-items: flex-end` itself, because a card with no breakdown would then need nothing to
|
|
137
134
|
* anchor against — only the sparkline opts into sitting at the row's bottom edge, which matters once
|
|
@@ -146,7 +143,7 @@
|
|
|
146
143
|
overflow: hidden;
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
@
|
|
146
|
+
@container basalt-card (max-width: 479.9px) {
|
|
150
147
|
.metricsRow {
|
|
151
148
|
flex-direction: column;
|
|
152
149
|
align-items: stretch;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* `sparklinePlacement` decides where the trend visual sits: `'bleed'` (default) keeps the historic
|
|
13
13
|
* full-width row beneath the hero value, bled past the card's own inset padding; `'right'` sits it
|
|
14
|
-
* beside the hero row, the reference-design look.
|
|
14
|
+
* beside the hero row, the reference-design look. In a card narrower than 480px, `'right'` collapses back to the
|
|
15
15
|
* `'bleed'` layout — CSS only, no JS branch (`stat-card.module.css`).
|
|
16
16
|
*
|
|
17
17
|
* `info` and `subtitle` are forwarded to that same `WidgetHeader`, and a hero KPI usually wants
|
|
@@ -277,7 +277,7 @@ export type StatCardProps = BasaltProps &
|
|
|
277
277
|
*/
|
|
278
278
|
sparkline?: ReactNode | ((size: StatCardSparklineSize) => ReactNode)
|
|
279
279
|
/** Where `sparkline` sits. `'bleed'` (default) is today's full-width row bled to the card edges;
|
|
280
|
-
* `'right'` sits it beside the hero value row. Collapses to `'bleed'`
|
|
280
|
+
* `'right'` sits it beside the hero value row. Collapses to `'bleed'` in a card narrower than 480px. */
|
|
281
281
|
sparklinePlacement?: 'bleed' | 'right'
|
|
282
282
|
/** Header-right slot (e.g. a ghost "..." menu trigger) — wrapped in `CtlSlot tier="widget"`, so a
|
|
283
283
|
* raw `ActionIcon` with no `size` lands on the 24px step the 28px header row holds (C1/C5). */
|
|
@@ -357,7 +357,7 @@ export function StatCard({
|
|
|
357
357
|
)}
|
|
358
358
|
{...(!bleeds && {
|
|
359
359
|
// CUSTOM PROPERTIES, not `flexBasis`/`height` directly. A React inline style beats every
|
|
360
|
-
// stylesheet rule, so an inline `flexBasis: 72` also applied
|
|
360
|
+
// stylesheet rule, so an inline `flexBasis: 72` also applied in a narrow card — where `'right'`
|
|
361
361
|
// collapses `.metricsRow` to a COLUMN and flex-basis is therefore the main-axis HEIGHT.
|
|
362
362
|
// The mobile sparkline rendered in a 72px-tall box holding 26px of bars, which is the dead
|
|
363
363
|
// band under every KPI value on a phone. Handed to CSS as values instead, the media query
|
|
@@ -400,6 +400,9 @@ export function StatCard({
|
|
|
400
400
|
// Anchors the tone rail. Set unconditionally so a card's stacking context does not change
|
|
401
401
|
// depending on whether it happens to have crossed a threshold this render.
|
|
402
402
|
position: 'relative',
|
|
403
|
+
// The card's own width drives its chrome (`widget-header.module.css`, `.metricsRow`).
|
|
404
|
+
containerType: 'inline-size',
|
|
405
|
+
containerName: 'basalt-card',
|
|
403
406
|
...style,
|
|
404
407
|
}}
|
|
405
408
|
data-tone={tone}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* StatGroup's column law and its optional rail, in CSS (law C9).
|
|
3
3
|
*
|
|
4
4
|
* **The law keys on the CONTAINER, not the viewport.** A viewport breakpoint answers the wrong
|
|
5
|
-
* question — the same one `
|
|
6
|
-
*
|
|
5
|
+
* question — the same one `resolveContainerClass` refuses to ask (`tokens/size-classes.ts`): the
|
|
6
|
+
* shell has regions that change the AVAILABLE width with the viewport held still (a
|
|
7
7
|
* `PageAside` claiming its 300px, the sidebar collapsing 256px → its 48px rail, a consumer's own
|
|
8
8
|
* split layout), and `@media` cannot see any of them. Measured defect: a `PageAside` mounted on a
|
|
9
9
|
* 1512px desktop left the KPI row ~956px wide, `@media` still resolved `lg`, and four cells of
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
* boundary at the 16px initial font size — 768px and 1200px — but `em` inside a `@container`
|
|
27
27
|
* condition resolves against the CONTAINER's font-size rather than the root's, so a consumer panel
|
|
28
28
|
* with smaller text would silently move the breakpoint. px is the only spelling that cannot drift.
|
|
29
|
-
* (`47.99375em` → `767.9px`, `74.99375em` → `1199.9px
|
|
30
|
-
* uses.) A condition still cannot read a custom property either way, so the numbers are written;
|
|
29
|
+
* (`47.99375em` → `767.9px`, `74.99375em` → `1199.9px`.) A condition still cannot read a custom property either way, so the numbers are written;
|
|
31
30
|
* the COUNTS are custom properties the component sets per instance.
|
|
32
31
|
*/
|
|
33
32
|
|
|
@@ -44,6 +43,19 @@
|
|
|
44
43
|
gap: var(--vx-space-stack-md);
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
/* 260px is StatGroup's own literal (docs/DESIGN-CORE.md § Layout, elevation, shapes), not a container class: one column below it,
|
|
47
|
+
* so every cell leads a row and the rail draws no leading hairline at all. */
|
|
48
|
+
@container basalt-stat-group (max-width: 259.9px) {
|
|
49
|
+
.root {
|
|
50
|
+
grid-template-columns: minmax(0, 1fr);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.root[data-divided] > *:nth-child(n) {
|
|
54
|
+
border-inline-start: 0;
|
|
55
|
+
padding-inline-start: 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
@container basalt-stat-group (min-width: 768px) {
|
|
48
60
|
.root {
|
|
49
61
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* **`cols` is the DESKTOP count, and it is the only number a consumer states.** The law is
|
|
11
11
|
* `base 1 → sm min(cols, 2) → lg cols`: a phone gets one column because a dashboard card holds a
|
|
12
12
|
* chart; a tablet gets at most two because a third makes every chart narrower than
|
|
13
|
-
* `
|
|
13
|
+
* `CONTAINER_CLASSES.regular`; the full count lands at `lg`. Mantine's `cols={{ … }}` object is
|
|
14
14
|
* deliberately NOT forwarded — a responsive object on the public prop is exactly the seam that let
|
|
15
15
|
* five call sites disagree.
|
|
16
16
|
*
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
* ACTUALLY has. The shell moves that width without moving the viewport — a `PageAside` claiming its
|
|
20
20
|
* 300px, the sidebar collapsing 256px → its 48px rail, a consumer's split pane — and a `@media`
|
|
21
21
|
* query sees none of it: it kept three chart columns in a 956px content box and every chart in the
|
|
22
|
-
* row went narrower than `
|
|
23
|
-
* avoid. See `widget-grid.module.css`'s header for the fallback.
|
|
22
|
+
* row went narrower than `CONTAINER_CLASSES.regular`, which is the exact condition the `lg` rung
|
|
23
|
+
* exists to avoid. See `widget-grid.module.css`'s header for the fallback.
|
|
24
24
|
*
|
|
25
|
-
* **This is the sanctioned place `lg` enters the package.**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* **This is the sanctioned place `lg` enters the package.** A consumer never writes a
|
|
26
|
+
* breakpoint — component layout keys on the container's own width
|
|
27
|
+
* (`docs/DESIGN-CORE.md` § Layout, elevation, shapes); `WidgetGrid` and `StatGroup` own their `lg`
|
|
28
|
+
* container rung internally so nobody else has to.
|
|
28
29
|
*
|
|
29
30
|
* `WidgetGrid.Item` is for the one card that is wider than the rest — a hero chart beside two
|
|
30
31
|
* stacked panels. `span` is likewise the DESKTOP span and is CLAMPED to the live column count at
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* by name with rem fallbacks at their level-0 px value (36/28/6), same idiom as ChartCard's
|
|
10
10
|
* `--mantine-spacing-*` fallbacks.
|
|
11
11
|
*
|
|
12
|
-
* Layout
|
|
13
|
-
*
|
|
12
|
+
* Layout keys on the enclosing card's OWN width (`container: basalt-card`, declared by ChartCard /
|
|
13
|
+
* StatCard / Section — docs/DESIGN-CORE.md § Layout, elevation, shapes), never the viewport: below 480px the subtitle folds
|
|
14
|
+
* into the info glyph; from 480px (wave 8) `value` + `delta` move onto the title row. Wrapping and
|
|
15
|
+
* shrinking are unconditional. The subtitle ALSO folds (regardless of width) whenever the card's own
|
|
16
|
+
* height is under 280px — see the `[data-basalt-card-short]` block below (wave 8's header-economy
|
|
17
|
+
* law). No enclosing card, no container: the standard layout. Container conditions are px literals
|
|
18
|
+
* of CONTAINER_CLASSES.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
.root {
|
|
@@ -18,73 +23,15 @@
|
|
|
18
23
|
flex-direction: column;
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
/* The row WRAPS only when it does not fit (a lone kebab stays beside the title; a wide `actions` slot
|
|
27
|
+
* takes its own right-aligned line instead of squeezing the heading) — unconditional, so composers
|
|
28
|
+
* with no `basalt-card` container (`SettingsSection`, `BasaltDataTable`) keep the safety net. */
|
|
21
29
|
.titleRow {
|
|
22
30
|
display: flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
23
32
|
align-items: center;
|
|
24
33
|
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
|
-
* WRAP ONLY WHEN IT DOES NOT FIT (M4 follow-up) — `flex: 1 1 100%` forced EVERY actions slot onto
|
|
46
|
-
* its own row, even a single 24px icon that fit beside the title fine (a KPI card's lone `···`
|
|
47
|
-
* menu dropped under the title and pushed the value down with it). `flex: 0 1 auto` restores
|
|
48
|
-
* ordinary flex sizing: no grow, so a short slot stays put beside the title on the shared row;
|
|
49
|
-
* `min-width: 0` plus `flex-shrink: 1` (part of the same shorthand, overriding the base rule's
|
|
50
|
-
* `flex-shrink: 0`) let it shrink and, once `.titleRow`'s own `flex-wrap: wrap` decides it truly
|
|
51
|
-
* does not fit, drop to its own line. `max-width: 100%` is the cap once it IS alone on that line —
|
|
52
|
-
* without it a `.actions` still carrying unshrinkable content (the `visibleFrom="sm"` control plus
|
|
53
|
-
* a nowrap switch `Group`, see below) can overflow the card rather than shrink into it. The base
|
|
54
|
-
* rule's `margin-inline-start: auto` still applies in both states — right-aligned beside the title
|
|
55
|
-
* when it fits, right-aligned as the lone item on its own wrapped line when it does not.
|
|
56
|
-
*
|
|
57
|
-
* `.actions` keeps its OWN `flex-wrap: wrap` from the prior fix: `Section` can hand it a `tabs`
|
|
58
|
-
* control (`CtlSlot`-wrapped) AND an `actions` control (another `CtlSlot`) in the same slot, and
|
|
59
|
-
* without this a nowrap `Group` of switches beside a view-switch control gets squeezed into one
|
|
60
|
-
* shared line and overflows below its own min-content width — the exact regression this rule
|
|
61
|
-
* once fixed. It is orthogonal to the row-level "wrap only when it does not fit" law above: with
|
|
62
|
-
* one child, wrapping has nothing to do, so a lone `···` menu still stays on the title line.
|
|
63
|
-
*
|
|
64
|
-
* The real flex items get `flex: 1 1 auto; min-width: 0` so they can shrink or (via the `> * > *`
|
|
65
|
-
* twin) stack within whatever width `.actions` itself resolves to — a full-width phone `Select`
|
|
66
|
-
* still reads as its own full-width line because the CONTROL sets that itself (`ViewTabs`'
|
|
67
|
-
* `fullWidth`/`.phoneSelect`), not because `.actions` forces it.
|
|
68
|
-
*
|
|
69
|
-
* The `> * > *` twin exists because `CtlSlot` (`theme/ctl-theme.tsx`) wraps a home's actions in a
|
|
70
|
-
* `display: contents` box — invisible to the flex box tree, so its OWN children (not it) are the
|
|
71
|
-
* real flex items. `display: contents` elements still match `.actions > *` by DOM structure but
|
|
72
|
-
* carry no box to apply `flex`/`min-width` to, hence styling one level deeper as well; a call site
|
|
73
|
-
* that skips `CtlSlot` and hands `actions` a real element directly gets the same rule from the
|
|
74
|
-
* first selector.
|
|
75
|
-
*/
|
|
76
|
-
.actions {
|
|
77
|
-
flex: 0 1 auto;
|
|
78
|
-
flex-wrap: wrap;
|
|
79
|
-
min-width: 0;
|
|
80
|
-
max-width: 100%;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.actions > *,
|
|
84
|
-
.actions > * > * {
|
|
85
|
-
flex: 1 1 auto;
|
|
86
|
-
min-width: 0;
|
|
87
|
-
}
|
|
34
|
+
row-gap: var(--vx-space-stack-sm);
|
|
88
35
|
}
|
|
89
36
|
|
|
90
37
|
.root[data-tier='section'] .titleRow {
|
|
@@ -203,59 +150,28 @@
|
|
|
203
150
|
}
|
|
204
151
|
|
|
205
152
|
/*
|
|
206
|
-
* The hero-metric row
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*/
|
|
211
|
-
/*
|
|
212
|
-
* The hero-metric row: the value, then its `DeltaBadge`.
|
|
213
|
-
*
|
|
214
|
-
* The gap is the 8px vertical-rhythm step (`--vx-space-stack-sm`), not the 4px it shipped as. 4px is
|
|
215
|
-
* the rhythm's SMALLEST step — the distance between a label and the thing it labels — and at that
|
|
216
|
-
* distance the badge read as a superscript hanging off the numeral rather than as a second fact
|
|
217
|
-
* beside it. It is a token and not a literal so it tracks the density knob with every other spacing
|
|
218
|
-
* in the header (`deriveSpacing`, `tokens/palette.ts`); `theme/layout-rhythm-css.test.ts`'s sibling
|
|
219
|
-
* assertion in `widget-header.test.tsx` pins it against exactly the literal it used to be.
|
|
220
|
-
*
|
|
221
|
-
* `align-items: center`, not `baseline`. The badge is a BOX with its own padding and a mono 11.5px
|
|
222
|
-
* label, so aligning its text baseline to a 24px KPI numeral's baseline put the box itself low —
|
|
223
|
-
* its top edge sat below the numeral's x-height and its bottom edge hung under the numeral's
|
|
224
|
-
* baseline. A chip beside a numeral is centred on the numeral, the same law the `count` tag in the
|
|
225
|
-
* title row above already follows.
|
|
153
|
+
* The hero-metric row: the value, then its `DeltaBadge`, 8px apart (`--vx-space-stack-sm`, tracks
|
|
154
|
+
* density). It always WRAPS — the badge drops under the value when they do not fit, the value never
|
|
155
|
+
* ellipsizes. `center`, not `baseline`: the badge is a padded box, so a chip beside a numeral is
|
|
156
|
+
* centred on it (same law as the `count` tag above).
|
|
226
157
|
*/
|
|
227
158
|
.metrics {
|
|
228
159
|
display: flex;
|
|
229
|
-
flex-wrap:
|
|
160
|
+
flex-wrap: wrap;
|
|
230
161
|
align-items: center;
|
|
231
162
|
gap: var(--vx-space-stack-sm);
|
|
163
|
+
/* Tight when the badge wraps under the value. */
|
|
164
|
+
row-gap: 2px;
|
|
232
165
|
margin-top: 2px;
|
|
233
166
|
min-width: 0;
|
|
234
167
|
}
|
|
235
168
|
|
|
236
|
-
/*
|
|
237
|
-
*
|
|
238
|
-
* `
|
|
239
|
-
* a numeral can — but a KPI's numeral IS the content, and in a 2-up card grid at 390px
|
|
240
|
-
* `$1294.9k ▲18.9%` rendered as `$129… ▲18.9%`. Truncating the number to protect the delta about the
|
|
241
|
-
* number is the wrong way round. Conditional like the title row above it: a short value keeps the
|
|
242
|
-
* badge beside it, and only a value that would otherwise clip gives up the line.
|
|
243
|
-
*/
|
|
244
|
-
@media (max-width: 47.99375em) {
|
|
245
|
-
.metrics {
|
|
246
|
-
flex-wrap: wrap;
|
|
247
|
-
row-gap: 2px;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/* The value is the row's shrinkable half and the delta badge is not: a pre-formatted numeral can
|
|
252
|
-
* ellipsize and still read, a `+18.9%` cannot. Without the clamp the badge grew past the header box
|
|
253
|
-
* and painted over `StatCard`'s 72px sparkline slot beside it. */
|
|
169
|
+
/* NEVER truncated: the numeral IS the content. It shrinks with the card's own width instead —
|
|
170
|
+
* `cqi` against `basalt-card` (the clamp, at the end of this file), from the `kpi` step down to the
|
|
171
|
+
* `lg` step (16px), and `overflow-wrap` breaks a pathological string rather than clipping it. */
|
|
254
172
|
.value {
|
|
255
173
|
min-width: 0;
|
|
256
|
-
overflow:
|
|
257
|
-
text-overflow: ellipsis;
|
|
258
|
-
white-space: nowrap;
|
|
174
|
+
overflow-wrap: anywhere;
|
|
259
175
|
font-family: var(--basalt-font-mono);
|
|
260
176
|
font-size: var(--vx-text-kpi);
|
|
261
177
|
font-weight: 600;
|
|
@@ -266,8 +182,8 @@
|
|
|
266
182
|
/*
|
|
267
183
|
* The unit beside the hero value. Mono like the number it qualifies (so `412 TSS` sits on one
|
|
268
184
|
* baseline grid), `--vx-text-sm` and `--vx-muted` so it never competes with the numeral — the whole
|
|
269
|
-
* point is that a unit is the SMALL half of the pair. `flex-shrink: 0`:
|
|
270
|
-
*
|
|
185
|
+
* point is that a unit is the SMALL half of the pair. `flex-shrink: 0`: a unit clipped to `TS…`
|
|
186
|
+
* would state nothing.
|
|
271
187
|
*/
|
|
272
188
|
.unit {
|
|
273
189
|
flex-shrink: 0;
|
|
@@ -278,14 +194,28 @@
|
|
|
278
194
|
color: var(--vx-muted);
|
|
279
195
|
}
|
|
280
196
|
|
|
281
|
-
/* The C1/C5 home for header actions — always pinned right, regardless of what else is present.
|
|
197
|
+
/* The C1/C5 home for header actions — always pinned right, regardless of what else is present.
|
|
198
|
+
* Law C15: adopts the shared hit overlay (`data-basalt-hit-scope`, `styles.css`); `--vx-hit-gap`
|
|
199
|
+
* caps the expansion at the control gap so neighbouring 24px icons never overlap. */
|
|
282
200
|
.actions {
|
|
201
|
+
--vx-hit-gap: var(--vx-space-control-gap);
|
|
283
202
|
display: inline-flex;
|
|
284
|
-
|
|
203
|
+
/* No grow, but it may shrink and wrap once `.titleRow` decides it does not fit, capped at 100% once
|
|
204
|
+
* alone on its line. Its flex items shrink too; the `> * > *` twin reaches through `CtlSlot`'s
|
|
205
|
+
* `display: contents` wrapper, whose own children are the real items. */
|
|
206
|
+
flex: 0 1 auto;
|
|
207
|
+
flex-wrap: wrap;
|
|
285
208
|
align-items: center;
|
|
286
209
|
gap: var(--vx-space-control-gap, 0.375rem);
|
|
287
210
|
margin-inline-start: auto;
|
|
288
211
|
min-width: 0;
|
|
212
|
+
max-width: 100%;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.actions > *,
|
|
216
|
+
.actions > * > * {
|
|
217
|
+
flex: 1 1 auto;
|
|
218
|
+
min-width: 0;
|
|
289
219
|
}
|
|
290
220
|
|
|
291
221
|
/* Directly under the title row, 4px off it — 1px put it inside the title's own line box, so a
|
|
@@ -306,6 +236,7 @@
|
|
|
306
236
|
/* ── info glyph ─────────────────────────────────────────────────────────────────────────────── */
|
|
307
237
|
|
|
308
238
|
.info {
|
|
239
|
+
--vx-hit-gap: var(--vx-space-control-gap);
|
|
309
240
|
position: relative;
|
|
310
241
|
display: inline-flex;
|
|
311
242
|
flex-shrink: 0;
|
|
@@ -330,6 +261,12 @@
|
|
|
330
261
|
color: var(--vx-muted);
|
|
331
262
|
}
|
|
332
263
|
|
|
264
|
+
/* Folded-subtitle pieces: hidden until a `basalt-card` narrower than 480px shows them (below). */
|
|
265
|
+
.foldGlyph,
|
|
266
|
+
.foldNote {
|
|
267
|
+
display: none;
|
|
268
|
+
}
|
|
269
|
+
|
|
333
270
|
/*
|
|
334
271
|
* The tooltip bubble reads like every other floating surface — panel bg + `shadow-card` + card
|
|
335
272
|
* radius (docs/DESIGN-SPEC.md §5). It opens DOWNWARD by convention, into whatever sits below the
|
|
@@ -383,33 +320,108 @@
|
|
|
383
320
|
opacity: 0.6;
|
|
384
321
|
}
|
|
385
322
|
|
|
323
|
+
/* Card narrower than 480px: the subtitle folds into the info glyph (`InfoGlyph`'s `note`, or
|
|
324
|
+
* `.foldGlyph` when there is no `info`) — exactly one of the two shows. */
|
|
325
|
+
@container basalt-card (max-width: 479.9px) {
|
|
326
|
+
.subtitle {
|
|
327
|
+
display: none;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.foldGlyph {
|
|
331
|
+
display: inline-flex;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.foldNote {
|
|
335
|
+
display: block;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
386
339
|
/*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* definition extends past the box — the clipping only exists for their loading overlay, painted
|
|
393
|
-
* under `[data-loading]`, which is excluded here.
|
|
340
|
+
* The same fold, triggered by the CARD'S OWN HEIGHT instead of its width (wave 8's header-economy
|
|
341
|
+
* law) — a short-but-wide chart card (a KPI-style plot with little vertical room) still needs the
|
|
342
|
+
* subtitle line back. Not a `@container basalt-card` SIZE query: `container-type: size` on an
|
|
343
|
+
* auto-height card collapses it to 0 (see `ChartCard`'s `CARD_SHORT_HEIGHT` doc), so `ChartCard`
|
|
344
|
+
* measures itself with a ResizeObserver instead and sets `data-basalt-card-short` under 280px.
|
|
394
345
|
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
346
|
+
[data-basalt-card-short] .subtitle {
|
|
347
|
+
display: none;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
[data-basalt-card-short] .foldGlyph {
|
|
351
|
+
display: inline-flex;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
[data-basalt-card-short] .foldNote {
|
|
355
|
+
display: block;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* A short card's KPI numeral is the header's own tallest line (the merged title row has no other
|
|
359
|
+
* source of height once the subtitle folds) — the same floor the width `cqi` clamp already uses,
|
|
360
|
+
* so a short card never introduces a THIRD value size. */
|
|
361
|
+
[data-basalt-card-short] .value {
|
|
362
|
+
font-size: var(--vx-text-lg);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* The KPI numeral shrinks with its card. `(width >= 0px)` is always true but only inside a
|
|
366
|
+
* `basalt-card` container, so `cqi` never resolves against an unrelated one (or the viewport) —
|
|
367
|
+
* with no card the numeral stays at the plain `kpi` step above. 8cqi reaches the 24px `kpi` step at
|
|
368
|
+
* a 300px card. */
|
|
369
|
+
@container basalt-card (width >= 0px) {
|
|
370
|
+
.value {
|
|
371
|
+
font-size: clamp(var(--vx-text-lg), 8cqi, var(--vx-text-kpi));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/*
|
|
376
|
+
* From 480px (`CONTAINER_CLASSES.regular`, wave 8's header-economy law) `value` + `delta` ride the
|
|
377
|
+
* title row: the title row dissolves (`display: contents`) so heading, info, count, metrics and
|
|
378
|
+
* actions share the root's one flex line, and `order` puts the metrics before the actions. The
|
|
379
|
+
* metrics take the free space (`margin-inline-start: auto`), which pushes metrics AND actions
|
|
380
|
+
* right; with no metrics, `.actions` keeps its own auto margin. The heading keeps the tier's row
|
|
381
|
+
* height (`min-height` was on `.titleRow`), so a title-less header does not collapse.
|
|
382
|
+
*
|
|
383
|
+
* This threshold moved from `wide` (800px) in wave 5/7 — most cards sit in the 480-799px `regular`
|
|
384
|
+
* band, which never reached the merged row before, keeping the taller stacked layout on the
|
|
385
|
+
* majority of dashboard cards (wave 8, `docs/waves/PLAN.md`).
|
|
386
|
+
*/
|
|
387
|
+
@container basalt-card (min-width: 480px) {
|
|
388
|
+
.root {
|
|
389
|
+
flex-direction: row;
|
|
390
|
+
flex-wrap: wrap;
|
|
391
|
+
align-items: center;
|
|
392
|
+
column-gap: var(--vx-space-control-gap, 0.375rem);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.titleRow {
|
|
396
|
+
display: contents;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.root[data-tier='section'] .heading {
|
|
400
|
+
min-height: var(--vx-space-section-header-height, 2.25rem);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.root[data-tier='widget'] .heading,
|
|
404
|
+
.root[data-tier='group'] .heading {
|
|
405
|
+
min-height: var(--vx-space-widget-header-height, 1.75rem);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.metrics {
|
|
409
|
+
order: 1;
|
|
410
|
+
margin-block: 0;
|
|
411
|
+
margin-inline-start: auto;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.actions {
|
|
415
|
+
order: 2;
|
|
398
416
|
}
|
|
399
417
|
|
|
400
|
-
.
|
|
401
|
-
|
|
418
|
+
.root:has(.metrics) .actions {
|
|
419
|
+
margin-inline-start: 0;
|
|
402
420
|
}
|
|
403
421
|
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
left: 50%;
|
|
409
|
-
translate: -50% -50%;
|
|
410
|
-
width: 100%;
|
|
411
|
-
height: 100%;
|
|
412
|
-
min-width: var(--vx-space-touch-control-height, 2.25rem);
|
|
413
|
-
min-height: var(--vx-space-touch-control-height, 2.25rem);
|
|
422
|
+
.subtitle,
|
|
423
|
+
.sparkline {
|
|
424
|
+
order: 3;
|
|
425
|
+
flex: 0 0 100%;
|
|
414
426
|
}
|
|
415
427
|
}
|
|
@@ -17,6 +17,8 @@ declare const classes: {
|
|
|
17
17
|
readonly sparkline: string
|
|
18
18
|
readonly info: string
|
|
19
19
|
readonly infoTrigger: string
|
|
20
|
+
readonly foldGlyph: string
|
|
21
|
+
readonly foldNote: string
|
|
20
22
|
readonly infoBubble: string
|
|
21
23
|
readonly deltaBadge: string
|
|
22
24
|
readonly deltaGlyph: string
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* 88%/550 title treatment. `tier: 'group'` also renders an `<h3>`, but at the quietest rank on the
|
|
10
10
|
* page: a mono, uppercase, faint micro-label — an inspector/aside group heading, one step below a
|
|
11
11
|
* `widget` tier and resolved automatically by `Section` when it renders on the `PageAside` panel
|
|
12
|
-
* surface (never a call-site prop). The title row holds title/icon/info/count/actions
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* surface (never a call-site prop). The title row holds title/icon/info/count/actions; `value` +
|
|
13
|
+
* `delta` render on their own hero-metric row beneath it, and move onto the title row when the
|
|
14
|
+
* enclosing card (`container: basalt-card`) is 480px or wider — CSS only (wave 8: was 800px). The
|
|
15
|
+
* subtitle also folds into the info glyph whenever the card's own height is under 280px, regardless
|
|
16
|
+
* of width. `StatCard`, `ChartCard`, `Section`, `SettingsSection`/`DangerZone` and
|
|
15
17
|
* `BasaltDataTable` each compose this and render nothing else above their body (wave 3,
|
|
16
18
|
* docs/CONTROLS-SPEC.md §2.2's composer table).
|
|
17
19
|
*
|
|
@@ -62,7 +64,9 @@ export type WidgetHeaderProps = BasaltProps &
|
|
|
62
64
|
title: string
|
|
63
65
|
/** Optional leading icon, rendered before the title. Decorative — hidden from assistive tech. */
|
|
64
66
|
icon?: ReactNode
|
|
65
|
-
/** Optional muted line rendered below the title row.
|
|
67
|
+
/** Optional muted line rendered below the title row. In a card narrower than 480px, or shorter
|
|
68
|
+
* than 280px regardless of width, it folds into the info glyph's bubble instead (CSS only —
|
|
69
|
+
* exactly one of the two shows). */
|
|
66
70
|
subtitle?: string
|
|
67
71
|
/**
|
|
68
72
|
* Renders an info glyph BESIDE the heading (never inside it — this text would otherwise become
|
|
@@ -72,8 +76,8 @@ export type WidgetHeaderProps = BasaltProps &
|
|
|
72
76
|
* click — so a keyboard user gets it too. Mantine-free: a plain state bubble, not a Tooltip.
|
|
73
77
|
*/
|
|
74
78
|
info?: string
|
|
75
|
-
/** Pre-formatted metric value (mono, hero size). Renders on its own row with
|
|
76
|
-
* under the title row
|
|
79
|
+
/** Pre-formatted metric value (mono, hero size, never truncated). Renders on its own row with
|
|
80
|
+
* `delta` under the title row, or on the title row in a card 480px or wider. */
|
|
77
81
|
value?: string
|
|
78
82
|
/**
|
|
79
83
|
* The value's unit, rendered immediately after it on the hero row: muted, mono, `--vx-text-sm`.
|
|
@@ -170,7 +174,22 @@ export type WidgetHeaderDeltaProps = Pick<
|
|
|
170
174
|
* composes it so an inspector row's `hint` is the same glyph, bubble and keyboard behaviour a
|
|
171
175
|
* `WidgetHeader`'s `info` is — not a second one that drifts.
|
|
172
176
|
*/
|
|
173
|
-
export function InfoGlyph({
|
|
177
|
+
export function InfoGlyph({
|
|
178
|
+
text,
|
|
179
|
+
note,
|
|
180
|
+
ariaLabel = 'More information',
|
|
181
|
+
className,
|
|
182
|
+
}: {
|
|
183
|
+
text: string
|
|
184
|
+
/** A second paragraph in the bubble, shown only where `.foldNote` is (the folded subtitle). */
|
|
185
|
+
note?: string
|
|
186
|
+
/** The GLYPH's own accessible name. Default `'More information'`; `WidgetHeader` overrides it to
|
|
187
|
+
* include the subtitle text for its fold-glyph mount (`info === undefined`), where this trigger
|
|
188
|
+
* IS the subtitle's only accessible surface — the bubble it opens is otherwise unreachable
|
|
189
|
+
* without first knowing there is something to open. */
|
|
190
|
+
ariaLabel?: string
|
|
191
|
+
className?: string
|
|
192
|
+
}) {
|
|
174
193
|
const [open, setOpen] = useState(false)
|
|
175
194
|
const tipId = useId()
|
|
176
195
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
@@ -186,14 +205,15 @@ export function InfoGlyph({ text }: { text: string }) {
|
|
|
186
205
|
}, [open])
|
|
187
206
|
|
|
188
207
|
return (
|
|
189
|
-
<span className={classes.info}>
|
|
208
|
+
<span className={cx(classes.info, className)}>
|
|
190
209
|
<button
|
|
191
210
|
ref={triggerRef}
|
|
192
211
|
type="button"
|
|
212
|
+
data-basalt-hit
|
|
193
213
|
className={classes.infoTrigger}
|
|
194
214
|
// The GLYPH is named, not the text — `text` reaches AT through `aria-describedby` below, so
|
|
195
215
|
// it never lands in the heading's or the button's accessible name.
|
|
196
|
-
aria-label=
|
|
216
|
+
aria-label={ariaLabel}
|
|
197
217
|
aria-describedby={open ? tipId : undefined}
|
|
198
218
|
data-open={open}
|
|
199
219
|
onMouseEnter={() => setOpen(true)}
|
|
@@ -212,6 +232,7 @@ export function InfoGlyph({ text }: { text: string }) {
|
|
|
212
232
|
{open && (
|
|
213
233
|
<span id={tipId} role="tooltip" className={classes.infoBubble}>
|
|
214
234
|
{text}
|
|
235
|
+
{note !== undefined && <span className={classes.foldNote}>{note}</span>}
|
|
215
236
|
</span>
|
|
216
237
|
)}
|
|
217
238
|
</span>
|
|
@@ -258,9 +279,22 @@ export function WidgetHeader({
|
|
|
258
279
|
<span className={classes.titleText}>{title}</span>
|
|
259
280
|
</Heading>
|
|
260
281
|
{/* OUTSIDE the heading on purpose — see `info`'s own doc. */}
|
|
261
|
-
{info !== undefined &&
|
|
282
|
+
{info !== undefined && (
|
|
283
|
+
<InfoGlyph text={info} {...(subtitle !== undefined && { note: subtitle })} />
|
|
284
|
+
)}
|
|
285
|
+
{info === undefined && subtitle !== undefined && (
|
|
286
|
+
<InfoGlyph
|
|
287
|
+
text={subtitle}
|
|
288
|
+
className={classes.foldGlyph}
|
|
289
|
+
ariaLabel={`More information: ${subtitle}`}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
262
292
|
{count !== undefined && <span className={classes.count}>{count}</span>}
|
|
263
|
-
{actions !== undefined &&
|
|
293
|
+
{actions !== undefined && (
|
|
294
|
+
<span className={classes.actions} data-basalt-hit-scope>
|
|
295
|
+
{actions}
|
|
296
|
+
</span>
|
|
297
|
+
)}
|
|
264
298
|
</div>
|
|
265
299
|
{(value !== undefined || delta !== undefined) && (
|
|
266
300
|
<div className={cx(classes.metrics, classNames?.metric)}>
|