basalt-ui 1.30.2 → 1.32.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 +54 -0
- package/MIGRATING.md +256 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +5 -5
- package/configs/oxlint-plugin.js +371 -4
- package/configs/oxlint-plugin.test.ts +327 -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 +47 -82
- 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 +138 -145
- 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 +68 -111
- package/dist/charts/kinds/band-chrome.d.ts +31 -0
- package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
- package/dist/charts/kinds/band-chrome.js +71 -0
- 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 +264 -145
- 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 +56 -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 +116 -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 +86 -73
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +20 -4
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +54 -27
- 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 +33 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +18 -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 +75 -34
- 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/primitives/legend-disclosure.d.ts +27 -0
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
- package/dist/charts/primitives/legend-disclosure.js +153 -0
- 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 +13 -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 +2 -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 +33 -66
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-grid.module.css +11 -38
- 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-state.d.ts +83 -0
- package/dist/data/data-table-state.d.ts.map +1 -0
- package/dist/data/data-table-state.js +139 -0
- package/dist/data/data-table.d.ts +56 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +436 -336
- 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 +20 -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 +68 -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/notifications/center.d.ts.map +1 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +30 -8
- 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/aside-docking.d.ts +31 -0
- package/dist/shell/aside-docking.d.ts.map +1 -0
- package/dist/shell/aside-docking.js +19 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +56 -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 +37 -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/theme-lab/derive-controls.d.ts.map +1 -1
- 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 +18 -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 +25 -61
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +21 -26
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +23 -65
- package/src/charts/kinds/band-chrome.tsx +91 -0
- 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 -111
- package/src/charts/primitives/ChartCard.tsx +119 -48
- package/src/charts/primitives/ChartFrame.tsx +204 -60
- package/src/charts/primitives/ChartLegend.tsx +115 -78
- package/src/charts/primitives/ChartTooltip.tsx +24 -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 +62 -20
- package/src/charts/primitives/chart-card-context.ts +46 -0
- package/src/charts/primitives/chart-frame-layout.ts +207 -182
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/primitives/legend-disclosure.tsx +177 -0
- 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 +13 -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 +2 -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 +33 -66
- package/src/dashboard/widget-grid.module.css +11 -38
- 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-state.ts +218 -0
- 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 +547 -299
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +20 -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 +188 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/notifications/center.tsx +3 -0
- package/src/provider/index.tsx +60 -7
- 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/aside-docking.ts +54 -0
- package/src/shell/index.tsx +178 -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 +37 -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/theme-lab/derive-controls.tsx +2 -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
|
@@ -12,15 +12,43 @@
|
|
|
12
12
|
* The header renders only when at least one of title/info/value/actions/icon/count is set — ending
|
|
13
13
|
* the `''`-as-hidden-header sentinel a consumer used to reach for otherwise.
|
|
14
14
|
*/
|
|
15
|
+
import { useMemo, useReducer, useState } from 'react'
|
|
15
16
|
import type { CSSProperties, ReactNode } from 'react'
|
|
16
17
|
import { cx } from '../../common/props'
|
|
17
18
|
import type { BasaltProps, SlotStylesProps } from '../../common/props'
|
|
18
19
|
import { VX } from '../../tokens'
|
|
19
20
|
import { WidgetHeader } from '../../dashboard/widget-header'
|
|
20
21
|
import type { DeltaPolarity } from '../../dashboard/delta-badge'
|
|
22
|
+
import { useChartSize } from '../hooks/useChartSize'
|
|
23
|
+
import { useHysteresis } from '../hooks/use-hysteresis'
|
|
24
|
+
import { ChartCardContext, legendSlotReducer } from './chart-card-context'
|
|
21
25
|
import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
|
|
22
26
|
import type { ChartState } from './ChartPending'
|
|
23
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Below this rendered height the header folds its subtitle regardless of width (wave 8's
|
|
30
|
+
* header-economy law, `docs/CHARTS-SPEC.md` §5) — a short-but-wide card (a KPI-style
|
|
31
|
+
* chart card) still needs the vertical space back. A `@container basalt-card` SIZE query would be
|
|
32
|
+
* the natural expression, but `container-type: size` on a box with no explicit height (the common,
|
|
33
|
+
* non-grid-stretched case) collapses it to 0 — CSS containment requires a definite size in the
|
|
34
|
+
* contained axis, and a ChartCard's height is ordinarily derived from its content (VERIFIED: making
|
|
35
|
+
* the root a size container drops `card-chrome.layout.test.ts`'s unstretched short-card fixture to
|
|
36
|
+
* a 0px box). `useChartSize` (the same measuring primitive `ChartFrame` uses) reads the rendered
|
|
37
|
+
* height instead, and `data-basalt-card-short` is the CSS hook `widget-header.module.css` reacts to
|
|
38
|
+
* with the same subtitle-fold declarations as the width query.
|
|
39
|
+
*/
|
|
40
|
+
const CARD_SHORT_HEIGHT = 280
|
|
41
|
+
/**
|
|
42
|
+
* Hysteresis band on the flag above (`docs/waves/PLAN.md` wave 3, P0-1). `short` folds the header,
|
|
43
|
+
* which changes the card's OWN measured height by the fold delta — a card whose unfolded height
|
|
44
|
+
* lands in `[280, 280 + Δ)` is bistable, and the async RO → rAF → debounce → setState → commit path
|
|
45
|
+
* can sit one reading behind, which turned the bistability into a perpetual flip-flop on the
|
|
46
|
+
* "Sales by channel" dashboard card (27-52 times per 2s, every viewport). Entering short still
|
|
47
|
+
* happens below `CARD_SHORT_HEIGHT`; LEAVING it needs `CARD_SHORT_HEIGHT + SHORT_EXIT_SLACK`, sized
|
|
48
|
+
* to clear the largest fold delta (subtitle line + inset + value row) with headroom.
|
|
49
|
+
*/
|
|
50
|
+
const SHORT_EXIT_SLACK = 24
|
|
51
|
+
|
|
24
52
|
// Surfaces resolve per theme via CSS vars, so the styles are static (no useMemo/isDark).
|
|
25
53
|
// Depth = `shadow-card` (a whisper shadow + a 1px ring baked into the same value), never a
|
|
26
54
|
// `border` property (docs/DESIGN-SPEC.md §5) — so a ChartCard reads IDENTICALLY to a Mantine
|
|
@@ -31,7 +59,16 @@ import type { ChartState } from './ChartPending'
|
|
|
31
59
|
// `WidgetHeader`'s info bubble (`widget-header.module.css`'s `.infoBubble`, which is where the
|
|
32
60
|
// tooltip this file used to own now lives) opens downward past the header's own box, and must be
|
|
33
61
|
// able to overhang the card edge without being invisibly cut off.
|
|
62
|
+
//
|
|
63
|
+
// The root is a flex column and a `basalt-card` inline-size container (the card's own width drives
|
|
64
|
+
// its header chrome, `docs/CHARTS-SPEC.md` §8): the body takes the row's spare height when a grid stretches
|
|
65
|
+
// the card.
|
|
34
66
|
const cardStyle: CSSProperties = {
|
|
67
|
+
display: 'flex',
|
|
68
|
+
flexDirection: 'column',
|
|
69
|
+
minHeight: 0,
|
|
70
|
+
containerType: 'inline-size',
|
|
71
|
+
containerName: 'basalt-card',
|
|
35
72
|
borderRadius: VX.radiusCard,
|
|
36
73
|
boxShadow: VX.shadowCard,
|
|
37
74
|
backgroundColor: VX.surface.panel,
|
|
@@ -45,6 +82,11 @@ const bodyClipStyle: CSSProperties = {
|
|
|
45
82
|
borderBottomLeftRadius: VX.radiusCard,
|
|
46
83
|
borderBottomRightRadius: VX.radiusCard,
|
|
47
84
|
overflow: 'hidden',
|
|
85
|
+
flex: '1 1 auto',
|
|
86
|
+
minHeight: 0,
|
|
87
|
+
// A column, so a `ChartFrame` inside can grow into the body a stretched grid row hands the card.
|
|
88
|
+
display: 'flex',
|
|
89
|
+
flexDirection: 'column',
|
|
48
90
|
padding: '2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)',
|
|
49
91
|
}
|
|
50
92
|
// Card inset = the `cardInsetY`/`cardInsetX` role tokens (vertical / horizontal), the SAME two
|
|
@@ -60,6 +102,13 @@ const bodyClipStyle: CSSProperties = {
|
|
|
60
102
|
const headerWrapStyle: CSSProperties = {
|
|
61
103
|
padding: 'var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px',
|
|
62
104
|
}
|
|
105
|
+
// A short card (wave 8) trims the header's own top inset — the subtitle line and the wide-tier
|
|
106
|
+
// value/delta row are both already gone by the time `short` is true, so the full card inset reads
|
|
107
|
+
// as dead air rather than rhythm. Bottom stays 4px (unchanged) so the header-to-body seam matches
|
|
108
|
+
// every other ChartCard.
|
|
109
|
+
const headerWrapShortStyle: CSSProperties = {
|
|
110
|
+
padding: '4px var(--vx-space-card-inset-x, 0.8125rem) 4px',
|
|
111
|
+
}
|
|
63
112
|
|
|
64
113
|
/**
|
|
65
114
|
* The three boxes a `ChartCard` paints, and therefore the slot set a consumer may class
|
|
@@ -143,6 +192,8 @@ export function ChartCard({
|
|
|
143
192
|
style,
|
|
144
193
|
children,
|
|
145
194
|
}: ChartCardProps) {
|
|
195
|
+
const [legendSlot, setLegendSlot] = useState<HTMLDivElement | null>(null)
|
|
196
|
+
const { ref: sizeRef, height: measuredHeight } = useChartSize()
|
|
146
197
|
const resolvedState = resolveChartState({ ...(state !== undefined && { state }) })
|
|
147
198
|
const hasHeader =
|
|
148
199
|
title !== undefined ||
|
|
@@ -151,59 +202,79 @@ export function ChartCard({
|
|
|
151
202
|
actions !== undefined ||
|
|
152
203
|
icon !== undefined ||
|
|
153
204
|
count !== undefined
|
|
205
|
+
// Hysteresis (P0-1, `SHORT_EXIT_SLACK`'s own doc): which threshold applies depends on the
|
|
206
|
+
// PREVIOUS committed flag, so a card whose unfolded height sits in the bistable band settles
|
|
207
|
+
// instead of flip-flopping forever.
|
|
208
|
+
const short = useHysteresis(
|
|
209
|
+
(wasShort) =>
|
|
210
|
+
measuredHeight > 0 &&
|
|
211
|
+
measuredHeight < (wasShort ? CARD_SHORT_HEIGHT + SHORT_EXIT_SLACK : CARD_SHORT_HEIGHT),
|
|
212
|
+
false,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
const [legendClaimants, dispatchLegendSlot] = useReducer(legendSlotReducer, [])
|
|
216
|
+
const context = useMemo(
|
|
217
|
+
() => ({ legendSlot, inCard: true, short, legendClaimants, dispatchLegendSlot }),
|
|
218
|
+
[legendSlot, short, legendClaimants],
|
|
219
|
+
)
|
|
154
220
|
|
|
155
221
|
return (
|
|
156
|
-
<
|
|
157
|
-
className={cx(classNames?.root, className)}
|
|
158
|
-
style={{ ...cardStyle, ...style }}
|
|
159
|
-
{...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
|
|
160
|
-
>
|
|
161
|
-
{hasHeader && (
|
|
162
|
-
<div
|
|
163
|
-
{...(classNames?.header !== undefined && { className: classNames.header })}
|
|
164
|
-
style={headerWrapStyle}
|
|
165
|
-
>
|
|
166
|
-
<WidgetHeader
|
|
167
|
-
tier="widget"
|
|
168
|
-
title={title ?? ''}
|
|
169
|
-
{...(icon !== undefined && { icon })}
|
|
170
|
-
{...(subtitle !== undefined && { subtitle })}
|
|
171
|
-
{...(info !== undefined && { info })}
|
|
172
|
-
{...(value !== undefined && { value })}
|
|
173
|
-
{...(delta !== undefined && { delta })}
|
|
174
|
-
{...(deltaPeriod !== undefined && { deltaPeriod })}
|
|
175
|
-
{...(deltaPolarity !== undefined && { deltaPolarity })}
|
|
176
|
-
{...(count !== undefined && { count })}
|
|
177
|
-
{...(actions !== undefined && {
|
|
178
|
-
actions: <span data-basalt-tier="widget">{actions}</span>,
|
|
179
|
-
})}
|
|
180
|
-
/>
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
222
|
+
<ChartCardContext.Provider value={context}>
|
|
183
223
|
<div
|
|
184
|
-
{
|
|
185
|
-
|
|
224
|
+
ref={sizeRef}
|
|
225
|
+
className={cx(classNames?.root, className)}
|
|
226
|
+
style={{ ...cardStyle, ...style }}
|
|
227
|
+
{...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
|
|
228
|
+
{...(short && { 'data-basalt-card-short': '' })}
|
|
186
229
|
>
|
|
187
|
-
{
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
{hasHeader && (
|
|
231
|
+
<div
|
|
232
|
+
{...(classNames?.header !== undefined && { className: classNames.header })}
|
|
233
|
+
style={short ? headerWrapShortStyle : headerWrapStyle}
|
|
234
|
+
>
|
|
235
|
+
<WidgetHeader
|
|
236
|
+
tier="widget"
|
|
237
|
+
title={title ?? ''}
|
|
238
|
+
{...(icon !== undefined && { icon })}
|
|
239
|
+
{...(subtitle !== undefined && { subtitle })}
|
|
240
|
+
{...(info !== undefined && { info })}
|
|
241
|
+
{...(value !== undefined && { value })}
|
|
242
|
+
{...(delta !== undefined && { delta })}
|
|
243
|
+
{...(deltaPeriod !== undefined && { deltaPeriod })}
|
|
244
|
+
{...(deltaPolarity !== undefined && { deltaPolarity })}
|
|
245
|
+
{...(count !== undefined && { count })}
|
|
246
|
+
{...(actions !== undefined && {
|
|
247
|
+
actions: <span data-basalt-tier="widget">{actions}</span>,
|
|
248
|
+
})}
|
|
249
|
+
/>
|
|
250
|
+
<div ref={setLegendSlot} data-basalt-legend-slot="" />
|
|
251
|
+
</div>
|
|
205
252
|
)}
|
|
253
|
+
<div
|
|
254
|
+
{...(classNames?.body !== undefined && { className: classNames.body })}
|
|
255
|
+
style={bodyClipStyle}
|
|
256
|
+
>
|
|
257
|
+
{resolvedState === 'pending' ? (
|
|
258
|
+
<ChartPending width="100%" height={placeholderHeight} />
|
|
259
|
+
) : resolvedState === 'error' ? (
|
|
260
|
+
<ChartError
|
|
261
|
+
width="100%"
|
|
262
|
+
height={placeholderHeight}
|
|
263
|
+
error={state?.error}
|
|
264
|
+
action={stateAction}
|
|
265
|
+
/>
|
|
266
|
+
) : resolvedState === 'empty' ? (
|
|
267
|
+
<ChartEmpty
|
|
268
|
+
width="100%"
|
|
269
|
+
height={placeholderHeight}
|
|
270
|
+
{...(typeof state?.empty === 'string' && { label: state.empty })}
|
|
271
|
+
action={stateAction}
|
|
272
|
+
/>
|
|
273
|
+
) : (
|
|
274
|
+
children
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
206
277
|
</div>
|
|
207
|
-
</
|
|
278
|
+
</ChartCardContext.Provider>
|
|
208
279
|
)
|
|
209
280
|
}
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
-
import { useCallback, useState } from 'react'
|
|
2
|
+
import { useCallback, useContext, useId, useLayoutEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
3
4
|
import type { BasaltProps } from '../../common/props'
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import {
|
|
6
|
+
deprecatedProp,
|
|
7
|
+
ignoredProp,
|
|
8
|
+
legendSlotContention,
|
|
9
|
+
plotBelowFloor,
|
|
10
|
+
} from '../../common/errors'
|
|
11
|
+
import { reportOnce, useValidateProps } from '../../common/validate'
|
|
12
|
+
import { SizeClassHintContext, useSizeClass } from '../../shell/use-size-class'
|
|
6
13
|
import { VX } from '../../tokens'
|
|
7
14
|
import { useChartSize } from '../hooks/useChartSize'
|
|
8
15
|
import { deriveLegend } from '../series'
|
|
9
16
|
import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series'
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
resolveLegendRollup,
|
|
14
|
-
resolvePlotRect,
|
|
15
|
-
} from './chart-frame-layout'
|
|
17
|
+
import { ChartCardContext } from './chart-card-context'
|
|
18
|
+
import { resolveChartLayout, resolveFrameClass } from './chart-layout'
|
|
19
|
+
import { resolveFrameHeight, resolveLegendRollup, resolvePlotRect } from './chart-frame-layout'
|
|
16
20
|
import type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
17
|
-
import { ChartTierProvider } from './chart-tier'
|
|
21
|
+
import { ChartTierProvider, useCoarsePointer, useViewportHeight } from './chart-tier'
|
|
18
22
|
import { ChartLegend } from './ChartLegend'
|
|
19
23
|
import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
|
|
20
24
|
import type { ChartState } from './ChartPending'
|
|
21
25
|
|
|
22
26
|
/** Re-exported for `ChartFrame.test.tsx` and any other consumer that previously reached these
|
|
23
27
|
* through `ChartFrame` — the layout math itself lives in `./chart-frame-layout` (pure, DOM-free). */
|
|
24
|
-
export {
|
|
25
|
-
legendEntryCap,
|
|
26
|
-
resolveLegendRollup,
|
|
27
|
-
resolvePlotRect,
|
|
28
|
-
resolveChartTier,
|
|
29
|
-
} from './chart-frame-layout'
|
|
28
|
+
export { legendEntryCap, resolveLegendRollup, resolvePlotRect } from './chart-frame-layout'
|
|
30
29
|
export type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
31
30
|
|
|
32
|
-
const DEFAULT_HEIGHT = 240
|
|
33
31
|
const DEFAULT_MIN_WIDTH = 200
|
|
34
32
|
|
|
35
33
|
/** Legend configuration for {@link ChartFrame}. Omit entirely (or pass `{}`) for the default
|
|
@@ -38,22 +36,12 @@ export type ChartFrameLegend = {
|
|
|
38
36
|
/** Default 'bottom'. */
|
|
39
37
|
placement?: LegendPlacement
|
|
40
38
|
/**
|
|
41
|
-
*
|
|
42
|
-
* into
|
|
39
|
+
* @deprecated Removed in 1.32.0 — the legend now fits by measured width every time and folds
|
|
40
|
+
* overflow into an `All N` disclosure (`docs/CHARTS-SPEC.md` §8); drop the prop.
|
|
43
41
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* (`entries.slice(0, cap)`); nothing here measures rows.
|
|
48
|
-
*
|
|
49
|
-
* An explicit value WINS OUTRIGHT — over the phone tier's two-row default (`chartTierMetrics`)
|
|
50
|
-
* AND over a `fill` frame's measured fit (`legendEntryCap`). 1.30.0 fixed only the first half:
|
|
51
|
-
* the fit still ran `Math.min(fitted, caller)` on top, so meteo's 7-entry meteogram legend in a
|
|
52
|
-
* 150px docked row rendered 2 entries at `maxRows` 3, 6 AND 99 — five series drawn in colours
|
|
53
|
-
* nothing named. Both losers are DEFAULTS (the tier's, and `VX.minPlotHeight`); this is the one
|
|
54
|
-
* number a caller stated, so under `fill` the PLOT yields the height instead
|
|
55
|
-
* ({@link resolvePlotRect}'s `legendWins`). State a number too large for the box and the plot
|
|
56
|
-
* gets what is left — visible, and yours.
|
|
42
|
+
* Until then an explicit value still WINS OUTRIGHT over the measured fit, and it counts ENTRIES,
|
|
43
|
+
* not rows: `maxRows: 3` on a 7-entry legend renders three entries. Under `fill` the PLOT yields
|
|
44
|
+
* the height ({@link resolvePlotRect}'s `legendWins`).
|
|
57
45
|
*/
|
|
58
46
|
maxRows?: number
|
|
59
47
|
/** Visually separate role: series | overlay | reference. */
|
|
@@ -68,20 +56,32 @@ export type ChartFrameLegend = {
|
|
|
68
56
|
* plot, the tooltip, and the auto domain together (`docs/CHARTS-SPEC.md` §5).
|
|
69
57
|
*/
|
|
70
58
|
toggle?: boolean
|
|
59
|
+
/**
|
|
60
|
+
* Internal escape hatch for a kind that never wants its legend portalled into a `ChartCard`
|
|
61
|
+
* header, whatever the placement — `Donut` sets this (`docs/waves/PLAN.md` wave 3, P1-4): its
|
|
62
|
+
* side/bottom legend is the ring's only key, so it must stay under or beside the ring rather than
|
|
63
|
+
* follow every other frame's default of claiming an available header slot. `false` keeps the
|
|
64
|
+
* legend in its own band regardless of `placement`; omitted (default) behaves as today. Not
|
|
65
|
+
* something a consumer composing a kind's own `legend` prop can reach — kinds compose `ChartFrame`
|
|
66
|
+
* directly to set it.
|
|
67
|
+
*/
|
|
68
|
+
headerSlot?: boolean
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
export type ChartFrameProps = BasaltProps & {
|
|
74
72
|
/** Series identity — drives the derived legend. Pass the SAME array the kind draws + tooltips from. */
|
|
75
73
|
series: readonly SeriesStyle[]
|
|
76
74
|
/**
|
|
77
|
-
* Height in pixels, or one per
|
|
78
|
-
* Default
|
|
75
|
+
* Height in pixels, or one per container step. Used when neither `aspectRatio` nor `fill` is set.
|
|
76
|
+
* Default: derived from the measured width and container class (`resolveChartLayout`). Inside a
|
|
77
|
+
* `ChartCard` the resulting height is a MINIMUM — the frame grows into a taller card body. Passing it WITH `fill` is a wiring mistake, not a fallback — `fill` wins and
|
|
79
78
|
* this is never read, which now warns once in dev (`common/errors.ts`' `ignoredProp`).
|
|
80
79
|
*
|
|
81
|
-
* `{ base: 180,
|
|
82
|
-
* steps are compared against the frame's own MEASURED width, not the viewport,
|
|
83
|
-
* by a `PageAside` gets the short height on a desktop too. A plain number is
|
|
84
|
-
* supported ({@link resolveFrameHeight})
|
|
80
|
+
* `{ base: 180, wide: 260 }` is the declarative escape from a page of fixed-height charts: the
|
|
81
|
+
* `regular`/`wide` steps are compared against the frame's own MEASURED width, not the viewport,
|
|
82
|
+
* so a chart squeezed by a `PageAside` gets the short height on a desktop too. A plain number is
|
|
83
|
+
* an override and stays supported ({@link resolveFrameHeight}); one that leaves the plot under
|
|
84
|
+
* `VX.minPlotHeight` warns once in dev.
|
|
85
85
|
*/
|
|
86
86
|
height?: number | ResponsiveChartHeight
|
|
87
87
|
/** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set — which warns
|
|
@@ -136,6 +136,10 @@ export type PlotRect = {
|
|
|
136
136
|
width: number
|
|
137
137
|
height: number
|
|
138
138
|
hidden: ReadonlySet<string>
|
|
139
|
+
/** Whether THIS frame's legend is currently portalled into a `ChartCard` header slot — a chart
|
|
140
|
+
* composing `ChartFrame` (e.g. `CartesianChart`'s end labels, R2C-10) reads this to skip a
|
|
141
|
+
* band-legend-only affordance once the header already names every series for free. */
|
|
142
|
+
legendInHeader: boolean
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
const outerStyle = (fill: boolean, vertical: boolean): CSSProperties => ({
|
|
@@ -219,6 +223,66 @@ export function ChartFrame({
|
|
|
219
223
|
}: ChartFrameProps): ReactNode {
|
|
220
224
|
const { ref: containerRef, width: containerW, height: containerH } = useChartSize()
|
|
221
225
|
const { ref: legendRef, width: legendW, height: legendH } = useChartSize()
|
|
226
|
+
const { ref: slotRef, width: slotMeasuredW } = useChartSize()
|
|
227
|
+
const {
|
|
228
|
+
inCard,
|
|
229
|
+
legendSlot,
|
|
230
|
+
short: cardShort,
|
|
231
|
+
legendClaimants,
|
|
232
|
+
dispatchLegendSlot,
|
|
233
|
+
} = useContext(ChartCardContext)
|
|
234
|
+
const frameId = useId()
|
|
235
|
+
const [slotSeedW, setSlotSeedW] = useState(0)
|
|
236
|
+
const placement = legend === false ? 'bottom' : (legend.placement ?? 'bottom')
|
|
237
|
+
const vertical = placement === 'left' || placement === 'right'
|
|
238
|
+
// `Donut` sets `headerSlot: false` (P1-4) — it never wants the slot, so it must never CLAIM it
|
|
239
|
+
// either, or it would silently deny a sibling frame in the same card that does want it. A
|
|
240
|
+
// vertical (left/right) legend never portals into the header either (`inHeader` below requires
|
|
241
|
+
// `!vertical`), so it must not claim the slot from a sibling that could actually use it.
|
|
242
|
+
const wantsHeaderSlot =
|
|
243
|
+
legendSlot !== null && legend !== false && legend.headerSlot !== false && !vertical
|
|
244
|
+
// Every non-null state replaces the plot, and all three suppress the legend for one reason: a
|
|
245
|
+
// legend naming a series with nothing to point at is its own small lie.
|
|
246
|
+
const resolvedState = resolveChartState({ ...(state !== undefined && { state }), isPending })
|
|
247
|
+
const legendVisible = legend !== false && resolvedState === null
|
|
248
|
+
// Ownership is DERIVED from the card's claimant queue (`legendSlotReducer`), never mirrored here:
|
|
249
|
+
// the first frame to claim owns the slot, a later one is denied until everything ahead of it
|
|
250
|
+
// releases, and each frame's only job is to claim while it wants the slot and release on the way
|
|
251
|
+
// out (P2-9).
|
|
252
|
+
//
|
|
253
|
+
// Denied means THIS frame's own claim is in the queue and is not its head — never "someone else
|
|
254
|
+
// owns it" read before this frame has claimed. A new frame's first render can still see an
|
|
255
|
+
// owner that is unmounting in the same commit (a tab switch, a different `useId`); that is a
|
|
256
|
+
// hand-off, and warning off that stale head reported contention for a frame about to own it.
|
|
257
|
+
const ownsSlot = wantsHeaderSlot && legendClaimants[0] === frameId
|
|
258
|
+
const slotDenied = wantsHeaderSlot && !ownsSlot && legendClaimants.includes(frameId)
|
|
259
|
+
useLayoutEffect(() => {
|
|
260
|
+
if (!wantsHeaderSlot) return undefined
|
|
261
|
+
dispatchLegendSlot({ type: 'claim', id: frameId })
|
|
262
|
+
return () => dispatchLegendSlot({ type: 'release', id: frameId })
|
|
263
|
+
}, [wantsHeaderSlot, frameId, dispatchLegendSlot])
|
|
264
|
+
useLayoutEffect(() => {
|
|
265
|
+
if (slotDenied && legendVisible) reportOnce('ChartFrame', legendSlotContention('ChartFrame'))
|
|
266
|
+
}, [slotDenied, legendVisible])
|
|
267
|
+
// Only the owner observes the slot — its width is what a header legend fits. Seeded synchronously
|
|
268
|
+
// (P2-8) so the first paint already knows the slot's real width, instead of assuming 0 until the
|
|
269
|
+
// ResizeObserver's first (asynchronous) callback — which is what let a header legend paint once in
|
|
270
|
+
// the band before jumping to the header on every mount.
|
|
271
|
+
useLayoutEffect(() => {
|
|
272
|
+
if (!ownsSlot || legendSlot === null) return undefined
|
|
273
|
+
setSlotSeedW(legendSlot.getBoundingClientRect().width)
|
|
274
|
+
slotRef(legendSlot)
|
|
275
|
+
return () => {
|
|
276
|
+
slotRef(null)
|
|
277
|
+
setSlotSeedW(0)
|
|
278
|
+
}
|
|
279
|
+
}, [ownsSlot, legendSlot, slotRef])
|
|
280
|
+
const viewportClass = useSizeClass()
|
|
281
|
+
// No `BasaltProvider` (a charts-only consumer) means no viewport hint: the unmeasured first frame
|
|
282
|
+
// resolves to the regular (desktop) class, not phone chrome.
|
|
283
|
+
const sizeClass = useContext(SizeClassHintContext) === undefined ? 'medium' : viewportClass
|
|
284
|
+
const coarse = useCoarsePointer()
|
|
285
|
+
const viewportH = useViewportHeight()
|
|
222
286
|
const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set())
|
|
223
287
|
|
|
224
288
|
const toggleKey = useCallback((key: string) => {
|
|
@@ -255,25 +319,90 @@ export function ChartFrame({
|
|
|
255
319
|
],
|
|
256
320
|
[fill, height, aspectRatio],
|
|
257
321
|
)
|
|
322
|
+
const legendItems = legend === false ? [] : deriveLegend(series)
|
|
323
|
+
useValidateProps(
|
|
324
|
+
'ChartFrame',
|
|
325
|
+
() =>
|
|
326
|
+
// A cap covering every entry rolls nothing up — it is how `Donut` states "never roll up".
|
|
327
|
+
legend !== false && legend.maxRows !== undefined && legend.maxRows < legendItems.length
|
|
328
|
+
? deprecatedProp(
|
|
329
|
+
'ChartFrame',
|
|
330
|
+
'legend.maxRows',
|
|
331
|
+
'nothing (the legend fits by measured width)',
|
|
332
|
+
'1.32.0',
|
|
333
|
+
`Its UNIT changed from rows to entries: \`maxRows: ${legend.maxRows}\` is now read as a cap of ${legend.maxRows} legend ENTRIES (not rows), with the rest folded into an \`All N\` disclosure. An explicit value still wins this release.`,
|
|
334
|
+
)
|
|
335
|
+
: null,
|
|
336
|
+
[legend, legendItems.length],
|
|
337
|
+
)
|
|
338
|
+
const statedHeight =
|
|
339
|
+
height === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: resolveFrameHeight(height, resolveFrameClass({ frameW: containerW, sizeClass }))
|
|
342
|
+
// Memoized on scalars (the legend by its keys+labels) so `ChartTierProvider`'s value is stable
|
|
343
|
+
// across renders that change nothing. No slot, not the owner, or not measured yet → `slotW` 0 →
|
|
344
|
+
// a band. `slotSeedW` (P2-8) covers the gap before the ResizeObserver's own first callback.
|
|
345
|
+
const slotW = ownsSlot ? Math.max(slotMeasuredW, slotSeedW) : 0
|
|
346
|
+
const groups = legend !== false && legend.groups === true
|
|
347
|
+
// Role and note both feed the resolver (`orderEntries`'s grouping, `legendEntryWidth`'s measured
|
|
348
|
+
// fit), so a change to either — a series moving `role`, a note appearing/disappearing — has to
|
|
349
|
+
// invalidate this memo exactly like a key or label change does; keying on key+label alone missed
|
|
350
|
+
// both and served a stale layout until something else happened to change first.
|
|
351
|
+
const legendKey = legendItems
|
|
352
|
+
.map((item) => `${item.key}\u0000${item.label}\u0000${item.role ?? ''}\u0000${item.note ?? ''}`)
|
|
353
|
+
.join('\u0001')
|
|
354
|
+
const layout = useMemo(
|
|
355
|
+
() =>
|
|
356
|
+
resolveChartLayout({
|
|
357
|
+
frameW: containerW,
|
|
358
|
+
slotW,
|
|
359
|
+
viewportH,
|
|
360
|
+
legendItems,
|
|
361
|
+
sizeClass,
|
|
362
|
+
coarse,
|
|
363
|
+
cardShort,
|
|
364
|
+
groups,
|
|
365
|
+
...(statedHeight !== undefined && { override: { height: statedHeight } }),
|
|
366
|
+
}),
|
|
367
|
+
// `legendItems` is a fresh array per render; `legendKey` is its identity.
|
|
368
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
369
|
+
[containerW, slotW, viewportH, legendKey, sizeClass, coarse, cardShort, groups, statedHeight],
|
|
370
|
+
)
|
|
258
371
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
372
|
+
// The resolver gates every placement: none at micro, else the card's header slot when there is
|
|
373
|
+
// one AND this frame owns it (P2-9) — `slotW` is already 0 for a non-owner or a `headerSlot:
|
|
374
|
+
// false` kind (P1-4), so `fit.where` already reads 'band' for both; `ownsSlot` here is only the
|
|
375
|
+
// (harmless) belt to that suspenders.
|
|
376
|
+
const fit = layout.legend.mode === 'dots' || layout.legend.mode === 'chips' ? layout.legend : null
|
|
377
|
+
const legendMode = fit?.mode
|
|
378
|
+
const showLegend = legendVisible && fit !== null
|
|
379
|
+
const inHeader = showLegend && ownsSlot && fit?.where === 'header'
|
|
380
|
+
const sideLegendWidth = showLegend && vertical ? legendW : 0
|
|
381
|
+
const topBottomLegendHeight = showLegend && !vertical && !inHeader ? legendH : 0
|
|
266
382
|
|
|
383
|
+
const computedHeight =
|
|
384
|
+
aspectRatio !== undefined ? Math.round(containerW / aspectRatio) : layout.height
|
|
385
|
+
// In a card the computed height is a floor: a stretched grid row hands the body more, and the
|
|
386
|
+
// frame (flex 1 in the body) takes it. Never below what the legend band plus the plot floor need.
|
|
387
|
+
const grows = inCard && !fill
|
|
267
388
|
const resolvedHeight = fill
|
|
268
389
|
? containerH
|
|
269
|
-
:
|
|
270
|
-
? Math.
|
|
271
|
-
:
|
|
272
|
-
|
|
273
|
-
const sideLegendWidth = legendVisible && vertical ? legendW : 0
|
|
274
|
-
const topBottomLegendHeight = legendVisible && !vertical ? legendH : 0
|
|
390
|
+
: grows
|
|
391
|
+
? Math.max(computedHeight, containerH)
|
|
392
|
+
: computedHeight
|
|
393
|
+
const cardMinHeight = Math.max(computedHeight, VX.minPlotHeight + topBottomLegendHeight)
|
|
275
394
|
|
|
276
|
-
|
|
395
|
+
useValidateProps(
|
|
396
|
+
'ChartFrame',
|
|
397
|
+
() =>
|
|
398
|
+
typeof height === 'number' &&
|
|
399
|
+
!fill &&
|
|
400
|
+
aspectRatio === undefined &&
|
|
401
|
+
height - topBottomLegendHeight < VX.minPlotHeight
|
|
402
|
+
? plotBelowFloor('ChartFrame', height, VX.minPlotHeight)
|
|
403
|
+
: null,
|
|
404
|
+
[height, fill, aspectRatio, topBottomLegendHeight],
|
|
405
|
+
)
|
|
277
406
|
const togglable = legend !== false && (legend.toggle ?? legendItems.length > 1)
|
|
278
407
|
|
|
279
408
|
// The cap, and who pays for it — one pure decision ({@link resolveLegendRollup} states the three
|
|
@@ -283,8 +412,8 @@ export function ChartFrame({
|
|
|
283
412
|
// plot pays for. Top/bottom only: a side legend costs width, not height.
|
|
284
413
|
const { maxRows, legendWins } = resolveLegendRollup({
|
|
285
414
|
...(legend !== false && legend.maxRows !== undefined && { statedMaxRows: legend.maxRows }),
|
|
286
|
-
|
|
287
|
-
fillBand:
|
|
415
|
+
...(!vertical && fit !== null && fit.overflow > 0 && { fittedMaxRows: fit.visible }),
|
|
416
|
+
fillBand: showLegend && fill && !inHeader && !vertical,
|
|
288
417
|
items: legendItems,
|
|
289
418
|
containerW,
|
|
290
419
|
available: resolvedHeight - VX.minPlotHeight,
|
|
@@ -300,7 +429,7 @@ export function ChartFrame({
|
|
|
300
429
|
})
|
|
301
430
|
|
|
302
431
|
const legendNode =
|
|
303
|
-
legend === false || resolvedState !== null ? null : (
|
|
432
|
+
legend === false || resolvedState !== null || !showLegend ? null : (
|
|
304
433
|
<div ref={legendRef} style={legendWrapperStyle(vertical)}>
|
|
305
434
|
<ChartLegend
|
|
306
435
|
items={legendItems}
|
|
@@ -310,6 +439,7 @@ export function ChartFrame({
|
|
|
310
439
|
{...(chartId !== undefined && { chartId })}
|
|
311
440
|
{...(legend.groups !== undefined && { groups: legend.groups })}
|
|
312
441
|
{...(maxRows !== undefined && { maxRows })}
|
|
442
|
+
{...(legendMode !== undefined && { mode: legendMode })}
|
|
313
443
|
{...(legend.highlighted !== undefined && { highlighted: legend.highlighted })}
|
|
314
444
|
{...(legend.onHighlight !== undefined && { onHighlight: legend.onHighlight })}
|
|
315
445
|
/>
|
|
@@ -317,15 +447,26 @@ export function ChartFrame({
|
|
|
317
447
|
)
|
|
318
448
|
|
|
319
449
|
return (
|
|
320
|
-
<ChartTierProvider
|
|
450
|
+
<ChartTierProvider containerClass={layout.containerClass}>
|
|
321
451
|
<div
|
|
322
452
|
ref={containerRef}
|
|
323
453
|
{...(className !== undefined && { className })}
|
|
324
|
-
style={{
|
|
454
|
+
style={{
|
|
455
|
+
...outerStyle(fill, vertical),
|
|
456
|
+
...(grows && { flex: '1 1 0', minHeight: cardMinHeight }),
|
|
457
|
+
...style,
|
|
458
|
+
}}
|
|
325
459
|
{...(ariaLabel !== undefined && { role: 'group', 'aria-label': ariaLabel })}
|
|
326
460
|
{...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
|
|
327
461
|
>
|
|
328
|
-
{
|
|
462
|
+
{inHeader &&
|
|
463
|
+
legendNode !== null &&
|
|
464
|
+
legendSlot !== null &&
|
|
465
|
+
createPortal(legendNode, legendSlot)}
|
|
466
|
+
{!inHeader &&
|
|
467
|
+
legendNode !== null &&
|
|
468
|
+
(placement === 'top' || placement === 'left') &&
|
|
469
|
+
legendNode}
|
|
329
470
|
{plot.width > 0 &&
|
|
330
471
|
plot.height > 0 &&
|
|
331
472
|
(resolvedState === 'pending' ? (
|
|
@@ -339,9 +480,12 @@ export function ChartFrame({
|
|
|
339
480
|
{...(typeof state?.empty === 'string' && { label: state.empty })}
|
|
340
481
|
/>
|
|
341
482
|
) : (
|
|
342
|
-
children({ ...plot, hidden })
|
|
483
|
+
children({ ...plot, hidden, legendInHeader: inHeader })
|
|
343
484
|
))}
|
|
344
|
-
{
|
|
485
|
+
{!inHeader &&
|
|
486
|
+
legendNode !== null &&
|
|
487
|
+
(placement === 'bottom' || placement === 'right') &&
|
|
488
|
+
legendNode}
|
|
345
489
|
</div>
|
|
346
490
|
</ChartTierProvider>
|
|
347
491
|
)
|