@djangocfg/ui-tools 2.1.431 → 2.1.432
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/package.json +28 -14
- package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
- package/src/common/CodePanelHeader/index.ts +1 -0
- package/src/common/blocks/BlockError.tsx +46 -0
- package/src/common/blocks/index.ts +34 -0
- package/src/common/blocks/schemas.ts +143 -0
- package/src/common/blocks/types.ts +76 -0
- package/src/common/chips/README.md +67 -0
- package/src/common/chips/UrlChip.tsx +105 -0
- package/src/common/chips/index.ts +20 -0
- package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/common/link-preview/index.ts +12 -0
- package/src/common/link-preview/types.ts +44 -0
- package/src/common/tiptap/index.ts +10 -0
- package/src/common/tiptap/useNodeAttrs.ts +128 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
- package/src/tools/chat/README.md +88 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/constants.ts +9 -2
- package/src/tools/chat/core/extractFirstUrl.ts +45 -0
- package/src/tools/chat/core/reducer.ts +32 -8
- package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
- package/src/tools/chat/core/transport/mock.ts +4 -1
- package/src/tools/chat/hooks/useChat.ts +20 -20
- package/src/tools/chat/lazy.tsx +10 -0
- package/src/tools/chat/messages/Attachments.tsx +267 -42
- package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
- package/src/tools/chat/messages/MessageBubble.tsx +175 -31
- package/src/tools/chat/messages/MessageList.tsx +107 -21
- package/src/tools/chat/messages/ToolCalls.tsx +28 -13
- package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
- package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
- package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
- package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
- package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
- package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
- package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
- package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
- package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
- package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
- package/src/tools/chat/messages/index.ts +13 -0
- package/src/tools/chat/public.ts +5 -1
- package/src/tools/chat/shell/ChatRoot.tsx +29 -0
- package/src/tools/chat/styles/bubbleTokens.ts +18 -0
- package/src/tools/chat/styles/index.ts +1 -0
- package/src/tools/chat/types/attachment.ts +26 -2
- package/src/tools/chat/types/block.ts +56 -9
- package/src/tools/chat/types/blockSchemas.ts +197 -0
- package/src/tools/chat/types/config.ts +33 -0
- package/src/tools/chat/types/events.ts +14 -7
- package/src/tools/chat/types/index.ts +4 -2
- package/src/tools/chat/types/message.ts +7 -0
- package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
- package/src/tools/dev/Map/README.md +30 -345
- package/src/tools/dev/Map/cards/index.ts +6 -0
- package/src/tools/dev/Map/cards/types.ts +67 -0
- package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
- package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
- package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
- package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
- package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
- package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
- package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
- package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
- package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
- package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
- package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
- package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
- package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
- package/src/tools/dev/Map/components/MapControls.tsx +27 -0
- package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
- package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
- package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
- package/src/tools/dev/Map/components/index.ts +35 -1
- package/src/tools/dev/Map/geocode/index.ts +2 -0
- package/src/tools/dev/Map/geocode/photon.ts +265 -0
- package/src/tools/dev/Map/hooks/index.ts +37 -0
- package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
- package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
- package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
- package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
- package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
- package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
- package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
- package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
- package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
- package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
- package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
- package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
- package/src/tools/dev/Map/index.ts +59 -2
- package/src/tools/dev/Map/layers/index.ts +1 -0
- package/src/tools/dev/Map/layers/symbol.ts +93 -0
- package/src/tools/dev/Map/lazy.tsx +120 -0
- package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
- package/src/tools/dev/Map/styles/index.ts +62 -0
- package/src/tools/dev/Map/styles/terrain.ts +29 -0
- package/src/tools/dev/Map/types.ts +31 -3
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
- package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
- package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
- package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
- package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
- package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
- package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
- package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
- package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
- package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
- package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
- package/src/tools/forms/MarkdownEditor/README.md +84 -0
- package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
- package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
- package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
- package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
- package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
- package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
- package/src/tools/forms/MarkdownEditor/index.ts +19 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
- package/src/tools/forms/MarkdownEditor/styles.css +134 -2
- package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
- package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
- package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
- package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
- package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
- package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
- package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
- package/src/tools/forms/NotionEditor/README.md +109 -188
- package/src/tools/forms/NotionEditor/extensions.ts +15 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
- package/src/tools/forms/NotionEditor/styles.css +25 -0
- package/src/tools/forms/NotionEditor/types.ts +9 -0
- package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
- package/src/tools/input/SpeechRecognition/types.ts +10 -0
- package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
- package/src/tools/media/ImageViewer/README.md +9 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
- package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
- package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
- package/src/tools/media/ImageViewer/components/index.ts +2 -0
- package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
- package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
- package/src/tools/media/ImageViewer/types.ts +10 -0
- package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
- package/src/tools/media/ImageViewer/utils/index.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
- package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
- package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
- package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
- package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
- package/src/tools/visual/charts/SmoothLine/README.md +97 -0
- package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
- package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
- package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
- package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
- package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
- package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
- package/src/tools/visual/indicators/Fps/types.ts +1 -1
- package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
- package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// useSmoothSeries — the CANVAS streaming-chart engine shared by SmoothLine
|
|
2
|
+
// (generic) and by host-bespoke charts (e.g. cmdop's ECG, which keeps its own
|
|
3
|
+
// QRS geometry but reuses this scroll driver).
|
|
4
|
+
//
|
|
5
|
+
// THE CANVAS MODEL (Smoothie / uPlot / Grafana — one writer, one clock):
|
|
6
|
+
// every rAF frame we `clearRect` the whole canvas and redraw the ENTIRE line at
|
|
7
|
+
// its current wall-clock-scrolled X. The "new point arrived" and "everything
|
|
8
|
+
// scrolled" are the SAME per-frame draw — there is no second thing to fall out
|
|
9
|
+
// of sync, so the jump-then-catch-up is impossible BY CONSTRUCTION.
|
|
10
|
+
//
|
|
11
|
+
// WHY WE LEFT SVG-transform (the bug this rewrite kills):
|
|
12
|
+
// The old engine animated TWO independent things on TWO clocks that had to agree
|
|
13
|
+
// every frame:
|
|
14
|
+
// (1) the SVG path `d` — rebuilt in a React effect ONCE per data tick (≈1 Hz,
|
|
15
|
+
// jittery), in data-space; and
|
|
16
|
+
// (2) the `<g>` transform — animated EVERY rAF frame (60 Hz) off the wall clock.
|
|
17
|
+
// The on-screen position of a point was `pathX(point) + transformX(frame)`. Those
|
|
18
|
+
// two writes land in DIFFERENT frames on DIFFERENT clocks — at the data-tick seam
|
|
19
|
+
// the path jumped one slot left while the transform's phase-reset landed a frame
|
|
20
|
+
// early/late → a one-frame double-shift (JUMP) then the transform gliding back
|
|
21
|
+
// (slow CATCH-UP). That was the user's "jump then slowly catch up to the right".
|
|
22
|
+
// No amount of EMA / delay / clipPath tuning could close it because the root was
|
|
23
|
+
// the two-writer/two-clock split, not a tuning bug. Canvas removes the split.
|
|
24
|
+
//
|
|
25
|
+
// WHAT WE KEEP (the parts that already worked — they were never the bug):
|
|
26
|
+
// • MEASURED cadence (EMA of inter-sample arrival deltas) → estimates the real
|
|
27
|
+
// feed rate so a jittery 1-Hz feed still glides at roughly the right speed (no
|
|
28
|
+
// hardcoded tweenMs for the slide).
|
|
29
|
+
// • DELAY/LAG — hold the freshest sample one interval off the right edge so the
|
|
30
|
+
// next point is already known before we draw it (Smoothie `delay`). On canvas
|
|
31
|
+
// the off-edge point simply sits past `width` and the scroll slides it in; no
|
|
32
|
+
// clipPath needed — `clearRect` to the canvas box clips for free.
|
|
33
|
+
// • CARRY across the commit seam — the fractional glide still in flight at a
|
|
34
|
+
// commit is carried, never snapped to 0, so the scroll is continuous.
|
|
35
|
+
// • prefers-reduced-motion → a single static draw (no loop); pause on
|
|
36
|
+
// document.hidden.
|
|
37
|
+
//
|
|
38
|
+
// RESIDUAL-JITTER POLISH (the micro-wobble this revision kills):
|
|
39
|
+
// The canvas rewrite removed the big jerk, but a slight wobble remained. Its root:
|
|
40
|
+
// the per-frame scroll VELOCITY was `slotW / measuredInterval`, and `measuredInterval`
|
|
41
|
+
// is an EMA recomputed on EVERY sample. A jittery feed nudged that EMA tick-to-tick,
|
|
42
|
+
// so the visible scroll speed micro-adjusted at every commit seam — a perceptible
|
|
43
|
+
// "поддёргивание". The professional fix (Smoothie / uPlot / Grafana): DECOUPLE the
|
|
44
|
+
// visible velocity from per-sample timing.
|
|
45
|
+
// • SLEWED velocity — the rAF loop scrolls off `effectiveIntervalRef`, a value
|
|
46
|
+
// that creeps SLOWLY toward the EMA target (a small fraction per frame). The
|
|
47
|
+
// EMA still tracks the cadence for adaptation, but the on-screen speed only
|
|
48
|
+
// ever changes imperceptibly — no per-tick snap. The `delay` BUFFER (not the
|
|
49
|
+
// velocity) absorbs the arrival jitter.
|
|
50
|
+
// • LARGER delay buffer — phase may glide further past 1.0 (≈1.8 slots of slack
|
|
51
|
+
// instead of ~1.35) so a late/early sample is swallowed by the buffer long
|
|
52
|
+
// before it could perturb the visible scroll.
|
|
53
|
+
// • CONSISTENT frame timestamp — the rAF callback uses its OWN `time` arg for the
|
|
54
|
+
// phase calc, so there is no `performance.now()` drift between sub-steps of one
|
|
55
|
+
// frame. The commit effect still stamps with `performance.now()` (it runs
|
|
56
|
+
// outside rAF); the loop reconciles both on the same monotonic clock.
|
|
57
|
+
// • C0-CONTINUOUS commit seam — the carry keeps the fractional leftover so the
|
|
58
|
+
// scroll position never hops across a commit.
|
|
59
|
+
//
|
|
60
|
+
// THE PER-FRAME LOOP (one writer, one clock):
|
|
61
|
+
// 1. now = rAF time arg (one clock, one timestamp per frame)
|
|
62
|
+
// 2. phase = carry + (now − lastCommit) / effectiveInterval // slots, wall-clock
|
|
63
|
+
// 3. scrollX = −slotW · (1 − clampedPhase) // px, ≤ 0
|
|
64
|
+
// 4. dpr-scale the ctx, clearRect the box, call host draw(ctx, { scrollX, … })
|
|
65
|
+
// The host draws point i at `i·slotW + scrollX` — newest point sits just past
|
|
66
|
+
// the right edge and slides in; oldest fall off the left. Same geometry the SVG
|
|
67
|
+
// builders drew, now emitted to ctx via moveTo/lineTo/bezierCurveTo.
|
|
68
|
+
|
|
69
|
+
'use client';
|
|
70
|
+
|
|
71
|
+
import * as React from 'react';
|
|
72
|
+
|
|
73
|
+
/** SSR-safe `prefers-reduced-motion: reduce` subscription. */
|
|
74
|
+
export function useReducedMotion(): boolean {
|
|
75
|
+
const [reduced, setReduced] = React.useState(false);
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
79
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
80
|
+
const update = () => setReduced(mq.matches);
|
|
81
|
+
update();
|
|
82
|
+
mq.addEventListener('change', update);
|
|
83
|
+
return () => mq.removeEventListener('change', update);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return reduced;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Per-frame draw context handed to the host's {@link DrawFn}. All coordinates are
|
|
91
|
+
* in CSS pixels over the chart's logical `width × height` box — the engine has
|
|
92
|
+
* already applied the `devicePixelRatio` transform to the `ctx`, so the host
|
|
93
|
+
* draws in plain logical units and gets crisp retina output for free.
|
|
94
|
+
*/
|
|
95
|
+
export interface DrawContext {
|
|
96
|
+
/**
|
|
97
|
+
* The current wall-clock scroll offset, in CSS px (≤ 0). Add it to each point's
|
|
98
|
+
* data-space X so the trace slides left: the host draws point `i` at
|
|
99
|
+
* `i·slotWidth + scrollX`. At a fresh commit `scrollX ≈ −slotWidth` (newest
|
|
100
|
+
* point one slot off the right edge) and glides to `0` over one interval.
|
|
101
|
+
*/
|
|
102
|
+
scrollX: number;
|
|
103
|
+
/** Per-slot scroll distance in CSS px (`width / (drawnCount − 1)`), or 0. */
|
|
104
|
+
slotWidth: number;
|
|
105
|
+
/** Logical chart width in CSS px (the consumer's `width` prop / viewBox). */
|
|
106
|
+
width: number;
|
|
107
|
+
/** Logical chart height in CSS px. */
|
|
108
|
+
height: number;
|
|
109
|
+
/** Device pixel ratio the ctx is scaled by (host rarely needs it). */
|
|
110
|
+
dpr: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Host-supplied per-frame draw. Called after the engine has sized + dpr-scaled
|
|
115
|
+
* the ctx and `clearRect`-ed the box. The host strokes/fills its geometry at the
|
|
116
|
+
* scrolled X. SmoothLine passes a monotone-cubic polyline drawer; the ECG passes
|
|
117
|
+
* its bespoke QRS drawer (three colour passes in one callback).
|
|
118
|
+
*
|
|
119
|
+
* The draw receives the CURRENT committed values via closure (the host rebuilds
|
|
120
|
+
* the callback when `values`/`epoch` change), so there is no per-frame Y-tween —
|
|
121
|
+
* the shape is fixed between ticks, only the window scrolls.
|
|
122
|
+
*/
|
|
123
|
+
export type DrawFn = (ctx: CanvasRenderingContext2D, frame: DrawContext) => void;
|
|
124
|
+
|
|
125
|
+
export interface SmoothSeriesConfig {
|
|
126
|
+
/** Target series, oldest-first. A change advances the scroll + redraws. */
|
|
127
|
+
values: readonly number[];
|
|
128
|
+
/**
|
|
129
|
+
* Seed cadence (ms) for the scroll velocity — used ONLY as the initial guess
|
|
130
|
+
* and the floor/ceiling clamp bounds, NOT as the fixed slide speed. The engine
|
|
131
|
+
* MEASURES the real inter-sample interval (an EMA of the deltas between datum
|
|
132
|
+
* arrivals) as a cadence target, then SLEWS the visible scroll velocity slowly
|
|
133
|
+
* toward it — so a jittery 1 Hz feed glides at a rock-steady speed (the velocity
|
|
134
|
+
* is decoupled from per-sample timing; the `delay` buffer absorbs the jitter).
|
|
135
|
+
* Pass the rough expected cadence (stats ≈ 1000, a 30 s quality series ≈ 30000).
|
|
136
|
+
* @default 1000
|
|
137
|
+
*/
|
|
138
|
+
tweenMs?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Continuous left-scroll. When set, the engine drives a constant-velocity
|
|
141
|
+
* wall-clock scroll so the window slides like a hospital monitor instead of
|
|
142
|
+
* teleporting. `slotWidth` is derived from `width / (drawnCount − 1)`. When
|
|
143
|
+
* false the engine still draws (static), it just never scrolls. @default false
|
|
144
|
+
*/
|
|
145
|
+
scroll?: boolean;
|
|
146
|
+
/** Logical chart width (CSS px) used to derive the scroll slot-width. @default 240 */
|
|
147
|
+
width?: number;
|
|
148
|
+
/** Logical chart height (CSS px). @default 40 */
|
|
149
|
+
height?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Optional retarget trigger. The engine normally advances when the numeric
|
|
152
|
+
* `values` change. A sliding window of IDENTICAL tokens (e.g. a steady ECG
|
|
153
|
+
* ok-stream where the envelope stays all-1s) produces no numeric change, so the
|
|
154
|
+
* scroll would never advance. Bump `epoch` once per tick (the host's sample
|
|
155
|
+
* count works well) to advance the scroll even when `values` are unchanged.
|
|
156
|
+
*/
|
|
157
|
+
epoch?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Hold the right edge back by one interval so the newest point is already known
|
|
160
|
+
* before it scrolls into view (Smoothie / chartjs-streaming `delay`). When set,
|
|
161
|
+
* the freshest value is dropped from the drawn set and lives off the right edge,
|
|
162
|
+
* revealed only by the scroll — so the right edge reads as a continuous stream
|
|
163
|
+
* instead of "growing then snapping". Requires `scroll`. @default true
|
|
164
|
+
*/
|
|
165
|
+
delay?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Per-frame draw callback (host geometry). The drawn values are passed via
|
|
168
|
+
* closure — see {@link DrawFn}. The host should rebuild this (`useMemo` /
|
|
169
|
+
* `useCallback`) when its committed values or styling change.
|
|
170
|
+
*/
|
|
171
|
+
draw: DrawFn;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SmoothSeriesHandles {
|
|
175
|
+
/** Attach to the `<canvas>` the engine sizes (dpr) and drives. */
|
|
176
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
177
|
+
/**
|
|
178
|
+
* Force one static redraw at scrollX = 0 (newest point flush at the right edge).
|
|
179
|
+
* The engine calls this itself on reduced-motion / resize / `scroll=false`; the
|
|
180
|
+
* host rarely needs it, but it is exposed for an imperative refresh.
|
|
181
|
+
*/
|
|
182
|
+
redraw: () => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Drive a constant-velocity CANVAS streaming line from a changing `values` array.
|
|
187
|
+
*
|
|
188
|
+
* Returns a `canvasRef` the host attaches to one `<canvas>`. The engine sizes the
|
|
189
|
+
* canvas to its box × devicePixelRatio, runs a single persistent wall-clock rAF
|
|
190
|
+
* loop, and every frame `clearRect`s and calls the host `draw` at the current
|
|
191
|
+
* scroll offset — one writer, one clock, so the SVG-era jump-then-catch-up cannot
|
|
192
|
+
* occur. On reduced-motion / `scroll=false` it never starts the loop and renders
|
|
193
|
+
* a single static frame.
|
|
194
|
+
*/
|
|
195
|
+
export function useSmoothSeries(config: SmoothSeriesConfig): SmoothSeriesHandles {
|
|
196
|
+
const {
|
|
197
|
+
values,
|
|
198
|
+
tweenMs = 1000,
|
|
199
|
+
scroll = false,
|
|
200
|
+
width = 240,
|
|
201
|
+
height = 40,
|
|
202
|
+
epoch,
|
|
203
|
+
delay = true,
|
|
204
|
+
draw,
|
|
205
|
+
} = config;
|
|
206
|
+
|
|
207
|
+
const reduced = useReducedMotion();
|
|
208
|
+
|
|
209
|
+
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
|
|
210
|
+
|
|
211
|
+
// Latest draw callback without retriggering effects every render.
|
|
212
|
+
const drawRef = React.useRef(draw);
|
|
213
|
+
drawRef.current = draw;
|
|
214
|
+
|
|
215
|
+
const targetKey = React.useMemo(() => values.join(','), [values]);
|
|
216
|
+
|
|
217
|
+
// With `delay` on we draw the buffer MINUS its freshest point so the right edge
|
|
218
|
+
// is always a point we already know — the newest value lives off the right edge,
|
|
219
|
+
// revealed by the scroll. One slot of room is the lag.
|
|
220
|
+
const useLag = scroll && delay && values.length > 2;
|
|
221
|
+
const drawnCount = useLag ? Math.max(0, values.length - 1) : values.length;
|
|
222
|
+
|
|
223
|
+
// Slot width: the per-tick scroll distance, in logical (CSS) px.
|
|
224
|
+
const slotW = React.useMemo(() => {
|
|
225
|
+
if (!scroll) return 0;
|
|
226
|
+
return drawnCount > 1 ? width / (drawnCount - 1) : 0;
|
|
227
|
+
}, [scroll, drawnCount, width]);
|
|
228
|
+
const slotWRef = React.useRef(slotW);
|
|
229
|
+
slotWRef.current = slotW;
|
|
230
|
+
|
|
231
|
+
// Seed cadence — the floor/ceiling clamp and the initial guess for the EMA.
|
|
232
|
+
const seedIntervalRef = React.useRef(Math.max(1, tweenMs));
|
|
233
|
+
seedIntervalRef.current = Math.max(1, tweenMs);
|
|
234
|
+
|
|
235
|
+
// MEASURED cadence (EMA of inter-sample deltas). This is the cadence ESTIMATE
|
|
236
|
+
// that tracks the feed rate; it is NOT what the visible scroll runs off directly
|
|
237
|
+
// (that micro-wobbles on a jittery feed). It is the slew TARGET for the velocity.
|
|
238
|
+
const measuredIntervalRef = React.useRef(Math.max(1, tweenMs));
|
|
239
|
+
// EFFECTIVE cadence — what the visible scroll velocity actually uses. It creeps
|
|
240
|
+
// slowly toward `measuredInterval` (a few % per frame), so the on-screen speed
|
|
241
|
+
// never snaps tick-to-tick even when the EMA wobbles. This is the residual-jitter
|
|
242
|
+
// fix: the velocity is decoupled from per-sample timing; the `delay` buffer eats
|
|
243
|
+
// the arrival jitter, not the velocity.
|
|
244
|
+
const effectiveIntervalRef = React.useRef(Math.max(1, tweenMs));
|
|
245
|
+
// Wall-clock timestamp of the LAST commit (datum arrival). The scroll phase is a
|
|
246
|
+
// pure function of (now − lastCommit) / measuredInterval — position is driven by
|
|
247
|
+
// wall-clock, not by integrating per-frame dt (which drifts). Self-correcting: a
|
|
248
|
+
// slow frame just lands further along the same line.
|
|
249
|
+
const lastCommitRef = React.useRef<number>(0);
|
|
250
|
+
// Carry-over phase: the fractional glide still in flight at the moment of a
|
|
251
|
+
// commit. We DON'T snap it to 0 — we keep gliding from the leftover so the
|
|
252
|
+
// scroll never teleports across the commit seam (chartjs `preservation`).
|
|
253
|
+
const carryRef = React.useRef<number>(0);
|
|
254
|
+
|
|
255
|
+
// The DPR the canvas backing store is currently sized for.
|
|
256
|
+
const dprRef = React.useRef(1);
|
|
257
|
+
|
|
258
|
+
// ── Size the canvas backing store to box × devicePixelRatio and scale the ctx
|
|
259
|
+
// so all host drawing is in logical (CSS) px and stays crisp on retina. Returns
|
|
260
|
+
// a ready, cleared, dpr-scaled 2D context (or null pre-mount / SSR).
|
|
261
|
+
const prepareCtx = React.useCallback((): CanvasRenderingContext2D | null => {
|
|
262
|
+
const canvas = canvasRef.current;
|
|
263
|
+
if (!canvas || typeof window === 'undefined') return null;
|
|
264
|
+
|
|
265
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
266
|
+
// Logical size: width/height props (the viewBox), but the canvas stretches to
|
|
267
|
+
// its container width via CSS — so prefer the measured client box when laid
|
|
268
|
+
// out, falling back to the prop box pre-layout.
|
|
269
|
+
const cssW = canvas.clientWidth || width;
|
|
270
|
+
const cssH = canvas.clientHeight || height;
|
|
271
|
+
const bw = Math.round(cssW * dpr);
|
|
272
|
+
const bh = Math.round(cssH * dpr);
|
|
273
|
+
if (canvas.width !== bw) canvas.width = bw;
|
|
274
|
+
if (canvas.height !== bh) canvas.height = bh;
|
|
275
|
+
dprRef.current = dpr;
|
|
276
|
+
|
|
277
|
+
const ctx = canvas.getContext('2d');
|
|
278
|
+
if (!ctx) return null;
|
|
279
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
280
|
+
ctx.clearRect(0, 0, bw, bh);
|
|
281
|
+
// Scale logical→device, AND map the logical `width` (viewBox) onto the
|
|
282
|
+
// measured CSS width so geometry built against `width` stretches to fill the
|
|
283
|
+
// container — the canvas analogue of SVG `preserveAspectRatio="none"`.
|
|
284
|
+
const sx = (cssW / width) * dpr;
|
|
285
|
+
const sy = (cssH / height) * dpr;
|
|
286
|
+
ctx.setTransform(sx, 0, 0, sy, 0, 0);
|
|
287
|
+
return ctx;
|
|
288
|
+
}, [width, height]);
|
|
289
|
+
|
|
290
|
+
// ── One static draw at scrollX = 0 (newest point flush at the right edge). Used
|
|
291
|
+
// for reduced-motion, scroll=false, and resize. Geometry is in logical units.
|
|
292
|
+
const redraw = React.useCallback(() => {
|
|
293
|
+
const ctx = prepareCtx();
|
|
294
|
+
if (!ctx) return;
|
|
295
|
+
drawRef.current(ctx, {
|
|
296
|
+
scrollX: 0,
|
|
297
|
+
slotWidth: slotWRef.current,
|
|
298
|
+
width,
|
|
299
|
+
height,
|
|
300
|
+
dpr: dprRef.current,
|
|
301
|
+
});
|
|
302
|
+
}, [prepareCtx, width, height]);
|
|
303
|
+
|
|
304
|
+
// ── Measure the inter-sample interval on every data advance (EMA) and commit.
|
|
305
|
+
React.useEffect(() => {
|
|
306
|
+
if (typeof window === 'undefined') return;
|
|
307
|
+
const now =
|
|
308
|
+
typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
309
|
+
|
|
310
|
+
if (lastCommitRef.current > 0) {
|
|
311
|
+
const deltaMs = now - lastCommitRef.current;
|
|
312
|
+
// Ignore absurd deltas (backgrounded tab → huge gap; double-fire → ~0).
|
|
313
|
+
// Clamp to a generous band (¼×…4×) around the seed so a single outlier
|
|
314
|
+
// can't whip the velocity but genuine cadence changes still adapt.
|
|
315
|
+
const lo = seedIntervalRef.current / 4;
|
|
316
|
+
const hi = seedIntervalRef.current * 4;
|
|
317
|
+
if (deltaMs >= lo && deltaMs <= hi) {
|
|
318
|
+
const alpha = 0.25; // ~4-sample memory
|
|
319
|
+
measuredIntervalRef.current =
|
|
320
|
+
alpha * deltaMs + (1 - alpha) * measuredIntervalRef.current;
|
|
321
|
+
}
|
|
322
|
+
// Carry the in-flight glide across the seam. At commit the phase has glided
|
|
323
|
+
// to ~1.0 (on time), past (late), or short (early). Keep the FRACTIONAL
|
|
324
|
+
// leftover so the next slot resumes from exactly where this one was — no
|
|
325
|
+
// snap. The drawn set simultaneously loses its oldest / gains a slot, so
|
|
326
|
+
// phase-minus-1 keeps the on-screen position continuous across the seam.
|
|
327
|
+
//
|
|
328
|
+
// The carry MUST be measured against the SAME interval the visible loop
|
|
329
|
+
// scrolls off (the slewed effectiveInterval), not the raw EMA — otherwise a
|
|
330
|
+
// tick-to-tick mismatch between the carry's basis and the loop's basis would
|
|
331
|
+
// re-introduce a sub-pixel seam hop. This keeps the position C0-continuous.
|
|
332
|
+
const phase = deltaMs / effectiveIntervalRef.current;
|
|
333
|
+
carryRef.current = Math.max(-0.25, Math.min(0.5, phase - 1));
|
|
334
|
+
} else {
|
|
335
|
+
carryRef.current = 0;
|
|
336
|
+
}
|
|
337
|
+
lastCommitRef.current = now;
|
|
338
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
339
|
+
}, [targetKey, epoch]);
|
|
340
|
+
|
|
341
|
+
// ── Static path: when not scrolling (or reduced motion), redraw on any data /
|
|
342
|
+
// geometry change. (When scrolling, the rAF loop owns the canvas.)
|
|
343
|
+
React.useEffect(() => {
|
|
344
|
+
if (scroll && !reduced) return; // the loop owns it
|
|
345
|
+
redraw();
|
|
346
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
347
|
+
}, [scroll, reduced, targetKey, epoch, width, height]);
|
|
348
|
+
|
|
349
|
+
// ── The single persistent wall-clock rAF loop: clearRect → host draw at the
|
|
350
|
+
// current scroll offset, every frame. One writer, one clock.
|
|
351
|
+
React.useEffect(() => {
|
|
352
|
+
if (!scroll || reduced || typeof window === 'undefined') return;
|
|
353
|
+
|
|
354
|
+
let raf = 0;
|
|
355
|
+
// How fast the visible velocity creeps toward the measured-cadence target,
|
|
356
|
+
// per frame. Small → the on-screen speed only ever drifts imperceptibly, so a
|
|
357
|
+
// wobbling EMA never shows up as a per-tick velocity snap. A genuine cadence
|
|
358
|
+
// change (e.g. feed slows from 1 Hz → 0.5 Hz) is still tracked, just gently
|
|
359
|
+
// (~60 frames ≈ 1 s to converge ~70%). This is the velocity decouple.
|
|
360
|
+
const VELOCITY_SLEW = 0.02;
|
|
361
|
+
const tick = (time: number) => {
|
|
362
|
+
raf = requestAnimationFrame(tick);
|
|
363
|
+
if (document.hidden) return; // pause while backgrounded (cheap poll)
|
|
364
|
+
|
|
365
|
+
const ctx = prepareCtx();
|
|
366
|
+
if (!ctx) return;
|
|
367
|
+
|
|
368
|
+
// ONE timestamp per frame, from rAF — used for BOTH the phase calc here and
|
|
369
|
+
// (via lastCommit, stamped on the same monotonic clock) the seam math. No
|
|
370
|
+
// performance.now() drift between sub-steps of a single frame.
|
|
371
|
+
const now = time;
|
|
372
|
+
|
|
373
|
+
// Slew the EFFECTIVE interval toward the EMA target a small fraction each
|
|
374
|
+
// frame. The visible scroll runs off THIS, so its velocity changes are
|
|
375
|
+
// gradual — the residual micro-jitter (velocity snapping every commit) is
|
|
376
|
+
// gone. The buffer, not the velocity, absorbs the arrival jitter.
|
|
377
|
+
const target = measuredIntervalRef.current;
|
|
378
|
+
effectiveIntervalRef.current +=
|
|
379
|
+
(target - effectiveIntervalRef.current) * VELOCITY_SLEW;
|
|
380
|
+
const effInterval = effectiveIntervalRef.current || 1;
|
|
381
|
+
|
|
382
|
+
let scrollX = 0;
|
|
383
|
+
if (lastCommitRef.current > 0) {
|
|
384
|
+
// Wall-clock phase since the last commit, in slots (Smoothie model — pure
|
|
385
|
+
// function of time, self-correcting, frame-rate independent). `carry` is
|
|
386
|
+
// the leftover from the previous slot so the glide is continuous across
|
|
387
|
+
// the commit seam. A LATE sample lets phase glide PAST 1 into the `delay`
|
|
388
|
+
// buffer (off the right edge, clipped by clearRect) rather than freezing.
|
|
389
|
+
// The buffer is generous (≈1.8 slots) so an early/late arrival is wholly
|
|
390
|
+
// swallowed by slack and never perturbs the visible speed; floor at 0 so
|
|
391
|
+
// an early phase holds the left edge.
|
|
392
|
+
const elapsed = now - lastCommitRef.current;
|
|
393
|
+
const phase = carryRef.current + elapsed / effInterval;
|
|
394
|
+
const pos = Math.min(1.8, Math.max(0, phase));
|
|
395
|
+
scrollX = -slotWRef.current * (1 - pos);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
drawRef.current(ctx, {
|
|
399
|
+
scrollX,
|
|
400
|
+
slotWidth: slotWRef.current,
|
|
401
|
+
width,
|
|
402
|
+
height,
|
|
403
|
+
dpr: dprRef.current,
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
raf = requestAnimationFrame(tick);
|
|
408
|
+
return () => cancelAnimationFrame(raf);
|
|
409
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
410
|
+
}, [scroll, reduced, width, height, prepareCtx]);
|
|
411
|
+
|
|
412
|
+
// ── Redraw on container resize (fluid width). The rAF loop already re-measures
|
|
413
|
+
// every frame when scrolling; this covers the static / reduced-motion case.
|
|
414
|
+
React.useEffect(() => {
|
|
415
|
+
if (typeof window === 'undefined' || typeof ResizeObserver === 'undefined') {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const canvas = canvasRef.current;
|
|
419
|
+
if (!canvas) return;
|
|
420
|
+
const ro = new ResizeObserver(() => {
|
|
421
|
+
if (!scroll || reduced) redraw();
|
|
422
|
+
});
|
|
423
|
+
ro.observe(canvas);
|
|
424
|
+
return () => ro.disconnect();
|
|
425
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
426
|
+
}, [scroll, reduced, redraw]);
|
|
427
|
+
|
|
428
|
+
return { canvasRef, redraw };
|
|
429
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/** Lottie block — thin chat wrapper around `LottiePlayer`. */
|
|
4
|
-
|
|
5
|
-
import { LazyLottiePlayer } from '../../../../media/LottiePlayer/lazy';
|
|
6
|
-
import type { LottieBlock } from '../../../types/block';
|
|
7
|
-
import type { BlockRendererProps } from './types';
|
|
8
|
-
|
|
9
|
-
export default function LottieBlockRenderer({ block, ctx }: BlockRendererProps<LottieBlock>) {
|
|
10
|
-
return <LazyLottiePlayer src={block.src} size={ctx.appearance === 'compact' ? 'sm' : 'md'} />;
|
|
11
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, type ReactNode } from 'react'
|
|
4
|
-
import Map, { type ViewStateChangeEvent } from 'react-map-gl/maplibre'
|
|
5
|
-
import { ExternalLink, RotateCcw } from 'lucide-react'
|
|
6
|
-
import { cn } from '@djangocfg/ui-core/lib'
|
|
7
|
-
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
8
|
-
|
|
9
|
-
import { MapProvider, useMapContext } from '../context'
|
|
10
|
-
import { MAP_STYLES } from '../styles'
|
|
11
|
-
import type { MapViewport, MapStyleKey } from '../types'
|
|
12
|
-
|
|
13
|
-
export interface MapContainerProps {
|
|
14
|
-
children?: ReactNode
|
|
15
|
-
initialViewport?: Partial<MapViewport>
|
|
16
|
-
mapStyle?: MapStyleKey | string
|
|
17
|
-
interactiveLayerIds?: string[]
|
|
18
|
-
className?: string
|
|
19
|
-
style?: React.CSSProperties
|
|
20
|
-
cursor?: string
|
|
21
|
-
attributionControl?: boolean
|
|
22
|
-
reuseMaps?: boolean
|
|
23
|
-
/** URL to open in external maps app (shows "Open in Maps" button if provided) */
|
|
24
|
-
openInMapsUrl?: string
|
|
25
|
-
/** Label for the open in maps button */
|
|
26
|
-
openInMapsLabel?: string
|
|
27
|
-
/** Auto-reset to initial viewport after N ms of inactivity (0 = disabled) */
|
|
28
|
-
autoResetDelay?: number
|
|
29
|
-
/** Show reset button */
|
|
30
|
-
showResetButton?: boolean
|
|
31
|
-
/** Accessible label for the map region (screen readers) */
|
|
32
|
-
ariaLabel?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface MapInnerProps extends Omit<MapContainerProps, 'initialViewport'> {}
|
|
36
|
-
|
|
37
|
-
function MapInner({
|
|
38
|
-
children,
|
|
39
|
-
mapStyle = 'light',
|
|
40
|
-
interactiveLayerIds,
|
|
41
|
-
style,
|
|
42
|
-
cursor,
|
|
43
|
-
attributionControl = true,
|
|
44
|
-
reuseMaps = true,
|
|
45
|
-
openInMapsUrl,
|
|
46
|
-
openInMapsLabel = 'Open in Maps',
|
|
47
|
-
autoResetDelay = 0,
|
|
48
|
-
showResetButton = false,
|
|
49
|
-
ariaLabel = 'Interactive map',
|
|
50
|
-
}: MapInnerProps) {
|
|
51
|
-
const { mapRef, viewport, setViewport, setIsLoaded, resetToInitial, initialViewport } = useMapContext()
|
|
52
|
-
const resetTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
53
|
-
const isInteractingRef = useRef(false)
|
|
54
|
-
|
|
55
|
-
// Check if viewport has changed from initial
|
|
56
|
-
const hasViewportChanged =
|
|
57
|
-
Math.abs(viewport.longitude - initialViewport.longitude) > 0.0001 ||
|
|
58
|
-
Math.abs(viewport.latitude - initialViewport.latitude) > 0.0001 ||
|
|
59
|
-
Math.abs(viewport.zoom - initialViewport.zoom) > 0.1
|
|
60
|
-
|
|
61
|
-
const handleMove = useCallback(
|
|
62
|
-
(evt: ViewStateChangeEvent) => {
|
|
63
|
-
setViewport({
|
|
64
|
-
longitude: evt.viewState.longitude,
|
|
65
|
-
latitude: evt.viewState.latitude,
|
|
66
|
-
zoom: evt.viewState.zoom,
|
|
67
|
-
bearing: evt.viewState.bearing,
|
|
68
|
-
pitch: evt.viewState.pitch,
|
|
69
|
-
})
|
|
70
|
-
},
|
|
71
|
-
[setViewport]
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
const handleMoveStart = useCallback(() => {
|
|
75
|
-
isInteractingRef.current = true
|
|
76
|
-
// Clear any pending reset timer
|
|
77
|
-
if (resetTimerRef.current) {
|
|
78
|
-
clearTimeout(resetTimerRef.current)
|
|
79
|
-
resetTimerRef.current = null
|
|
80
|
-
}
|
|
81
|
-
}, [])
|
|
82
|
-
|
|
83
|
-
const handleMoveEnd = useCallback(() => {
|
|
84
|
-
isInteractingRef.current = false
|
|
85
|
-
// Start auto-reset timer if enabled
|
|
86
|
-
if (autoResetDelay > 0) {
|
|
87
|
-
resetTimerRef.current = setTimeout(() => {
|
|
88
|
-
if (!isInteractingRef.current) {
|
|
89
|
-
resetToInitial()
|
|
90
|
-
}
|
|
91
|
-
}, autoResetDelay)
|
|
92
|
-
}
|
|
93
|
-
}, [autoResetDelay, resetToInitial])
|
|
94
|
-
|
|
95
|
-
const handleLoad = useCallback(() => {
|
|
96
|
-
setIsLoaded(true)
|
|
97
|
-
}, [setIsLoaded])
|
|
98
|
-
|
|
99
|
-
// Cleanup timer on unmount
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
return () => {
|
|
102
|
-
if (resetTimerRef.current) {
|
|
103
|
-
clearTimeout(resetTimerRef.current)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
const resolvedStyle = mapStyle in MAP_STYLES
|
|
109
|
-
? MAP_STYLES[mapStyle as MapStyleKey]
|
|
110
|
-
: mapStyle
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<>
|
|
114
|
-
<Map
|
|
115
|
-
ref={mapRef}
|
|
116
|
-
{...viewport}
|
|
117
|
-
onMove={handleMove}
|
|
118
|
-
onMoveStart={handleMoveStart}
|
|
119
|
-
onMoveEnd={handleMoveEnd}
|
|
120
|
-
onLoad={handleLoad}
|
|
121
|
-
mapStyle={resolvedStyle}
|
|
122
|
-
interactiveLayerIds={interactiveLayerIds}
|
|
123
|
-
attributionControl={attributionControl ? {} : false}
|
|
124
|
-
reuseMaps={reuseMaps}
|
|
125
|
-
style={{
|
|
126
|
-
width: '100%',
|
|
127
|
-
height: '100%',
|
|
128
|
-
...style,
|
|
129
|
-
}}
|
|
130
|
-
cursor={cursor}
|
|
131
|
-
aria-label={ariaLabel}
|
|
132
|
-
>
|
|
133
|
-
{children}
|
|
134
|
-
</Map>
|
|
135
|
-
|
|
136
|
-
{/* Map overlay buttons */}
|
|
137
|
-
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
|
138
|
-
{/* Reset button */}
|
|
139
|
-
{showResetButton && hasViewportChanged && (
|
|
140
|
-
<button
|
|
141
|
-
type="button"
|
|
142
|
-
onClick={resetToInitial}
|
|
143
|
-
aria-label="Reset map view"
|
|
144
|
-
className={cn(
|
|
145
|
-
'inline-flex items-center gap-2 px-3 py-2 rounded-lg',
|
|
146
|
-
'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',
|
|
147
|
-
'hover:bg-background transition-colors shadow-sm border border-border',
|
|
148
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
149
|
-
)}
|
|
150
|
-
>
|
|
151
|
-
<RotateCcw className="w-4 h-4" aria-hidden="true" />
|
|
152
|
-
Reset
|
|
153
|
-
</button>
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
|
-
{/* Open in Maps button */}
|
|
157
|
-
{openInMapsUrl && (
|
|
158
|
-
<a
|
|
159
|
-
href={openInMapsUrl}
|
|
160
|
-
target="_blank"
|
|
161
|
-
rel="noopener noreferrer"
|
|
162
|
-
className={cn(
|
|
163
|
-
'inline-flex items-center gap-2 px-4 py-2 rounded-lg',
|
|
164
|
-
'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',
|
|
165
|
-
'hover:bg-background transition-colors shadow-sm border border-border',
|
|
166
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
167
|
-
)}
|
|
168
|
-
>
|
|
169
|
-
<ExternalLink className="w-4 h-4" aria-hidden="true" />
|
|
170
|
-
{openInMapsLabel}
|
|
171
|
-
</a>
|
|
172
|
-
)}
|
|
173
|
-
</div>
|
|
174
|
-
</>
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function MapContainer({
|
|
179
|
-
children,
|
|
180
|
-
initialViewport,
|
|
181
|
-
className,
|
|
182
|
-
...props
|
|
183
|
-
}: MapContainerProps) {
|
|
184
|
-
return (
|
|
185
|
-
<div className={className} style={{ width: '100%', height: '100%', position: 'relative' }}>
|
|
186
|
-
<MapProvider initialViewport={initialViewport}>
|
|
187
|
-
<MapInner {...props}>{children}</MapInner>
|
|
188
|
-
</MapProvider>
|
|
189
|
-
</div>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Use this when you need the map inside an existing MapProvider
|
|
195
|
-
*/
|
|
196
|
-
export function MapView(props: Omit<MapContainerProps, 'initialViewport'>) {
|
|
197
|
-
return <MapInner {...props} />
|
|
198
|
-
}
|