@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.
Files changed (171) hide show
  1. package/package.json +28 -14
  2. package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
  3. package/src/common/CodePanelHeader/index.ts +1 -0
  4. package/src/common/blocks/BlockError.tsx +46 -0
  5. package/src/common/blocks/index.ts +34 -0
  6. package/src/common/blocks/schemas.ts +143 -0
  7. package/src/common/blocks/types.ts +76 -0
  8. package/src/common/chips/README.md +67 -0
  9. package/src/common/chips/UrlChip.tsx +105 -0
  10. package/src/common/chips/index.ts +20 -0
  11. package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
  12. package/src/common/link-preview/index.ts +12 -0
  13. package/src/common/link-preview/types.ts +44 -0
  14. package/src/common/tiptap/index.ts +10 -0
  15. package/src/common/tiptap/useNodeAttrs.ts +128 -0
  16. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
  17. package/src/tools/chat/README.md +88 -2
  18. package/src/tools/chat/composer/Composer.tsx +45 -12
  19. package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
  20. package/src/tools/chat/constants.ts +9 -2
  21. package/src/tools/chat/core/extractFirstUrl.ts +45 -0
  22. package/src/tools/chat/core/reducer.ts +32 -8
  23. package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
  24. package/src/tools/chat/core/transport/mock.ts +4 -1
  25. package/src/tools/chat/hooks/useChat.ts +20 -20
  26. package/src/tools/chat/lazy.tsx +10 -0
  27. package/src/tools/chat/messages/Attachments.tsx +267 -42
  28. package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
  29. package/src/tools/chat/messages/MessageBubble.tsx +175 -31
  30. package/src/tools/chat/messages/MessageList.tsx +107 -21
  31. package/src/tools/chat/messages/ToolCalls.tsx +28 -13
  32. package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
  33. package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
  34. package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
  35. package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
  36. package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
  37. package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
  38. package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
  39. package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
  40. package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
  41. package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
  42. package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
  43. package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
  44. package/src/tools/chat/messages/index.ts +13 -0
  45. package/src/tools/chat/public.ts +5 -1
  46. package/src/tools/chat/shell/ChatRoot.tsx +29 -0
  47. package/src/tools/chat/styles/bubbleTokens.ts +18 -0
  48. package/src/tools/chat/styles/index.ts +1 -0
  49. package/src/tools/chat/types/attachment.ts +26 -2
  50. package/src/tools/chat/types/block.ts +56 -9
  51. package/src/tools/chat/types/blockSchemas.ts +197 -0
  52. package/src/tools/chat/types/config.ts +33 -0
  53. package/src/tools/chat/types/events.ts +14 -7
  54. package/src/tools/chat/types/index.ts +4 -2
  55. package/src/tools/chat/types/message.ts +7 -0
  56. package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
  57. package/src/tools/dev/Map/README.md +30 -345
  58. package/src/tools/dev/Map/cards/index.ts +6 -0
  59. package/src/tools/dev/Map/cards/types.ts +67 -0
  60. package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
  61. package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
  62. package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
  63. package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
  64. package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
  65. package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
  66. package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
  67. package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
  68. package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
  69. package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
  70. package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
  71. package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
  72. package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
  73. package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
  74. package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
  75. package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
  76. package/src/tools/dev/Map/components/MapControls.tsx +27 -0
  77. package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
  78. package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
  79. package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
  80. package/src/tools/dev/Map/components/index.ts +35 -1
  81. package/src/tools/dev/Map/geocode/index.ts +2 -0
  82. package/src/tools/dev/Map/geocode/photon.ts +265 -0
  83. package/src/tools/dev/Map/hooks/index.ts +37 -0
  84. package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
  85. package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
  86. package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
  87. package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
  88. package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
  89. package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
  90. package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
  91. package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
  92. package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
  93. package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
  94. package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
  95. package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
  96. package/src/tools/dev/Map/index.ts +59 -2
  97. package/src/tools/dev/Map/layers/index.ts +1 -0
  98. package/src/tools/dev/Map/layers/symbol.ts +93 -0
  99. package/src/tools/dev/Map/lazy.tsx +120 -0
  100. package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
  101. package/src/tools/dev/Map/styles/index.ts +62 -0
  102. package/src/tools/dev/Map/styles/terrain.ts +29 -0
  103. package/src/tools/dev/Map/types.ts +31 -3
  104. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
  105. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
  106. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
  107. package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
  108. package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
  109. package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
  110. package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
  111. package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
  112. package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
  113. package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
  114. package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
  115. package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
  116. package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
  117. package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
  118. package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
  119. package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
  120. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
  121. package/src/tools/forms/MarkdownEditor/README.md +84 -0
  122. package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
  123. package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
  124. package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
  125. package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
  126. package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
  127. package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
  128. package/src/tools/forms/MarkdownEditor/index.ts +19 -0
  129. package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
  130. package/src/tools/forms/MarkdownEditor/styles.css +134 -2
  131. package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
  132. package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
  133. package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
  134. package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
  135. package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
  136. package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
  137. package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
  138. package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
  139. package/src/tools/forms/NotionEditor/README.md +109 -188
  140. package/src/tools/forms/NotionEditor/extensions.ts +15 -0
  141. package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
  142. package/src/tools/forms/NotionEditor/styles.css +25 -0
  143. package/src/tools/forms/NotionEditor/types.ts +9 -0
  144. package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
  145. package/src/tools/input/SpeechRecognition/types.ts +10 -0
  146. package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
  147. package/src/tools/media/ImageViewer/README.md +9 -0
  148. package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
  149. package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
  150. package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
  151. package/src/tools/media/ImageViewer/components/index.ts +2 -0
  152. package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
  153. package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
  154. package/src/tools/media/ImageViewer/types.ts +10 -0
  155. package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
  156. package/src/tools/media/ImageViewer/utils/index.ts +4 -0
  157. package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
  158. package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
  159. package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
  160. package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
  161. package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
  162. package/src/tools/visual/charts/SmoothLine/README.md +97 -0
  163. package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
  164. package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
  165. package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
  166. package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
  167. package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
  168. package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
  169. package/src/tools/visual/indicators/Fps/types.ts +1 -1
  170. package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
  171. 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
+ }
@@ -18,7 +18,7 @@ export const fpsVariants = cva(
18
18
  },
19
19
  status: {
20
20
  good: 'text-primary',
21
- warning: 'text-orange-500',
21
+ warning: 'text-warning',
22
22
  error: 'text-destructive',
23
23
  },
24
24
  },
@@ -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
- }