@brett_lamy/docstream 1.2.4 → 1.4.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.
Files changed (216) hide show
  1. package/README.md +76 -14
  2. package/dist/assets.d.ts +3 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +20 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/demo/DemoViewer.d.ts +64 -0
  7. package/dist/demo/DemoViewer.d.ts.map +1 -0
  8. package/dist/demo/DemoViewer.js +426 -0
  9. package/dist/demo/DemoViewer.js.map +1 -0
  10. package/dist/demo/context.d.ts +14 -0
  11. package/dist/demo/context.d.ts.map +1 -0
  12. package/dist/demo/context.js +25 -0
  13. package/dist/demo/context.js.map +1 -0
  14. package/dist/demo/glob.d.ts +60 -0
  15. package/dist/demo/glob.d.ts.map +1 -0
  16. package/dist/demo/glob.js +158 -0
  17. package/dist/demo/glob.js.map +1 -0
  18. package/dist/demo/index.d.ts +10 -0
  19. package/dist/demo/index.d.ts.map +1 -0
  20. package/dist/demo/index.js +6 -0
  21. package/dist/demo/index.js.map +1 -0
  22. package/dist/demo/markdown.d.ts +60 -0
  23. package/dist/demo/markdown.d.ts.map +1 -0
  24. package/dist/demo/markdown.js +181 -0
  25. package/dist/demo/markdown.js.map +1 -0
  26. package/dist/demo/surface.d.ts +11 -0
  27. package/dist/demo/surface.d.ts.map +1 -0
  28. package/dist/demo/surface.js +69 -0
  29. package/dist/demo/surface.js.map +1 -0
  30. package/dist/demo/types.d.ts +91 -0
  31. package/dist/demo/types.d.ts.map +1 -0
  32. package/dist/demo/types.js +2 -0
  33. package/dist/demo/types.js.map +1 -0
  34. package/dist/docs/CollapsibleCode.d.ts +14 -0
  35. package/dist/docs/CollapsibleCode.d.ts.map +1 -0
  36. package/dist/docs/CollapsibleCode.js +19 -0
  37. package/dist/docs/CollapsibleCode.js.map +1 -0
  38. package/dist/docs/DocsRenderer.d.ts +49 -0
  39. package/dist/docs/DocsRenderer.d.ts.map +1 -0
  40. package/dist/docs/DocsRenderer.js +277 -0
  41. package/dist/docs/DocsRenderer.js.map +1 -0
  42. package/dist/docs/HighlightedCode.d.ts +6 -0
  43. package/dist/docs/HighlightedCode.d.ts.map +1 -0
  44. package/dist/docs/HighlightedCode.js +40 -0
  45. package/dist/docs/HighlightedCode.js.map +1 -0
  46. package/dist/docs/Mermaid.d.ts +7 -0
  47. package/dist/docs/Mermaid.d.ts.map +1 -0
  48. package/dist/docs/Mermaid.js +39 -0
  49. package/dist/docs/Mermaid.js.map +1 -0
  50. package/dist/docs/PageActions.d.ts +33 -0
  51. package/dist/docs/PageActions.d.ts.map +1 -0
  52. package/dist/docs/PageActions.js +115 -0
  53. package/dist/docs/PageActions.js.map +1 -0
  54. package/dist/docs/controls.d.ts +29 -0
  55. package/dist/docs/controls.d.ts.map +1 -0
  56. package/dist/docs/controls.js +46 -0
  57. package/dist/docs/controls.js.map +1 -0
  58. package/dist/docs/copy.d.ts +24 -0
  59. package/dist/docs/copy.d.ts.map +1 -0
  60. package/dist/docs/copy.js +62 -0
  61. package/dist/docs/copy.js.map +1 -0
  62. package/dist/docs/math.d.ts +3 -0
  63. package/dist/docs/math.d.ts.map +1 -0
  64. package/dist/docs/math.js +12 -0
  65. package/dist/docs/math.js.map +1 -0
  66. package/dist/docs/reference.d.ts +27 -0
  67. package/dist/docs/reference.d.ts.map +1 -0
  68. package/dist/docs/reference.js +73 -0
  69. package/dist/docs/reference.js.map +1 -0
  70. package/dist/docs/tabs-sync.d.ts +5 -0
  71. package/dist/docs/tabs-sync.d.ts.map +1 -0
  72. package/dist/docs/tabs-sync.js +58 -0
  73. package/dist/docs/tabs-sync.js.map +1 -0
  74. package/dist/gitbook/ast.d.ts +506 -0
  75. package/dist/gitbook/ast.d.ts.map +1 -0
  76. package/dist/gitbook/ast.js +8 -0
  77. package/dist/gitbook/ast.js.map +1 -0
  78. package/dist/gitbook/attrs.d.ts +14 -0
  79. package/dist/gitbook/attrs.d.ts.map +1 -0
  80. package/dist/gitbook/attrs.js +36 -0
  81. package/dist/gitbook/attrs.js.map +1 -0
  82. package/dist/gitbook/flatten.d.ts +14 -0
  83. package/dist/gitbook/flatten.d.ts.map +1 -0
  84. package/dist/gitbook/flatten.js +45 -0
  85. package/dist/gitbook/flatten.js.map +1 -0
  86. package/dist/gitbook/index.d.ts +10 -0
  87. package/dist/gitbook/index.d.ts.map +1 -0
  88. package/dist/gitbook/index.js +8 -0
  89. package/dist/gitbook/index.js.map +1 -0
  90. package/dist/gitbook/inline.d.ts +28 -0
  91. package/dist/gitbook/inline.d.ts.map +1 -0
  92. package/dist/gitbook/inline.js +956 -0
  93. package/dist/gitbook/inline.js.map +1 -0
  94. package/dist/gitbook/outline.d.ts +16 -0
  95. package/dist/gitbook/outline.d.ts.map +1 -0
  96. package/dist/gitbook/outline.js +44 -0
  97. package/dist/gitbook/outline.js.map +1 -0
  98. package/dist/gitbook/package-managers.d.ts +12 -0
  99. package/dist/gitbook/package-managers.d.ts.map +1 -0
  100. package/dist/gitbook/package-managers.js +93 -0
  101. package/dist/gitbook/package-managers.js.map +1 -0
  102. package/dist/gitbook/parse.d.ts +44 -0
  103. package/dist/gitbook/parse.d.ts.map +1 -0
  104. package/dist/gitbook/parse.js +1113 -0
  105. package/dist/gitbook/parse.js.map +1 -0
  106. package/dist/gitbook/serialize.d.ts +23 -0
  107. package/dist/gitbook/serialize.d.ts.map +1 -0
  108. package/dist/gitbook/serialize.js +498 -0
  109. package/dist/gitbook/serialize.js.map +1 -0
  110. package/dist/index.d.ts +34 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +24 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/openapi/OpenApiOperation.d.ts +6 -0
  115. package/dist/openapi/OpenApiOperation.d.ts.map +1 -0
  116. package/dist/openapi/OpenApiOperation.js +112 -0
  117. package/dist/openapi/OpenApiOperation.js.map +1 -0
  118. package/dist/playground/InlineDemoPreview.d.ts +24 -0
  119. package/dist/playground/InlineDemoPreview.d.ts.map +1 -0
  120. package/dist/playground/InlineDemoPreview.js +48 -0
  121. package/dist/playground/InlineDemoPreview.js.map +1 -0
  122. package/dist/playground/PlaygroundStreamdown.d.ts +16 -0
  123. package/dist/playground/PlaygroundStreamdown.d.ts.map +1 -0
  124. package/dist/playground/PlaygroundStreamdown.js +22 -0
  125. package/dist/playground/PlaygroundStreamdown.js.map +1 -0
  126. package/dist/playground/ReactCodePreview.d.ts +64 -0
  127. package/dist/playground/ReactCodePreview.d.ts.map +1 -0
  128. package/dist/playground/ReactCodePreview.js +108 -0
  129. package/dist/playground/ReactCodePreview.js.map +1 -0
  130. package/dist/playground/filesystem.d.ts +27 -0
  131. package/dist/playground/filesystem.d.ts.map +1 -0
  132. package/dist/playground/filesystem.js +65 -0
  133. package/dist/playground/filesystem.js.map +1 -0
  134. package/dist/playground/index.d.ts +9 -0
  135. package/dist/playground/index.d.ts.map +1 -0
  136. package/dist/playground/index.js +7 -0
  137. package/dist/playground/index.js.map +1 -0
  138. package/dist/playground/project.d.ts +57 -0
  139. package/dist/playground/project.d.ts.map +1 -0
  140. package/dist/playground/project.js +137 -0
  141. package/dist/playground/project.js.map +1 -0
  142. package/dist/replay/NativeReplay.d.ts +9 -0
  143. package/dist/replay/NativeReplay.d.ts.map +1 -0
  144. package/dist/replay/NativeReplay.js +173 -0
  145. package/dist/replay/NativeReplay.js.map +1 -0
  146. package/dist/replay/ReplayPreview.d.ts +33 -0
  147. package/dist/replay/ReplayPreview.d.ts.map +1 -0
  148. package/dist/replay/ReplayPreview.js +91 -0
  149. package/dist/replay/ReplayPreview.js.map +1 -0
  150. package/dist/replay/index.d.ts +5 -0
  151. package/dist/replay/index.d.ts.map +1 -0
  152. package/dist/replay/index.js +4 -0
  153. package/dist/replay/index.js.map +1 -0
  154. package/dist/replay/sanitize.d.ts +4 -0
  155. package/dist/replay/sanitize.d.ts.map +1 -0
  156. package/dist/replay/sanitize.js +86 -0
  157. package/dist/replay/sanitize.js.map +1 -0
  158. package/dist/replay/url.d.ts +8 -0
  159. package/dist/replay/url.d.ts.map +1 -0
  160. package/dist/replay/url.js +41 -0
  161. package/dist/replay/url.js.map +1 -0
  162. package/dist/source/SourcePreview.d.ts +13 -0
  163. package/dist/source/SourcePreview.d.ts.map +1 -0
  164. package/dist/source/SourcePreview.js +47 -0
  165. package/dist/source/SourcePreview.js.map +1 -0
  166. package/dist/source/client.d.ts +7 -0
  167. package/dist/source/client.d.ts.map +1 -0
  168. package/dist/source/client.js +52 -0
  169. package/dist/source/client.js.map +1 -0
  170. package/dist/source/index.d.ts +6 -0
  171. package/dist/source/index.d.ts.map +1 -0
  172. package/dist/source/index.js +3 -0
  173. package/dist/source/index.js.map +1 -0
  174. package/dist/source/types.d.ts +33 -0
  175. package/dist/source/types.d.ts.map +1 -0
  176. package/dist/source/types.js +2 -0
  177. package/dist/source/types.js.map +1 -0
  178. package/dist/streamdown.d.ts +19 -0
  179. package/dist/streamdown.d.ts.map +1 -0
  180. package/dist/streamdown.js +15 -0
  181. package/dist/streamdown.js.map +1 -0
  182. package/dist/styles.css +2441 -0
  183. package/dist/video/VideoEmbed.d.ts +17 -0
  184. package/dist/video/VideoEmbed.d.ts.map +1 -0
  185. package/dist/video/VideoEmbed.js +12 -0
  186. package/dist/video/VideoEmbed.js.map +1 -0
  187. package/dist/video/index.d.ts +3 -0
  188. package/dist/video/index.d.ts.map +1 -0
  189. package/dist/video/index.js +2 -0
  190. package/dist/video/index.js.map +1 -0
  191. package/dist/vite/index.d.ts +21 -0
  192. package/dist/vite/index.d.ts.map +1 -0
  193. package/dist/vite/index.js +137 -0
  194. package/dist/vite/index.js.map +1 -0
  195. package/dist/viz/VizEmbed.d.ts +18 -0
  196. package/dist/viz/VizEmbed.d.ts.map +1 -0
  197. package/dist/viz/VizEmbed.js +12 -0
  198. package/dist/viz/VizEmbed.js.map +1 -0
  199. package/dist/viz/index.d.ts +4 -0
  200. package/dist/viz/index.d.ts.map +1 -0
  201. package/dist/viz/index.js +3 -0
  202. package/dist/viz/index.js.map +1 -0
  203. package/package.json +70 -34
  204. package/src/demo/types.ts +2 -2
  205. package/src/docs/DocsRenderer.tsx +31 -7
  206. package/src/docs/Mermaid.tsx +5 -3
  207. package/src/docs/math.ts +3 -0
  208. package/src/index.ts +10 -28
  209. package/src/playground/PlaygroundStreamdown.tsx +28 -61
  210. package/src/playground/filesystem.ts +10 -190
  211. package/src/playground/index.ts +4 -0
  212. package/src/playground/project.ts +194 -0
  213. package/src/replay/NativeReplay.tsx +236 -0
  214. package/src/replay/ReplayPreview.tsx +7 -226
  215. package/src/styles.css +91 -1
  216. package/src/replay/styles.css +0 -87
