basalt-ui 1.30.2 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -0
- package/MIGRATING.md +172 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +1 -1
- package/configs/oxlint-plugin.js +254 -4
- package/configs/oxlint-plugin.test.ts +145 -2
- package/configs/oxlint.json +2 -1
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +41 -43
- package/dist/agent-chat/thread-workspace.d.ts +2 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +3 -4
- package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
- package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
- package/dist/agent-chat/use-keyboard-inset.js +34 -0
- package/dist/charts/cursor/touch-pin.d.ts +45 -0
- package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
- package/dist/charts/cursor/touch-pin.js +79 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
- package/dist/charts/hooks/use-discrete-cursor.js +198 -0
- package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
- package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
- package/dist/charts/hooks/use-hysteresis.js +12 -0
- package/dist/charts/hooks/useBandPlot.d.ts +7 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +62 -18
- package/dist/charts/hooks/useChartCursor.d.ts +40 -11
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +102 -11
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +0 -10
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +54 -41
- package/dist/charts/kinds/Donut.d.ts +9 -3
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +110 -48
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +145 -131
- package/dist/charts/kinds/Heatmap.d.ts +7 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +37 -16
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +83 -68
- package/dist/charts/kinds/donut-layout.d.ts +47 -0
- package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
- package/dist/charts/kinds/donut-layout.js +52 -0
- package/dist/charts/layout/auto-margin.d.ts +7 -2
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +12 -3
- package/dist/charts/layout/end-labels.d.ts +64 -0
- package/dist/charts/layout/end-labels.d.ts.map +1 -0
- package/dist/charts/layout/end-labels.js +76 -0
- package/dist/charts/primitives/Axes.d.ts +32 -4
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +62 -16
- package/dist/charts/primitives/CartesianChart.d.ts +7 -6
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +271 -125
- package/dist/charts/primitives/ChartCard.d.ts +0 -14
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +74 -21
- package/dist/charts/primitives/ChartFrame.d.ts +28 -22
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +133 -34
- package/dist/charts/primitives/ChartLegend.d.ts +19 -15
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +212 -71
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +4 -3
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +37 -26
- package/dist/charts/primitives/HoverOverlay.module.css +13 -0
- package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/dist/charts/primitives/chart-card-context.d.ts +29 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +13 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +76 -29
- package/dist/charts/primitives/chart-layout.d.ts +112 -0
- package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-layout.js +175 -0
- package/dist/charts/primitives/chart-tier.d.ts +13 -11
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
- package/dist/charts/primitives/chart-tier.js +42 -13
- package/dist/charts/series.d.ts +4 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/utils/ticks.d.ts +27 -8
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +35 -11
- package/dist/common/errors.d.ts +11 -1
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +16 -2
- package/dist/common/hit-floor.d.ts +10 -0
- package/dist/common/hit-floor.d.ts.map +1 -0
- package/dist/common/hit-floor.js +6 -0
- package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
- package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/common/use-measured-widths.d.ts +37 -0
- package/dist/common/use-measured-widths.d.ts.map +1 -0
- package/dist/common/use-measured-widths.js +51 -0
- package/dist/common/use-media-query.d.ts +6 -0
- package/dist/common/use-media-query.d.ts.map +1 -0
- package/dist/common/use-media-query.js +40 -0
- package/dist/common/validate.d.ts +9 -0
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +9 -4
- package/dist/content/article-layout.d.ts +4 -2
- package/dist/content/article-layout.d.ts.map +1 -1
- package/dist/content/article-layout.js +43 -3
- package/dist/content/article-layout.module.css +45 -4
- package/dist/content/article-layout.module.css.d.ts +3 -0
- package/dist/content/code-block.d.ts.map +1 -1
- package/dist/content/code-block.js +40 -2
- package/dist/content/code-block.module.css +32 -2
- package/dist/content/prose.module.css +13 -8
- package/dist/controls/actions.d.ts +29 -6
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +158 -27
- package/dist/controls/actions.module.css +14 -1
- package/dist/controls/actions.module.css.d.ts +1 -0
- package/dist/controls/controls.module.css +8 -31
- package/dist/controls/enum-filter.js +2 -2
- package/dist/controls/filter-context.d.ts +10 -0
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-context.js +2 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +1 -0
- package/dist/controls/index.d.ts +1 -1
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +0 -2
- package/dist/controls/multi-select-filter.js +2 -2
- package/dist/controls/sync-button.module.css +1 -1
- package/dist/dashboard/section.d.ts.map +1 -1
- package/dist/dashboard/section.js +1 -0
- package/dist/dashboard/section.module.css +1 -26
- package/dist/dashboard/stat-card.d.ts +1 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +4 -1
- package/dist/dashboard/stat-card.module.css +6 -9
- package/dist/dashboard/stat-group.module.css +16 -4
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-header.d.ts +14 -4
- package/dist/dashboard/widget-header.d.ts.map +1 -1
- package/dist/dashboard/widget-header.js +23 -6
- package/dist/dashboard/widget-header.module.css +145 -133
- package/dist/dashboard/widget-header.module.css.d.ts +2 -0
- package/dist/data/data-table.d.ts +23 -0
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +339 -141
- package/dist/data/data-table.module.css +94 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +10 -0
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.js +3 -3
- package/dist/forms/form-layout.module.css +30 -5
- package/dist/forms/form-layout.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +8 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -2
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +22 -2
- package/dist/shell/app-brand.d.ts.map +1 -1
- package/dist/shell/app-brand.js +1 -0
- package/dist/shell/app-brand.module.css +2 -2
- package/dist/shell/app-header.module.css +18 -8
- package/dist/shell/app-main.module.css +54 -3
- package/dist/shell/app-main.module.css.d.ts +1 -0
- package/dist/shell/app-mobile-nav.module.css +16 -4
- package/dist/shell/app-sidebar.module.css +4 -6
- package/dist/shell/index.d.ts +12 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +57 -16
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/page-aside.d.ts +22 -3
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +27 -13
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +10 -5
- package/dist/shell/page-bar.module.css +10 -34
- package/dist/shell/use-breakpoint.d.ts +3 -4
- package/dist/shell/use-breakpoint.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.js +3 -25
- package/dist/shell/use-size-class.d.ts +6 -0
- package/dist/shell/use-size-class.d.ts.map +1 -0
- package/dist/shell/use-size-class.js +19 -0
- package/dist/state/persisted-or-local.d.ts +4 -1
- package/dist/state/persisted-or-local.d.ts.map +1 -1
- package/dist/state/persisted-or-local.js +4 -3
- package/dist/state/persisted.d.ts +10 -0
- package/dist/state/persisted.d.ts.map +1 -1
- package/dist/state/persisted.js +44 -14
- package/dist/styles.css +41 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +11 -3
- package/dist/theme/controls.module.css +7 -0
- package/dist/theme/ctl-theme.d.ts +1 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/ctl-theme.js +12 -4
- package/dist/theme/floating.module.css +36 -0
- package/dist/theme/floating.module.css.d.ts +3 -0
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +34 -5
- package/dist/theme/nav-link.module.css +7 -0
- package/dist/tokens/index.d.ts +7 -16
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +23 -28
- package/dist/tokens/palette.d.ts +5 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +8 -30
- package/dist/tokens/size-classes.d.ts +40 -0
- package/dist/tokens/size-classes.d.ts.map +1 -0
- package/dist/tokens/size-classes.js +29 -0
- package/dist/tokens.css +8 -4
- package/llms.txt +3 -3
- package/package.json +17 -17
- package/src/agent-chat/composer.tsx +74 -47
- package/src/agent-chat/thread-workspace.tsx +8 -11
- package/src/agent-chat/use-keyboard-inset.ts +68 -0
- package/src/charts/cursor/touch-pin.ts +138 -0
- package/src/charts/hooks/use-discrete-cursor.ts +335 -0
- package/src/charts/hooks/use-hysteresis.ts +26 -0
- package/src/charts/hooks/useBandPlot.ts +90 -27
- package/src/charts/hooks/useChartCursor.ts +211 -26
- package/src/charts/index.ts +0 -8
- package/src/charts/kinds/BandStrip.tsx +11 -1
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +19 -4
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +15 -3
- package/src/charts/kinds/donut-layout.ts +82 -0
- package/src/charts/layout/auto-margin.ts +24 -5
- package/src/charts/layout/end-labels.ts +135 -0
- package/src/charts/primitives/Axes.tsx +112 -21
- package/src/charts/primitives/CartesianChart.tsx +282 -68
- package/src/charts/primitives/ChartCard.tsx +141 -48
- package/src/charts/primitives/ChartFrame.tsx +243 -60
- package/src/charts/primitives/ChartLegend.tsx +257 -73
- package/src/charts/primitives/ChartTooltip.tsx +8 -3
- package/src/charts/primitives/Crosshair.tsx +2 -2
- package/src/charts/primitives/HoverOverlay.module.css +13 -0
- package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/src/charts/primitives/HoverOverlay.tsx +15 -4
- package/src/charts/primitives/chart-card-context.ts +37 -0
- package/src/charts/primitives/chart-frame-layout.ts +214 -177
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/series.ts +4 -1
- package/src/charts/utils/ticks.ts +95 -19
- package/src/common/errors.ts +25 -1
- package/src/common/hit-floor.ts +9 -0
- package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
- package/src/common/use-measured-widths.ts +88 -0
- package/src/common/use-media-query.ts +56 -0
- package/src/common/validate.ts +17 -4
- package/src/content/article-layout.module.css +45 -4
- package/src/content/article-layout.module.css.d.ts +3 -0
- package/src/content/article-layout.tsx +60 -5
- package/src/content/code-block.module.css +32 -2
- package/src/content/code-block.tsx +47 -3
- package/src/content/prose.module.css +13 -8
- package/src/controls/actions.module.css +14 -1
- package/src/controls/actions.module.css.d.ts +1 -0
- package/src/controls/actions.tsx +223 -75
- package/src/controls/controls.module.css +8 -31
- package/src/controls/enum-filter.tsx +2 -2
- package/src/controls/filter-context.tsx +11 -0
- package/src/controls/filter-pill.tsx +1 -0
- package/src/controls/index.ts +0 -1
- package/src/controls/multi-select-filter.tsx +2 -2
- package/src/controls/sync-button.module.css +1 -1
- package/src/dashboard/section.module.css +1 -26
- package/src/dashboard/section.tsx +1 -0
- package/src/dashboard/stat-card.module.css +6 -9
- package/src/dashboard/stat-card.tsx +6 -3
- package/src/dashboard/stat-group.module.css +16 -4
- package/src/dashboard/widget-grid.tsx +7 -6
- package/src/dashboard/widget-header.module.css +145 -133
- package/src/dashboard/widget-header.module.css.d.ts +2 -0
- package/src/dashboard/widget-header.tsx +45 -11
- package/src/data/data-table.module.css +94 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +503 -164
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +30 -5
- package/src/forms/form-layout.module.css.d.ts +1 -0
- package/src/forms/form-layout.tsx +12 -10
- package/src/guard/index.ts +183 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/provider/index.tsx +53 -1
- package/src/shell/app-brand.module.css +2 -2
- package/src/shell/app-brand.tsx +1 -0
- package/src/shell/app-header.module.css +18 -8
- package/src/shell/app-main.module.css +54 -3
- package/src/shell/app-main.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.module.css +16 -4
- package/src/shell/app-sidebar.module.css +4 -6
- package/src/shell/index.tsx +200 -129
- package/src/shell/mobile-nav-model.ts +1 -1
- package/src/shell/page-aside.tsx +70 -21
- package/src/shell/page-bar.module.css +10 -34
- package/src/shell/page-bar.tsx +10 -5
- package/src/shell/use-breakpoint.ts +9 -45
- package/src/shell/use-size-class.ts +31 -0
- package/src/state/persisted-or-local.ts +10 -4
- package/src/state/persisted.ts +76 -17
- package/src/styles.css +41 -0
- package/src/surfaces.ts +9 -1
- package/src/theme/controls.module.css +7 -0
- package/src/theme/ctl-theme.tsx +15 -3
- package/src/theme/floating.module.css +36 -0
- package/src/theme/floating.module.css.d.ts +3 -0
- package/src/theme/index.ts +46 -8
- package/src/theme/nav-link.module.css +7 -0
- package/src/tokens/index.ts +20 -30
- package/src/tokens/palette.ts +6 -28
- package/src/tokens/size-classes.ts +46 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
- /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
|
@@ -1,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'
|
|
@@ -357,7 +357,13 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
357
357
|
|
|
358
358
|
return (
|
|
359
359
|
<>
|
|
360
|
-
<svg
|
|
360
|
+
<svg
|
|
361
|
+
ref={band.svgRef}
|
|
362
|
+
width={plot.width}
|
|
363
|
+
height={plot.height}
|
|
364
|
+
// `touch-action` on the plot `<svg>`: Chrome ignores it on the overlay `<rect>` (`R2C-2`).
|
|
365
|
+
style={{ touchAction: 'pan-y' }}
|
|
366
|
+
>
|
|
361
367
|
<defs>
|
|
362
368
|
<HatchPattern
|
|
363
369
|
id={hatchId}
|
|
@@ -382,12 +388,16 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
382
388
|
scale={scale}
|
|
383
389
|
tickValues={band.tickValues}
|
|
384
390
|
tickFormat={(v) => formatX(String(v))}
|
|
391
|
+
anchorTerminals={band.xAnchorTerminals}
|
|
392
|
+
{...(band.xWrapWidth !== undefined && { wrapWidth: band.xWrapWidth })}
|
|
385
393
|
/>
|
|
386
394
|
|
|
387
395
|
<HoverOverlay
|
|
388
396
|
width={plotWidth}
|
|
389
397
|
height={stripHeight}
|
|
390
398
|
onMove={cursor.onPointerMove}
|
|
399
|
+
onDown={cursor.onPointerDown}
|
|
400
|
+
onUp={cursor.onPointerUp}
|
|
391
401
|
onLeave={cursor.onPointerLeave}
|
|
392
402
|
onKeyDown={cursor.onKeyDown}
|
|
393
403
|
onBlur={cursor.onBlur}
|