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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { CSSProperties,
|
|
2
|
-
import { useState } from 'react'
|
|
1
|
+
import type { CSSProperties, MouseEvent, ReactNode } from 'react'
|
|
3
2
|
import type { BasaltProps } from '../../common/props'
|
|
4
3
|
import { alpha, VX } from '../../tokens'
|
|
5
4
|
import type { SeriesRole, LegendPlacement } from '../series'
|
|
6
|
-
import {
|
|
5
|
+
import { DOTS_HIT_GAP, LEGEND_DOT_SIZE, LEGEND_ROW_GAP, orderEntries } from './chart-frame-layout'
|
|
6
|
+
import { useChartMetrics, useCoarsePointer } from './chart-tier'
|
|
7
|
+
import { LegendDisclosure, useLegendDisclosure } from './legend-disclosure'
|
|
7
8
|
|
|
8
9
|
export type LegendEntry = {
|
|
9
10
|
key: string
|
|
@@ -51,43 +52,28 @@ const LEGEND_ITEM_BUTTON: CSSProperties = {
|
|
|
51
52
|
textAlign: 'inherit',
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
const GROUP_ORDER: SeriesRole[] = ['series', 'overlay', 'reference']
|
|
55
|
-
|
|
56
55
|
/** Stable empty set — a fresh `new Set()` default would be a new identity on every render. */
|
|
57
56
|
const NO_HIDDEN: ReadonlySet<string> = new Set()
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/** Order entries series → overlay → reference and record where a divider belongs. */
|
|
62
|
-
function orderEntries(
|
|
63
|
-
items: LegendEntry[],
|
|
64
|
-
groups: boolean,
|
|
65
|
-
): { entries: LegendEntry[]; dividerAfter: Set<number> } {
|
|
66
|
-
if (!groups) return { entries: items, dividerAfter: new Set() }
|
|
67
|
-
|
|
68
|
-
const dividerAfter = new Set<number>()
|
|
69
|
-
const entries: LegendEntry[] = []
|
|
70
|
-
const nonEmptyGroups = GROUP_ORDER.map((role) =>
|
|
71
|
-
items.filter((item) => roleOf(item) === role),
|
|
72
|
-
).filter((group) => group.length > 0)
|
|
73
|
-
nonEmptyGroups.forEach((group, i) => {
|
|
74
|
-
entries.push(...group)
|
|
75
|
-
if (i < nonEmptyGroups.length - 1) dividerAfter.add(entries.length - 1)
|
|
76
|
-
})
|
|
77
|
-
return { entries, dividerAfter }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function wrapperStyle(placement: LegendPlacement, fontSize: number): CSSProperties {
|
|
58
|
+
function wrapperStyle(placement: LegendPlacement, fontSize: number, dots: boolean): CSSProperties {
|
|
81
59
|
const vertical = placement === 'left' || placement === 'right'
|
|
60
|
+
const gap = dots ? DOTS_HIT_GAP : VX.legendGap
|
|
82
61
|
return {
|
|
83
62
|
display: 'flex',
|
|
84
63
|
flexDirection: vertical ? 'column' : 'row',
|
|
85
64
|
flexWrap: vertical ? 'nowrap' : 'wrap',
|
|
86
65
|
alignItems: vertical ? 'flex-start' : 'center',
|
|
87
|
-
justifyContent:
|
|
88
|
-
columnGap:
|
|
89
|
-
|
|
90
|
-
|
|
66
|
+
justifyContent: 'flex-start',
|
|
67
|
+
columnGap: gap,
|
|
68
|
+
// Always the tight row gap (R2C-9): the wider dots COLUMN gap is a hit-overlap cap, not
|
|
69
|
+
// breathing room between wrapped rows — a dots-mode header legend is 1 row by law
|
|
70
|
+
// (`LEGEND_ROWS.header` in `chart-layout.ts`) anyway, so this only ever shows in the band.
|
|
71
|
+
rowGap: LEGEND_ROW_GAP,
|
|
72
|
+
// Caps each entry's `[data-basalt-hit]` overlay at the real inter-entry gap (styles.css).
|
|
73
|
+
...({ '--vx-hit-gap': `${gap}px` } as CSSProperties),
|
|
74
|
+
// A dots-mode row has no text baseline to breathe around — the header-fold law (wave 8) is
|
|
75
|
+
// the reason this row exists at all, so it gets the same tight treatment.
|
|
76
|
+
padding: dots ? '4px 0 2px' : '8px 0 2px',
|
|
91
77
|
fontSize,
|
|
92
78
|
color: VX.muted,
|
|
93
79
|
}
|
|
@@ -205,6 +191,27 @@ const LEGEND_CHILD_STYLE: CSSProperties = {
|
|
|
205
191
|
opacity: 0.75,
|
|
206
192
|
}
|
|
207
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The whole entry collapses to its color, in 'dots' mode (`docs/CHARTS-SPEC.md` §5: a short or
|
|
196
|
+
* `compact`-container header has no room for a text label per entry) — a plain 8px circle
|
|
197
|
+
* regardless of the series' own shape, since a two-tone `split`/`splitLine` swatch cannot express
|
|
198
|
+
* two colors in one dot. The name survives via the button's own `aria-label`; sighted users get
|
|
199
|
+
* color-coded identity only, same trade the `compact` container class already made for the y axis.
|
|
200
|
+
*/
|
|
201
|
+
function LegendDot({ item }: { item: LegendEntry }) {
|
|
202
|
+
return (
|
|
203
|
+
<span
|
|
204
|
+
style={{
|
|
205
|
+
width: LEGEND_DOT_SIZE,
|
|
206
|
+
height: LEGEND_DOT_SIZE,
|
|
207
|
+
borderRadius: '50%', // theme-allow raw-surface — a circle shape, not a surface corner
|
|
208
|
+
backgroundColor: item.color,
|
|
209
|
+
flexShrink: 0,
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
208
215
|
/** Compact swatch for a folded child entry — smaller than {@link LegendSwatch}, line/bar only
|
|
209
216
|
* (folded companions are simple line or bar series; `split`/`splitLine` never fold). */
|
|
210
217
|
function LegendChildSwatch({ item }: { item: LegendEntry }) {
|
|
@@ -256,24 +263,19 @@ function LegendChild({ item }: { item: LegendEntry }) {
|
|
|
256
263
|
* static legends without hover interactivity. `chartId` namespaces `split` clipPath ids so two
|
|
257
264
|
* legends on the same page never collide. `groups` renders series → hairline divider → overlay →
|
|
258
265
|
* reference bands instead of one flat smear. `maxRows` caps the number of rendered ENTRIES and
|
|
259
|
-
* rolls the remainder into
|
|
260
|
-
* the name (`entries.slice(0, cap)` below is the whole of it
|
|
261
|
-
*
|
|
262
|
-
* `ChartTierMetrics.legendMaxRows`, so it stays until a minor can rename both.
|
|
266
|
+
* rolls the remainder into an `All N` disclosure chip — it is an entry count, not a row count, in
|
|
267
|
+
* spite of the name (`entries.slice(0, cap)` below is the whole of it). `ChartFrame` derives the
|
|
268
|
+
* cap from the measured fit (`resolveChartLayout().legend`); a caller-stated value wins.
|
|
263
269
|
*
|
|
264
270
|
* `onToggle` makes an entry a real toggle: clicking it hides the series in the plot, the tooltip,
|
|
265
271
|
* and the auto y-domain together (`ChartFrame` owns the `hidden` set — see `docs/CHARTS-SPEC.md`
|
|
266
|
-
* §5).
|
|
272
|
+
* §5). A hidden entry is announced via `aria-pressed`.
|
|
267
273
|
*
|
|
268
|
-
* The
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* unnamed, and a `+6 more` that is a `<span>` gives a phone reader no way to learn them — a
|
|
274
|
-
* categorical encoding the chart draws and refuses to decode. The chip is a `<button>` carrying
|
|
275
|
-
* `aria-expanded`: expanded, every entry renders, the legend band grows, and `ChartFrame`'s
|
|
276
|
-
* observer re-flows the frame around it (the plot keeps `VX.minPlotHeight`).
|
|
274
|
+
* **The rollup is a disclosure, not a label.** `All N` is a `<button>` carrying `aria-expanded`
|
|
275
|
+
* that opens EVERY entry, toggles included, in a popover under the chip on a fine pointer and a
|
|
276
|
+
* bottom sheet on a coarse one. Both are fixed-position and portalled, so opening one never grows
|
|
277
|
+
* the legend band or moves the plot. Escape, an outside press, a scroll or a resize closes it, and
|
|
278
|
+
* focus returns to the chip on every close.
|
|
277
279
|
*/
|
|
278
280
|
export function ChartLegend({
|
|
279
281
|
items,
|
|
@@ -285,6 +287,7 @@ export function ChartLegend({
|
|
|
285
287
|
placement = 'bottom',
|
|
286
288
|
groups = false,
|
|
287
289
|
maxRows,
|
|
290
|
+
mode = 'chips',
|
|
288
291
|
className,
|
|
289
292
|
style,
|
|
290
293
|
}: BasaltProps & {
|
|
@@ -299,40 +302,55 @@ export function ChartLegend({
|
|
|
299
302
|
placement?: LegendPlacement
|
|
300
303
|
groups?: boolean
|
|
301
304
|
maxRows?: number
|
|
305
|
+
/**
|
|
306
|
+
* `'dots'` collapses every VISIBLE entry to a plain color dot with no label (the name still
|
|
307
|
+
* reaches assistive tech via `aria-label`) — `resolveChartLayout`'s header-slot legend forces
|
|
308
|
+
* this for a short `ChartCard` (wave 11, `docs/CHARTS-SPEC.md` §5) so the legend band's height and
|
|
309
|
+
* width both shrink before it eats plot space. The `All N` disclosure panel is unaffected: it
|
|
310
|
+
* always shows the full swatch + label, `mode` only governs the inline row. Default `'chips'`
|
|
311
|
+
* (today's swatch + label rendering) for every other caller.
|
|
312
|
+
*/
|
|
313
|
+
mode?: 'dots' | 'chips'
|
|
302
314
|
}) {
|
|
303
|
-
const tier =
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
315
|
+
const tier = useChartMetrics()
|
|
316
|
+
const coarse = useCoarsePointer()
|
|
317
|
+
const { open, panelId, openFrom, close, panel } = useLegendDisclosure(coarse)
|
|
318
|
+
|
|
319
|
+
const handleEnter = (key: string) => onHighlight?.(key)
|
|
309
320
|
const handleLeave = () => onHighlight?.(null)
|
|
310
|
-
const handleToggle = (e: MouseEvent<HTMLButtonElement>) => {
|
|
311
|
-
const key = e.currentTarget.dataset['legendKey']
|
|
312
|
-
if (key !== undefined) onToggle?.(key)
|
|
313
|
-
}
|
|
314
321
|
|
|
315
322
|
const idPrefix = chartId ? `${chartId}-` : ''
|
|
316
323
|
const vertical = placement === 'left' || placement === 'right'
|
|
317
324
|
const { entries, dividerAfter } = orderEntries(items, groups)
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const visible = rolledUp > 0 && !expanded ? entries.slice(0, cap) : entries
|
|
325
|
+
const rolledUp = maxRows === undefined ? 0 : Math.max(entries.length - maxRows, 0)
|
|
326
|
+
const visible = rolledUp > 0 ? entries.slice(0, maxRows) : entries
|
|
321
327
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
328
|
+
// The disclosure panel is a second copy of the same entries: it gets its own id namespace and
|
|
329
|
+
// no `data-legend-key`, so a page query for the band's entries never finds the panel's twins.
|
|
330
|
+
const entryButton = (item: LegendEntry, inPanel = false): ReactNode => {
|
|
331
|
+
// A dots-mode entry names nothing on its own face — its whole point is the color — so a tap
|
|
332
|
+
// opens the same All-N sheet the overflow chip does, rather than silently hiding a series the
|
|
333
|
+
// reader was trying to identify (R2C-8: "what is this?" used to toggle it off instead). Gated
|
|
334
|
+
// on `mode` alone: with `legend.toggle: false` (no `onToggle`) a dots entry still has no OTHER
|
|
335
|
+
// way to name itself, so it still needs the title + disclosure — `onToggle !== undefined` used
|
|
336
|
+
// to suppress both just because toggling happened to be off.
|
|
337
|
+
const isDotTrigger = !inPanel && mode === 'dots'
|
|
338
|
+
return (
|
|
325
339
|
<button
|
|
326
340
|
key={item.key}
|
|
327
341
|
type="button"
|
|
328
|
-
data-
|
|
342
|
+
data-basalt-hit
|
|
343
|
+
{...(!inPanel && { 'data-legend-key': item.key })}
|
|
329
344
|
// The note is the whole point for a series that is invisible in the plot, so it has to
|
|
330
345
|
// reach a screen reader too — an explicit aria-label would otherwise replace it.
|
|
331
346
|
aria-label={item.note ? `${item.label} — ${item.note}` : item.label}
|
|
332
|
-
{...(
|
|
347
|
+
{...(isDotTrigger && { title: item.label, 'aria-haspopup': 'dialog' as const })}
|
|
348
|
+
{...(isDotTrigger
|
|
349
|
+
? { 'aria-expanded': open, 'aria-controls': panelId }
|
|
350
|
+
: onToggle !== undefined && { 'aria-pressed': !hidden.has(item.key) })}
|
|
333
351
|
style={{
|
|
334
352
|
...LEGEND_ITEM_BUTTON,
|
|
335
|
-
cursor: onToggle === undefined ? 'default' : 'pointer',
|
|
353
|
+
cursor: onToggle === undefined && !isDotTrigger ? 'default' : 'pointer',
|
|
336
354
|
// Toggled-off wins over hover-dimming: a hidden series must read as hidden even while
|
|
337
355
|
// it is the one being hovered.
|
|
338
356
|
opacity: hidden.has(item.key)
|
|
@@ -342,20 +360,33 @@ export function ChartLegend({
|
|
|
342
360
|
: 0.3,
|
|
343
361
|
textDecoration: hidden.has(item.key) ? 'line-through' : 'none',
|
|
344
362
|
}}
|
|
345
|
-
{...(
|
|
346
|
-
|
|
363
|
+
{...(isDotTrigger
|
|
364
|
+
? { onClick: (e: MouseEvent<HTMLButtonElement>) => openFrom(e.currentTarget) }
|
|
365
|
+
: onToggle !== undefined && { onClick: () => onToggle(item.key) })}
|
|
366
|
+
onMouseEnter={() => handleEnter(item.key)}
|
|
347
367
|
onMouseLeave={handleLeave}
|
|
348
|
-
onFocus={handleEnter}
|
|
368
|
+
onFocus={() => handleEnter(item.key)}
|
|
349
369
|
onBlur={handleLeave}
|
|
350
370
|
>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
{!inPanel && mode === 'dots' ? (
|
|
372
|
+
<LegendDot item={item} />
|
|
373
|
+
) : (
|
|
374
|
+
<>
|
|
375
|
+
<LegendSwatch item={item} idPrefix={inPanel ? `${idPrefix}disclosure-` : idPrefix} />
|
|
376
|
+
<span>{item.label}</span>
|
|
377
|
+
{item.note ? <span style={LEGEND_NOTE_STYLE}>{item.note}</span> : null}
|
|
378
|
+
{item.children?.map((child) => (
|
|
379
|
+
<LegendChild key={child.key} item={child} />
|
|
380
|
+
))}
|
|
381
|
+
</>
|
|
382
|
+
)}
|
|
383
|
+
</button>
|
|
358
384
|
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const nodes: ReactNode[] = []
|
|
388
|
+
visible.forEach((item, i) => {
|
|
389
|
+
nodes.push(entryButton(item))
|
|
359
390
|
if (dividerAfter.has(i))
|
|
360
391
|
nodes.push(<LegendDivider key={`divider-${item.key}`} columnLayout={vertical} />)
|
|
361
392
|
})
|
|
@@ -364,11 +395,14 @@ export function ChartLegend({
|
|
|
364
395
|
<button
|
|
365
396
|
key="legend-more"
|
|
366
397
|
type="button"
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
data-basalt-hit
|
|
399
|
+
aria-expanded={open}
|
|
400
|
+
aria-haspopup="dialog"
|
|
401
|
+
aria-controls={panelId}
|
|
402
|
+
onClick={(e) => (open ? close() : openFrom(e.currentTarget))}
|
|
369
403
|
style={{ ...LEGEND_ITEM_BUTTON, cursor: 'pointer', textDecoration: 'underline' }}
|
|
370
404
|
>
|
|
371
|
-
{
|
|
405
|
+
{`All ${entries.length}`}
|
|
372
406
|
</button>,
|
|
373
407
|
)
|
|
374
408
|
}
|
|
@@ -376,9 +410,12 @@ export function ChartLegend({
|
|
|
376
410
|
return (
|
|
377
411
|
<div
|
|
378
412
|
{...(className !== undefined && { className })}
|
|
379
|
-
style={{ ...wrapperStyle(placement, tier.legendFontSize), ...style }}
|
|
413
|
+
style={{ ...wrapperStyle(placement, tier.legendFontSize, mode === 'dots'), ...style }}
|
|
380
414
|
>
|
|
381
415
|
{nodes}
|
|
416
|
+
<LegendDisclosure {...panel} fontSize={tier.legendFontSize}>
|
|
417
|
+
{entries.map((item) => entryButton(item, true))}
|
|
418
|
+
</LegendDisclosure>
|
|
382
419
|
</div>
|
|
383
420
|
)
|
|
384
421
|
}
|
|
@@ -2,8 +2,9 @@ import type { CSSProperties, ReactNode } from 'react'
|
|
|
2
2
|
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
3
3
|
import { createPortal } from 'react-dom'
|
|
4
4
|
import { VX } from '../../tokens'
|
|
5
|
+
import type { TooltipRowData } from '../series'
|
|
5
6
|
import { fmtTooltipDate } from '../utils/format'
|
|
6
|
-
import {
|
|
7
|
+
import { useChartMetrics } from './chart-tier'
|
|
7
8
|
|
|
8
9
|
// Panel bg + shadow-card, radius 8 (docs/DESIGN-SPEC.md §5's "Tooltip/popover/menu" idiom) — the
|
|
9
10
|
// same depth-via-shadow treatment as ChartCard, never a `border` property. Surfaces resolve per
|
|
@@ -11,7 +12,7 @@ import { useChartTierMetrics } from './chart-tier'
|
|
|
11
12
|
const TOOLTIP_STYLES: CSSProperties = {
|
|
12
13
|
position: 'fixed',
|
|
13
14
|
pointerEvents: 'none',
|
|
14
|
-
zIndex:
|
|
15
|
+
zIndex: VX.zIndexFloating,
|
|
15
16
|
backgroundColor: VX.surface.panel,
|
|
16
17
|
borderRadius: 'var(--vx-radius-floating)',
|
|
17
18
|
padding: '0',
|
|
@@ -136,6 +137,21 @@ export function TooltipRow({
|
|
|
136
137
|
)
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/** The derived rows (`deriveTooltipRows`), one {@link TooltipRow} each. */
|
|
141
|
+
export function TooltipRows({ rows }: { rows: readonly TooltipRowData[] }) {
|
|
142
|
+
return rows.map((row) => (
|
|
143
|
+
<TooltipRow
|
|
144
|
+
key={row.key}
|
|
145
|
+
color={row.color}
|
|
146
|
+
label={row.label}
|
|
147
|
+
value={row.value}
|
|
148
|
+
shape={row.shape}
|
|
149
|
+
dashed={row.dashed}
|
|
150
|
+
{...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
|
|
151
|
+
/>
|
|
152
|
+
))
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
export function TooltipBody({ children }: { children: ReactNode }) {
|
|
140
156
|
return <div style={{ padding: '5px 0' }}>{children}</div>
|
|
141
157
|
}
|
|
@@ -191,7 +207,7 @@ export function ChartTooltipFloat({
|
|
|
191
207
|
const ref = useRef<HTMLDivElement>(null)
|
|
192
208
|
const [box, setBox] = useState<{ width: number; height: number } | null>(null)
|
|
193
209
|
const isClient = useIsClient()
|
|
194
|
-
const { tooltipMinWidth } =
|
|
210
|
+
const { tooltipMinWidth } = useChartMetrics()
|
|
195
211
|
|
|
196
212
|
useLayoutEffect(() => {
|
|
197
213
|
const el = ref.current
|
|
@@ -215,6 +231,11 @@ export function ChartTooltipFloat({
|
|
|
215
231
|
const vw = window.innerWidth
|
|
216
232
|
const vh = window.innerHeight
|
|
217
233
|
|
|
234
|
+
// An anchor outside the viewport means the chart that owns it has scrolled off-screen. Clamping
|
|
235
|
+
// would paint a follower's box over unrelated chrome (the bottom nav — `R2C-3`); render nothing
|
|
236
|
+
// instead.
|
|
237
|
+
if (anchor.y < 0 || anchor.y > vh) return null
|
|
238
|
+
|
|
218
239
|
const flipsLeft = anchor.x + TOOLTIP_GAP + width + VIEWPORT_MARGIN > vw
|
|
219
240
|
const left = flipsLeft
|
|
220
241
|
? Math.max(VIEWPORT_MARGIN, anchor.x - TOOLTIP_GAP - width)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { VX } from '../../tokens'
|
|
3
|
-
import {
|
|
3
|
+
import { useChartMetrics } from './chart-tier'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The shared vertical hover crosshair — one implementation for what used to be five hand-rolled
|
|
@@ -38,6 +38,6 @@ export function SeriesDot({
|
|
|
38
38
|
color: string
|
|
39
39
|
r?: number
|
|
40
40
|
}): ReactNode {
|
|
41
|
-
const { dotR } =
|
|
41
|
+
const { dotR } = useChartMetrics()
|
|
42
42
|
return <circle cx={cx} cy={cy} r={r ?? dotR} fill={color} stroke={VX.dotStroke} strokeWidth={2} />
|
|
43
43
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `HoverOverlay` — the transparent plot rect every cartesian kind scrubs through. A tap focuses it
|
|
3
|
+
* on pointerdown, so the UA's own outline would ring the whole plot on every phone tap (`R2C-15`);
|
|
4
|
+
* keyboard focus keeps a VX accent inset instead.
|
|
5
|
+
*/
|
|
6
|
+
.overlay {
|
|
7
|
+
outline: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.overlay:focus-visible {
|
|
11
|
+
outline: 1px solid var(--vx-accent);
|
|
12
|
+
outline-offset: -1px;
|
|
13
|
+
}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import type { FocusEventHandler, KeyboardEventHandler, PointerEventHandler } from 'react'
|
|
2
|
+
import type { ChartCursor } from '../hooks/useChartCursor'
|
|
3
|
+
import styles from './HoverOverlay.module.css'
|
|
4
|
+
|
|
5
|
+
/** Props of {@link HoverOverlay}. */
|
|
6
|
+
export type HoverOverlayProps = {
|
|
7
|
+
width: number
|
|
8
|
+
height: number
|
|
9
|
+
onMove: PointerEventHandler<SVGRectElement>
|
|
10
|
+
/** Coarse-pointer tap: resolves + shows the tooltip immediately, with no preceding `pointermove`
|
|
11
|
+
* (`docs/CHARTS-SPEC.md` §4, Tooltip). The press is provisional; `onUp` commits it. */
|
|
12
|
+
onDown?: PointerEventHandler<SVGRectElement>
|
|
13
|
+
/** Commits the provisional touch press so the pin survives the lift. */
|
|
14
|
+
onUp?: PointerEventHandler<SVGRectElement>
|
|
15
|
+
onLeave: PointerEventHandler<SVGRectElement>
|
|
16
|
+
/** Present = the overlay is focusable and scrubs on ←/→. */
|
|
17
|
+
onKeyDown?: KeyboardEventHandler<SVGRectElement>
|
|
18
|
+
onBlur?: FocusEventHandler<SVGRectElement>
|
|
19
|
+
ariaLabel?: string
|
|
20
|
+
/** Index of the focused point — announced as the slider position. */
|
|
21
|
+
valueNow?: number
|
|
22
|
+
/** Last index of the domain. */
|
|
23
|
+
valueMax?: number
|
|
24
|
+
/** Human-readable label for the focused point (the formatted x key). */
|
|
25
|
+
valueText?: string
|
|
26
|
+
}
|
|
2
27
|
|
|
3
28
|
/**
|
|
4
29
|
* Transparent <rect> that captures pointer events (mouse + touch + pen) for tooltip + crosshair
|
|
@@ -11,6 +36,8 @@ export function HoverOverlay({
|
|
|
11
36
|
width,
|
|
12
37
|
height,
|
|
13
38
|
onMove,
|
|
39
|
+
onDown,
|
|
40
|
+
onUp,
|
|
14
41
|
onLeave,
|
|
15
42
|
onKeyDown,
|
|
16
43
|
onBlur,
|
|
@@ -18,24 +45,14 @@ export function HoverOverlay({
|
|
|
18
45
|
valueNow,
|
|
19
46
|
valueMax,
|
|
20
47
|
valueText,
|
|
21
|
-
}: {
|
|
22
|
-
width: number
|
|
23
|
-
height: number
|
|
24
|
-
onMove: PointerEventHandler<SVGRectElement>
|
|
25
|
-
onLeave: PointerEventHandler<SVGRectElement>
|
|
26
|
-
/** Present = the overlay is focusable and scrubs on ←/→. */
|
|
27
|
-
onKeyDown?: KeyboardEventHandler<SVGRectElement>
|
|
28
|
-
onBlur?: FocusEventHandler<SVGRectElement>
|
|
29
|
-
ariaLabel?: string
|
|
30
|
-
/** Index of the focused point — announced as the slider position. */
|
|
31
|
-
valueNow?: number
|
|
32
|
-
/** Last index of the domain. */
|
|
33
|
-
valueMax?: number
|
|
34
|
-
/** Human-readable label for the focused point (the formatted x key). */
|
|
35
|
-
valueText?: string
|
|
36
|
-
}) {
|
|
48
|
+
}: HoverOverlayProps) {
|
|
37
49
|
return (
|
|
50
|
+
// `touch-action: pan-y` lives on the plot's `<svg>` (each kind's own), NOT here: Chrome ignores
|
|
51
|
+
// `touch-action` on an SVG child `<rect>`, so the browser claims a horizontal pan and cancels
|
|
52
|
+
// the pointer (`R2C-2`). The `overlay` class owns the focus ring: no UA outline on tap, a VX
|
|
53
|
+
// accent inset under `:focus-visible` (`R2C-15`).
|
|
38
54
|
<rect
|
|
55
|
+
className={styles.overlay}
|
|
39
56
|
width={width}
|
|
40
57
|
height={height}
|
|
41
58
|
fill="transparent"
|
|
@@ -54,13 +71,38 @@ export function HoverOverlay({
|
|
|
54
71
|
onKeyDown,
|
|
55
72
|
...(onBlur !== undefined && { onBlur }),
|
|
56
73
|
})}
|
|
57
|
-
// `pan-y`, not `none`: `none` would turn a full-width chart into a scroll dead zone on a
|
|
58
|
-
// phone. `pan-y` lets vertical page scroll pass through while a horizontal drag still
|
|
59
|
-
// reaches this overlay to scrub the chart.
|
|
60
|
-
style={{ touchAction: 'pan-y' }}
|
|
61
74
|
onPointerMove={onMove}
|
|
75
|
+
{...(onDown !== undefined && { onPointerDown: onDown })}
|
|
76
|
+
{...(onUp !== undefined && { onPointerUp: onUp })}
|
|
62
77
|
onPointerLeave={onLeave}
|
|
63
78
|
onPointerCancel={onLeave}
|
|
64
79
|
/>
|
|
65
80
|
)
|
|
66
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Everything a FOCUSABLE overlay over indexed data takes besides its rect: the cursor's pointer and
|
|
85
|
+
* keyboard handlers plus the slider label/position. `CartesianChart`, `DualPanel`'s top pane and
|
|
86
|
+
* the banded kinds all wire it identically, so it is stated once.
|
|
87
|
+
*/
|
|
88
|
+
export function cursorSliderProps<T>(input: {
|
|
89
|
+
cursor: ChartCursor<T>
|
|
90
|
+
data: readonly T[]
|
|
91
|
+
getX: (d: T) => string
|
|
92
|
+
formatX: (key: string) => string
|
|
93
|
+
ariaLabel: string | undefined
|
|
94
|
+
}): Omit<HoverOverlayProps, 'width' | 'height'> {
|
|
95
|
+
const { cursor, data, getX, formatX, ariaLabel } = input
|
|
96
|
+
const { point } = cursor
|
|
97
|
+
return {
|
|
98
|
+
onMove: cursor.onPointerMove,
|
|
99
|
+
onDown: cursor.onPointerDown,
|
|
100
|
+
onUp: cursor.onPointerUp,
|
|
101
|
+
onLeave: cursor.onPointerLeave,
|
|
102
|
+
onKeyDown: cursor.onKeyDown,
|
|
103
|
+
onBlur: cursor.onBlur,
|
|
104
|
+
valueMax: Math.max(data.length - 1, 0),
|
|
105
|
+
...(point !== null && { valueNow: data.indexOf(point), valueText: formatX(getX(point)) }),
|
|
106
|
+
...(ariaLabel !== undefined && { ariaLabel }),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createContext } from 'react'
|
|
2
|
+
import type { Dispatch } from 'react'
|
|
3
|
+
|
|
4
|
+
/** One frame claiming or releasing the card's header legend slot. */
|
|
5
|
+
export type LegendSlotAction = { type: 'claim' | 'release'; id: string }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The ONE owner of the legend-slot protocol (`docs/waves/PLAN.md` wave 3, P2-9): claimants in claim
|
|
9
|
+
* order, and the head owns the slot. A later claimant is denied simply by not being the head, and
|
|
10
|
+
* it takes over the moment everything ahead of it releases — no retry signal, no ownership mirrored
|
|
11
|
+
* in a ref. Returns the same array on a no-op so a repeated claim/release never re-renders the card.
|
|
12
|
+
*/
|
|
13
|
+
export function legendSlotReducer(
|
|
14
|
+
claimants: readonly string[],
|
|
15
|
+
{ type, id }: LegendSlotAction,
|
|
16
|
+
): readonly string[] {
|
|
17
|
+
const held = claimants.includes(id)
|
|
18
|
+
if (type === 'claim') return held ? claimants : [...claimants, id]
|
|
19
|
+
return held ? claimants.filter((c) => c !== id) : claimants
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `ChartCard`'s internal context: `inCard` (a frame inside a card treats its computed height as a
|
|
24
|
+
* MINIMUM and grows into the card body), the legend slot — the empty header-row box a chart will portal its legend into
|
|
25
|
+
* (`docs/CHARTS-SPEC.md` §5, Legend), so a card has one chrome band, not two — and `short`, the same
|
|
26
|
+
* `measuredHeight < CARD_SHORT_HEIGHT` flag `ChartCard` already computes for the header-fold law
|
|
27
|
+
* (`data-basalt-card-short`), threaded through so `ChartFrame` can fold its header-slot legend to
|
|
28
|
+
* 'dots' before it eats the plot floor (`docs/CHARTS-SPEC.md` §5, wave 11). `null`/`false` until the slot
|
|
29
|
+
* mounts, and outside any card. Deliberately not exported from any barrel.
|
|
30
|
+
*
|
|
31
|
+
* `legendClaimants` is {@link legendSlotReducer}'s queue (its head owns the slot) and
|
|
32
|
+
* `dispatchLegendSlot` feeds it. Outside any card there is no slot, so nothing ever claims.
|
|
33
|
+
*/
|
|
34
|
+
export const ChartCardContext = createContext<{
|
|
35
|
+
legendSlot: HTMLDivElement | null
|
|
36
|
+
inCard: boolean
|
|
37
|
+
short: boolean
|
|
38
|
+
legendClaimants: readonly string[]
|
|
39
|
+
dispatchLegendSlot: Dispatch<LegendSlotAction>
|
|
40
|
+
}>({
|
|
41
|
+
legendSlot: null,
|
|
42
|
+
inCard: false,
|
|
43
|
+
short: false,
|
|
44
|
+
legendClaimants: [],
|
|
45
|
+
dispatchLegendSlot: () => {},
|
|
46
|
+
})
|