basalt-ui 1.30.1 → 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 +38 -0
- package/MIGRATING.md +218 -14
- 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 +31 -17
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +149 -32
- package/dist/charts/primitives/ChartLegend.d.ts +20 -14
- 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 -112
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +83 -31
- 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 +22 -1
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +20 -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 +272 -53
- package/src/charts/primitives/ChartLegend.tsx +258 -72
- 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 +227 -156
- 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 +39 -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,33 +1,33 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
-
import { useCallback, useState } from 'react'
|
|
2
|
+
import { useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
3
4
|
import type { BasaltProps } from '../../common/props'
|
|
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'
|
|
4
13
|
import { VX } from '../../tokens'
|
|
5
14
|
import { useChartSize } from '../hooks/useChartSize'
|
|
6
15
|
import { deriveLegend } from '../series'
|
|
7
16
|
import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
resolveLegendRollup,
|
|
12
|
-
resolvePlotRect,
|
|
13
|
-
} 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'
|
|
14
20
|
import type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
15
|
-
import { ChartTierProvider } from './chart-tier'
|
|
21
|
+
import { ChartTierProvider, useCoarsePointer, useViewportHeight } from './chart-tier'
|
|
16
22
|
import { ChartLegend } from './ChartLegend'
|
|
17
23
|
import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
|
|
18
24
|
import type { ChartState } from './ChartPending'
|
|
19
25
|
|
|
20
26
|
/** Re-exported for `ChartFrame.test.tsx` and any other consumer that previously reached these
|
|
21
27
|
* through `ChartFrame` — the layout math itself lives in `./chart-frame-layout` (pure, DOM-free). */
|
|
22
|
-
export {
|
|
23
|
-
legendEntryCap,
|
|
24
|
-
resolveLegendRollup,
|
|
25
|
-
resolvePlotRect,
|
|
26
|
-
resolveChartTier,
|
|
27
|
-
} from './chart-frame-layout'
|
|
28
|
+
export { legendEntryCap, resolveLegendRollup, resolvePlotRect } from './chart-frame-layout'
|
|
28
29
|
export type { ResponsiveChartHeight } from './chart-frame-layout'
|
|
29
30
|
|
|
30
|
-
const DEFAULT_HEIGHT = 240
|
|
31
31
|
const DEFAULT_MIN_WIDTH = 200
|
|
32
32
|
|
|
33
33
|
/** Legend configuration for {@link ChartFrame}. Omit entirely (or pass `{}`) for the default
|
|
@@ -36,16 +36,12 @@ export type ChartFrameLegend = {
|
|
|
36
36
|
/** Default 'bottom'. */
|
|
37
37
|
placement?: LegendPlacement
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
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.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* the
|
|
44
|
-
* 150px docked row rendered 2 entries at `maxRows` 3, 6 AND 99 — five series drawn in colours
|
|
45
|
-
* nothing named. Both losers are DEFAULTS (the tier's, and `VX.minPlotHeight`); this is the one
|
|
46
|
-
* number a caller stated, so under `fill` the PLOT yields the height instead
|
|
47
|
-
* ({@link resolvePlotRect}'s `legendWins`). State a number too large for the box and the plot
|
|
48
|
-
* 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`).
|
|
49
45
|
*/
|
|
50
46
|
maxRows?: number
|
|
51
47
|
/** Visually separate role: series | overlay | reference. */
|
|
@@ -60,22 +56,36 @@ export type ChartFrameLegend = {
|
|
|
60
56
|
* plot, the tooltip, and the auto domain together (`docs/CHARTS-SPEC.md` §5).
|
|
61
57
|
*/
|
|
62
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
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export type ChartFrameProps = BasaltProps & {
|
|
66
72
|
/** Series identity — drives the derived legend. Pass the SAME array the kind draws + tooltips from. */
|
|
67
73
|
series: readonly SeriesStyle[]
|
|
68
74
|
/**
|
|
69
|
-
* Height in pixels, or one per
|
|
70
|
-
* 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
|
|
78
|
+
* this is never read, which now warns once in dev (`common/errors.ts`' `ignoredProp`).
|
|
71
79
|
*
|
|
72
|
-
* `{ base: 180,
|
|
73
|
-
* steps are compared against the frame's own MEASURED width, not the viewport,
|
|
74
|
-
* by a `PageAside` gets the short height on a desktop too. A plain number is
|
|
75
|
-
* 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.
|
|
76
85
|
*/
|
|
77
86
|
height?: number | ResponsiveChartHeight
|
|
78
|
-
/** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set
|
|
87
|
+
/** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set — which warns
|
|
88
|
+
* once in dev, for the same reason `height` does. */
|
|
79
89
|
aspectRatio?: number
|
|
80
90
|
/** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
|
|
81
91
|
fill?: boolean
|
|
@@ -126,6 +136,10 @@ export type PlotRect = {
|
|
|
126
136
|
width: number
|
|
127
137
|
height: number
|
|
128
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
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
const outerStyle = (fill: boolean, vertical: boolean): CSSProperties => ({
|
|
@@ -209,6 +223,104 @@ export function ChartFrame({
|
|
|
209
223
|
}: ChartFrameProps): ReactNode {
|
|
210
224
|
const { ref: containerRef, width: containerW, height: containerH } = useChartSize()
|
|
211
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
|
+
claimLegendSlot,
|
|
232
|
+
releaseLegendSlot,
|
|
233
|
+
legendSlotVersion,
|
|
234
|
+
} = useContext(ChartCardContext)
|
|
235
|
+
const frameId = useId()
|
|
236
|
+
const [ownsSlot, setOwnsSlot] = useState(false)
|
|
237
|
+
const [slotSeedW, setSlotSeedW] = useState(0)
|
|
238
|
+
// Mirrors `ownsSlot` synchronously so the retry effect below can read "do I currently own the
|
|
239
|
+
// slot" without waiting a render for the state to catch up — see the two effects' own doc.
|
|
240
|
+
const ownsSlotRef = useRef(false)
|
|
241
|
+
const placement = legend === false ? 'bottom' : (legend.placement ?? 'bottom')
|
|
242
|
+
const vertical = placement === 'left' || placement === 'right'
|
|
243
|
+
// `Donut` sets `headerSlot: false` (P1-4) — it never wants the slot, so it must never CLAIM it
|
|
244
|
+
// either, or it would silently deny a sibling frame in the same card that does want it. A
|
|
245
|
+
// vertical (left/right) legend never portals into the header either (`inHeader` below requires
|
|
246
|
+
// `!vertical`), so it must not claim the slot from a sibling that could actually use it.
|
|
247
|
+
const wantsHeaderSlot = legend !== false && legend.headerSlot !== false && !vertical
|
|
248
|
+
// Every non-null state replaces the plot, and all three suppress the legend for one reason: a
|
|
249
|
+
// legend naming a series with nothing to point at is its own small lie. Computed here (ahead of
|
|
250
|
+
// the claim effect below) only because that effect's own dev warning needs it.
|
|
251
|
+
const resolvedState = resolveChartState({ ...(state !== undefined && { state }), isPending })
|
|
252
|
+
const legendVisible = legend !== false && resolvedState === null
|
|
253
|
+
// The card's header slot is observed like the frame is: its width is what a header legend fits.
|
|
254
|
+
// Only the frame that CLAIMS the slot (first to mount, P2-9) observes it or seeds its width — a
|
|
255
|
+
// second frame in the same card never measures a slot it will not portal into. The contention
|
|
256
|
+
// warning is reported HERE, from the deterministic `owns` this effect just computed, rather than
|
|
257
|
+
// as a separate reactive check on `ownsSlot` state — that state takes one extra render to catch up
|
|
258
|
+
// with the claim, and a passive effect reacting to the interim (stale) value warned about frames
|
|
259
|
+
// that went on to win the claim one render later.
|
|
260
|
+
// One claim attempt, shared by the owner effect below (real identity changes) and the
|
|
261
|
+
// denied-frame retry effect further down (a sibling's release). Reads `legendVisible` at CALL
|
|
262
|
+
// time through the closure, not through a dependency array of its own effect, so the two callers
|
|
263
|
+
// stay free to key on whatever triggers each of them.
|
|
264
|
+
const tryClaim = useCallback((): boolean => {
|
|
265
|
+
if (legendSlot === null || !wantsHeaderSlot) return false
|
|
266
|
+
const owns = claimLegendSlot(frameId)
|
|
267
|
+
ownsSlotRef.current = owns
|
|
268
|
+
setOwnsSlot(owns)
|
|
269
|
+
if (!owns) {
|
|
270
|
+
setSlotSeedW(0)
|
|
271
|
+
if (legendVisible) reportOnce('ChartFrame', legendSlotContention('ChartFrame'))
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
// Seeded synchronously (P2-8) so the very first commit already knows the slot's real width,
|
|
275
|
+
// instead of assuming 0 until the ResizeObserver's first (asynchronous) callback — which is
|
|
276
|
+
// what let a header legend paint once in the band before jumping to the header on every mount.
|
|
277
|
+
setSlotSeedW(legendSlot.getBoundingClientRect().width)
|
|
278
|
+
slotRef(legendSlot)
|
|
279
|
+
return true
|
|
280
|
+
}, [legendSlot, wantsHeaderSlot, legendVisible, frameId, claimLegendSlot, slotRef])
|
|
281
|
+
|
|
282
|
+
// Releases the slot only if THIS frame currently owns it, read live off `ownsSlotRef` at
|
|
283
|
+
// cleanup time rather than a snapshot closed over when the effect ran — so it correctly releases
|
|
284
|
+
// whichever effect (this one's own claim, or the retry effect's) most recently won it.
|
|
285
|
+
const releaseIfOwned = useCallback(() => {
|
|
286
|
+
if (!ownsSlotRef.current) return
|
|
287
|
+
ownsSlotRef.current = false
|
|
288
|
+
releaseLegendSlot(frameId)
|
|
289
|
+
slotRef(null)
|
|
290
|
+
}, [releaseLegendSlot, frameId, slotRef])
|
|
291
|
+
|
|
292
|
+
// The OWNER effect: claims/releases only on a real identity change (the slot itself, whether this
|
|
293
|
+
// frame even wants one, or a remount) — never on `legendSlotVersion`. A prior version keyed this
|
|
294
|
+
// effect on `legendSlotVersion` too, so the owner's own cleanup (which unconditionally released)
|
|
295
|
+
// bumped the version, which re-ran this very effect, which released again — an infinite
|
|
296
|
+
// release → bump → re-run loop that surfaced as "Maximum update depth exceeded" under StrictMode
|
|
297
|
+
// replay or a `legendVisible` flip. Only a DENIED frame reacts to the version now (below).
|
|
298
|
+
useLayoutEffect(() => {
|
|
299
|
+
if (legendSlot === null || !wantsHeaderSlot) {
|
|
300
|
+
setOwnsSlot(false)
|
|
301
|
+
setSlotSeedW(0)
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
tryClaim()
|
|
305
|
+
return releaseIfOwned
|
|
306
|
+
}, [legendSlot, wantsHeaderSlot, tryClaim, releaseIfOwned])
|
|
307
|
+
|
|
308
|
+
// The DENIED-frame retry: fires only when the slot's ownership changes elsewhere
|
|
309
|
+
// (`legendSlotVersion` bumps on a release). A frame that already owns the slot short-circuits via
|
|
310
|
+
// `ownsSlotRef` — this effect never releases what it claims; the owner effect's `releaseIfOwned`
|
|
311
|
+
// cleanup (keyed on stable identity, not on this version) is the one place release happens, for
|
|
312
|
+
// whichever frame currently holds the slot.
|
|
313
|
+
useLayoutEffect(() => {
|
|
314
|
+
if (ownsSlotRef.current) return
|
|
315
|
+
tryClaim()
|
|
316
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
317
|
+
}, [legendSlotVersion])
|
|
318
|
+
const viewportClass = useSizeClass()
|
|
319
|
+
// No `BasaltProvider` (a charts-only consumer) means no viewport hint: the unmeasured first frame
|
|
320
|
+
// resolves to the regular (desktop) class, not phone chrome.
|
|
321
|
+
const sizeClass = useContext(SizeClassHintContext) === undefined ? 'medium' : viewportClass
|
|
322
|
+
const coarse = useCoarsePointer()
|
|
323
|
+
const viewportH = useViewportHeight()
|
|
212
324
|
const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set())
|
|
213
325
|
|
|
214
326
|
const toggleKey = useCallback((key: string) => {
|
|
@@ -220,24 +332,116 @@ export function ChartFrame({
|
|
|
220
332
|
})
|
|
221
333
|
}, [])
|
|
222
334
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
335
|
+
// The three sizing props are mutually exclusive and `fill` wins, silently — the type cannot say
|
|
336
|
+
// so without a union prop, which is a bigger change than a patch carries. Warn instead: a
|
|
337
|
+
// consumer passed `height` alongside `fill` for the life of a file and kept eight height
|
|
338
|
+
// constants (and a whole `compact` branch) that never moved a pixel, with nothing from tsc,
|
|
339
|
+
// oxlint or check-theme saying a word.
|
|
340
|
+
useValidateProps(
|
|
341
|
+
'ChartFrame',
|
|
342
|
+
() => [
|
|
343
|
+
fill && height !== undefined
|
|
344
|
+
? ignoredProp(
|
|
345
|
+
'ChartFrame',
|
|
346
|
+
'height',
|
|
347
|
+
"`fill` is set — a fill frame takes its parent cell's measured height. Pass one or the other.",
|
|
348
|
+
)
|
|
349
|
+
: null,
|
|
350
|
+
fill && aspectRatio !== undefined
|
|
351
|
+
? ignoredProp(
|
|
352
|
+
'ChartFrame',
|
|
353
|
+
'aspectRatio',
|
|
354
|
+
"`fill` is set — a fill frame takes its parent cell's measured height. Pass one or the other.",
|
|
355
|
+
)
|
|
356
|
+
: null,
|
|
357
|
+
],
|
|
358
|
+
[fill, height, aspectRatio],
|
|
359
|
+
)
|
|
360
|
+
const legendItems = legend === false ? [] : deriveLegend(series)
|
|
361
|
+
useValidateProps(
|
|
362
|
+
'ChartFrame',
|
|
363
|
+
() =>
|
|
364
|
+
// A cap covering every entry rolls nothing up — it is how `Donut` states "never roll up".
|
|
365
|
+
legend !== false && legend.maxRows !== undefined && legend.maxRows < legendItems.length
|
|
366
|
+
? deprecatedProp(
|
|
367
|
+
'ChartFrame',
|
|
368
|
+
'legend.maxRows',
|
|
369
|
+
'nothing (the legend fits by measured width)',
|
|
370
|
+
'1.32.0',
|
|
371
|
+
'Overflow folds into an `All N` disclosure; an explicit value still wins this release.',
|
|
372
|
+
)
|
|
373
|
+
: null,
|
|
374
|
+
[legend, legendItems.length],
|
|
375
|
+
)
|
|
376
|
+
const statedHeight =
|
|
377
|
+
height === undefined
|
|
378
|
+
? undefined
|
|
379
|
+
: resolveFrameHeight(height, resolveFrameClass({ frameW: containerW, sizeClass }))
|
|
380
|
+
// Memoized on scalars (the legend by its keys+labels) so `ChartTierProvider`'s value is stable
|
|
381
|
+
// across renders that change nothing. No slot, not the owner, or not measured yet → `slotW` 0 →
|
|
382
|
+
// a band. `slotSeedW` (P2-8) covers the gap before the ResizeObserver's own first callback.
|
|
383
|
+
const slotW = legendSlot === null || !ownsSlot ? 0 : Math.max(slotMeasuredW, slotSeedW)
|
|
384
|
+
const groups = legend !== false && legend.groups === true
|
|
385
|
+
// Role and note both feed the resolver (`orderEntries`'s grouping, `legendEntryWidth`'s measured
|
|
386
|
+
// fit), so a change to either — a series moving `role`, a note appearing/disappearing — has to
|
|
387
|
+
// invalidate this memo exactly like a key or label change does; keying on key+label alone missed
|
|
388
|
+
// both and served a stale layout until something else happened to change first.
|
|
389
|
+
const legendKey = legendItems
|
|
390
|
+
.map((item) => `${item.key}\u0000${item.label}\u0000${item.role ?? ''}\u0000${item.note ?? ''}`)
|
|
391
|
+
.join('\u0001')
|
|
392
|
+
const layout = useMemo(
|
|
393
|
+
() =>
|
|
394
|
+
resolveChartLayout({
|
|
395
|
+
frameW: containerW,
|
|
396
|
+
slotW,
|
|
397
|
+
viewportH,
|
|
398
|
+
legendItems,
|
|
399
|
+
sizeClass,
|
|
400
|
+
coarse,
|
|
401
|
+
cardShort,
|
|
402
|
+
groups,
|
|
403
|
+
...(statedHeight !== undefined && { override: { height: statedHeight } }),
|
|
404
|
+
}),
|
|
405
|
+
// `legendItems` is a fresh array per render; `legendKey` is its identity.
|
|
406
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
407
|
+
[containerW, slotW, viewportH, legendKey, sizeClass, coarse, cardShort, groups, statedHeight],
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// The resolver gates every placement: none at micro, else the card's header slot when there is
|
|
411
|
+
// one AND this frame owns it (P2-9) — `slotW` is already 0 for a non-owner or a `headerSlot:
|
|
412
|
+
// false` kind (P1-4), so `fit.where` already reads 'band' for both; `ownsSlot` here is only the
|
|
413
|
+
// (harmless) belt to that suspenders.
|
|
414
|
+
const fit = layout.legend.mode === 'dots' || layout.legend.mode === 'chips' ? layout.legend : null
|
|
415
|
+
const legendMode = fit?.mode
|
|
416
|
+
const showLegend = legendVisible && fit !== null
|
|
417
|
+
const inHeader =
|
|
418
|
+
showLegend && !vertical && legendSlot !== null && ownsSlot && fit?.where === 'header'
|
|
419
|
+
const sideLegendWidth = showLegend && vertical ? legendW : 0
|
|
420
|
+
const topBottomLegendHeight = showLegend && !vertical && !inHeader ? legendH : 0
|
|
230
421
|
|
|
422
|
+
const computedHeight =
|
|
423
|
+
aspectRatio !== undefined ? Math.round(containerW / aspectRatio) : layout.height
|
|
424
|
+
// In a card the computed height is a floor: a stretched grid row hands the body more, and the
|
|
425
|
+
// frame (flex 1 in the body) takes it. Never below what the legend band plus the plot floor need.
|
|
426
|
+
const grows = inCard && !fill
|
|
231
427
|
const resolvedHeight = fill
|
|
232
428
|
? containerH
|
|
233
|
-
:
|
|
234
|
-
? Math.
|
|
235
|
-
:
|
|
429
|
+
: grows
|
|
430
|
+
? Math.max(computedHeight, containerH)
|
|
431
|
+
: computedHeight
|
|
432
|
+
const cardMinHeight = Math.max(computedHeight, VX.minPlotHeight + topBottomLegendHeight)
|
|
236
433
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
434
|
+
useValidateProps(
|
|
435
|
+
'ChartFrame',
|
|
436
|
+
() =>
|
|
437
|
+
typeof height === 'number' &&
|
|
438
|
+
!fill &&
|
|
439
|
+
aspectRatio === undefined &&
|
|
440
|
+
height - topBottomLegendHeight < VX.minPlotHeight
|
|
441
|
+
? plotBelowFloor('ChartFrame', height, VX.minPlotHeight)
|
|
442
|
+
: null,
|
|
443
|
+
[height, fill, aspectRatio, topBottomLegendHeight],
|
|
444
|
+
)
|
|
241
445
|
const togglable = legend !== false && (legend.toggle ?? legendItems.length > 1)
|
|
242
446
|
|
|
243
447
|
// The cap, and who pays for it — one pure decision ({@link resolveLegendRollup} states the three
|
|
@@ -247,8 +451,8 @@ export function ChartFrame({
|
|
|
247
451
|
// plot pays for. Top/bottom only: a side legend costs width, not height.
|
|
248
452
|
const { maxRows, legendWins } = resolveLegendRollup({
|
|
249
453
|
...(legend !== false && legend.maxRows !== undefined && { statedMaxRows: legend.maxRows }),
|
|
250
|
-
|
|
251
|
-
fillBand:
|
|
454
|
+
...(!vertical && fit !== null && fit.overflow > 0 && { fittedMaxRows: fit.visible }),
|
|
455
|
+
fillBand: showLegend && fill && !inHeader && !vertical,
|
|
252
456
|
items: legendItems,
|
|
253
457
|
containerW,
|
|
254
458
|
available: resolvedHeight - VX.minPlotHeight,
|
|
@@ -264,7 +468,7 @@ export function ChartFrame({
|
|
|
264
468
|
})
|
|
265
469
|
|
|
266
470
|
const legendNode =
|
|
267
|
-
legend === false || resolvedState !== null ? null : (
|
|
471
|
+
legend === false || resolvedState !== null || !showLegend ? null : (
|
|
268
472
|
<div ref={legendRef} style={legendWrapperStyle(vertical)}>
|
|
269
473
|
<ChartLegend
|
|
270
474
|
items={legendItems}
|
|
@@ -274,6 +478,7 @@ export function ChartFrame({
|
|
|
274
478
|
{...(chartId !== undefined && { chartId })}
|
|
275
479
|
{...(legend.groups !== undefined && { groups: legend.groups })}
|
|
276
480
|
{...(maxRows !== undefined && { maxRows })}
|
|
481
|
+
{...(legendMode !== undefined && { mode: legendMode })}
|
|
277
482
|
{...(legend.highlighted !== undefined && { highlighted: legend.highlighted })}
|
|
278
483
|
{...(legend.onHighlight !== undefined && { onHighlight: legend.onHighlight })}
|
|
279
484
|
/>
|
|
@@ -281,15 +486,26 @@ export function ChartFrame({
|
|
|
281
486
|
)
|
|
282
487
|
|
|
283
488
|
return (
|
|
284
|
-
<ChartTierProvider
|
|
489
|
+
<ChartTierProvider containerClass={layout.containerClass}>
|
|
285
490
|
<div
|
|
286
491
|
ref={containerRef}
|
|
287
492
|
{...(className !== undefined && { className })}
|
|
288
|
-
style={{
|
|
493
|
+
style={{
|
|
494
|
+
...outerStyle(fill, vertical),
|
|
495
|
+
...(grows && { flex: '1 1 0', minHeight: cardMinHeight }),
|
|
496
|
+
...style,
|
|
497
|
+
}}
|
|
289
498
|
{...(ariaLabel !== undefined && { role: 'group', 'aria-label': ariaLabel })}
|
|
290
499
|
{...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
|
|
291
500
|
>
|
|
292
|
-
{
|
|
501
|
+
{inHeader &&
|
|
502
|
+
legendNode !== null &&
|
|
503
|
+
legendSlot !== null &&
|
|
504
|
+
createPortal(legendNode, legendSlot)}
|
|
505
|
+
{!inHeader &&
|
|
506
|
+
legendNode !== null &&
|
|
507
|
+
(placement === 'top' || placement === 'left') &&
|
|
508
|
+
legendNode}
|
|
293
509
|
{plot.width > 0 &&
|
|
294
510
|
plot.height > 0 &&
|
|
295
511
|
(resolvedState === 'pending' ? (
|
|
@@ -303,9 +519,12 @@ export function ChartFrame({
|
|
|
303
519
|
{...(typeof state?.empty === 'string' && { label: state.empty })}
|
|
304
520
|
/>
|
|
305
521
|
) : (
|
|
306
|
-
children({ ...plot, hidden })
|
|
522
|
+
children({ ...plot, hidden, legendInHeader: inHeader })
|
|
307
523
|
))}
|
|
308
|
-
{
|
|
524
|
+
{!inHeader &&
|
|
525
|
+
legendNode !== null &&
|
|
526
|
+
(placement === 'bottom' || placement === 'right') &&
|
|
527
|
+
legendNode}
|
|
309
528
|
</div>
|
|
310
529
|
</ChartTierProvider>
|
|
311
530
|
)
|