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
|
@@ -20,7 +20,13 @@ import type { ReactNode, RefObject } from 'react'
|
|
|
20
20
|
import type { ChartMargin } from '../../tokens'
|
|
21
21
|
import type { CursorResolution } from '../cursor/resolve'
|
|
22
22
|
import { autoMargin } from '../layout/auto-margin'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
isTightClass,
|
|
25
|
+
planXLabels,
|
|
26
|
+
resolveMarginFloor,
|
|
27
|
+
resolveYPlacement,
|
|
28
|
+
} from '../primitives/chart-layout'
|
|
29
|
+
import { useChartContainerClass, useChartMetrics } from '../primitives/chart-tier'
|
|
24
30
|
import { maxTextWidth } from '../utils/measure-text'
|
|
25
31
|
import { smartTicks, xLabelPxFor } from '../utils/ticks'
|
|
26
32
|
import { useChartCursor } from './useChartCursor'
|
|
@@ -150,6 +156,13 @@ export type BandPlot<T> = {
|
|
|
150
156
|
/** Drawn width of one band — the pitch minus a 1px separator, floored at 1. */
|
|
151
157
|
bandWidth: number
|
|
152
158
|
tickValues: string[]
|
|
159
|
+
/** Where the left axis paints: inside the plot (compact), not at all (micro), else outside. An
|
|
160
|
+
* explicit `margin.left` keeps it outside. Same law as `CartesianChart`. */
|
|
161
|
+
yPlacement: 'outside' | 'inside' | 'none'
|
|
162
|
+
/** Terminal x labels anchor inward (compact/micro). */
|
|
163
|
+
xAnchorTerminals: boolean
|
|
164
|
+
/** Width to wrap x labels to; undefined when they do not wrap. */
|
|
165
|
+
xWrapWidth: number | undefined
|
|
153
166
|
svgRef: RefObject<SVGSVGElement | null>
|
|
154
167
|
tooltipAnchor: CursorAnchor | null
|
|
155
168
|
showTooltip: boolean
|
|
@@ -181,18 +194,44 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
181
194
|
// two things `CartesianChart` reads off it: the tick font its gutters are MEASURED at (the same
|
|
182
195
|
// one `AxisBottomDate` paints — measured labels must be the painted labels, §1) and the tightened
|
|
183
196
|
// margin FLOORS (`docs/CHARTS-SPEC.md` §8).
|
|
184
|
-
const tier =
|
|
197
|
+
const tier = useChartMetrics()
|
|
198
|
+
const containerClass = useChartContainerClass()
|
|
199
|
+
const tight = isTightClass(containerClass)
|
|
185
200
|
|
|
186
201
|
const hasLeftAxis = leftLabels !== undefined && leftLabels.length > 0
|
|
202
|
+
// Ladder step 5: inside labels reserve no gutter. An explicit `margin.left` keeps them outside.
|
|
203
|
+
const yPlacement = resolveYPlacement(containerClass, {
|
|
204
|
+
hasLeftAxis,
|
|
205
|
+
marginOverrideLeft: marginOverride?.left,
|
|
206
|
+
})
|
|
187
207
|
const xLabelsAll = useMemo(() => data.map((d) => formatX(getX(d))), [data, getX, formatX])
|
|
188
208
|
|
|
209
|
+
// Ladder step 4 (wrap; a band axis cannot rotate, so it thins instead). Sized from the token
|
|
210
|
+
// floors because the real gutters depend on the wrapped line count. A band chart's x labels are
|
|
211
|
+
// always categorical by construction (one slot per datum) — no whitespace heuristic needed.
|
|
212
|
+
const xPlan = useMemo(
|
|
213
|
+
() =>
|
|
214
|
+
planXLabels({
|
|
215
|
+
labels: xLabelsAll,
|
|
216
|
+
plotWidth: width - tier.margin.left - tier.margin.right,
|
|
217
|
+
fontPx: tier.axisFont,
|
|
218
|
+
categorical: tight,
|
|
219
|
+
}),
|
|
220
|
+
[xLabelsAll, width, tier.margin, tier.axisFont, tight],
|
|
221
|
+
)
|
|
222
|
+
const xWrapWidth = xPlan.wrap ? xPlan.wrapPx : undefined
|
|
223
|
+
|
|
189
224
|
// ── Pass 1: gutters, from the labels that will actually be painted ──────────────────────────
|
|
190
225
|
const margin = useMemo<ChartMargin>(() => {
|
|
191
226
|
const measured = autoMargin({
|
|
192
|
-
...(leftLabels !== undefined && { left: leftLabels }),
|
|
227
|
+
...(leftLabels !== undefined && { left: yPlacement === 'outside' ? leftLabels : [] }),
|
|
193
228
|
bottom: xLabelsAll,
|
|
194
229
|
fontPx: tier.axisFont,
|
|
195
|
-
floor
|
|
230
|
+
// An inside-placed left axis paints IN the plot, so the outside label-width floor is dead
|
|
231
|
+
// space there — same law as `CartesianChart`'s `marginInput.floor`.
|
|
232
|
+
floor: resolveMarginFloor(yPlacement, tier.margin),
|
|
233
|
+
anchorTerminals: tight,
|
|
234
|
+
bottomLines: xPlan.lines,
|
|
196
235
|
...(marginOverride !== undefined && { override: marginOverride }),
|
|
197
236
|
})
|
|
198
237
|
if (marginOverride?.left !== undefined && marginOverride.right !== undefined) return measured
|
|
@@ -201,24 +240,38 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
201
240
|
// A band axis puts its FIRST tick on the plot's left edge, so the left gutter needs the same
|
|
202
241
|
// half-label law `autoMargin` already applies on the right. `autoMargin` cannot know that —
|
|
203
242
|
// its left law measures a left AXIS, which a band plot may not have.
|
|
204
|
-
const left = hasLeftAxis ? measured.left : Math.max(measured.left, halfXLabel)
|
|
243
|
+
const left = hasLeftAxis || tight ? measured.left : Math.max(measured.left, halfXLabel)
|
|
205
244
|
return {
|
|
206
245
|
...measured,
|
|
207
246
|
...(marginOverride?.left === undefined && {
|
|
208
|
-
left:
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
left:
|
|
248
|
+
hasLeftAxis || tight
|
|
249
|
+
? left
|
|
250
|
+
: capTerminalGutter(left, width, TERMINAL_GUTTER_MAX_FRACTION.left, tier.margin.left),
|
|
211
251
|
}),
|
|
212
252
|
...(marginOverride?.right === undefined && {
|
|
213
|
-
right:
|
|
214
|
-
measured.right
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
253
|
+
right: tight
|
|
254
|
+
? measured.right
|
|
255
|
+
: capTerminalGutter(
|
|
256
|
+
measured.right,
|
|
257
|
+
width,
|
|
258
|
+
TERMINAL_GUTTER_MAX_FRACTION.right,
|
|
259
|
+
tier.margin.right,
|
|
260
|
+
),
|
|
219
261
|
}),
|
|
220
262
|
}
|
|
221
|
-
}, [
|
|
263
|
+
}, [
|
|
264
|
+
leftLabels,
|
|
265
|
+
xLabelsAll,
|
|
266
|
+
marginOverride,
|
|
267
|
+
hasLeftAxis,
|
|
268
|
+
width,
|
|
269
|
+
tier.axisFont,
|
|
270
|
+
tier.margin,
|
|
271
|
+
tight,
|
|
272
|
+
yPlacement,
|
|
273
|
+
xPlan.lines,
|
|
274
|
+
])
|
|
222
275
|
|
|
223
276
|
const plotWidth = Math.max(width - margin.left - margin.right, 0)
|
|
224
277
|
|
|
@@ -249,12 +302,17 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
249
302
|
[scale],
|
|
250
303
|
)
|
|
251
304
|
|
|
305
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
252
306
|
const cursor = useChartCursor<T>({
|
|
253
307
|
data: bands,
|
|
254
308
|
chartId,
|
|
255
309
|
getKey: getX,
|
|
256
310
|
xScale: bandCenter,
|
|
257
311
|
marginLeft: margin.left,
|
|
312
|
+
// Both callers (`BandStrip`, `MirroredBars`) mount exactly one `<svg ref={band.svgRef}>`, so
|
|
313
|
+
// it doubles as the tap-outside boundary (`docs/CHARTS-SPEC.md` §4, Tooltip) with no separate
|
|
314
|
+
// passthrough option — reusing the ref this hook already returns, not a new one.
|
|
315
|
+
boundaryRef: svgRef,
|
|
258
316
|
...(cursorResolution !== undefined && { resolution: cursorResolution }),
|
|
259
317
|
})
|
|
260
318
|
|
|
@@ -262,20 +320,19 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
262
320
|
// same measured-spacing law `CartesianChart` applies (`docs/CHARTS-SPEC.md` §1). `keys` (not
|
|
263
321
|
// `xLabelsAll`) because ticks are chosen from the post-fold key list.
|
|
264
322
|
const xLabelPx = useMemo(
|
|
265
|
-
() =>
|
|
266
|
-
|
|
323
|
+
() =>
|
|
324
|
+
xWrapWidth === undefined ? xLabelPxFor(keys.map(formatX), tier.axisFont) : xPlan.labelPx,
|
|
325
|
+
[keys, formatX, tier.axisFont, xWrapWidth, xPlan.labelPx],
|
|
267
326
|
)
|
|
268
327
|
|
|
269
|
-
const tickValues = useMemo(
|
|
270
|
-
()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
]
|
|
275
|
-
|
|
276
|
-
)
|
|
328
|
+
const tickValues = useMemo(() => {
|
|
329
|
+
if (xTickValues !== undefined) return [...xTickValues(keys, plotWidth)]
|
|
330
|
+
// Micro: the two terminals only.
|
|
331
|
+
if (containerClass === 'micro')
|
|
332
|
+
return keys.length > 1 ? [keys[0]!, keys[keys.length - 1]!] : [...keys]
|
|
333
|
+
return [...smartTicks(keys, plotWidth, xLabelPx, tight)]
|
|
334
|
+
}, [keys, plotWidth, xTickValues, xLabelPx, containerClass, tight])
|
|
277
335
|
|
|
278
|
-
const svgRef = useRef<SVGSVGElement>(null)
|
|
279
336
|
const point = cursor.point
|
|
280
337
|
const crosshairX = point === null ? null : (bandCenter(getX(point)) ?? null)
|
|
281
338
|
|
|
@@ -284,7 +341,10 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
284
341
|
// no pointer under it, so it is always positioned via the anchored (crosshair) path.
|
|
285
342
|
const isFollowerRender = !cursor.isSource && cfg?.onFollow === true
|
|
286
343
|
const showTooltip = tooltip !== false && point !== null && (cursor.isSource || isFollowerRender)
|
|
287
|
-
|
|
344
|
+
// An unset `follow` also anchors on touch (spec §5) — same law as `CartesianChart`'s
|
|
345
|
+
// `useAnchoredPosition`; an EXPLICIT `follow` always wins over that default.
|
|
346
|
+
const useAnchored =
|
|
347
|
+
cfg?.follow === false || (cfg?.follow === undefined && cursor.isTouch) || isFollowerRender
|
|
288
348
|
const anchorX = useAnchored ? crosshairX : null
|
|
289
349
|
const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
|
|
290
350
|
const tooltipAnchor =
|
|
@@ -307,6 +367,9 @@ export function useBandPlot<T>(input: UseBandPlotInput<T>): BandPlot<T> {
|
|
|
307
367
|
step,
|
|
308
368
|
bandWidth,
|
|
309
369
|
tickValues,
|
|
370
|
+
yPlacement,
|
|
371
|
+
xAnchorTerminals: tight,
|
|
372
|
+
xWrapWidth,
|
|
310
373
|
svgRef,
|
|
311
374
|
tooltipAnchor,
|
|
312
375
|
showTooltip,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { localPoint } from '@visx/event'
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
-
import type { KeyboardEvent, PointerEvent } from 'react'
|
|
3
|
+
import type { KeyboardEvent, PointerEvent, RefObject } from 'react'
|
|
4
4
|
import type { CursorResolution } from '../cursor/resolve'
|
|
5
5
|
import { buildDomainIndex, classifyDomain, resolveCursorPoint } from '../cursor/resolve'
|
|
6
6
|
import { useCursorState, useCursorStore } from '../cursor/scope'
|
|
7
|
+
import type { CursorState } from '../cursor/store'
|
|
8
|
+
import { useDismissOnOutside, useTouchPin } from '../cursor/touch-pin'
|
|
7
9
|
|
|
8
10
|
/** Viewport-space pointer anchor the tooltip positions against. */
|
|
9
11
|
export type CursorAnchor = { x: number; y: number }
|
|
@@ -15,23 +17,45 @@ export type ChartCursor<T> = {
|
|
|
15
17
|
isSource: boolean
|
|
16
18
|
/** Latest pointer position in viewport coords, or null when not hovered. */
|
|
17
19
|
anchor: CursorAnchor | null
|
|
20
|
+
/**
|
|
21
|
+
* True when the interaction that most recently placed THIS chart's cursor had
|
|
22
|
+
* `event.pointerType !== 'mouse'` (touch or pen). Callers fold it into the anchored-tooltip
|
|
23
|
+
* condition (`tooltip.follow` defaulting to `false` on touch, docs/CHARTS-SPEC.md §4) — a follower's own
|
|
24
|
+
* anchoring is unaffected, since a follower is never the one whose pointer set this flag.
|
|
25
|
+
*/
|
|
26
|
+
isTouch: boolean
|
|
18
27
|
onPointerMove: (event: PointerEvent<SVGRectElement>) => void
|
|
19
|
-
|
|
28
|
+
/** Coarse-pointer equivalent of hover: resolves the nearest point and shows the tooltip
|
|
29
|
+
* immediately, with no preceding `pointermove` — a plain tap otherwise fires only
|
|
30
|
+
* `pointerdown`/`pointerup`. The press is PROVISIONAL: it shows the tooltip but does not commit a
|
|
31
|
+
* pin, which only `onPointerUp` does. Also what MOVES an existing touch pin to a new tap elsewhere
|
|
32
|
+
* in the same chart (fine pointers ignore it; `onPointerMove` already covers hover for them). */
|
|
33
|
+
onPointerDown: (event: PointerEvent<SVGRectElement>) => void
|
|
34
|
+
/** Commits the provisional touch press — the same `pointerId` as the matching `onPointerDown` —
|
|
35
|
+
* so the pin survives the lift. A `pointerup` for any other pointer is ignored. */
|
|
36
|
+
onPointerUp: (event: PointerEvent<SVGRectElement>) => void
|
|
37
|
+
/** `pointerleave`/`pointercancel`, sharing one handler. A still-UNCOMMITTED touch press cancelled
|
|
38
|
+
* here is the browser taking the gesture (a scroll) and clears; once committed, touch no-ops so a
|
|
39
|
+
* stray `pointercancel` cannot undo the pin. A fine pointer always clears. */
|
|
40
|
+
onPointerLeave: (event: PointerEvent<SVGRectElement>) => void
|
|
20
41
|
onKeyDown: (event: KeyboardEvent<SVGRectElement>) => void
|
|
21
42
|
onBlur: () => void
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
/**
|
|
25
46
|
* Wires a chart into the ambient cursor store: snaps the pointer to the nearest own point,
|
|
26
|
-
* broadcasts it, and reads back a sibling's broadcast through domain-aware resolution
|
|
27
|
-
*
|
|
28
|
-
*
|
|
47
|
+
* broadcasts it, and reads back a sibling's broadcast through domain-aware resolution — automatic,
|
|
48
|
+
* no `resolveKey` escape hatch.
|
|
49
|
+
*
|
|
50
|
+
* The tooltip anchor is coalesced through `requestAnimationFrame` (one local write per frame on a
|
|
51
|
+
* fast scrub); the BROADCAST is deduped instead, so a within-bucket scrub costs nothing and a real
|
|
52
|
+
* bucket change reaches siblings on the same tick.
|
|
29
53
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
54
|
+
* Touch model (`docs/CHARTS-SPEC.md` §4): a fine pointer keeps hovering. A coarse pointer
|
|
55
|
+
* (`onPointerDown`) resolves and shows the tooltip immediately, drag-scrubs via `onPointerMove`,
|
|
56
|
+
* and COMMITS a pin on `onPointerUp` — `onPointerLeave` (also wired to `pointercancel`) no-ops once
|
|
57
|
+
* committed. A press the browser turns into a scroll clears instead. The pin moves on a fresh tap,
|
|
58
|
+
* clears on a `document` tap outside `boundaryRef`, on any scroll, or on Escape.
|
|
35
59
|
*/
|
|
36
60
|
export function useChartCursor<T>({
|
|
37
61
|
data,
|
|
@@ -40,6 +64,7 @@ export function useChartCursor<T>({
|
|
|
40
64
|
xScale,
|
|
41
65
|
marginLeft,
|
|
42
66
|
resolution = 'nearest',
|
|
67
|
+
boundaryRef,
|
|
43
68
|
}: {
|
|
44
69
|
data: readonly T[]
|
|
45
70
|
chartId: string
|
|
@@ -53,10 +78,38 @@ export function useChartCursor<T>({
|
|
|
53
78
|
* series keyed by its Monday, a monthly series keyed by its 1st) — see {@link CursorResolution}.
|
|
54
79
|
*/
|
|
55
80
|
resolution?: CursorResolution
|
|
81
|
+
/**
|
|
82
|
+
* Element a touch tap OUTSIDE dismisses the pin against (`docs/CHARTS-SPEC.md` §4). One
|
|
83
|
+
* capture-phase `document` `pointerdown` listener clears the pin when the target falls outside
|
|
84
|
+
* this ref — added on pinning, removed on unpin/unmount. Omit to skip outside-dismiss (a tap
|
|
85
|
+
* elsewhere in the chart and Escape still work); never throws for a missing ref.
|
|
86
|
+
*/
|
|
87
|
+
boundaryRef?: RefObject<Element | null>
|
|
56
88
|
}): ChartCursor<T> {
|
|
57
89
|
const store = useCursorStore()
|
|
58
90
|
const cursor = useCursorState()
|
|
59
91
|
const [anchor, setAnchor] = useState<CursorAnchor | null>(null)
|
|
92
|
+
// Whether the interaction that most recently placed THIS chart's cursor was coarse (touch/pen).
|
|
93
|
+
// Branched per EVENT (`event.pointerType`), never a static media query — a hybrid device can
|
|
94
|
+
// carry both inputs, and a media query can't tell which one just fired.
|
|
95
|
+
const [isTouch, setIsTouch] = useState(false)
|
|
96
|
+
// Whether THIS chart currently holds a COMMITTED touch/pen pin — deliberately separate from
|
|
97
|
+
// `isTouch` above, which a subsequent fine-pointer move resets to `false` (that IS the bug this
|
|
98
|
+
// guards against) and which `clear()` never touches. Set on a successful `onPointerUp` commit,
|
|
99
|
+
// cleared by `clear()` OR by losing source ownership — mirrors `useDiscreteCursor`'s `pinned`
|
|
100
|
+
// state, kept as a ref here since nothing needs to re-render off it directly.
|
|
101
|
+
const pinnedRef = useRef(false)
|
|
102
|
+
// A SIBLING chart stealing the shared cursor source (its own touch tap) never calls THIS chart's
|
|
103
|
+
// `clear()` — nothing here observes that tap directly. Left unguarded, `pinnedRef` stayed `true`
|
|
104
|
+
// forever once set, so a later mouse hover on this chart — after it legitimately re-claims
|
|
105
|
+
// `source` — read a stale pin and froze the crosshair (`resolveNearestAt`'s
|
|
106
|
+
// `pointerType === 'mouse' && pinnedRef.current && store.get().source === chartId` guard blocking
|
|
107
|
+
// a hover that was never actually still pinned). Reset the moment this chart is no longer the
|
|
108
|
+
// cursor's source — the one signal that unambiguously means whatever touch pin this chart held
|
|
109
|
+
// is gone, stolen or not.
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (cursor.source !== chartId) pinnedRef.current = false
|
|
112
|
+
}, [cursor.source, chartId])
|
|
60
113
|
|
|
61
114
|
// Accessors read through refs so the pointer callbacks stay referentially stable: the natural
|
|
62
115
|
// call style is an inline arrow, fresh every render, and re-creating these would re-bind the
|
|
@@ -76,9 +129,18 @@ export function useChartCursor<T>({
|
|
|
76
129
|
|
|
77
130
|
const frameRef = useRef<number | null>(null)
|
|
78
131
|
const pendingRef = useRef<CursorAnchor | null>(null)
|
|
132
|
+
// `pointermove` floods far faster than one nearest-point search is worth — an untethered scrub
|
|
133
|
+
// can fire dozens of native events between paints. `moveFrameRef`/`pendingMoveRef` coalesce the
|
|
134
|
+
// O(n) search itself (not just the anchor, which `frameRef`/`pendingRef` already batch) to one
|
|
135
|
+
// per animation frame — see `onPointerMove` below.
|
|
136
|
+
const moveFrameRef = useRef<number | null>(null)
|
|
137
|
+
const pendingMoveRef = useRef<{ px: number; anchor: CursorAnchor; pointerType: string } | null>(
|
|
138
|
+
null,
|
|
139
|
+
)
|
|
79
140
|
useEffect(
|
|
80
141
|
() => () => {
|
|
81
142
|
if (frameRef.current !== null) cancelAnimationFrame(frameRef.current)
|
|
143
|
+
if (moveFrameRef.current !== null) cancelAnimationFrame(moveFrameRef.current)
|
|
82
144
|
// Release the shared cursor if this chart still owns it. Unmounting while hovered (a filter
|
|
83
145
|
// drops the chart, a tab switches) fires no leave/blur, so the store would keep pointing at
|
|
84
146
|
// a dead chartId — and any sibling whose domain resolves that stale key would paint a ghost
|
|
@@ -97,17 +159,29 @@ export function useChartCursor<T>({
|
|
|
97
159
|
})
|
|
98
160
|
}, [])
|
|
99
161
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (local === null) return
|
|
109
|
-
const px = local.x - marginLeft
|
|
162
|
+
// The provisional-press commit/revert machine (`docs/CHARTS-SPEC.md` §4) — shared with
|
|
163
|
+
// `useDiscreteCursor`. `getCommitted` snapshots the STORE's current value (not this chart's own
|
|
164
|
+
// `point`), because the shared cursor is page-wide: a committed pin can belong to a different
|
|
165
|
+
// chart than the one now taking a second, cancelled press.
|
|
166
|
+
const touchPin = useTouchPin<CursorState>({
|
|
167
|
+
getCommitted: () => store.get(),
|
|
168
|
+
setCommitted: (v) => store.set(v.key, v.source, v.kind),
|
|
169
|
+
})
|
|
110
170
|
|
|
171
|
+
// The nearest-point search plus the broadcast — shared by the immediate (tap) and throttled
|
|
172
|
+
// (move) resolve paths below, so they cannot drift apart. `px` is the plot-local x, already run
|
|
173
|
+
// through `localPoint` by the caller: a `requestAnimationFrame` boundary has no business holding
|
|
174
|
+
// a live `PointerEvent`, only the plain numbers it already read out of one.
|
|
175
|
+
const resolveNearestAt = useCallback(
|
|
176
|
+
(px: number, anchor: CursorAnchor, pointerType: string) => {
|
|
177
|
+
if (data.length === 0) return
|
|
178
|
+
// A committed touch/pen pin must not be silently overridden by a fine-pointer hover on a
|
|
179
|
+
// hybrid device (a touchscreen laptop with a mouse/trackpad) — mirrors `useDiscreteCursor`'s
|
|
180
|
+
// `pinned` guard on its own hover path. Gated on `store.get().source` (not the reactive
|
|
181
|
+
// `isSource` derived later in this hook) so a pin a SIBLING chart has since stolen no longer
|
|
182
|
+
// blocks this chart's own mouse hover just because this chart's stale `pinnedRef` is still
|
|
183
|
+
// true.
|
|
184
|
+
if (pointerType === 'mouse' && pinnedRef.current && store.get().source === chartId) return
|
|
111
185
|
let closest = data[0] as T
|
|
112
186
|
let minDist = Infinity
|
|
113
187
|
for (const d of data) {
|
|
@@ -118,11 +192,79 @@ export function useChartCursor<T>({
|
|
|
118
192
|
closest = d
|
|
119
193
|
}
|
|
120
194
|
}
|
|
121
|
-
|
|
122
|
-
scheduleAnchor(
|
|
195
|
+
setIsTouch(pointerType !== 'mouse')
|
|
196
|
+
scheduleAnchor(anchor)
|
|
123
197
|
store.set(getKeyRef.current(closest), chartId, kind)
|
|
124
198
|
},
|
|
125
|
-
[data,
|
|
199
|
+
[data, chartId, store, scheduleAnchor, kind],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Explicit reference node: `localPoint(event)` resolves it from `event.target`, which is the
|
|
203
|
+
// overlay rect only while the overlay is genuinely the topmost hit element. Passing
|
|
204
|
+
// `currentTarget` pins the coordinate space to the overlay regardless of what a kind draws above it.
|
|
205
|
+
const localX = useCallback(
|
|
206
|
+
(event: PointerEvent<SVGRectElement>): number | null => {
|
|
207
|
+
const local = localPoint(event.currentTarget, event.nativeEvent)
|
|
208
|
+
return local === null ? null : local.x - marginLeft
|
|
209
|
+
},
|
|
210
|
+
[marginLeft],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
// A tap resolves immediately — one discrete event, not a flood, so it needs no throttle.
|
|
214
|
+
const resolveImmediate = useCallback(
|
|
215
|
+
(event: PointerEvent<SVGRectElement>) => {
|
|
216
|
+
const px = localX(event)
|
|
217
|
+
if (px === null) return
|
|
218
|
+
resolveNearestAt(px, { x: event.clientX, y: event.clientY }, event.pointerType)
|
|
219
|
+
},
|
|
220
|
+
[localX, resolveNearestAt],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const onPointerMove = useCallback(
|
|
224
|
+
(event: PointerEvent<SVGRectElement>) => {
|
|
225
|
+
const px = localX(event)
|
|
226
|
+
if (px === null) return
|
|
227
|
+
pendingMoveRef.current = {
|
|
228
|
+
px,
|
|
229
|
+
anchor: { x: event.clientX, y: event.clientY },
|
|
230
|
+
pointerType: event.pointerType,
|
|
231
|
+
}
|
|
232
|
+
if (moveFrameRef.current !== null) return
|
|
233
|
+
moveFrameRef.current = requestAnimationFrame(() => {
|
|
234
|
+
moveFrameRef.current = null
|
|
235
|
+
const pending = pendingMoveRef.current
|
|
236
|
+
pendingMoveRef.current = null
|
|
237
|
+
if (pending !== null) resolveNearestAt(pending.px, pending.anchor, pending.pointerType)
|
|
238
|
+
})
|
|
239
|
+
},
|
|
240
|
+
[localX, resolveNearestAt],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// A tap fires `pointerdown` + `pointerup` with no `pointermove` in between, so hover's resolve
|
|
244
|
+
// path never runs for it — this is what makes a tap show anything at all. It also MOVES an
|
|
245
|
+
// existing touch pin: a fresh `pointerdown` elsewhere in the chart re-resolves and re-broadcasts
|
|
246
|
+
// exactly like the first one, with nothing extra to wire.
|
|
247
|
+
const onPointerDown = useCallback(
|
|
248
|
+
(event: PointerEvent<SVGRectElement>) => {
|
|
249
|
+
// A coarse press is PROVISIONAL: snapshot whatever is committed now (so a cancel can restore
|
|
250
|
+
// it) BEFORE the resolve below overwrites the store. A fine pointer never begins a press —
|
|
251
|
+
// its hover path has no pin to protect.
|
|
252
|
+
if (event.pointerType !== 'mouse') touchPin.begin(event.pointerId)
|
|
253
|
+
resolveImmediate(event)
|
|
254
|
+
},
|
|
255
|
+
[resolveImmediate, touchPin],
|
|
256
|
+
)
|
|
257
|
+
// A completed tap: the pin is now committed, so `onPointerLeave`'s touch no-op keeps it past the
|
|
258
|
+
// lift. Only the tracked pointer commits — a stray `pointerup` is ignored. `pointerType ===
|
|
259
|
+
// 'mouse'` is checked FIRST and unconditionally: some browsers alias a touch's `pointerId` to the
|
|
260
|
+
// same value (1) a mouse pointer uses, so without this an unrelated mouse `pointerup` could commit
|
|
261
|
+
// an abandoned touch press it never tracked (mirrors `useDiscreteCursor`'s identical guard).
|
|
262
|
+
const onPointerUp = useCallback(
|
|
263
|
+
(event: PointerEvent<SVGRectElement>) => {
|
|
264
|
+
if (event.pointerType === 'mouse') return
|
|
265
|
+
if (touchPin.commit(event.pointerId)) pinnedRef.current = true
|
|
266
|
+
},
|
|
267
|
+
[touchPin],
|
|
126
268
|
)
|
|
127
269
|
|
|
128
270
|
const clear = useCallback(() => {
|
|
@@ -130,12 +272,35 @@ export function useChartCursor<T>({
|
|
|
130
272
|
cancelAnimationFrame(frameRef.current)
|
|
131
273
|
frameRef.current = null
|
|
132
274
|
}
|
|
275
|
+
if (moveFrameRef.current !== null) {
|
|
276
|
+
cancelAnimationFrame(moveFrameRef.current)
|
|
277
|
+
moveFrameRef.current = null
|
|
278
|
+
}
|
|
133
279
|
pendingRef.current = null
|
|
280
|
+
pendingMoveRef.current = null
|
|
281
|
+
touchPin.reset()
|
|
282
|
+
pinnedRef.current = false
|
|
134
283
|
setAnchor(null)
|
|
135
284
|
// Only clear the SHARED cursor if this chart still owns it: moving fast from chart A to B lets
|
|
136
285
|
// A's leave fire after B's move, and an unconditional clear would wipe B's cursor.
|
|
137
286
|
if (store.get().source === chartId) store.set(null, null, null)
|
|
138
|
-
}, [store, chartId])
|
|
287
|
+
}, [store, chartId, touchPin])
|
|
288
|
+
|
|
289
|
+
// `pointerleave`/`pointercancel` share this one handler (`HoverOverlay` wires both to `onLeave`).
|
|
290
|
+
// A still-uncommitted touch press cancelled here is the browser taking the gesture (a scroll):
|
|
291
|
+
// `touchPin.cancel` restores whatever pin was committed before this press started (or leaves it
|
|
292
|
+
// clear when nothing was), rather than dropping a pin this press never owned. Once COMMITTED by
|
|
293
|
+
// touch it is a no-op — a stray `pointercancel` must not undo the pin the tap was meant to set.
|
|
294
|
+
// Escape's `clear()` in `onKeyDown` bypasses this guard entirely, by design: it is the one
|
|
295
|
+
// dismissal that must work regardless of pointer type.
|
|
296
|
+
const onPointerLeave = useCallback(
|
|
297
|
+
(event: PointerEvent<SVGRectElement>) => {
|
|
298
|
+
if (touchPin.cancel(event.pointerId)) return
|
|
299
|
+
if (isTouch) return
|
|
300
|
+
clear()
|
|
301
|
+
},
|
|
302
|
+
[isTouch, clear, touchPin],
|
|
303
|
+
)
|
|
139
304
|
|
|
140
305
|
const onKeyDown = useCallback(
|
|
141
306
|
(event: KeyboardEvent<SVGRectElement>) => {
|
|
@@ -169,13 +334,33 @@ export function useChartCursor<T>({
|
|
|
169
334
|
// always carries its own kind by construction.
|
|
170
335
|
const point =
|
|
171
336
|
cursor.key === null || cursor.kind !== kind ? null : resolveCursorPoint(index, cursor.key)
|
|
337
|
+
const isSource = cursor.source === chartId
|
|
338
|
+
|
|
339
|
+
// Tap-outside dismissal (spec §5): while THIS chart is both pinned by touch and still the cursor's
|
|
340
|
+
// source, a capture-phase `document` `pointerdown`/`scroll`/`keydown` (shared with
|
|
341
|
+
// `useDiscreteCursor`, see `touch-pin.ts`) clears it the moment the event target falls outside
|
|
342
|
+
// `boundaryRef`. Gating on `isSource` (not `isTouch` alone) means the listener follows the pin —
|
|
343
|
+
// a sibling chart stealing the source on its own touch tap detaches this one instead of leaving a
|
|
344
|
+
// second stale listener alive.
|
|
345
|
+
const isInsideBoundary = useCallback(
|
|
346
|
+
(target: Node): boolean => boundaryRef?.current?.contains(target) === true,
|
|
347
|
+
[boundaryRef],
|
|
348
|
+
)
|
|
349
|
+
useDismissOnOutside({
|
|
350
|
+
enabled: isTouch && isSource && boundaryRef !== undefined,
|
|
351
|
+
isInside: isInsideBoundary,
|
|
352
|
+
onDismiss: clear,
|
|
353
|
+
})
|
|
172
354
|
|
|
173
355
|
return {
|
|
174
356
|
point,
|
|
175
|
-
isSource
|
|
357
|
+
isSource,
|
|
176
358
|
anchor,
|
|
359
|
+
isTouch,
|
|
177
360
|
onPointerMove,
|
|
178
|
-
|
|
361
|
+
onPointerDown,
|
|
362
|
+
onPointerUp,
|
|
363
|
+
onPointerLeave,
|
|
179
364
|
onKeyDown,
|
|
180
365
|
onBlur: clear,
|
|
181
366
|
}
|
package/src/charts/index.ts
CHANGED
|
@@ -84,14 +84,6 @@ export {
|
|
|
84
84
|
type ResolvedChartState,
|
|
85
85
|
resolveChartState,
|
|
86
86
|
} from './primitives/ChartPending'
|
|
87
|
-
export {
|
|
88
|
-
useChartTier,
|
|
89
|
-
useChartTierMetrics,
|
|
90
|
-
chartTierMetrics,
|
|
91
|
-
resolveChartTier,
|
|
92
|
-
type ChartTier,
|
|
93
|
-
type ChartTierMetrics,
|
|
94
|
-
} from './primitives/chart-tier'
|
|
95
87
|
export { Crosshair, SeriesDot } from './primitives/Crosshair'
|
|
96
88
|
export { HatchPattern, hatchFill, hatchSizeFor } from './primitives/HatchPattern'
|
|
97
89
|
export { ChartCard, type ChartCardProps, type ChartCardSlot } from './primitives/ChartCard'
|