@bendyline/squisq-editor-react 2.3.4 → 2.4.1
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/README.md +4 -2
- package/dist/{chunk-GNIVYDZH.js → chunk-KR4VLXUM.js} +10998 -8247
- package/dist/{chunk-6VDYKI3L.js → chunk-LJRHKXDV.js} +3 -1
- package/dist/chunk-LOTY7AOS.js +1925 -0
- package/dist/{chunk-5JMHFAVW.js → chunk-PDCKJCOS.js} +1200 -868
- package/dist/chunk-V4NBQF5C.js +62 -0
- package/dist/chunk-V6Z7GG55.js +16 -0
- package/dist/{chunk-54UGTQBO.js → chunk-WDX6UDL5.js} +1 -1
- package/dist/{chunk-NITZVAXL.js → chunk-WLJ623UZ.js} +24 -0
- package/dist/index.d.ts +289 -118
- package/dist/index.js +52 -20
- package/dist/json-editor/index.js +2 -2
- package/dist/monaco.d.ts +26 -0
- package/dist/monaco.js +144 -10
- package/dist/monacoLanguageDetection-DEyUW-BS.d.ts +18 -0
- package/dist/monacoSuggestions-MDBODZ7F.js +3 -0
- package/dist/recorder/index.d.ts +7 -407
- package/dist/recorder/index.js +3 -3
- package/dist/recorder-C5tAYUE3.d.ts +508 -0
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/index.js +6 -5
- package/dist/{shell-C-KkTBz7.d.ts → shell-CU4GpGuq.d.ts} +76 -6
- package/dist/styles/index.css +845 -26
- package/dist/teleprompter/index.d.ts +57 -246
- package/dist/teleprompter/index.js +12 -3
- package/dist/useNarrationStage-Bqo18PBw.d.ts +313 -0
- package/package.json +8 -6
- package/dist/chunk-5Q4JN4I5.js +0 -132
- package/dist/chunk-MJJK7YQB.js +0 -949
package/dist/chunk-MJJK7YQB.js
DELETED
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Icon
|
|
3
|
-
} from "./chunk-GS7QWYFT.js";
|
|
4
|
-
import {
|
|
5
|
-
buildFilename,
|
|
6
|
-
buildTimingJson,
|
|
7
|
-
encodeTimingJson,
|
|
8
|
-
requestCameraStream,
|
|
9
|
-
requestMicStream,
|
|
10
|
-
resolveFormat,
|
|
11
|
-
supportsDisplayMedia,
|
|
12
|
-
supportsMediaRecorder,
|
|
13
|
-
supportsUserMedia,
|
|
14
|
-
timingPathFor,
|
|
15
|
-
useStreamPreview
|
|
16
|
-
} from "./chunk-5Q4JN4I5.js";
|
|
17
|
-
|
|
18
|
-
// src/recorder/sources/screenStream.ts
|
|
19
|
-
function mixAudioTracks(streams) {
|
|
20
|
-
const sources = streams.map((s) => s.getAudioTracks()).flat().filter((t) => t.readyState === "live");
|
|
21
|
-
if (sources.length === 0) return null;
|
|
22
|
-
const AC = window.AudioContext;
|
|
23
|
-
if (typeof AC === "undefined") return null;
|
|
24
|
-
const ctx = new AC();
|
|
25
|
-
const dest = ctx.createMediaStreamDestination();
|
|
26
|
-
for (const track of sources) {
|
|
27
|
-
const src = ctx.createMediaStreamSource(new MediaStream([track]));
|
|
28
|
-
src.connect(dest);
|
|
29
|
-
}
|
|
30
|
-
const [mixed] = dest.stream.getAudioTracks();
|
|
31
|
-
if (!mixed) return null;
|
|
32
|
-
return { track: mixed, context: ctx };
|
|
33
|
-
}
|
|
34
|
-
async function requestScreenStream(options) {
|
|
35
|
-
if (!supportsDisplayMedia()) {
|
|
36
|
-
throw new Error("navigator.mediaDevices.getDisplayMedia is not available in this environment.");
|
|
37
|
-
}
|
|
38
|
-
const video = options?.video ?? true;
|
|
39
|
-
const systemAudio = options?.systemAudio ?? false;
|
|
40
|
-
const includeMic = options?.includeMicrophone ?? false;
|
|
41
|
-
const displayStream = await navigator.mediaDevices.getDisplayMedia({
|
|
42
|
-
video,
|
|
43
|
-
audio: systemAudio
|
|
44
|
-
});
|
|
45
|
-
if (!includeMic) {
|
|
46
|
-
return {
|
|
47
|
-
stream: displayStream,
|
|
48
|
-
dispose: () => {
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
if (!supportsUserMedia()) {
|
|
53
|
-
return {
|
|
54
|
-
stream: displayStream,
|
|
55
|
-
dispose: () => {
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
let micStream = null;
|
|
60
|
-
try {
|
|
61
|
-
micStream = await navigator.mediaDevices.getUserMedia({
|
|
62
|
-
audio: options?.microphoneConstraints ?? true,
|
|
63
|
-
video: false
|
|
64
|
-
});
|
|
65
|
-
} catch (err) {
|
|
66
|
-
displayStream.getTracks().forEach((t) => t.stop());
|
|
67
|
-
throw err;
|
|
68
|
-
}
|
|
69
|
-
const mix = mixAudioTracks([displayStream, micStream]);
|
|
70
|
-
if (!mix) {
|
|
71
|
-
micStream.getTracks().forEach((t) => t.stop());
|
|
72
|
-
return {
|
|
73
|
-
stream: displayStream,
|
|
74
|
-
dispose: () => {
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const [videoTrack] = displayStream.getVideoTracks();
|
|
79
|
-
const output = new MediaStream();
|
|
80
|
-
if (videoTrack) output.addTrack(videoTrack);
|
|
81
|
-
output.addTrack(mix.track);
|
|
82
|
-
const systemAudioTracks = displayStream.getAudioTracks();
|
|
83
|
-
let disposed = false;
|
|
84
|
-
const dispose = () => {
|
|
85
|
-
if (disposed) return;
|
|
86
|
-
disposed = true;
|
|
87
|
-
micStream?.getTracks().forEach((t) => t.stop());
|
|
88
|
-
micStream = null;
|
|
89
|
-
systemAudioTracks.forEach((t) => t.stop());
|
|
90
|
-
void mix.context.close().catch(() => {
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
return { stream: output, dispose };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/recorder/hooks/useMediaRecorder.ts
|
|
97
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
98
|
-
async function acquireStream(source, opts) {
|
|
99
|
-
switch (source) {
|
|
100
|
-
case "mic": {
|
|
101
|
-
const audio = typeof opts.audioConstraints === "object" ? opts.audioConstraints : void 0;
|
|
102
|
-
const stream = await requestMicStream(audio);
|
|
103
|
-
return { stream, dispose: () => {
|
|
104
|
-
} };
|
|
105
|
-
}
|
|
106
|
-
case "camera": {
|
|
107
|
-
const stream = await requestCameraStream({
|
|
108
|
-
video: opts.videoConstraints ?? true,
|
|
109
|
-
audio: opts.includeMicrophone === false ? false : opts.audioConstraints ?? true
|
|
110
|
-
});
|
|
111
|
-
return { stream, dispose: () => {
|
|
112
|
-
} };
|
|
113
|
-
}
|
|
114
|
-
case "screen":
|
|
115
|
-
case "screen+mic": {
|
|
116
|
-
const handle = await requestScreenStream({
|
|
117
|
-
video: opts.videoConstraints ?? true,
|
|
118
|
-
systemAudio: opts.systemAudio ?? false,
|
|
119
|
-
includeMicrophone: source === "screen+mic",
|
|
120
|
-
microphoneConstraints: typeof opts.audioConstraints === "object" ? opts.audioConstraints : void 0
|
|
121
|
-
});
|
|
122
|
-
return { stream: handle.stream, dispose: handle.dispose };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function captureKindFor(source) {
|
|
127
|
-
return source === "mic" ? "audio" : "video";
|
|
128
|
-
}
|
|
129
|
-
function getCaptureKind(source) {
|
|
130
|
-
return captureKindFor(source);
|
|
131
|
-
}
|
|
132
|
-
function useMediaRecorder(options = {}) {
|
|
133
|
-
const [state, setState] = useState("idle");
|
|
134
|
-
const [stream, setStream] = useState(null);
|
|
135
|
-
const [blob, setBlob] = useState(null);
|
|
136
|
-
const [format, setFormat] = useState(null);
|
|
137
|
-
const [durationMs, setDurationMs] = useState(0);
|
|
138
|
-
const [error, setError] = useState(null);
|
|
139
|
-
const recorderRef = useRef(null);
|
|
140
|
-
const chunksRef = useRef([]);
|
|
141
|
-
const disposeStreamRef = useRef(null);
|
|
142
|
-
const startTimestampRef = useRef(null);
|
|
143
|
-
const tickerRef = useRef(null);
|
|
144
|
-
const stopResolversRef = useRef([]);
|
|
145
|
-
const stopPromiseRef = useRef(null);
|
|
146
|
-
const requestPromiseRef = useRef(null);
|
|
147
|
-
const lifecycleRef = useRef(0);
|
|
148
|
-
const optionsRef = useRef(options);
|
|
149
|
-
optionsRef.current = options;
|
|
150
|
-
const clearTicker = useCallback(() => {
|
|
151
|
-
if (tickerRef.current !== null) {
|
|
152
|
-
clearInterval(tickerRef.current);
|
|
153
|
-
tickerRef.current = null;
|
|
154
|
-
}
|
|
155
|
-
}, []);
|
|
156
|
-
const releaseStream = useCallback(() => {
|
|
157
|
-
const s = recorderRef.current?.stream;
|
|
158
|
-
if (s) {
|
|
159
|
-
s.getTracks().forEach((t) => t.stop());
|
|
160
|
-
}
|
|
161
|
-
setStream((current) => {
|
|
162
|
-
current?.getTracks().forEach((t) => t.stop());
|
|
163
|
-
return null;
|
|
164
|
-
});
|
|
165
|
-
disposeStreamRef.current?.();
|
|
166
|
-
disposeStreamRef.current = null;
|
|
167
|
-
}, []);
|
|
168
|
-
const reset = useCallback(() => {
|
|
169
|
-
setBlob(null);
|
|
170
|
-
setDurationMs(0);
|
|
171
|
-
setError(null);
|
|
172
|
-
chunksRef.current = [];
|
|
173
|
-
startTimestampRef.current = null;
|
|
174
|
-
clearTicker();
|
|
175
|
-
const rec = recorderRef.current;
|
|
176
|
-
if (rec && rec.state === "inactive" && rec.stream.active) {
|
|
177
|
-
setState("ready");
|
|
178
|
-
} else {
|
|
179
|
-
setState("idle");
|
|
180
|
-
}
|
|
181
|
-
}, [clearTicker]);
|
|
182
|
-
const cancel = useCallback(() => {
|
|
183
|
-
lifecycleRef.current += 1;
|
|
184
|
-
const rec = recorderRef.current;
|
|
185
|
-
if (rec && rec.state !== "inactive") {
|
|
186
|
-
try {
|
|
187
|
-
rec.ondataavailable = null;
|
|
188
|
-
rec.onstop = null;
|
|
189
|
-
rec.onerror = null;
|
|
190
|
-
rec.stop();
|
|
191
|
-
} catch {
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
recorderRef.current = null;
|
|
195
|
-
releaseStream();
|
|
196
|
-
clearTicker();
|
|
197
|
-
chunksRef.current = [];
|
|
198
|
-
startTimestampRef.current = null;
|
|
199
|
-
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
200
|
-
stopPromiseRef.current = null;
|
|
201
|
-
requestPromiseRef.current = null;
|
|
202
|
-
setBlob(null);
|
|
203
|
-
setDurationMs(0);
|
|
204
|
-
setError(null);
|
|
205
|
-
setState("idle");
|
|
206
|
-
}, [clearTicker, releaseStream]);
|
|
207
|
-
const request = useCallback(async () => {
|
|
208
|
-
if (requestPromiseRef.current) return requestPromiseRef.current;
|
|
209
|
-
if (recorderRef.current?.stream.active) return;
|
|
210
|
-
if (!supportsMediaRecorder()) {
|
|
211
|
-
const err = new Error("MediaRecorder is not supported in this environment.");
|
|
212
|
-
setError(err);
|
|
213
|
-
setState("error");
|
|
214
|
-
throw err;
|
|
215
|
-
}
|
|
216
|
-
const lifecycle = ++lifecycleRef.current;
|
|
217
|
-
const requestPromise = (async () => {
|
|
218
|
-
setError(null);
|
|
219
|
-
setState("requesting");
|
|
220
|
-
let acquired = null;
|
|
221
|
-
try {
|
|
222
|
-
const source = optionsRef.current.source ?? "mic";
|
|
223
|
-
acquired = await acquireStream(source, optionsRef.current);
|
|
224
|
-
const { stream: nextStream, dispose } = acquired;
|
|
225
|
-
if (lifecycle !== lifecycleRef.current) {
|
|
226
|
-
nextStream.getTracks().forEach((track) => track.stop());
|
|
227
|
-
dispose();
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const resolved = resolveFormat(captureKindFor(source), optionsRef.current.mimeType);
|
|
231
|
-
const recorderOptions = {};
|
|
232
|
-
if (resolved.mimeType) recorderOptions.mimeType = resolved.mimeType;
|
|
233
|
-
if (optionsRef.current.bitsPerSecond) {
|
|
234
|
-
recorderOptions.bitsPerSecond = optionsRef.current.bitsPerSecond;
|
|
235
|
-
}
|
|
236
|
-
const recorder = new MediaRecorder(nextStream, recorderOptions);
|
|
237
|
-
recorder.ondataavailable = (e) => {
|
|
238
|
-
if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
|
|
239
|
-
};
|
|
240
|
-
recorder.onstop = () => {
|
|
241
|
-
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
242
|
-
const recordedType = recorder.mimeType || resolved.mimeType || "application/octet-stream";
|
|
243
|
-
const finalBlob = new Blob(chunksRef.current, { type: recordedType });
|
|
244
|
-
chunksRef.current = [];
|
|
245
|
-
setBlob(finalBlob);
|
|
246
|
-
setState("stopped");
|
|
247
|
-
clearTicker();
|
|
248
|
-
stopResolversRef.current.splice(0).forEach((resolve) => resolve(finalBlob));
|
|
249
|
-
stopPromiseRef.current = null;
|
|
250
|
-
};
|
|
251
|
-
recorder.onerror = (event) => {
|
|
252
|
-
if (recorderRef.current !== recorder || lifecycle !== lifecycleRef.current) return;
|
|
253
|
-
const detail = event.error;
|
|
254
|
-
const err = detail instanceof Error ? detail : new Error("Recorder error");
|
|
255
|
-
setError(err);
|
|
256
|
-
setState("error");
|
|
257
|
-
clearTicker();
|
|
258
|
-
stopResolversRef.current.splice(0).forEach((resolve) => resolve(null));
|
|
259
|
-
stopPromiseRef.current = null;
|
|
260
|
-
};
|
|
261
|
-
recorderRef.current = recorder;
|
|
262
|
-
disposeStreamRef.current = dispose;
|
|
263
|
-
setStream(nextStream);
|
|
264
|
-
setFormat(resolved);
|
|
265
|
-
setBlob(null);
|
|
266
|
-
setDurationMs(0);
|
|
267
|
-
setState("ready");
|
|
268
|
-
acquired = null;
|
|
269
|
-
} catch (err) {
|
|
270
|
-
if (acquired) {
|
|
271
|
-
acquired.stream.getTracks().forEach((track) => track.stop());
|
|
272
|
-
acquired.dispose();
|
|
273
|
-
}
|
|
274
|
-
const normalized = err instanceof Error ? err : new Error("Stream acquisition failed");
|
|
275
|
-
if (lifecycle === lifecycleRef.current) {
|
|
276
|
-
setError(normalized);
|
|
277
|
-
setState("error");
|
|
278
|
-
}
|
|
279
|
-
throw normalized;
|
|
280
|
-
} finally {
|
|
281
|
-
if (lifecycle === lifecycleRef.current) requestPromiseRef.current = null;
|
|
282
|
-
}
|
|
283
|
-
})();
|
|
284
|
-
requestPromiseRef.current = requestPromise;
|
|
285
|
-
return requestPromise;
|
|
286
|
-
}, [clearTicker]);
|
|
287
|
-
const start = useCallback(() => {
|
|
288
|
-
const rec = recorderRef.current;
|
|
289
|
-
if (!rec) {
|
|
290
|
-
const err = new Error("Recorder is not ready. Call request() first.");
|
|
291
|
-
setError(err);
|
|
292
|
-
setState("error");
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (rec.state === "recording") return;
|
|
296
|
-
chunksRef.current = [];
|
|
297
|
-
setBlob(null);
|
|
298
|
-
setDurationMs(0);
|
|
299
|
-
startTimestampRef.current = Date.now();
|
|
300
|
-
rec.start(1e3);
|
|
301
|
-
setState("recording");
|
|
302
|
-
clearTicker();
|
|
303
|
-
tickerRef.current = setInterval(() => {
|
|
304
|
-
if (startTimestampRef.current !== null) {
|
|
305
|
-
setDurationMs(Date.now() - startTimestampRef.current);
|
|
306
|
-
}
|
|
307
|
-
}, 100);
|
|
308
|
-
}, [clearTicker]);
|
|
309
|
-
const stop = useCallback(() => {
|
|
310
|
-
if (stopPromiseRef.current) return stopPromiseRef.current;
|
|
311
|
-
const rec = recorderRef.current;
|
|
312
|
-
if (!rec || rec.state === "inactive") {
|
|
313
|
-
return Promise.resolve(blob);
|
|
314
|
-
}
|
|
315
|
-
setState("stopping");
|
|
316
|
-
const stopPromise = new Promise((resolve) => {
|
|
317
|
-
stopResolversRef.current.push(resolve);
|
|
318
|
-
try {
|
|
319
|
-
rec.stop();
|
|
320
|
-
} catch (err) {
|
|
321
|
-
const normalized = err instanceof Error ? err : new Error("Failed to stop recorder");
|
|
322
|
-
setError(normalized);
|
|
323
|
-
setState("error");
|
|
324
|
-
clearTicker();
|
|
325
|
-
stopResolversRef.current.splice(0).forEach((r) => r(null));
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
stopPromiseRef.current = stopPromise;
|
|
329
|
-
void stopPromise.finally(() => {
|
|
330
|
-
if (stopPromiseRef.current === stopPromise) stopPromiseRef.current = null;
|
|
331
|
-
});
|
|
332
|
-
return stopPromise;
|
|
333
|
-
}, [blob, clearTicker]);
|
|
334
|
-
useEffect(() => {
|
|
335
|
-
const pendingResolvers = stopResolversRef.current;
|
|
336
|
-
return () => {
|
|
337
|
-
lifecycleRef.current += 1;
|
|
338
|
-
requestPromiseRef.current = null;
|
|
339
|
-
const rec = recorderRef.current;
|
|
340
|
-
if (rec && rec.state !== "inactive") {
|
|
341
|
-
try {
|
|
342
|
-
rec.ondataavailable = null;
|
|
343
|
-
rec.onstop = null;
|
|
344
|
-
rec.onerror = null;
|
|
345
|
-
rec.stop();
|
|
346
|
-
} catch {
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
releaseStream();
|
|
350
|
-
clearTicker();
|
|
351
|
-
pendingResolvers.splice(0).forEach((resolve) => resolve(null));
|
|
352
|
-
stopPromiseRef.current = null;
|
|
353
|
-
};
|
|
354
|
-
}, [releaseStream, clearTicker]);
|
|
355
|
-
return {
|
|
356
|
-
state,
|
|
357
|
-
stream,
|
|
358
|
-
blob,
|
|
359
|
-
mimeType: format?.mimeType ?? null,
|
|
360
|
-
extension: format?.extension ?? null,
|
|
361
|
-
directory: format?.directory ?? null,
|
|
362
|
-
durationMs,
|
|
363
|
-
error,
|
|
364
|
-
request,
|
|
365
|
-
start,
|
|
366
|
-
stop,
|
|
367
|
-
cancel,
|
|
368
|
-
reset
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// src/recorder/RecorderModal.tsx
|
|
373
|
-
import { useCallback as useCallback2, useEffect as useEffect2, useId, useRef as useRef2, useState as useState2 } from "react";
|
|
374
|
-
|
|
375
|
-
// src/modal/useModalDialog.ts
|
|
376
|
-
import { useModalDialog } from "@bendyline/squisq-react";
|
|
377
|
-
|
|
378
|
-
// src/recorder/RecorderModal.tsx
|
|
379
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
380
|
-
var overlayStyle = {
|
|
381
|
-
position: "fixed",
|
|
382
|
-
inset: 0,
|
|
383
|
-
background: "rgba(0, 0, 0, 0.5)",
|
|
384
|
-
display: "flex",
|
|
385
|
-
alignItems: "center",
|
|
386
|
-
justifyContent: "center",
|
|
387
|
-
zIndex: 1e4
|
|
388
|
-
};
|
|
389
|
-
function recorderThemeStyle(colorScheme) {
|
|
390
|
-
const dark = colorScheme === "dark";
|
|
391
|
-
return {
|
|
392
|
-
colorScheme,
|
|
393
|
-
"--squisq-recorder-surface": `var(--squisq-bg, ${dark ? "#1f2937" : "#fffdf7"})`,
|
|
394
|
-
"--squisq-recorder-input": `var(--squisq-input-bg, ${dark ? "#374151" : "#fff"})`,
|
|
395
|
-
"--squisq-recorder-border": `var(--squisq-border, ${dark ? "#4b5563" : "#c9b98a"})`,
|
|
396
|
-
"--squisq-recorder-text": `var(--squisq-text, ${dark ? "#e5e7eb" : "#4a3c1f"})`,
|
|
397
|
-
"--squisq-recorder-muted": `var(--squisq-text-muted, ${dark ? "#9ca3af" : "#5a4a2a"})`,
|
|
398
|
-
"--squisq-recorder-accent": "var(--squisq-accent, #8b6914)",
|
|
399
|
-
"--squisq-recorder-accent-text": "#fff",
|
|
400
|
-
"--squisq-recorder-danger": dark ? "#dc4c4c" : "#b33a3a",
|
|
401
|
-
"--squisq-recorder-danger-border": dark ? "#ef6a6a" : "#902929",
|
|
402
|
-
"--squisq-recorder-error-bg": dark ? "#3f151b" : "#fceeee",
|
|
403
|
-
"--squisq-recorder-error-border": dark ? "#7f1d1d" : "#d88a8a",
|
|
404
|
-
"--squisq-recorder-error-text": dark ? "#fecdd3" : "#8c2a2a"
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
var modalStyle = {
|
|
408
|
-
background: "var(--squisq-recorder-surface)",
|
|
409
|
-
border: "1px solid var(--squisq-recorder-border)",
|
|
410
|
-
borderRadius: 0,
|
|
411
|
-
padding: "24px 28px",
|
|
412
|
-
width: "min(560px, calc(100vw - 48px))",
|
|
413
|
-
maxHeight: "calc(100vh - 48px)",
|
|
414
|
-
overflowY: "auto",
|
|
415
|
-
boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
|
|
416
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
417
|
-
color: "var(--squisq-recorder-text)"
|
|
418
|
-
};
|
|
419
|
-
var titleStyle = {
|
|
420
|
-
margin: "0 0 16px 0",
|
|
421
|
-
fontSize: 18,
|
|
422
|
-
fontWeight: 600,
|
|
423
|
-
color: "var(--squisq-recorder-text)"
|
|
424
|
-
};
|
|
425
|
-
var labelStyle = {
|
|
426
|
-
display: "block",
|
|
427
|
-
fontSize: 13,
|
|
428
|
-
fontWeight: 500,
|
|
429
|
-
marginBottom: 4,
|
|
430
|
-
color: "var(--squisq-recorder-text)"
|
|
431
|
-
};
|
|
432
|
-
var inputStyle = {
|
|
433
|
-
width: "100%",
|
|
434
|
-
padding: "6px 8px",
|
|
435
|
-
fontSize: 13,
|
|
436
|
-
fontFamily: "inherit",
|
|
437
|
-
border: "1px solid var(--squisq-recorder-border)",
|
|
438
|
-
borderRadius: 0,
|
|
439
|
-
background: "var(--squisq-recorder-input)",
|
|
440
|
-
color: "var(--squisq-recorder-text)",
|
|
441
|
-
marginBottom: 12,
|
|
442
|
-
boxSizing: "border-box"
|
|
443
|
-
};
|
|
444
|
-
var textareaStyle = {
|
|
445
|
-
...inputStyle,
|
|
446
|
-
resize: "vertical",
|
|
447
|
-
minHeight: 72
|
|
448
|
-
};
|
|
449
|
-
var btnPrimary = {
|
|
450
|
-
padding: "8px 20px",
|
|
451
|
-
fontSize: 14,
|
|
452
|
-
fontFamily: "inherit",
|
|
453
|
-
fontWeight: 500,
|
|
454
|
-
cursor: "pointer",
|
|
455
|
-
background: "var(--squisq-recorder-accent)",
|
|
456
|
-
color: "var(--squisq-recorder-accent-text)",
|
|
457
|
-
border: "1px solid var(--squisq-recorder-accent)",
|
|
458
|
-
borderRadius: 0
|
|
459
|
-
};
|
|
460
|
-
var btnSecondary = {
|
|
461
|
-
padding: "8px 20px",
|
|
462
|
-
fontSize: 14,
|
|
463
|
-
fontFamily: "inherit",
|
|
464
|
-
fontWeight: 500,
|
|
465
|
-
cursor: "pointer",
|
|
466
|
-
background: "var(--squisq-recorder-input)",
|
|
467
|
-
color: "var(--squisq-recorder-text)",
|
|
468
|
-
border: "1px solid var(--squisq-recorder-border)",
|
|
469
|
-
borderRadius: 0
|
|
470
|
-
};
|
|
471
|
-
var btnDanger = {
|
|
472
|
-
...btnPrimary,
|
|
473
|
-
background: "var(--squisq-recorder-danger)",
|
|
474
|
-
borderColor: "var(--squisq-recorder-danger-border)"
|
|
475
|
-
};
|
|
476
|
-
var toggleRowStyle = {
|
|
477
|
-
display: "flex",
|
|
478
|
-
gap: 8,
|
|
479
|
-
marginBottom: 16
|
|
480
|
-
};
|
|
481
|
-
var toggleBase = {
|
|
482
|
-
padding: "6px 14px",
|
|
483
|
-
fontSize: 13,
|
|
484
|
-
fontFamily: "inherit",
|
|
485
|
-
cursor: "pointer",
|
|
486
|
-
background: "transparent",
|
|
487
|
-
color: "var(--squisq-recorder-text)",
|
|
488
|
-
border: "1px solid var(--squisq-recorder-border)",
|
|
489
|
-
borderRadius: 999
|
|
490
|
-
};
|
|
491
|
-
var toggleActive = {
|
|
492
|
-
...toggleBase,
|
|
493
|
-
color: "var(--squisq-recorder-accent-text)",
|
|
494
|
-
fontWeight: 600,
|
|
495
|
-
background: "var(--squisq-recorder-accent)",
|
|
496
|
-
borderColor: "var(--squisq-recorder-accent)"
|
|
497
|
-
};
|
|
498
|
-
var previewBoxStyle = {
|
|
499
|
-
width: "100%",
|
|
500
|
-
background: "#000",
|
|
501
|
-
borderRadius: 0,
|
|
502
|
-
marginBottom: 12,
|
|
503
|
-
overflow: "hidden",
|
|
504
|
-
aspectRatio: "16 / 9",
|
|
505
|
-
display: "flex",
|
|
506
|
-
alignItems: "center",
|
|
507
|
-
justifyContent: "center",
|
|
508
|
-
color: "#888",
|
|
509
|
-
fontSize: 13
|
|
510
|
-
};
|
|
511
|
-
var audioMeterStyle = {
|
|
512
|
-
width: "100%",
|
|
513
|
-
height: 56,
|
|
514
|
-
background: "var(--squisq-recorder-input)",
|
|
515
|
-
border: "1px solid var(--squisq-recorder-border)",
|
|
516
|
-
marginBottom: 12,
|
|
517
|
-
display: "flex",
|
|
518
|
-
alignItems: "center",
|
|
519
|
-
justifyContent: "center",
|
|
520
|
-
color: "var(--squisq-recorder-muted)",
|
|
521
|
-
fontSize: 13,
|
|
522
|
-
fontVariantNumeric: "tabular-nums"
|
|
523
|
-
};
|
|
524
|
-
var errorStyle = {
|
|
525
|
-
background: "var(--squisq-recorder-error-bg)",
|
|
526
|
-
border: "1px solid var(--squisq-recorder-error-border)",
|
|
527
|
-
color: "var(--squisq-recorder-error-text)",
|
|
528
|
-
padding: "8px 10px",
|
|
529
|
-
fontSize: 13,
|
|
530
|
-
marginBottom: 12
|
|
531
|
-
};
|
|
532
|
-
var buttonRowStyle = {
|
|
533
|
-
display: "flex",
|
|
534
|
-
gap: 8,
|
|
535
|
-
justifyContent: "flex-end",
|
|
536
|
-
marginTop: 8
|
|
537
|
-
};
|
|
538
|
-
var summaryStyle = {
|
|
539
|
-
margin: "0 0 12px 0",
|
|
540
|
-
fontSize: 12,
|
|
541
|
-
color: "var(--squisq-recorder-muted)"
|
|
542
|
-
};
|
|
543
|
-
var recordingStatusStyle = {
|
|
544
|
-
fontSize: 13,
|
|
545
|
-
fontVariantNumeric: "tabular-nums",
|
|
546
|
-
marginBottom: 12,
|
|
547
|
-
color: "var(--squisq-recorder-accent)",
|
|
548
|
-
fontWeight: 600
|
|
549
|
-
};
|
|
550
|
-
function formatDurationMs(ms) {
|
|
551
|
-
const totalSec = Math.floor(ms / 1e3);
|
|
552
|
-
const m = Math.floor(totalSec / 60);
|
|
553
|
-
const s = totalSec % 60;
|
|
554
|
-
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
555
|
-
}
|
|
556
|
-
function deriveSource(micOn, video) {
|
|
557
|
-
if (video === "camera") return "camera";
|
|
558
|
-
if (video === "screen") return micOn ? "screen+mic" : "screen";
|
|
559
|
-
return micOn ? "mic" : null;
|
|
560
|
-
}
|
|
561
|
-
function toggleStateFromMode(mode) {
|
|
562
|
-
switch (mode) {
|
|
563
|
-
case "mic":
|
|
564
|
-
return { micOn: true, video: "none" };
|
|
565
|
-
case "camera":
|
|
566
|
-
return { micOn: true, video: "camera" };
|
|
567
|
-
case "screen":
|
|
568
|
-
return { micOn: false, video: "screen" };
|
|
569
|
-
case "screen+mic":
|
|
570
|
-
return { micOn: true, video: "screen" };
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
var TOGGLES = [
|
|
574
|
-
{ key: "mic", label: "Microphone" },
|
|
575
|
-
{ key: "camera", label: "Camera" },
|
|
576
|
-
{ key: "screen", label: "Screen" }
|
|
577
|
-
];
|
|
578
|
-
function captureSummary(micOn, video) {
|
|
579
|
-
if (video === "camera") {
|
|
580
|
-
return micOn ? "Camera video with your microphone. Saved as a video clip." : "Camera video only (no microphone). Saved as a video clip.";
|
|
581
|
-
}
|
|
582
|
-
if (video === "screen") {
|
|
583
|
-
return micOn ? "Screen capture with your microphone mixed in. System audio when available." : "Screen capture (no microphone). System audio when available.";
|
|
584
|
-
}
|
|
585
|
-
return micOn ? "Voice-only audio. Pairs with a written script for auto-mapping to blocks." : "Pick at least one source to record.";
|
|
586
|
-
}
|
|
587
|
-
function RecorderModal({
|
|
588
|
-
mediaProvider,
|
|
589
|
-
container = null,
|
|
590
|
-
initialMode = "mic",
|
|
591
|
-
colorScheme = "light",
|
|
592
|
-
onClose,
|
|
593
|
-
onSave
|
|
594
|
-
}) {
|
|
595
|
-
const initialToggles = toggleStateFromMode(initialMode);
|
|
596
|
-
const [micOn, setMicOn] = useState2(initialToggles.micOn);
|
|
597
|
-
const [video, setVideo] = useState2(initialToggles.video);
|
|
598
|
-
const [sourceText, setSourceText] = useState2("");
|
|
599
|
-
const [basename, setBasename] = useState2("");
|
|
600
|
-
const [includeSystemAudio, setIncludeSystemAudio] = useState2(false);
|
|
601
|
-
const [isSaving, setIsSaving] = useState2(false);
|
|
602
|
-
const [saveError, setSaveError] = useState2(null);
|
|
603
|
-
const [playbackUrl, setPlaybackUrl] = useState2(null);
|
|
604
|
-
const overlayRef = useRef2(null);
|
|
605
|
-
const dialogRef = useRef2(null);
|
|
606
|
-
const headingId = useId();
|
|
607
|
-
const previewRef = useRef2(null);
|
|
608
|
-
const derivedSource = deriveSource(micOn, video);
|
|
609
|
-
const canCapture = derivedSource !== null;
|
|
610
|
-
const source = derivedSource ?? "mic";
|
|
611
|
-
const recorder = useMediaRecorder({
|
|
612
|
-
source,
|
|
613
|
-
includeMicrophone: video === "camera" ? micOn : void 0,
|
|
614
|
-
systemAudio: video === "screen" ? includeSystemAudio : false
|
|
615
|
-
});
|
|
616
|
-
useStreamPreview(previewRef, recorder.state === "stopped" ? null : recorder.stream);
|
|
617
|
-
useEffect2(() => {
|
|
618
|
-
if (!recorder.blob) {
|
|
619
|
-
setPlaybackUrl(null);
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const url = URL.createObjectURL(recorder.blob);
|
|
623
|
-
setPlaybackUrl(url);
|
|
624
|
-
return () => {
|
|
625
|
-
URL.revokeObjectURL(url);
|
|
626
|
-
};
|
|
627
|
-
}, [recorder.blob]);
|
|
628
|
-
const captureKey = `${source}:${video === "camera" ? micOn : ""}:${includeSystemAudio}`;
|
|
629
|
-
const previousKeyRef = useRef2(captureKey);
|
|
630
|
-
useEffect2(() => {
|
|
631
|
-
if (previousKeyRef.current !== captureKey) {
|
|
632
|
-
previousKeyRef.current = captureKey;
|
|
633
|
-
recorder.cancel();
|
|
634
|
-
}
|
|
635
|
-
}, [captureKey, recorder]);
|
|
636
|
-
const handleClose = useCallback2(() => {
|
|
637
|
-
recorder.cancel();
|
|
638
|
-
onClose();
|
|
639
|
-
}, [recorder, onClose]);
|
|
640
|
-
useModalDialog({ rootRef: overlayRef, dialogRef, onClose: handleClose });
|
|
641
|
-
const handleRequest = useCallback2(async () => {
|
|
642
|
-
setSaveError(null);
|
|
643
|
-
try {
|
|
644
|
-
await recorder.request();
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
}, [recorder]);
|
|
648
|
-
const handleStart = useCallback2(() => {
|
|
649
|
-
setSaveError(null);
|
|
650
|
-
recorder.start();
|
|
651
|
-
}, [recorder]);
|
|
652
|
-
const handleStop = useCallback2(async () => {
|
|
653
|
-
setSaveError(null);
|
|
654
|
-
await recorder.stop();
|
|
655
|
-
}, [recorder]);
|
|
656
|
-
const handleSave = useCallback2(async () => {
|
|
657
|
-
if (!recorder.blob || !recorder.mimeType || !recorder.extension || !recorder.directory) {
|
|
658
|
-
setSaveError("Nothing to save yet \u2014 record something first.");
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
setIsSaving(true);
|
|
662
|
-
setSaveError(null);
|
|
663
|
-
try {
|
|
664
|
-
const filename = buildFilename(
|
|
665
|
-
source === "mic" ? "audio" : "video",
|
|
666
|
-
recorder.extension,
|
|
667
|
-
basename
|
|
668
|
-
);
|
|
669
|
-
const relativeName = `${recorder.directory}/${filename}`;
|
|
670
|
-
const relativePath = await mediaProvider.addMedia(
|
|
671
|
-
relativeName,
|
|
672
|
-
recorder.blob,
|
|
673
|
-
recorder.mimeType
|
|
674
|
-
);
|
|
675
|
-
let hasTimingSidecar = false;
|
|
676
|
-
if (source === "mic") {
|
|
677
|
-
const timing = buildTimingJson(sourceText, recorder.durationMs / 1e3);
|
|
678
|
-
const encoded = encodeTimingJson(timing);
|
|
679
|
-
const sidecarPath = timingPathFor(relativePath);
|
|
680
|
-
if (container) {
|
|
681
|
-
await container.writeFile(sidecarPath, encoded, "application/json");
|
|
682
|
-
hasTimingSidecar = true;
|
|
683
|
-
} else {
|
|
684
|
-
const written = await mediaProvider.addMedia(sidecarPath, encoded, "application/json");
|
|
685
|
-
hasTimingSidecar = written === sidecarPath;
|
|
686
|
-
if (!hasTimingSidecar) {
|
|
687
|
-
console.warn(
|
|
688
|
-
`[squisq-recorder] timing.json was saved as "${written}" instead of "${sidecarPath}" \u2014 auto-mapping may not pick it up.`
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
const result = {
|
|
694
|
-
relativePath,
|
|
695
|
-
filename,
|
|
696
|
-
source,
|
|
697
|
-
mimeType: recorder.mimeType,
|
|
698
|
-
duration: recorder.durationMs / 1e3,
|
|
699
|
-
hasTimingSidecar
|
|
700
|
-
};
|
|
701
|
-
if (source === "mic") {
|
|
702
|
-
result.sourceText = sourceText;
|
|
703
|
-
}
|
|
704
|
-
onSave?.(result);
|
|
705
|
-
handleClose();
|
|
706
|
-
} catch (err) {
|
|
707
|
-
setSaveError(err instanceof Error ? err.message : "Failed to save recording");
|
|
708
|
-
} finally {
|
|
709
|
-
setIsSaving(false);
|
|
710
|
-
}
|
|
711
|
-
}, [recorder, source, basename, sourceText, mediaProvider, container, onSave, handleClose]);
|
|
712
|
-
const handleDiscard = useCallback2(() => {
|
|
713
|
-
recorder.reset();
|
|
714
|
-
}, [recorder]);
|
|
715
|
-
const isAudioOnly = source === "mic";
|
|
716
|
-
const showPreview = recorder.state !== "idle" && recorder.state !== "error";
|
|
717
|
-
const canRecord = recorder.state === "ready";
|
|
718
|
-
const canStop = recorder.state === "recording";
|
|
719
|
-
const canSave = recorder.state === "stopped" && recorder.blob !== null;
|
|
720
|
-
const isBusy = recorder.state === "requesting" || recorder.state === "stopping" || isSaving;
|
|
721
|
-
const togglesLocked = recorder.state === "recording" || recorder.state === "requesting" || canSave;
|
|
722
|
-
const toggleLockReason = canSave ? "Save or discard this recording before changing sources" : void 0;
|
|
723
|
-
const toggleActiveFor = (key) => key === "mic" ? micOn : video === key;
|
|
724
|
-
const onToggle = (key) => {
|
|
725
|
-
if (key === "mic") {
|
|
726
|
-
setMicOn((on) => !on);
|
|
727
|
-
} else {
|
|
728
|
-
setVideo((v) => v === key ? "none" : key);
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
return /* @__PURE__ */ jsx(
|
|
732
|
-
"div",
|
|
733
|
-
{
|
|
734
|
-
ref: overlayRef,
|
|
735
|
-
className: "squisq-editor-shell squisq-recorder-overlay",
|
|
736
|
-
"data-theme": colorScheme,
|
|
737
|
-
style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
|
|
738
|
-
children: /* @__PURE__ */ jsxs(
|
|
739
|
-
"div",
|
|
740
|
-
{
|
|
741
|
-
ref: dialogRef,
|
|
742
|
-
className: "squisq-editor-shell",
|
|
743
|
-
"data-theme": colorScheme,
|
|
744
|
-
style: { ...modalStyle, ...recorderThemeStyle(colorScheme) },
|
|
745
|
-
onClick: (e) => e.stopPropagation(),
|
|
746
|
-
role: "dialog",
|
|
747
|
-
"aria-modal": "true",
|
|
748
|
-
"aria-labelledby": headingId,
|
|
749
|
-
tabIndex: -1,
|
|
750
|
-
children: [
|
|
751
|
-
/* @__PURE__ */ jsx("h2", { id: headingId, style: titleStyle, children: "Record media" }),
|
|
752
|
-
/* @__PURE__ */ jsx("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLES.map((t) => {
|
|
753
|
-
const active = toggleActiveFor(t.key);
|
|
754
|
-
return /* @__PURE__ */ jsx(
|
|
755
|
-
"button",
|
|
756
|
-
{
|
|
757
|
-
type: "button",
|
|
758
|
-
"aria-pressed": active,
|
|
759
|
-
style: active ? toggleActive : toggleBase,
|
|
760
|
-
onClick: () => onToggle(t.key),
|
|
761
|
-
disabled: togglesLocked,
|
|
762
|
-
title: toggleLockReason,
|
|
763
|
-
children: t.label
|
|
764
|
-
},
|
|
765
|
-
t.key
|
|
766
|
-
);
|
|
767
|
-
}) }),
|
|
768
|
-
/* @__PURE__ */ jsx("p", { style: summaryStyle, children: captureSummary(micOn, video) }),
|
|
769
|
-
recorder.error && /* @__PURE__ */ jsx("div", { style: errorStyle, children: recorder.error.message }),
|
|
770
|
-
saveError && /* @__PURE__ */ jsx("div", { style: errorStyle, children: saveError }),
|
|
771
|
-
!showPreview && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx("span", { children: "Click Start Preview to start a recording." }) }),
|
|
772
|
-
showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx(
|
|
773
|
-
"video",
|
|
774
|
-
{
|
|
775
|
-
ref: previewRef,
|
|
776
|
-
autoPlay: true,
|
|
777
|
-
muted: true,
|
|
778
|
-
playsInline: true,
|
|
779
|
-
style: { width: "100%", height: "100%", objectFit: "contain" }
|
|
780
|
-
}
|
|
781
|
-
) }),
|
|
782
|
-
showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
783
|
-
"\u25CF Recording ",
|
|
784
|
-
formatDurationMs(recorder.durationMs)
|
|
785
|
-
] }) : /* @__PURE__ */ jsx(Fragment, { children: "Microphone ready" }) }),
|
|
786
|
-
recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsx("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx(
|
|
787
|
-
"video",
|
|
788
|
-
{
|
|
789
|
-
src: playbackUrl,
|
|
790
|
-
controls: true,
|
|
791
|
-
playsInline: true,
|
|
792
|
-
style: { width: "100%", height: "100%", objectFit: "contain" }
|
|
793
|
-
}
|
|
794
|
-
) }),
|
|
795
|
-
recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
|
|
796
|
-
/* @__PURE__ */ jsxs("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
|
|
797
|
-
"\u2713 Recorded ",
|
|
798
|
-
formatDurationMs(recorder.durationMs)
|
|
799
|
-
] }),
|
|
800
|
-
/* @__PURE__ */ jsx("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
|
|
801
|
-
] }),
|
|
802
|
-
source === "mic" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
803
|
-
/* @__PURE__ */ jsx("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
|
|
804
|
-
/* @__PURE__ */ jsx(
|
|
805
|
-
"textarea",
|
|
806
|
-
{
|
|
807
|
-
id: "recorder-source-text",
|
|
808
|
-
style: textareaStyle,
|
|
809
|
-
placeholder: "Type the text you're going to read aloud.",
|
|
810
|
-
value: sourceText,
|
|
811
|
-
onChange: (e) => setSourceText(e.target.value),
|
|
812
|
-
disabled: recorder.state === "recording"
|
|
813
|
-
}
|
|
814
|
-
)
|
|
815
|
-
] }),
|
|
816
|
-
video === "screen" && /* @__PURE__ */ jsxs(
|
|
817
|
-
"label",
|
|
818
|
-
{
|
|
819
|
-
style: {
|
|
820
|
-
display: "flex",
|
|
821
|
-
alignItems: "center",
|
|
822
|
-
gap: 6,
|
|
823
|
-
marginBottom: 12,
|
|
824
|
-
fontSize: 13
|
|
825
|
-
},
|
|
826
|
-
children: [
|
|
827
|
-
/* @__PURE__ */ jsx(
|
|
828
|
-
"input",
|
|
829
|
-
{
|
|
830
|
-
type: "checkbox",
|
|
831
|
-
style: { accentColor: "var(--squisq-recorder-accent)" },
|
|
832
|
-
checked: includeSystemAudio,
|
|
833
|
-
onChange: (e) => setIncludeSystemAudio(e.target.checked),
|
|
834
|
-
disabled: recorder.state === "recording" || recorder.state === "requesting"
|
|
835
|
-
}
|
|
836
|
-
),
|
|
837
|
-
"Include system audio (Chrome only)"
|
|
838
|
-
]
|
|
839
|
-
}
|
|
840
|
-
),
|
|
841
|
-
/* @__PURE__ */ jsx("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
|
|
842
|
-
/* @__PURE__ */ jsx(
|
|
843
|
-
"input",
|
|
844
|
-
{
|
|
845
|
-
id: "recorder-basename",
|
|
846
|
-
type: "text",
|
|
847
|
-
style: inputStyle,
|
|
848
|
-
placeholder: source === "mic" ? "narration" : "recording",
|
|
849
|
-
value: basename,
|
|
850
|
-
onChange: (e) => setBasename(e.target.value),
|
|
851
|
-
disabled: recorder.state === "recording"
|
|
852
|
-
}
|
|
853
|
-
),
|
|
854
|
-
recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs("div", { style: recordingStatusStyle, children: [
|
|
855
|
-
"\u25CF Recording ",
|
|
856
|
-
formatDurationMs(recorder.durationMs)
|
|
857
|
-
] }),
|
|
858
|
-
/* @__PURE__ */ jsxs("div", { style: buttonRowStyle, children: [
|
|
859
|
-
/* @__PURE__ */ jsx("button", { type: "button", style: btnSecondary, onClick: handleClose, disabled: isBusy, children: "Close" }),
|
|
860
|
-
(recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx(
|
|
861
|
-
"button",
|
|
862
|
-
{
|
|
863
|
-
type: "button",
|
|
864
|
-
style: btnPrimary,
|
|
865
|
-
onClick: handleRequest,
|
|
866
|
-
disabled: isBusy || !canCapture,
|
|
867
|
-
children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
|
|
868
|
-
}
|
|
869
|
-
),
|
|
870
|
-
canRecord && /* @__PURE__ */ jsx("button", { type: "button", style: btnPrimary, onClick: handleStart, disabled: isBusy, children: "Record" }),
|
|
871
|
-
canStop && /* @__PURE__ */ jsx("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
|
|
872
|
-
canSave && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
873
|
-
/* @__PURE__ */ jsx("button", { type: "button", style: btnSecondary, onClick: handleDiscard, disabled: isBusy, children: "Discard & re-record" }),
|
|
874
|
-
/* @__PURE__ */ jsx("button", { type: "button", style: btnPrimary, onClick: handleSave, disabled: isBusy, children: isSaving ? "Saving\u2026" : "Save to document" })
|
|
875
|
-
] })
|
|
876
|
-
] })
|
|
877
|
-
]
|
|
878
|
-
}
|
|
879
|
-
)
|
|
880
|
-
}
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// src/recorder/RecorderPanel.tsx
|
|
885
|
-
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
886
|
-
import { createPortal } from "react-dom";
|
|
887
|
-
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
888
|
-
function RecorderPanel({
|
|
889
|
-
mediaProvider,
|
|
890
|
-
container = null,
|
|
891
|
-
initialMode = "mic",
|
|
892
|
-
colorScheme = "light",
|
|
893
|
-
onSave,
|
|
894
|
-
tooltip = "Record media",
|
|
895
|
-
className,
|
|
896
|
-
open: controlledOpen,
|
|
897
|
-
onOpenChange,
|
|
898
|
-
showTrigger = true
|
|
899
|
-
}) {
|
|
900
|
-
const [uncontrolledOpen, setUncontrolledOpen] = useState3(false);
|
|
901
|
-
const open = controlledOpen ?? uncontrolledOpen;
|
|
902
|
-
const setOpen = useCallback3(
|
|
903
|
-
(nextOpen) => {
|
|
904
|
-
if (controlledOpen === void 0) setUncontrolledOpen(nextOpen);
|
|
905
|
-
onOpenChange?.(nextOpen);
|
|
906
|
-
},
|
|
907
|
-
[controlledOpen, onOpenChange]
|
|
908
|
-
);
|
|
909
|
-
const handleClose = useCallback3(() => setOpen(false), [setOpen]);
|
|
910
|
-
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
911
|
-
showTrigger && /* @__PURE__ */ jsx2(
|
|
912
|
-
"button",
|
|
913
|
-
{
|
|
914
|
-
type: "button",
|
|
915
|
-
className,
|
|
916
|
-
"data-tooltip": tooltip,
|
|
917
|
-
"aria-label": tooltip,
|
|
918
|
-
"aria-expanded": open,
|
|
919
|
-
onClick: () => setOpen(!open),
|
|
920
|
-
children: /* @__PURE__ */ jsx2(Icon, { icon: "fa-solid fa-microphone" })
|
|
921
|
-
}
|
|
922
|
-
),
|
|
923
|
-
open && typeof document !== "undefined" && createPortal(
|
|
924
|
-
/* @__PURE__ */ jsx2(
|
|
925
|
-
RecorderModal,
|
|
926
|
-
{
|
|
927
|
-
mediaProvider,
|
|
928
|
-
container,
|
|
929
|
-
initialMode,
|
|
930
|
-
colorScheme,
|
|
931
|
-
onClose: handleClose,
|
|
932
|
-
onSave: (result) => {
|
|
933
|
-
onSave?.(result);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
),
|
|
937
|
-
document.body
|
|
938
|
-
)
|
|
939
|
-
] });
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
export {
|
|
943
|
-
requestScreenStream,
|
|
944
|
-
getCaptureKind,
|
|
945
|
-
useMediaRecorder,
|
|
946
|
-
useModalDialog,
|
|
947
|
-
RecorderModal,
|
|
948
|
-
RecorderPanel
|
|
949
|
-
};
|