@@ -0,0 +1,236 @@
1
+ /* The rrweb player. Loaded on demand by ReplayPreview, so pages that only embed Loop QA
2
+ iframes (the markdown `{% embed %}` case) never download rrweb or its stylesheet. */
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { EventType, Replayer } from 'rrweb';
5
+ import type { eventWithTime } from 'rrweb';
6
+ import { sanitizeReplayEvent } from './sanitize';
7
+ import 'rrweb/dist/style.css';
8
+
9
+ const DEFAULT_SPEEDS = [0.5, 1, 2, 4, 8];
10
+ const SKIP_IDLE_THRESHOLD_MS = 5_000;
11
+
12
+ export default function NativeReplay({
13
+ events,
14
+ autoplay = false,
15
+ loop = false,
16
+ speeds = DEFAULT_SPEEDS,
17
+ }: {
18
+ events: eventWithTime[];
19
+ autoplay?: boolean;
20
+ loop?: boolean;
21
+ speeds?: number[];
22
+ }) {
23
+ const stageRef = useRef<HTMLDivElement>(null);
24
+ const playerRef = useRef<HTMLDivElement>(null);
25
+ const replayerRef = useRef<Replayer | null>(null);
26
+ const fedCountRef = useRef(0);
27
+ const [playing, setPlaying] = useState(false);
28
+ const [speed, setSpeed] = useState(1);
29
+ const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
30
+ const [scale, setScale] = useState(1);
31
+ const [currentTime, setCurrentTime] = useState(0);
32
+ const [totalTime, setTotalTime] = useState(0);
33
+ const [scrubbing, setScrubbing] = useState<number | null>(null);
34
+ const speedRef = useRef(speed);
35
+ const loopRef = useRef(loop);
36
+ const autoplayRef = useRef(autoplay);
37
+
38
+ const prepared = useMemo(
39
+ () => events.map((event) => sanitizeReplayEvent(event)).sort((a, b) => a.timestamp - b.timestamp),
40
+ [events],
41
+ );
42
+ const hasSnapshot = prepared.some((event) => event.type === EventType.FullSnapshot);
43
+
44
+ useEffect(() => {
45
+ if (replayerRef.current && prepared.length < fedCountRef.current) {
46
+ try {
47
+ replayerRef.current.destroy();
48
+ } catch {
49
+ // Ignore stale player teardown.
50
+ }
51
+ replayerRef.current = null;
52
+ fedCountRef.current = 0;
53
+ }
54
+ if (!hasSnapshot || !playerRef.current) return;
55
+
56
+ if (!replayerRef.current) {
57
+ const replayer = new Replayer(prepared, {
58
+ root: playerRef.current,
59
+ liveMode: false,
60
+ speed: speedRef.current,
61
+ skipInactive: true,
62
+ inactivePeriodThreshold: SKIP_IDLE_THRESHOLD_MS,
63
+ mouseTail: false,
64
+ showWarning: false,
65
+ });
66
+ replayer.on('resize', (payload) => {
67
+ const size = payload as { width?: number; height?: number };
68
+ if (size.width && size.height) setDims({ w: size.width, h: size.height });
69
+ });
70
+ replayer.on('finish', () => {
71
+ if (loopRef.current) {
72
+ replayer.play(0);
73
+ setPlaying(true);
74
+ } else {
75
+ setPlaying(false);
76
+ }
77
+ });
78
+ replayer.pause(0);
79
+ replayerRef.current = replayer;
80
+ fedCountRef.current = prepared.length;
81
+ if (autoplayRef.current) {
82
+ replayer.play(0);
83
+ setPlaying(true);
84
+ }
85
+ } else if (prepared.length > fedCountRef.current) {
86
+ for (let i = fedCountRef.current; i < prepared.length; i++) {
87
+ replayerRef.current.addEvent(prepared[i]!);
88
+ }
89
+ fedCountRef.current = prepared.length;
90
+ }
91
+ }, [hasSnapshot, prepared]);
92
+
93
+ useEffect(
94
+ () => () => {
95
+ try {
96
+ replayerRef.current?.destroy();
97
+ } catch {
98
+ // Ignore unmount teardown errors from rrweb.
99
+ }
100
+ replayerRef.current = null;
101
+ fedCountRef.current = 0;
102
+ },
103
+ [],
104
+ );
105
+
106
+ useEffect(() => {
107
+ speedRef.current = speed;
108
+ replayerRef.current?.setConfig({ speed });
109
+ }, [speed]);
110
+
111
+ useEffect(() => {
112
+ loopRef.current = loop;
113
+ }, [loop]);
114
+
115
+ useEffect(() => {
116
+ autoplayRef.current = autoplay;
117
+ }, [autoplay]);
118
+
119
+ const recalcScale = useCallback(() => {
120
+ if (!dims || !stageRef.current || dims.w <= 0) return;
121
+ setScale(Math.min(1, stageRef.current.clientWidth / dims.w));
122
+ }, [dims]);
123
+
124
+ useEffect(() => {
125
+ recalcScale();
126
+ window.addEventListener('resize', recalcScale);
127
+ return () => window.removeEventListener('resize', recalcScale);
128
+ }, [recalcScale]);
129
+
130
+ useEffect(() => {
131
+ if (!hasSnapshot) return;
132
+ const interval = window.setInterval(() => {
133
+ const replayer = replayerRef.current;
134
+ if (!replayer) return;
135
+ try {
136
+ const total = replayer.getMetaData().totalTime;
137
+ const current = Math.max(0, Math.min(replayer.getCurrentTime(), total));
138
+ setTotalTime(total);
139
+ if (scrubbing === null) setCurrentTime(current);
140
+ } catch {
141
+ // rrweb can briefly be unready while a full snapshot is applied.
142
+ }
143
+ }, 200);
144
+ return () => window.clearInterval(interval);
145
+ }, [hasSnapshot, scrubbing]);
146
+
147
+ const seek = useCallback(
148
+ (value: number) => {
149
+ const replayer = replayerRef.current;
150
+ if (!replayer) return;
151
+ const next = Math.max(0, Math.min(value, totalTime));
152
+ if (playing) replayer.play(next);
153
+ else replayer.pause(next);
154
+ setCurrentTime(next);
155
+ },
156
+ [playing, totalTime],
157
+ );
158
+
159
+ const toggle = () => {
160
+ const replayer = replayerRef.current;
161
+ if (!replayer) return;
162
+ if (playing) {
163
+ replayer.pause();
164
+ setPlaying(false);
165
+ return;
166
+ }
167
+ let offset = currentTime;
168
+ try {
169
+ if (offset >= replayer.getMetaData().totalTime - 1) offset = 0;
170
+ } catch {
171
+ offset = 0;
172
+ }
173
+ replayer.play(offset);
174
+ setPlaying(true);
175
+ };
176
+
177
+ const restart = () => {
178
+ replayerRef.current?.play(0);
179
+ setCurrentTime(0);
180
+ setPlaying(true);
181
+ };
182
+
183
+ if (!hasSnapshot) {
184
+ return (
185
+ <div className="replay-preview__empty">
186
+ {events.length ? `Captured ${events.length} events, but no full DOM snapshot is available.` : 'Replay data is not available yet.'}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ return (
192
+ <div className="replay-preview__native">
193
+ <div
194
+ ref={stageRef}
195
+ className="replay-preview__stage"
196
+ style={dims ? { height: Math.max(1, Math.round(dims.h * scale)) } : undefined}
197
+ >
198
+ <div
199
+ className="replay-preview__scaler"
200
+ style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}
201
+ >
202
+ <div ref={playerRef} className="replay-preview__player" aria-hidden="true" />
203
+ </div>
204
+ </div>
205
+ <div className="replay-preview__controls">
206
+ <button type="button" onClick={toggle} aria-label={playing ? 'Pause replay' : 'Play replay'}>
207
+ {playing ? 'Pause' : 'Play'}
208
+ </button>
209
+ <button type="button" onClick={restart} aria-label="Restart replay">
210
+ Restart
211
+ </button>
212
+ <input
213
+ type="range"
214
+ min={0}
215
+ max={Math.max(totalTime, 1)}
216
+ step={1}
217
+ value={scrubbing ?? currentTime}
218
+ onChange={(event) => setScrubbing(Number(event.target.value))}
219
+ onPointerUp={(event) => {
220
+ seek(Number((event.currentTarget as HTMLInputElement).value));
221
+ setScrubbing(null);
222
+ }}
223
+ aria-label="Replay position"
224
+ />
225
+ <select value={speed} onChange={(event) => setSpeed(Number(event.target.value))} aria-label="Replay speed">
226
+ {speeds.map((option) => (
227
+ <option key={option} value={option}>
228
+ {option}×
229
+ </option>
230
+ ))}
231
+ </select>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
@@ -1,10 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { EventType, Replayer } from 'rrweb';
1
+ import { Suspense, lazy, useEffect, useState } from 'react';
3
2
  import type { eventWithTime } from 'rrweb';
4
3
  import { normalizeReplayEmbedUrl } from './url';
5
- import { sanitizeReplayEvent } from './sanitize';
6
- import 'rrweb/dist/style.css';
7
- import './styles.css';
8
4
 
9
5
  export interface ReplayPageSource {
10
6
  kind: 'url';
@@ -45,7 +41,6 @@ export interface ReplayPreviewProps {
45
41
  }
46
42
 
47
43
  const DEFAULT_SPEEDS = [0.5, 1, 2, 4, 8];
48
- const SKIP_IDLE_THRESHOLD_MS = 5_000;
49
44
 
50
45
  function sourcePage(source: ReplaySource): ReplayPageSource | null {
51
46
  if (typeof source === 'string') return { kind: 'url', url: source };
@@ -86,228 +81,14 @@ function ReplayFrame({ source, title }: { source: ReplayPageSource; title?: stri
86
81
  );
87
82
  }
88
83
 
89
- function NativeReplay({
90
- events,
91
- autoplay = false,
92
- loop = false,
93
- speeds = DEFAULT_SPEEDS,
94
- }: {
95
- events: eventWithTime[];
96
- autoplay?: boolean;
97
- loop?: boolean;
98
- speeds?: number[];
99
- }) {
100
- const stageRef = useRef<HTMLDivElement>(null);
101
- const playerRef = useRef<HTMLDivElement>(null);
102
- const replayerRef = useRef<Replayer | null>(null);
103
- const fedCountRef = useRef(0);
104
- const [playing, setPlaying] = useState(false);
105
- const [speed, setSpeed] = useState(1);
106
- const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
107
- const [scale, setScale] = useState(1);
108
- const [currentTime, setCurrentTime] = useState(0);
109
- const [totalTime, setTotalTime] = useState(0);
110
- const [scrubbing, setScrubbing] = useState<number | null>(null);
111
- const speedRef = useRef(speed);
112
- const loopRef = useRef(loop);
113
- const autoplayRef = useRef(autoplay);
114
-
115
- const prepared = useMemo(
116
- () => events.map((event) => sanitizeReplayEvent(event)).sort((a, b) => a.timestamp - b.timestamp),
117
- [events],
118
- );
119
- const hasSnapshot = prepared.some((event) => event.type === EventType.FullSnapshot);
120
-
121
- useEffect(() => {
122
- if (replayerRef.current && prepared.length < fedCountRef.current) {
123
- try {
124
- replayerRef.current.destroy();
125
- } catch {
126
- // Ignore stale player teardown.
127
- }
128
- replayerRef.current = null;
129
- fedCountRef.current = 0;
130
- }
131
- if (!hasSnapshot || !playerRef.current) return;
132
-
133
- if (!replayerRef.current) {
134
- const replayer = new Replayer(prepared, {
135
- root: playerRef.current,
136
- liveMode: false,
137
- speed: speedRef.current,
138
- skipInactive: true,
139
- inactivePeriodThreshold: SKIP_IDLE_THRESHOLD_MS,
140
- mouseTail: false,
141
- showWarning: false,
142
- });
143
- replayer.on('resize', (payload) => {
144
- const size = payload as { width?: number; height?: number };
145
- if (size.width && size.height) setDims({ w: size.width, h: size.height });
146
- });
147
- replayer.on('finish', () => {
148
- if (loopRef.current) {
149
- replayer.play(0);
150
- setPlaying(true);
151
- } else {
152
- setPlaying(false);
153
- }
154
- });
155
- replayer.pause(0);
156
- replayerRef.current = replayer;
157
- fedCountRef.current = prepared.length;
158
- if (autoplayRef.current) {
159
- replayer.play(0);
160
- setPlaying(true);
161
- }
162
- } else if (prepared.length > fedCountRef.current) {
163
- for (let i = fedCountRef.current; i < prepared.length; i++) {
164
- replayerRef.current.addEvent(prepared[i]!);
165
- }
166
- fedCountRef.current = prepared.length;
167
- }
168
- }, [hasSnapshot, prepared]);
169
-
170
- useEffect(
171
- () => () => {
172
- try {
173
- replayerRef.current?.destroy();
174
- } catch {
175
- // Ignore unmount teardown errors from rrweb.
176
- }
177
- replayerRef.current = null;
178
- fedCountRef.current = 0;
179
- },
180
- [],
181
- );
182
-
183
- useEffect(() => {
184
- speedRef.current = speed;
185
- replayerRef.current?.setConfig({ speed });
186
- }, [speed]);
187
-
188
- useEffect(() => {
189
- loopRef.current = loop;
190
- }, [loop]);
191
-
192
- useEffect(() => {
193
- autoplayRef.current = autoplay;
194
- }, [autoplay]);
195
-
196
- const recalcScale = useCallback(() => {
197
- if (!dims || !stageRef.current || dims.w <= 0) return;
198
- setScale(Math.min(1, stageRef.current.clientWidth / dims.w));
199
- }, [dims]);
200
-
201
- useEffect(() => {
202
- recalcScale();
203
- window.addEventListener('resize', recalcScale);
204
- return () => window.removeEventListener('resize', recalcScale);
205
- }, [recalcScale]);
206
-
207
- useEffect(() => {
208
- if (!hasSnapshot) return;
209
- const interval = window.setInterval(() => {
210
- const replayer = replayerRef.current;
211
- if (!replayer) return;
212
- try {
213
- const total = replayer.getMetaData().totalTime;
214
- const current = Math.max(0, Math.min(replayer.getCurrentTime(), total));
215
- setTotalTime(total);
216
- if (scrubbing === null) setCurrentTime(current);
217
- } catch {
218
- // rrweb can briefly be unready while a full snapshot is applied.
219
- }
220
- }, 200);
221
- return () => window.clearInterval(interval);
222
- }, [hasSnapshot, scrubbing]);
223
-
224
- const seek = useCallback(
225
- (value: number) => {
226
- const replayer = replayerRef.current;
227
- if (!replayer) return;
228
- const next = Math.max(0, Math.min(value, totalTime));
229
- if (playing) replayer.play(next);
230
- else replayer.pause(next);
231
- setCurrentTime(next);
232
- },
233
- [playing, totalTime],
234
- );
235
-
236
- const toggle = () => {
237
- const replayer = replayerRef.current;
238
- if (!replayer) return;
239
- if (playing) {
240
- replayer.pause();
241
- setPlaying(false);
242
- return;
243
- }
244
- let offset = currentTime;
245
- try {
246
- if (offset >= replayer.getMetaData().totalTime - 1) offset = 0;
247
- } catch {
248
- offset = 0;
249
- }
250
- replayer.play(offset);
251
- setPlaying(true);
252
- };
253
-
254
- const restart = () => {
255
- replayerRef.current?.play(0);
256
- setCurrentTime(0);
257
- setPlaying(true);
258
- };
259
-
260
- if (!hasSnapshot) {
261
- return (
262
- <div className="replay-preview__empty">
263
- {events.length ? `Captured ${events.length} events, but no full DOM snapshot is available.` : 'Replay data is not available yet.'}
264
- </div>
265
- );
266
- }
84
+ // rrweb (and its stylesheet) load on first use of an event-based source.
85
+ const LazyNativeReplay = lazy(() => import('./NativeReplay'));
267
86
 
87
+ function NativeReplay(props: { events: eventWithTime[]; autoplay?: boolean; loop?: boolean; speeds?: number[] }) {
268
88
  return (
269
- <div className="replay-preview__native">
270
- <div
271
- ref={stageRef}
272
- className="replay-preview__stage"
273
- style={dims ? { height: Math.max(1, Math.round(dims.h * scale)) } : undefined}
274
- >
275
- <div
276
- className="replay-preview__scaler"
277
- style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}
278
- >
279
- <div ref={playerRef} className="replay-preview__player" aria-hidden="true" />
280
- </div>
281
- </div>
282
- <div className="replay-preview__controls">
283
- <button type="button" onClick={toggle} aria-label={playing ? 'Pause replay' : 'Play replay'}>
284
- {playing ? 'Pause' : 'Play'}
285
- </button>
286
- <button type="button" onClick={restart} aria-label="Restart replay">
287
- Restart
288
- </button>
289
- <input
290
- type="range"
291
- min={0}
292
- max={Math.max(totalTime, 1)}
293
- step={1}
294
- value={scrubbing ?? currentTime}
295
- onChange={(event) => setScrubbing(Number(event.target.value))}
296
- onPointerUp={(event) => {
297
- seek(Number((event.currentTarget as HTMLInputElement).value));
298
- setScrubbing(null);
299
- }}
300
- aria-label="Replay position"
301
- />
302
- <select value={speed} onChange={(event) => setSpeed(Number(event.target.value))} aria-label="Replay speed">
303
- {speeds.map((option) => (
304
- <option key={option} value={option}>
305
- {option}×
306
- </option>
307
- ))}
308
- </select>
309
- </div>
310
- </div>
89
+ <Suspense fallback={<div className="replay-preview__empty">Loading replay…</div>}>
90
+ <LazyNativeReplay {...props} />
91
+ </Suspense>
311
92
  );
312
93
  }
313
94
 
package/src/styles.css CHANGED
@@ -1,4 +1,3 @@
1
- @import "streamdown/styles.css";
2
1
 
3
2
  [data-docstream],
4
3
  .docs-article {
@@ -2344,6 +2343,97 @@
2344
2343
  }
2345
2344
  }
2346
2345
 
2346
+ /* Replay embeds (ReplayPreview / ReplayEmbed). Part of the one package stylesheet so no
2347
+ module imports CSS on the eager path: the root entry stays importable from Node
2348
+ (SSR, vitest) without a CSS loader. */
2349
+ .replay-preview {
2350
+ width: 100%;
2351
+ min-width: 0;
2352
+ color: var(--replay-preview-fg, #e5e7eb);
2353
+ background: var(--replay-preview-bg, #0f172a);
2354
+ border: 1px solid var(--replay-preview-border, rgba(148, 163, 184, 0.28));
2355
+ border-radius: 12px;
2356
+ overflow: hidden;
2357
+ }
2358
+
2359
+ .replay-preview__frame {
2360
+ display: block;
2361
+ width: 100%;
2362
+ aspect-ratio: 16 / 9;
2363
+ border: 0;
2364
+ background: #0f172a;
2365
+ }
2366
+
2367
+ .replay-preview__invalid,
2368
+ .replay-preview__empty,
2369
+ .replay-preview__error {
2370
+ display: flex;
2371
+ align-items: center;
2372
+ justify-content: center;
2373
+ min-height: 140px;
2374
+ padding: 20px;
2375
+ color: var(--replay-preview-muted, #94a3b8);
2376
+ font: 13px/1.5 ui-sans-serif, system-ui, sans-serif;
2377
+ text-align: center;
2378
+ }
2379
+
2380
+ .replay-preview__invalid {
2381
+ color: var(--replay-preview-link, #7dd3fc);
2382
+ }
2383
+
2384
+ .replay-preview__error {
2385
+ color: #fda4af;
2386
+ }
2387
+
2388
+ .replay-preview__native {
2389
+ width: 100%;
2390
+ background: #0f172a;
2391
+ }
2392
+
2393
+ .replay-preview__stage {
2394
+ position: relative;
2395
+ width: 100%;
2396
+ min-height: 160px;
2397
+ overflow: hidden;
2398
+ background: #020617;
2399
+ }
2400
+
2401
+ .replay-preview__scaler {
2402
+ position: absolute;
2403
+ inset: 0 auto auto 0;
2404
+ }
2405
+
2406
+ .replay-preview__player {
2407
+ position: relative;
2408
+ overflow: hidden;
2409
+ }
2410
+
2411
+ .replay-preview__controls {
2412
+ display: flex;
2413
+ align-items: center;
2414
+ gap: 8px;
2415
+ padding: 8px 10px;
2416
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
2417
+ background: #111827;
2418
+ font: 12px/1.2 ui-sans-serif, system-ui, sans-serif;
2419
+ }
2420
+
2421
+ .replay-preview__controls button,
2422
+ .replay-preview__controls select {
2423
+ border: 1px solid rgba(148, 163, 184, 0.3);
2424
+ border-radius: 6px;
2425
+ padding: 5px 8px;
2426
+ color: inherit;
2427
+ background: #1e293b;
2428
+ cursor: pointer;
2429
+ }
2430
+
2431
+ .replay-preview__controls input[type='range'] {
2432
+ min-width: 60px;
2433
+ flex: 1;
2434
+ accent-color: #38bdf8;
2435
+ }
2436
+
2347
2437
  /* Keep every rendered table variant flush, after the shared block spacing rules. */
2348
2438
  :is([data-docstream], .docs-article) [data-docstream-blocks] > :is(.docs-table, table, .docs-cards) {
2349
2439
  margin-block-start: 0;
@@ -1,87 +0,0 @@
1
- .replay-preview {
2
- width: 100%;
3
- min-width: 0;
4
- color: var(--replay-preview-fg, #e5e7eb);
5
- background: var(--replay-preview-bg, #0f172a);
6
- border: 1px solid var(--replay-preview-border, rgba(148, 163, 184, 0.28));
7
- border-radius: 12px;
8
- overflow: hidden;
9
- }
10
-
11
- .replay-preview__frame {
12
- display: block;
13
- width: 100%;
14
- aspect-ratio: 16 / 9;
15
- border: 0;
16
- background: #0f172a;
17
- }
18
-
19
- .replay-preview__invalid,
20
- .replay-preview__empty,
21
- .replay-preview__error {
22
- display: flex;
23
- align-items: center;
24
- justify-content: center;
25
- min-height: 140px;
26
- padding: 20px;
27
- color: var(--replay-preview-muted, #94a3b8);
28
- font: 13px/1.5 ui-sans-serif, system-ui, sans-serif;
29
- text-align: center;
30
- }
31
-
32
- .replay-preview__invalid {
33
- color: var(--replay-preview-link, #7dd3fc);
34
- }
35
-
36
- .replay-preview__error {
37
- color: #fda4af;
38
- }
39
-
40
- .replay-preview__native {
41
- width: 100%;
42
- background: #0f172a;
43
- }
44
-
45
- .replay-preview__stage {
46
- position: relative;
47
- width: 100%;
48
- min-height: 160px;
49
- overflow: hidden;
50
- background: #020617;
51
- }
52
-
53
- .replay-preview__scaler {
54
- position: absolute;
55
- inset: 0 auto auto 0;
56
- }
57
-
58
- .replay-preview__player {
59
- position: relative;
60
- overflow: hidden;
61
- }
62
-
63
- .replay-preview__controls {
64
- display: flex;
65
- align-items: center;
66
- gap: 8px;
67
- padding: 8px 10px;
68
- border-top: 1px solid rgba(148, 163, 184, 0.2);
69
- background: #111827;
70
- font: 12px/1.2 ui-sans-serif, system-ui, sans-serif;
71
- }
72
-
73
- .replay-preview__controls button,
74
- .replay-preview__controls select {
75
- border: 1px solid rgba(148, 163, 184, 0.3);
76
- border-radius: 6px;
77
- padding: 5px 8px;
78
- color: inherit;
79
- background: #1e293b;
80
- cursor: pointer;
81
- }
82
-
83
- .replay-preview__controls input[type='range'] {
84
- min-width: 60px;
85
- flex: 1;
86
- accent-color: #38bdf8;
87
- }