@bendyline/squisq-editor-react 2.11.15 → 2.11.17

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.
@@ -19,7 +19,115 @@ import {
19
19
  timingPathFor,
20
20
  useNarrationStage,
21
21
  useStreamPreview
22
- } from "./chunk-34FRFDQ6.js";
22
+ } from "./chunk-XZVMKR5Q.js";
23
+
24
+ // src/recorder/sources/screenRegion.ts
25
+ function validateScreenCaptureRegion(region, width, height) {
26
+ const { x, y, width: w, height: h } = region;
27
+ if (![x, y, w, h].every((value) => Number.isSafeInteger(value) && value >= 0 && value <= 16384) || w === 0 || h === 0 || w * h > 16777216) {
28
+ throw new Error(
29
+ "Enter a valid recording region: non-negative left/top and positive whole-pixel dimensions (up to 16 megapixels)."
30
+ );
31
+ }
32
+ if (width !== void 0 && x + w > width || height !== void 0 && y + h > height) {
33
+ throw new Error(
34
+ `The recording region is outside the selected surface (${width} \xD7 ${height} pixels). Adjust the coordinates or choose another surface.`
35
+ );
36
+ }
37
+ }
38
+ async function cropScreenStream(source, region) {
39
+ validateScreenCaptureRegion(region);
40
+ const [sourceTrack] = source.getVideoTracks();
41
+ if (!sourceTrack) throw new Error("The selected surface has no video track.");
42
+ const video = document.createElement("video");
43
+ video.muted = true;
44
+ video.playsInline = true;
45
+ const canvas = document.createElement("canvas");
46
+ const context = canvas.getContext("2d");
47
+ if (!context || typeof canvas.captureStream !== "function")
48
+ throw new Error("Recording a region is not supported in this browser.");
49
+ canvas.width = region.width;
50
+ canvas.height = region.height;
51
+ let timer;
52
+ let output;
53
+ let disposed = false;
54
+ const dispose = () => {
55
+ if (disposed) return;
56
+ disposed = true;
57
+ clearInterval(timer);
58
+ sourceTrack.removeEventListener("ended", ended);
59
+ video.pause();
60
+ video.srcObject = null;
61
+ output?.getVideoTracks().forEach((track) => track.stop());
62
+ source.getTracks().forEach((track) => track.stop());
63
+ };
64
+ const ended = () => {
65
+ const track = output?.getVideoTracks()[0];
66
+ dispose();
67
+ track?.dispatchEvent(new Event("ended"));
68
+ };
69
+ try {
70
+ await new Promise((resolve, reject) => {
71
+ const cleanup = () => {
72
+ clearTimeout(timeout);
73
+ video.removeEventListener("loadeddata", ready);
74
+ video.removeEventListener("error", failed);
75
+ sourceTrack.removeEventListener("ended", failed);
76
+ };
77
+ const ready = () => {
78
+ cleanup();
79
+ resolve();
80
+ };
81
+ const failed = () => {
82
+ cleanup();
83
+ reject(new Error("Could not preview the selected screen. Choose it again."));
84
+ };
85
+ const timeout = setTimeout(failed, 1e4);
86
+ video.addEventListener("loadeddata", ready, { once: true });
87
+ video.addEventListener("error", failed, { once: true });
88
+ sourceTrack.addEventListener("ended", failed, { once: true });
89
+ try {
90
+ video.srcObject = source;
91
+ void video.play().catch(failed);
92
+ } catch {
93
+ failed();
94
+ }
95
+ });
96
+ if (sourceTrack.readyState === "ended")
97
+ throw new Error("Screen sharing ended. Choose a surface again.");
98
+ validateScreenCaptureRegion(region, video.videoWidth, video.videoHeight);
99
+ const draw = () => {
100
+ if (region.x + region.width > video.videoWidth || region.y + region.height > video.videoHeight || sourceTrack.readyState === "ended") {
101
+ ended();
102
+ return;
103
+ }
104
+ try {
105
+ context.drawImage(
106
+ video,
107
+ region.x,
108
+ region.y,
109
+ region.width,
110
+ region.height,
111
+ 0,
112
+ 0,
113
+ region.width,
114
+ region.height
115
+ );
116
+ } catch {
117
+ ended();
118
+ }
119
+ };
120
+ draw();
121
+ output = canvas.captureStream(30);
122
+ const stream = new MediaStream([...output.getVideoTracks(), ...source.getAudioTracks()]);
123
+ sourceTrack.addEventListener("ended", ended, { once: true });
124
+ timer = setInterval(draw, 1e3 / 30);
125
+ return { stream, dispose };
126
+ } catch (error) {
127
+ dispose();
128
+ throw error;
129
+ }
130
+ }
23
131
 
24
132
  // src/recorder/sources/screenStream.ts
25
133
  function mixAudioTracks(streams) {
@@ -38,6 +146,25 @@ function mixAudioTracks(streams) {
38
146
  return { track: mixed, context: ctx };
39
147
  }
40
148
  async function requestScreenStream(options) {
149
+ if (options?.crop) validateScreenCaptureRegion(options.crop);
150
+ const handle = await requestFullScreenStream(options);
151
+ if (!options?.crop) return handle;
152
+ try {
153
+ const cropped = await cropScreenStream(handle.stream, options.crop);
154
+ return {
155
+ stream: cropped.stream,
156
+ dispose: () => {
157
+ cropped.dispose();
158
+ handle.dispose();
159
+ }
160
+ };
161
+ } catch (error) {
162
+ handle.stream.getTracks().forEach((track) => track.stop());
163
+ handle.dispose();
164
+ throw error;
165
+ }
166
+ }
167
+ async function requestFullScreenStream(options) {
41
168
  if (!supportsDisplayMedia()) {
42
169
  throw new Error("navigator.mediaDevices.getDisplayMedia is not available in this environment.");
43
170
  }
@@ -198,6 +325,7 @@ async function acquireStream(source, opts) {
198
325
  case "screen+mic": {
199
326
  const handle = await requestScreenStream({
200
327
  video: opts.screenVideoConstraints ?? opts.videoConstraints ?? true,
328
+ crop: opts.screenRegion,
201
329
  systemAudio: opts.systemAudio ?? false,
202
330
  systemAudioConstraints: opts.screenAudioConstraints,
203
331
  includeMicrophone: source === "screen+mic",
@@ -209,6 +337,7 @@ async function acquireStream(source, opts) {
209
337
  }
210
338
  async function acquireDualStreams(opts, isStale) {
211
339
  const screen = await requestScreenStream({
340
+ crop: opts.screenRegion,
212
341
  video: opts.screenVideoConstraints ?? opts.videoConstraints ?? true,
213
342
  systemAudio: opts.systemAudio ?? false,
214
343
  systemAudioConstraints: opts.screenAudioConstraints,
@@ -583,6 +712,15 @@ function useMediaRecorder(options = {}) {
583
712
  };
584
713
  recorderRef.current = recorder;
585
714
  disposeStreamRef.current = dispose;
715
+ if (source === "screen" || source === "screen+mic") {
716
+ const [screenTrack] = nextStream.getVideoTracks();
717
+ if (screenTrack)
718
+ screenTrack.onended = () => {
719
+ if (lifecycle !== lifecycleRef.current) return;
720
+ if (stateRef.current === "recording") void stopFnRef.current?.();
721
+ else if (stateRef.current === "ready") cancelFnRef.current?.();
722
+ };
723
+ }
586
724
  setStream(nextStream);
587
725
  setFormat(resolved);
588
726
  setBlob(null);
@@ -1253,24 +1391,6 @@ function RecorderDeviceSettingsPanel({
1253
1391
  /* @__PURE__ */ jsx("legend", { style: legendStyle, children: screenEnabled ? "Screen capture" : "System audio capture" }),
1254
1392
  /* @__PURE__ */ jsxs("div", { style: gridStyle, children: [
1255
1393
  screenEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
1256
- supports("displaySurface") && /* @__PURE__ */ jsxs("label", { style: { ...fieldStyle, gridColumn: "1 / -1" }, children: [
1257
- "Preferred surface",
1258
- /* @__PURE__ */ jsxs(
1259
- "select",
1260
- {
1261
- value: value.screen.displaySurface ?? "",
1262
- disabled,
1263
- style: inputStyle,
1264
- onChange: (event) => patchScreen({ displaySurface: event.target.value || void 0 }),
1265
- children: [
1266
- /* @__PURE__ */ jsx("option", { value: "", children: "Browser picker default" }),
1267
- /* @__PURE__ */ jsx("option", { value: "monitor", children: "Entire monitor" }),
1268
- /* @__PURE__ */ jsx("option", { value: "window", children: "Application window" }),
1269
- /* @__PURE__ */ jsx("option", { value: "browser", children: "Browser tab" })
1270
- ]
1271
- }
1272
- )
1273
- ] }),
1274
1394
  supports("cursor") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1275
1395
  "Mouse cursor",
1276
1396
  /* @__PURE__ */ jsxs(
@@ -1698,8 +1818,9 @@ function isExpandedPanel(mode) {
1698
1818
  return mode !== "none";
1699
1819
  }
1700
1820
  function panelModeAfterToggle(current, target, checked) {
1701
- if (checked) return target;
1702
- return current === target ? "none" : current;
1821
+ const narration = target === "narration" ? checked : current === "narration" || current === "both";
1822
+ const slides = target === "slides" ? checked : current === "slides" || current === "both";
1823
+ return narration ? slides ? "both" : "narration" : slides ? "slides" : "none";
1703
1824
  }
1704
1825
  function clampSlideIndex(index, count) {
1705
1826
  if (count <= 0) return 0;
@@ -2059,6 +2180,134 @@ import {
2059
2180
  recordSlideShown
2060
2181
  } from "@bendyline/squisq/narration";
2061
2182
 
2183
+ // src/recorder/RecorderScreenSettings.tsx
2184
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2185
+ var field = { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 };
2186
+ var input = {
2187
+ width: "100%",
2188
+ boxSizing: "border-box",
2189
+ padding: "6px 8px",
2190
+ font: "inherit",
2191
+ color: "var(--squisq-recorder-text)",
2192
+ background: "var(--squisq-recorder-input)",
2193
+ border: "1px solid var(--squisq-recorder-border)"
2194
+ };
2195
+ function RecorderScreenSettings({
2196
+ value,
2197
+ onChange,
2198
+ disabled,
2199
+ onChoose,
2200
+ hasPreview
2201
+ }) {
2202
+ const crop = value.crop;
2203
+ return /* @__PURE__ */ jsxs4(
2204
+ "fieldset",
2205
+ {
2206
+ disabled,
2207
+ style: {
2208
+ margin: "0 0 12px",
2209
+ padding: 12,
2210
+ border: "1px solid var(--squisq-recorder-border)",
2211
+ fontSize: 13
2212
+ },
2213
+ children: [
2214
+ /* @__PURE__ */ jsx5("legend", { children: "What to record" }),
2215
+ /* @__PURE__ */ jsxs4(
2216
+ "div",
2217
+ {
2218
+ style: {
2219
+ display: "grid",
2220
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
2221
+ gap: 10
2222
+ },
2223
+ children: [
2224
+ /* @__PURE__ */ jsxs4("label", { style: field, children: [
2225
+ "Preferred surface",
2226
+ /* @__PURE__ */ jsxs4(
2227
+ "select",
2228
+ {
2229
+ style: input,
2230
+ value: value.displaySurface ?? "",
2231
+ onChange: (event) => onChange({ ...value, displaySurface: event.target.value || void 0 }),
2232
+ children: [
2233
+ /* @__PURE__ */ jsx5("option", { value: "", children: "Any available surface" }),
2234
+ /* @__PURE__ */ jsx5("option", { value: "monitor", children: "Entire screen" }),
2235
+ /* @__PURE__ */ jsx5("option", { value: "window", children: "Application window" }),
2236
+ /* @__PURE__ */ jsx5("option", { value: "browser", children: "Browser tab (when available)" })
2237
+ ]
2238
+ }
2239
+ )
2240
+ ] }),
2241
+ /* @__PURE__ */ jsxs4("label", { style: field, children: [
2242
+ "Capture area",
2243
+ /* @__PURE__ */ jsxs4(
2244
+ "select",
2245
+ {
2246
+ style: input,
2247
+ value: value.crop ? "region" : "full",
2248
+ onChange: (event) => onChange({
2249
+ ...value,
2250
+ crop: event.target.value === "region" ? { x: 0, y: 0, width: 1280, height: 720 } : void 0
2251
+ }),
2252
+ children: [
2253
+ /* @__PURE__ */ jsx5("option", { value: "full", children: "Whole selected surface" }),
2254
+ /* @__PURE__ */ jsx5("option", { value: "region", children: "Region by coordinates" })
2255
+ ]
2256
+ }
2257
+ )
2258
+ ] }),
2259
+ crop && /* @__PURE__ */ jsx5(
2260
+ "div",
2261
+ {
2262
+ style: {
2263
+ gridColumn: "1 / -1",
2264
+ display: "grid",
2265
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
2266
+ gap: 10
2267
+ },
2268
+ children: ["x", "y", "width", "height"].map((key) => /* @__PURE__ */ jsxs4("label", { style: field, children: [
2269
+ {
2270
+ x: "Left (px)",
2271
+ y: "Top (px)",
2272
+ width: "Region width (px)",
2273
+ height: "Region height (px)"
2274
+ }[key],
2275
+ /* @__PURE__ */ jsx5(
2276
+ "input",
2277
+ {
2278
+ style: input,
2279
+ type: "number",
2280
+ min: key === "x" || key === "y" ? 0 : 1,
2281
+ max: 16384,
2282
+ step: 1,
2283
+ value: Number.isFinite(crop[key]) ? crop[key] : "",
2284
+ onChange: (event) => onChange({ ...value, crop: { ...crop, [key]: event.target.valueAsNumber } })
2285
+ }
2286
+ )
2287
+ ] }, key))
2288
+ }
2289
+ )
2290
+ ]
2291
+ }
2292
+ ),
2293
+ /* @__PURE__ */ jsxs4("p", { style: { margin: "8px 0 0", color: "var(--squisq-recorder-muted)" }, children: [
2294
+ "Choose a screen, window or tab in the picker, then check the preview. Available choices depend on your browser or app.",
2295
+ value.crop && " Coordinates start at the selected surface\u2019s top-left, in captured pixels. Only this region is saved; audio is unchanged."
2296
+ ] }),
2297
+ /* @__PURE__ */ jsx5(
2298
+ "button",
2299
+ {
2300
+ type: "button",
2301
+ style: { ...input, marginTop: 10, width: "auto", cursor: "pointer" },
2302
+ onClick: onChoose,
2303
+ children: hasPreview ? "Change screen or window" : "Choose screen or window"
2304
+ }
2305
+ )
2306
+ ]
2307
+ }
2308
+ );
2309
+ }
2310
+
2062
2311
  // src/modal/useModalDialog.ts
2063
2312
  import { useModalDialog } from "@bendyline/squisq-react";
2064
2313
 
@@ -2085,7 +2334,7 @@ function narrationCaptureSummary(withCamera) {
2085
2334
  }
2086
2335
 
2087
2336
  // src/recorder/RecorderModal.tsx
2088
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2337
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2089
2338
  var overlayStyle = {
2090
2339
  position: "fixed",
2091
2340
  inset: 0,
@@ -2447,8 +2696,9 @@ function RecorderModal({
2447
2696
  const [cameraPlaybackUrl, setCameraPlaybackUrl] = useState3(null);
2448
2697
  const [playbackPositionMs, setPlaybackPositionMs] = useState3(0);
2449
2698
  const [panelMode, setPanelMode] = useState3("none");
2450
- const narrationOn = panelMode === "narration";
2451
- const slidesOn = panelMode === "slides";
2699
+ const narrationOn = panelMode === "narration" || panelMode === "both";
2700
+ const slidesOn = panelMode === "slides" || panelMode === "both";
2701
+ const narrationRecording = narrationOn && Boolean(narration?.recording);
2452
2702
  const expanded = isExpandedPanel(panelMode);
2453
2703
  const [narrationRequesting, setNarrationRequesting] = useState3(false);
2454
2704
  const [narrationPreview, setNarrationPreview] = useState3(null);
@@ -2494,6 +2744,7 @@ function RecorderModal({
2494
2744
  audioConstraints,
2495
2745
  videoConstraints: cameraConstraints,
2496
2746
  screenVideoConstraints: screenConstraints,
2747
+ screenRegion: deviceSettings.screen.crop,
2497
2748
  screenAudioConstraints,
2498
2749
  mimeType: (source === "mic" ? encoding.audioMimeType : encoding.videoMimeType).trim() || void 0,
2499
2750
  bitsPerSecond: recorderBitsPerSecond(encoding.bitsPerSecond),
@@ -2508,13 +2759,16 @@ function RecorderModal({
2508
2759
  systemAudio: canIncludeSystemAudio ? includeSystemAudio : false
2509
2760
  });
2510
2761
  const filenameSeed = recordingFilenameSeed(source, recorder.stream, micOn || includeSystemAudio);
2511
- const narrationAvailable = Boolean(narration && narration.recording);
2762
+ const narrationAvailable = Boolean(narration?.doc);
2512
2763
  const basenameRef = useRef2(basename);
2513
2764
  basenameRef.current = basename;
2514
2765
  const stage = useNarrationStage({
2515
2766
  doc: narration?.doc ?? null,
2516
2767
  maxRecordingBytes,
2517
2768
  recording: narration?.recording ?? null,
2769
+ // A prompter-only stage borrows the take's audio. It never requests or
2770
+ // stops capture tracks; the recorder remains their sole owner.
2771
+ analysisStream: narration?.recording ? void 0 : narrationOn && recorder.state !== "stopped" ? isDual ? recorder.camera?.stream ?? null : recorder.stream : null,
2518
2772
  getAudioBasename: () => basenameRef.current.trim() || void 0,
2519
2773
  micConstraints: audioConstraints,
2520
2774
  cameraConstraints,
@@ -2569,35 +2823,41 @@ function RecorderModal({
2569
2823
  useEffect4(() => {
2570
2824
  setAdvanceLog(EMPTY_ADVANCE_LOG);
2571
2825
  setSlideIndex(0);
2572
- }, [panelMode, slideDeckKey]);
2826
+ }, [slidesOn, slideDeckKey]);
2827
+ useEffect4(() => {
2828
+ if (!narrationOn || narrationRecording) return;
2829
+ const controller = stageRef.current.controller;
2830
+ if (recorder.state === "recording") controller.play();
2831
+ else controller.pause();
2832
+ }, [narrationOn, narrationRecording, recorder.state]);
2573
2833
  const handleDeviceSettingsChange = useCallback4(
2574
2834
  (next) => {
2575
2835
  setDeviceSettings(next);
2576
- if (narrationOn) {
2836
+ if (narrationRecording) {
2577
2837
  stageRef.current.controller.setPrefs({
2578
2838
  micDeviceId: next.audio.deviceId || null
2579
2839
  });
2580
2840
  }
2581
2841
  },
2582
- [narrationOn]
2842
+ [narrationRecording]
2583
2843
  );
2584
2844
  const narrationMicDeviceId = stage.controller.prefs.micDeviceId ?? "";
2585
2845
  useEffect4(() => {
2586
- if (!narrationOn) return;
2846
+ if (!narrationRecording) return;
2587
2847
  setDeviceSettings(
2588
2848
  (current) => current.audio.deviceId === narrationMicDeviceId ? current : {
2589
2849
  ...current,
2590
2850
  audio: { ...current.audio, deviceId: narrationMicDeviceId }
2591
2851
  }
2592
2852
  );
2593
- }, [narrationMicDeviceId, narrationOn]);
2853
+ }, [narrationMicDeviceId, narrationRecording]);
2594
2854
  const prevNarrationOnRef = useRef2(narrationOn);
2595
2855
  useEffect4(() => {
2596
2856
  const was = prevNarrationOnRef.current;
2597
2857
  prevNarrationOnRef.current = narrationOn;
2598
2858
  if (was === narrationOn) return;
2599
2859
  if (narrationOn) {
2600
- recorder.cancel();
2860
+ if (narrationRecording) recorder.cancel();
2601
2861
  } else {
2602
2862
  const s = stageRef.current;
2603
2863
  s.controller.pause();
@@ -2606,7 +2866,7 @@ function RecorderModal({
2606
2866
  }
2607
2867
  if (s.float.isOpen) s.float.close();
2608
2868
  }
2609
- }, [narrationOn, recorder]);
2869
+ }, [narrationOn, narrationRecording, recorder]);
2610
2870
  useEffect4(() => {
2611
2871
  if (!narrationOn) return;
2612
2872
  const onKey = (event) => {
@@ -2618,7 +2878,7 @@ function RecorderModal({
2618
2878
  return () => document.removeEventListener("keydown", onKey, true);
2619
2879
  }, [narrationOn]);
2620
2880
  const narrationMicLive = stage.controller.mic.status === "live";
2621
- const narrationPreviewWanted = narrationOn && stage.recorder.withCamera && narrationMicLive && stage.recorder.cameraStream === null && stage.recorder.state !== "processing" && stage.recorder.state !== "review" && stage.recorder.state !== "saving";
2881
+ const narrationPreviewWanted = narrationRecording && stage.recorder.withCamera && narrationMicLive && stage.recorder.cameraStream === null && stage.recorder.state !== "processing" && stage.recorder.state !== "review" && stage.recorder.state !== "saving";
2622
2882
  const narrationPreviewRef = useRef2(null);
2623
2883
  useEffect4(() => {
2624
2884
  const existing = narrationPreviewRef.current;
@@ -2667,20 +2927,20 @@ function RecorderModal({
2667
2927
  const handleNarrationRecord = useCallback4(() => {
2668
2928
  void stageRef.current.recorder.start();
2669
2929
  }, []);
2670
- const narrationCameraStream = narrationOn ? stage.recorder.cameraStream ?? narrationPreview : null;
2671
- const activePrimaryStream = narrationOn ? stage.controller.mic.stream : recorder.stream;
2672
- const activeCameraStream = narrationOn ? narrationCameraStream : recorder.camera?.stream ?? null;
2930
+ const narrationCameraStream = narrationRecording ? stage.recorder.cameraStream ?? narrationPreview : null;
2931
+ const activePrimaryStream = narrationRecording ? stage.controller.mic.stream : recorder.stream;
2932
+ const activeCameraStream = narrationRecording ? narrationCameraStream : recorder.camera?.stream ?? null;
2673
2933
  const { devices: mediaDevices, refresh: refreshMediaDevices } = useMediaDevices();
2674
2934
  useEffect4(() => {
2675
2935
  if (activePrimaryStream || activeCameraStream) refreshMediaDevices();
2676
2936
  }, [activeCameraStream, activePrimaryStream, refreshMediaDevices]);
2677
2937
  useStreamPreview(
2678
2938
  previewRef,
2679
- narrationOn ? narrationCameraStream : recorder.state === "stopped" ? null : recorder.stream
2939
+ narrationRecording ? narrationCameraStream : recorder.state === "stopped" ? null : recorder.stream
2680
2940
  );
2681
2941
  useStreamPreview(
2682
2942
  cameraPreviewRef,
2683
- !narrationOn && isDual && recorder.state !== "stopped" ? recorder.camera?.stream ?? null : null
2943
+ !narrationRecording && isDual && recorder.state !== "stopped" ? recorder.camera?.stream ?? null : null
2684
2944
  );
2685
2945
  useEffect4(() => {
2686
2946
  setPlaybackPositionMs(0);
@@ -2718,7 +2978,7 @@ function RecorderModal({
2718
2978
  }
2719
2979
  }, [captureKey, recorder]);
2720
2980
  const handleClose = useCallback4(() => {
2721
- if (!narrationOn) {
2981
+ if (!narrationRecording) {
2722
2982
  if (isSaving || recorder.state === "stopping" || recorder.state === "requesting") return;
2723
2983
  if ((recorder.blob || recorder.camera?.blob || recorder.state === "recording") && !window.confirm(
2724
2984
  "Discard this recording? Download or save it before closing to keep a copy."
@@ -2726,7 +2986,7 @@ function RecorderModal({
2726
2986
  return;
2727
2987
  }
2728
2988
  const s = stageRef.current;
2729
- if (closeNeedsConfirm(narrationOn, s.recorder.state, s.recorder.take !== null)) {
2989
+ if (closeNeedsConfirm(narrationRecording, s.recorder.state, s.recorder.take !== null)) {
2730
2990
  if (!window.confirm("Discard the current narration take?")) return;
2731
2991
  s.handleDiscard();
2732
2992
  }
@@ -2738,7 +2998,7 @@ function RecorderModal({
2738
2998
  }
2739
2999
  recorder.cancel();
2740
3000
  onClose();
2741
- }, [narrationOn, recorder, onClose, isSaving]);
3001
+ }, [narrationOn, narrationRecording, recorder, onClose, isSaving]);
2742
3002
  useModalDialog({
2743
3003
  rootRef: overlayRef,
2744
3004
  dialogRef,
@@ -2948,7 +3208,8 @@ function RecorderModal({
2948
3208
  dualSaveProgressRef.current = {};
2949
3209
  setAdvanceLog(EMPTY_ADVANCE_LOG);
2950
3210
  recorder.reset();
2951
- }, [recorder]);
3211
+ if (narrationOn) stageRef.current.controller.restart();
3212
+ }, [narrationOn, recorder]);
2952
3213
  const handlePlaybackTimeUpdate = useCallback4(
2953
3214
  (media) => {
2954
3215
  const currentMs = Number.isFinite(media.currentTime) ? media.currentTime * 1e3 : 0;
@@ -2985,7 +3246,7 @@ function RecorderModal({
2985
3246
  );
2986
3247
  const timingDownloadFilename = `${downloadFilename}.timing.json`;
2987
3248
  useEffect4(() => {
2988
- if (narrationOn || !canSave) {
3249
+ if (narrationRecording || !canSave) {
2989
3250
  setTimingDownloadUrl(null);
2990
3251
  return;
2991
3252
  }
@@ -3001,7 +3262,7 @@ function RecorderModal({
3001
3262
  setTimingDownloadUrl(url);
3002
3263
  return () => URL.revokeObjectURL(url);
3003
3264
  }, [
3004
- narrationOn,
3265
+ narrationRecording,
3005
3266
  canSave,
3006
3267
  recorder.durationMs,
3007
3268
  slidesOn,
@@ -3044,9 +3305,9 @@ function RecorderModal({
3044
3305
  recorder.blob !== null || recorder.camera?.blob != null,
3045
3306
  stage.recorder.state
3046
3307
  );
3047
- const deviceSettingsLocked = narrationOn ? !narrationRecorderIdle : togglesLocked;
3048
- const microphoneEnabled = narrationOn ? true : micOn;
3049
- const cameraEnabled = narrationOn ? stage.recorder.withCamera : cameraOn;
3308
+ const deviceSettingsLocked = narrationRecording ? !narrationRecorderIdle : togglesLocked;
3309
+ const microphoneEnabled = narrationRecording ? true : micOn;
3310
+ const cameraEnabled = narrationRecording ? stage.recorder.withCamera : cameraOn;
3050
3311
  const narrationToggleFor = (key) => {
3051
3312
  switch (key) {
3052
3313
  case "mic":
@@ -3082,14 +3343,14 @@ function RecorderModal({
3082
3343
  };
3083
3344
  }
3084
3345
  };
3085
- return /* @__PURE__ */ jsx5(
3346
+ return /* @__PURE__ */ jsx6(
3086
3347
  "div",
3087
3348
  {
3088
3349
  ref: overlayRef,
3089
3350
  className: "squisq-editor-shell squisq-recorder-overlay",
3090
3351
  "data-theme": colorScheme,
3091
3352
  style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
3092
- children: /* @__PURE__ */ jsxs4(
3353
+ children: /* @__PURE__ */ jsxs5(
3093
3354
  "div",
3094
3355
  {
3095
3356
  ref: dialogRef,
@@ -3108,431 +3369,453 @@ function RecorderModal({
3108
3369
  "aria-labelledby": headingId,
3109
3370
  tabIndex: -1,
3110
3371
  children: [
3111
- /* @__PURE__ */ jsx5("h2", { id: headingId, style: titleStyle, children: title }),
3112
- /* @__PURE__ */ jsxs4("div", { style: expanded ? bodyRowStyle : void 0, children: [
3113
- /* @__PURE__ */ jsxs4("div", { style: expanded ? leftColStyle : void 0, children: [
3114
- /* @__PURE__ */ jsx5("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
3115
- groupIndex > 0 && /* @__PURE__ */ jsx5("div", { "aria-hidden": "true", style: groupDividerStyle }),
3116
- /* @__PURE__ */ jsx5("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
3117
- if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
3118
- const props = narrationOn ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
3119
- return /* @__PURE__ */ jsx5(
3120
- "button",
3121
- {
3122
- type: "button",
3123
- "aria-pressed": props.active,
3124
- style: props.active ? toggleActive : toggleBase,
3125
- onClick: props.onClick,
3126
- disabled: props.disabled,
3127
- title: props.title,
3128
- children: t.label
3129
- },
3130
- t.key
3131
- );
3132
- }) })
3133
- ] }, group.label)) }),
3134
- /* @__PURE__ */ jsx5("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
3135
- narrationAvailable && /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3136
- /* @__PURE__ */ jsx5(
3137
- "input",
3138
- {
3139
- type: "checkbox",
3140
- style: { accentColor: "var(--squisq-recorder-accent)" },
3141
- checked: narrationOn,
3142
- onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "narration", e.target.checked)),
3143
- disabled: narrationToggleDisabled,
3144
- title: narrationToggleDisabled ? narrationOn ? "Finish or discard the narration take first" : "Save or discard this recording before switching modes" : void 0
3145
- }
3146
- ),
3147
- "Show narration mode"
3148
- ] }),
3149
- slidesAvailable && /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3150
- /* @__PURE__ */ jsx5(
3151
- "input",
3152
- {
3153
- type: "checkbox",
3154
- style: { accentColor: "var(--squisq-recorder-accent)" },
3155
- checked: slidesOn,
3156
- onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "slides", e.target.checked)),
3157
- disabled: narrationToggleDisabled,
3158
- title: narrationToggleDisabled ? "Save or discard this recording before switching modes" : void 0
3159
- }
3160
- ),
3161
- "Show slides mode"
3162
- ] }),
3163
- /* @__PURE__ */ jsx5(
3164
- RecorderDeviceQuickPicks,
3165
- {
3166
- devices: mediaDevices,
3167
- microphoneEnabled,
3168
- cameraEnabled,
3169
- audioDeviceId: deviceSettings.audio.deviceId,
3170
- cameraDeviceId: deviceSettings.camera.deviceId,
3171
- onAudioDeviceChange: (deviceId) => handleDeviceSettingsChange({
3172
- ...deviceSettings,
3173
- audio: { ...deviceSettings.audio, deviceId }
3174
- }),
3175
- onCameraDeviceChange: (deviceId) => handleDeviceSettingsChange({
3176
- ...deviceSettings,
3177
- camera: { ...deviceSettings.camera, deviceId }
3178
- }),
3179
- disabled: deviceSettingsLocked
3180
- }
3181
- ),
3182
- /* @__PURE__ */ jsx5(
3183
- RecorderDeviceSettingsPanel,
3184
- {
3185
- value: deviceSettings,
3186
- onChange: handleDeviceSettingsChange,
3187
- disabled: deviceSettingsLocked,
3188
- microphoneEnabled,
3189
- cameraEnabled,
3190
- screenEnabled: narrationOn ? false : screenOn,
3191
- systemAudioEnabled: !narrationOn && includeSystemAudio,
3192
- separateAudioRecorder: narrationOn,
3193
- primaryStream: activePrimaryStream,
3194
- cameraStream: activeCameraStream
3195
- }
3196
- ),
3197
- !narrationOn && recorder.error && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: recorder.error.message }),
3198
- !narrationOn && saveError && /* @__PURE__ */ jsxs4("div", { role: "alert", style: errorStyle, children: [
3199
- saveError,
3200
- /* @__PURE__ */ jsx5("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
3201
- ] }),
3202
- /* @__PURE__ */ jsxs4("p", { style: summaryStyle, children: [
3203
- "Recording size:",
3204
- " ",
3205
- formatRecordingBytes(
3206
- narrationOn ? stage.recorder.recordedBytes : recorder.recordedBytes
3207
- ),
3208
- ". Automatically stops at",
3209
- " ",
3210
- formatRecordingBytes(
3211
- narrationOn ? stage.recorder.maxRecordingBytes : recorder.maxRecordingBytes
3212
- ),
3213
- " ",
3214
- "total. Final encoding may add more data."
3215
- ] }),
3216
- !narrationOn && recorder.limitReached && /* @__PURE__ */ jsx5("p", { role: "status", style: recordingStatusStyle, children: "Recording stopped at the size limit. Your complete recording is retained; download a copy or save it to the document." }),
3217
- narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
3218
- narrationOn && narrationCameraStream && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5(
3219
- "video",
3220
- {
3221
- ref: previewRef,
3222
- autoPlay: true,
3223
- muted: true,
3224
- playsInline: true,
3225
- style: { width: "100%", height: "100%", objectFit: "contain" }
3226
- }
3227
- ) }),
3228
- narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx5("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
3229
- narrationOn && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
3230
- narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs4("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
3231
- /* @__PURE__ */ jsx5(
3232
- "div",
3233
- {
3234
- "aria-hidden": "true",
3235
- style: {
3236
- ...meterLevelStyle,
3237
- width: `${Math.round(Math.min(1, Math.max(0, stage.controller.micLevel)) * 100)}%`
3238
- }
3239
- }
3240
- ),
3241
- /* @__PURE__ */ jsx5("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
3242
- ] }),
3243
- !narrationOn && !showPreview && /* @__PURE__ */ jsx5("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx5("span", { children: "Click Start Preview to start a recording." }) }),
3244
- !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
3245
- /* @__PURE__ */ jsx5(
3246
- "video",
3247
- {
3248
- ref: previewRef,
3249
- autoPlay: true,
3250
- muted: true,
3251
- playsInline: true,
3252
- style: { width: "100%", height: "100%", objectFit: "contain" }
3253
- }
3254
- ),
3255
- isDual && /* @__PURE__ */ jsx5(
3256
- "video",
3257
- {
3258
- ref: cameraPreviewRef,
3259
- autoPlay: true,
3260
- muted: true,
3261
- playsInline: true,
3262
- "aria-label": "Camera preview",
3263
- style: {
3264
- position: "absolute",
3265
- right: "3%",
3266
- bottom: "6%",
3267
- width: "22%",
3268
- aspectRatio: "16 / 9",
3269
- objectFit: "cover",
3270
- background: "#000",
3271
- border: "1px solid rgba(255,255,255,0.6)"
3272
- }
3273
- }
3274
- )
3275
- ] }),
3276
- !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx5("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
3277
- "\u25CF Recording ",
3278
- formatDurationMs(recorder.durationMs)
3279
- ] }) : /* @__PURE__ */ jsx5(Fragment3, { children: "Microphone ready" }) }),
3280
- !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
3281
- /* @__PURE__ */ jsx5(
3282
- "video",
3283
- {
3284
- src: playbackUrl,
3285
- controls: true,
3286
- playsInline: true,
3287
- onTimeUpdate: (event) => handlePlaybackTimeUpdate(event.currentTarget),
3288
- onSeeking: (event) => handlePlaybackTimeUpdate(event.currentTarget),
3289
- onEnded: () => setPlaybackPositionMs(recorder.durationMs),
3290
- style: { width: "100%", height: "100%", objectFit: "contain" }
3291
- }
3292
- ),
3293
- /* @__PURE__ */ jsxs4(
3294
- "div",
3295
- {
3296
- role: "timer",
3297
- "aria-label": `Playback time: ${formatDurationMs(playbackPositionMs)} of ${formatDurationMs(recorder.durationMs)}`,
3298
- style: playbackTimeStyle,
3299
- children: [
3300
- formatDurationMs(playbackPositionMs),
3301
- " / ",
3302
- formatDurationMs(recorder.durationMs)
3303
- ]
3304
- }
3305
- )
3306
- ] }),
3307
- !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: 12 }, children: [
3308
- /* @__PURE__ */ jsx5("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
3309
- /* @__PURE__ */ jsx5(
3310
- "video",
3311
- {
3312
- src: cameraPlaybackUrl,
3313
- controls: true,
3314
- playsInline: true,
3315
- "aria-label": "Camera recording",
3316
- style: {
3317
- width: "48%",
3318
- display: "block",
3319
- marginLeft: "auto",
3320
- background: "#000"
3321
- }
3322
- }
3323
- )
3324
- ] }),
3325
- !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: 12 }, children: [
3326
- /* @__PURE__ */ jsxs4("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
3327
- "\u2713 Recorded ",
3328
- formatDurationMs(recorder.durationMs)
3329
- ] }),
3330
- /* @__PURE__ */ jsx5("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
3331
- ] }),
3332
- !narrationOn && canSave && playbackUrl && /* @__PURE__ */ jsxs4(
3333
- "div",
3334
- {
3335
- style: {
3336
- ...buttonRowStyle,
3337
- flexWrap: "wrap",
3338
- justifyContent: "flex-start",
3339
- marginBottom: 12
3340
- },
3341
- children: [
3342
- /* @__PURE__ */ jsx5("a", { href: playbackUrl, download: downloadFilename, style: btnSecondary, children: isDual ? "Download screen recording" : "Download recording" }),
3343
- isDual && cameraPlaybackUrl && /* @__PURE__ */ jsx5(
3344
- "a",
3372
+ /* @__PURE__ */ jsx6("h2", { id: headingId, style: titleStyle, children: title }),
3373
+ /* @__PURE__ */ jsxs5("div", { style: expanded ? bodyRowStyle : void 0, children: [
3374
+ /* @__PURE__ */ jsxs5(
3375
+ "div",
3376
+ {
3377
+ style: expanded ? {
3378
+ ...leftColStyle,
3379
+ ...panelMode === "both" ? { flex: "0 0 min(280px, 24vw)" } : {}
3380
+ } : void 0,
3381
+ children: [
3382
+ /* @__PURE__ */ jsx6("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs5(Fragment2, { children: [
3383
+ groupIndex > 0 && /* @__PURE__ */ jsx6("div", { "aria-hidden": "true", style: groupDividerStyle }),
3384
+ /* @__PURE__ */ jsx6("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
3385
+ if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
3386
+ const props = narrationRecording ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
3387
+ return /* @__PURE__ */ jsx6(
3388
+ "button",
3389
+ {
3390
+ type: "button",
3391
+ "aria-pressed": props.active,
3392
+ style: props.active ? toggleActive : toggleBase,
3393
+ onClick: props.onClick,
3394
+ disabled: props.disabled,
3395
+ title: props.title,
3396
+ children: t.label
3397
+ },
3398
+ t.key
3399
+ );
3400
+ }) })
3401
+ ] }, group.label)) }),
3402
+ /* @__PURE__ */ jsx6("p", { style: summaryStyle, children: narrationRecording ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
3403
+ narrationAvailable && /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3404
+ /* @__PURE__ */ jsx6(
3405
+ "input",
3345
3406
  {
3346
- href: cameraPlaybackUrl,
3347
- download: buildFilename(
3348
- "video",
3349
- recorder.camera?.extension ?? ".webm",
3350
- basename.trim() ? `${basename.trim()}-camera` : void 0,
3351
- micOn ? "camera-audio" : "camera"
3352
- ),
3353
- style: btnSecondary,
3354
- children: "Download camera recording"
3407
+ type: "checkbox",
3408
+ style: { accentColor: "var(--squisq-recorder-accent)" },
3409
+ checked: narrationOn,
3410
+ onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "narration", e.target.checked)),
3411
+ disabled: narrationToggleDisabled,
3412
+ title: narrationToggleDisabled ? narrationRecording ? "Finish or discard the narration take first" : "Save or discard this recording before switching modes" : void 0
3355
3413
  }
3356
3414
  ),
3357
- timingDownloadUrl && /* @__PURE__ */ jsx5(
3358
- "a",
3415
+ "Show narration mode"
3416
+ ] }),
3417
+ slidesAvailable && /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3418
+ /* @__PURE__ */ jsx6(
3419
+ "input",
3359
3420
  {
3360
- href: timingDownloadUrl,
3361
- download: timingDownloadFilename,
3362
- style: btnSecondary,
3363
- children: "Download recording timings"
3421
+ type: "checkbox",
3422
+ style: { accentColor: "var(--squisq-recorder-accent)" },
3423
+ checked: slidesOn,
3424
+ onChange: (e) => setPanelMode(panelModeAfterToggle(panelMode, "slides", e.target.checked)),
3425
+ disabled: narrationToggleDisabled,
3426
+ title: narrationToggleDisabled ? "Save or discard this recording before switching modes" : void 0
3364
3427
  }
3365
- )
3366
- ]
3367
- }
3368
- ),
3369
- !narrationOn && !slidesTimingCapture && source === "mic" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3370
- /* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
3371
- /* @__PURE__ */ jsx5(
3372
- "textarea",
3373
- {
3374
- id: "recorder-source-text",
3375
- style: textareaStyle,
3376
- placeholder: "Type the text you're going to read aloud.",
3377
- value: sourceText,
3378
- onChange: (e) => setSourceText(e.target.value),
3379
- disabled: recorder.state === "recording"
3380
- }
3381
- )
3382
- ] }),
3383
- /* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
3384
- /* @__PURE__ */ jsx5(
3385
- "input",
3386
- {
3387
- id: "recorder-basename",
3388
- type: "text",
3389
- style: inputStyle2,
3390
- placeholder: narrationOn ? "narration" : isDual ? "screen + camera" : filenameSeed,
3391
- value: basename,
3392
- onChange: (e) => setBasename(e.target.value),
3393
- disabled: narrationOn ? !narrationRecorderIdle : recorder.state === "recording"
3394
- }
3395
- ),
3396
- !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: recordingStatusStyle, children: [
3397
- "\u25CF Recording ",
3398
- formatDurationMs(recorder.durationMs)
3399
- ] }),
3400
- showTimingCheckbox && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3401
- /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3402
- /* @__PURE__ */ jsx5(
3403
- "input",
3428
+ ),
3429
+ "Show slides mode"
3430
+ ] }),
3431
+ /* @__PURE__ */ jsx6(
3432
+ RecorderDeviceQuickPicks,
3404
3433
  {
3405
- type: "checkbox",
3406
- style: { accentColor: "var(--squisq-recorder-accent)" },
3407
- checked: applySlideTimings,
3408
- onChange: (e) => setApplySlideTimings(e.target.checked),
3409
- disabled: isSaving
3434
+ devices: mediaDevices,
3435
+ microphoneEnabled,
3436
+ cameraEnabled,
3437
+ audioDeviceId: deviceSettings.audio.deviceId,
3438
+ cameraDeviceId: deviceSettings.camera.deviceId,
3439
+ onAudioDeviceChange: (deviceId) => handleDeviceSettingsChange({
3440
+ ...deviceSettings,
3441
+ audio: { ...deviceSettings.audio, deviceId }
3442
+ }),
3443
+ onCameraDeviceChange: (deviceId) => handleDeviceSettingsChange({
3444
+ ...deviceSettings,
3445
+ camera: { ...deviceSettings.camera, deviceId }
3446
+ }),
3447
+ disabled: deviceSettingsLocked
3410
3448
  }
3411
3449
  ),
3412
- SLIDE_TIMING_CHECKBOX_LABEL
3413
- ] }),
3414
- applySlideTimings && slideCoverageWarning && /* @__PURE__ */ jsx5("p", { style: summaryStyle, children: slideCoverageWarning })
3415
- ] }),
3416
- /* @__PURE__ */ jsxs4("div", { style: buttonRowStyle, children: [
3417
- /* @__PURE__ */ jsx5(
3418
- "button",
3419
- {
3420
- type: "button",
3421
- style: btnSecondary,
3422
- onClick: handleClose,
3423
- disabled: !narrationOn && isBusy,
3424
- children: "Close"
3425
- }
3426
- ),
3427
- narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3428
- narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx5(
3429
- "button",
3450
+ !narrationRecording && screenOn && /* @__PURE__ */ jsx6(
3451
+ RecorderScreenSettings,
3452
+ {
3453
+ value: deviceSettings.screen,
3454
+ onChange: (screen) => handleDeviceSettingsChange({ ...deviceSettings, screen }),
3455
+ disabled: deviceSettingsLocked,
3456
+ hasPreview: recorder.state === "ready",
3457
+ onChoose: () => {
3458
+ recorder.cancel();
3459
+ void handleRequest();
3460
+ }
3461
+ }
3462
+ ),
3463
+ /* @__PURE__ */ jsx6(
3464
+ RecorderDeviceSettingsPanel,
3430
3465
  {
3431
- type: "button",
3432
- style: btnPrimary,
3433
- onClick: () => void handleNarrationPreview(),
3434
- disabled: narrationRequesting,
3435
- children: narrationRequesting ? "Requesting\u2026" : "Start preview"
3466
+ value: deviceSettings,
3467
+ onChange: handleDeviceSettingsChange,
3468
+ disabled: deviceSettingsLocked,
3469
+ microphoneEnabled,
3470
+ cameraEnabled,
3471
+ screenEnabled: narrationRecording ? false : screenOn,
3472
+ systemAudioEnabled: !narrationRecording && includeSystemAudio,
3473
+ separateAudioRecorder: narrationRecording,
3474
+ primaryStream: activePrimaryStream,
3475
+ cameraStream: activeCameraStream
3436
3476
  }
3437
3477
  ),
3438
- narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
3439
- /* @__PURE__ */ jsx5(
3440
- "span",
3478
+ !narrationRecording && recorder.error && /* @__PURE__ */ jsx6("div", { style: errorStyle, children: recorder.error.message }),
3479
+ !narrationRecording && saveError && /* @__PURE__ */ jsxs5("div", { role: "alert", style: errorStyle, children: [
3480
+ saveError,
3481
+ /* @__PURE__ */ jsx6("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
3482
+ ] }),
3483
+ /* @__PURE__ */ jsxs5("p", { style: summaryStyle, children: [
3484
+ "Recording size:",
3485
+ " ",
3486
+ formatRecordingBytes(
3487
+ narrationRecording ? stage.recorder.recordedBytes : recorder.recordedBytes
3488
+ ),
3489
+ ". Automatically stops at",
3490
+ " ",
3491
+ formatRecordingBytes(
3492
+ narrationRecording ? stage.recorder.maxRecordingBytes : recorder.maxRecordingBytes
3493
+ ),
3494
+ " ",
3495
+ "total. Final encoding may add more data."
3496
+ ] }),
3497
+ !narrationRecording && recorder.limitReached && /* @__PURE__ */ jsx6("p", { role: "status", style: recordingStatusStyle, children: "Recording stopped at the size limit. Your complete recording is retained; download a copy or save it to the document." }),
3498
+ narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx6("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
3499
+ narrationRecording && narrationCameraStream && /* @__PURE__ */ jsx6("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx6(
3500
+ "video",
3501
+ {
3502
+ ref: previewRef,
3503
+ autoPlay: true,
3504
+ muted: true,
3505
+ playsInline: true,
3506
+ style: { width: "100%", height: "100%", objectFit: "contain" }
3507
+ }
3508
+ ) }),
3509
+ narrationRecording && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx6("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
3510
+ narrationRecording && !narrationCameraStream && !narrationTakeDone && stage.recorder.withCamera && /* @__PURE__ */ jsx6("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx6("span", { children: narrationPreviewWanted ? "Camera starting\u2026" : "Click Start preview to turn on your camera." }) }),
3511
+ narrationRecording && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs5("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
3512
+ /* @__PURE__ */ jsx6(
3513
+ "div",
3441
3514
  {
3442
- className: "squisq-recorder-record-dot",
3443
- style: recordDotFrameStyle,
3444
3515
  "aria-hidden": "true",
3445
- children: /* @__PURE__ */ jsx5(
3446
- "span",
3447
- {
3448
- className: "squisq-recorder-record-dot-center",
3449
- style: recordDotStyle
3450
- }
3451
- )
3516
+ style: {
3517
+ ...meterLevelStyle,
3518
+ width: `${Math.round(Math.min(1, Math.max(0, stage.controller.micLevel)) * 100)}%`
3519
+ }
3520
+ }
3521
+ ),
3522
+ /* @__PURE__ */ jsx6("span", { style: { position: "relative" }, children: stage.recorder.state === "recording" ? "\u25CF Recording narration" : narrationMicLive ? stage.controller.voiceActive ? "Voice detected" : "Microphone ready" : "Click Start preview to check your mic." })
3523
+ ] }),
3524
+ !narrationRecording && !showPreview && /* @__PURE__ */ jsx6("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx6("span", { children: screenOn ? "Choose a screen or window to preview what will be recorded." : "Click Start preview to prepare a recording." }) }),
3525
+ !narrationRecording && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
3526
+ /* @__PURE__ */ jsx6(
3527
+ "video",
3528
+ {
3529
+ ref: previewRef,
3530
+ autoPlay: true,
3531
+ muted: true,
3532
+ playsInline: true,
3533
+ style: { width: "100%", height: "100%", objectFit: "contain" }
3452
3534
  }
3453
3535
  ),
3454
- "Record"
3536
+ isDual && /* @__PURE__ */ jsx6(
3537
+ "video",
3538
+ {
3539
+ ref: cameraPreviewRef,
3540
+ autoPlay: true,
3541
+ muted: true,
3542
+ playsInline: true,
3543
+ "aria-label": "Camera preview",
3544
+ style: {
3545
+ position: "absolute",
3546
+ right: "3%",
3547
+ bottom: "6%",
3548
+ width: "22%",
3549
+ aspectRatio: "16 / 9",
3550
+ objectFit: "cover",
3551
+ background: "#000",
3552
+ border: "1px solid rgba(255,255,255,0.6)"
3553
+ }
3554
+ }
3555
+ )
3455
3556
  ] }),
3456
- (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx5(
3457
- "button",
3557
+ !narrationRecording && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx6("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
3558
+ "\u25CF Recording ",
3559
+ formatDurationMs(recorder.durationMs)
3560
+ ] }) : /* @__PURE__ */ jsx6(Fragment3, { children: "Microphone ready" }) }),
3561
+ !narrationRecording && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
3562
+ /* @__PURE__ */ jsx6(
3563
+ "video",
3564
+ {
3565
+ src: playbackUrl,
3566
+ controls: true,
3567
+ playsInline: true,
3568
+ onTimeUpdate: (event) => handlePlaybackTimeUpdate(event.currentTarget),
3569
+ onSeeking: (event) => handlePlaybackTimeUpdate(event.currentTarget),
3570
+ onEnded: () => setPlaybackPositionMs(recorder.durationMs),
3571
+ style: { width: "100%", height: "100%", objectFit: "contain" }
3572
+ }
3573
+ ),
3574
+ /* @__PURE__ */ jsxs5(
3575
+ "div",
3576
+ {
3577
+ role: "timer",
3578
+ "aria-label": `Playback time: ${formatDurationMs(playbackPositionMs)} of ${formatDurationMs(recorder.durationMs)}`,
3579
+ style: playbackTimeStyle,
3580
+ children: [
3581
+ formatDurationMs(playbackPositionMs),
3582
+ " / ",
3583
+ formatDurationMs(recorder.durationMs)
3584
+ ]
3585
+ }
3586
+ )
3587
+ ] }),
3588
+ !narrationRecording && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs5("div", { style: { marginBottom: 12 }, children: [
3589
+ /* @__PURE__ */ jsx6("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
3590
+ /* @__PURE__ */ jsx6(
3591
+ "video",
3592
+ {
3593
+ src: cameraPlaybackUrl,
3594
+ controls: true,
3595
+ playsInline: true,
3596
+ "aria-label": "Camera recording",
3597
+ style: {
3598
+ width: "48%",
3599
+ display: "block",
3600
+ marginLeft: "auto",
3601
+ background: "#000"
3602
+ }
3603
+ }
3604
+ )
3605
+ ] }),
3606
+ !narrationRecording && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: { marginBottom: 12 }, children: [
3607
+ /* @__PURE__ */ jsxs5("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
3608
+ "\u2713 Recorded ",
3609
+ formatDurationMs(recorder.durationMs)
3610
+ ] }),
3611
+ /* @__PURE__ */ jsx6("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
3612
+ ] }),
3613
+ !narrationRecording && canSave && playbackUrl && /* @__PURE__ */ jsxs5(
3614
+ "div",
3458
3615
  {
3459
- type: "button",
3460
- style: btnDanger,
3461
- onClick: () => void stage.recorder.stop(),
3462
- children: "Stop"
3616
+ style: {
3617
+ ...buttonRowStyle,
3618
+ flexWrap: "wrap",
3619
+ justifyContent: "flex-start",
3620
+ marginBottom: 12
3621
+ },
3622
+ children: [
3623
+ /* @__PURE__ */ jsx6("a", { href: playbackUrl, download: downloadFilename, style: btnSecondary, children: isDual ? "Download screen recording" : "Download recording" }),
3624
+ isDual && cameraPlaybackUrl && /* @__PURE__ */ jsx6(
3625
+ "a",
3626
+ {
3627
+ href: cameraPlaybackUrl,
3628
+ download: buildFilename(
3629
+ "video",
3630
+ recorder.camera?.extension ?? ".webm",
3631
+ basename.trim() ? `${basename.trim()}-camera` : void 0,
3632
+ micOn ? "camera-audio" : "camera"
3633
+ ),
3634
+ style: btnSecondary,
3635
+ children: "Download camera recording"
3636
+ }
3637
+ ),
3638
+ timingDownloadUrl && /* @__PURE__ */ jsx6(
3639
+ "a",
3640
+ {
3641
+ href: timingDownloadUrl,
3642
+ download: timingDownloadFilename,
3643
+ style: btnSecondary,
3644
+ children: "Download recording timings"
3645
+ }
3646
+ )
3647
+ ]
3463
3648
  }
3464
3649
  ),
3465
- stage.recorder.state === "processing" && /* @__PURE__ */ jsx5("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
3466
- stage.recorder.state === "saving" && /* @__PURE__ */ jsx5("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
3467
- stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3468
- /* @__PURE__ */ jsx5("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
3469
- /* @__PURE__ */ jsx5(
3470
- "button",
3650
+ !narrationOn && !slidesTimingCapture && source === "mic" && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3651
+ /* @__PURE__ */ jsx6("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
3652
+ /* @__PURE__ */ jsx6(
3653
+ "textarea",
3471
3654
  {
3472
- type: "button",
3473
- style: btnPrimary,
3474
- onClick: () => void stage.handleSave(),
3475
- children: "Save to document"
3655
+ id: "recorder-source-text",
3656
+ style: textareaStyle,
3657
+ placeholder: "Type the text you're going to read aloud.",
3658
+ value: sourceText,
3659
+ onChange: (e) => setSourceText(e.target.value),
3660
+ disabled: recorder.state === "recording"
3476
3661
  }
3477
3662
  )
3478
- ] })
3479
- ] }),
3480
- !narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3481
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx5(
3482
- "button",
3663
+ ] }),
3664
+ /* @__PURE__ */ jsx6("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
3665
+ /* @__PURE__ */ jsx6(
3666
+ "input",
3483
3667
  {
3484
- type: "button",
3485
- style: btnPrimary,
3486
- onClick: handleRequest,
3487
- disabled: isBusy || !canCapture,
3488
- children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
3668
+ id: "recorder-basename",
3669
+ type: "text",
3670
+ style: inputStyle2,
3671
+ placeholder: narrationRecording ? "narration" : isDual ? "screen + camera" : filenameSeed,
3672
+ value: basename,
3673
+ onChange: (e) => setBasename(e.target.value),
3674
+ disabled: narrationRecording ? !narrationRecorderIdle : recorder.state === "recording"
3489
3675
  }
3490
3676
  ),
3491
- canRecord && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
3492
- /* @__PURE__ */ jsx5(
3493
- "span",
3494
- {
3495
- className: "squisq-recorder-record-dot",
3496
- style: recordDotFrameStyle,
3497
- "aria-hidden": "true",
3498
- children: /* @__PURE__ */ jsx5(
3499
- "span",
3500
- {
3501
- className: "squisq-recorder-record-dot-center",
3502
- style: recordDotStyle
3503
- }
3504
- )
3505
- }
3506
- ),
3507
- "Record"
3677
+ !narrationRecording && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: recordingStatusStyle, children: [
3678
+ "\u25CF Recording ",
3679
+ formatDurationMs(recorder.durationMs)
3680
+ ] }),
3681
+ showTimingCheckbox && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3682
+ /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3683
+ /* @__PURE__ */ jsx6(
3684
+ "input",
3685
+ {
3686
+ type: "checkbox",
3687
+ style: { accentColor: "var(--squisq-recorder-accent)" },
3688
+ checked: applySlideTimings,
3689
+ onChange: (e) => setApplySlideTimings(e.target.checked),
3690
+ disabled: isSaving
3691
+ }
3692
+ ),
3693
+ SLIDE_TIMING_CHECKBOX_LABEL
3694
+ ] }),
3695
+ applySlideTimings && slideCoverageWarning && /* @__PURE__ */ jsx6("p", { style: summaryStyle, children: slideCoverageWarning })
3508
3696
  ] }),
3509
- canStop && /* @__PURE__ */ jsx5("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
3510
- canSave && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3511
- /* @__PURE__ */ jsx5(
3697
+ /* @__PURE__ */ jsxs5("div", { style: buttonRowStyle, children: [
3698
+ /* @__PURE__ */ jsx6(
3512
3699
  "button",
3513
3700
  {
3514
3701
  type: "button",
3515
3702
  style: btnSecondary,
3516
- onClick: handleDiscard,
3517
- disabled: isBusy,
3518
- children: "Discard & re-record"
3703
+ onClick: handleClose,
3704
+ disabled: !narrationRecording && isBusy,
3705
+ children: "Close"
3519
3706
  }
3520
3707
  ),
3521
- /* @__PURE__ */ jsx5(
3522
- "button",
3523
- {
3524
- type: "button",
3525
- style: btnPrimary,
3526
- onClick: handleSave,
3527
- disabled: isBusy,
3528
- children: isSaving ? "Saving\u2026" : "Save to document"
3529
- }
3530
- )
3708
+ narrationRecording && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3709
+ narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx6(
3710
+ "button",
3711
+ {
3712
+ type: "button",
3713
+ style: btnPrimary,
3714
+ onClick: () => void handleNarrationPreview(),
3715
+ disabled: narrationRequesting,
3716
+ children: narrationRequesting ? "Requesting\u2026" : "Start preview"
3717
+ }
3718
+ ),
3719
+ narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs5("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
3720
+ /* @__PURE__ */ jsx6(
3721
+ "span",
3722
+ {
3723
+ className: "squisq-recorder-record-dot",
3724
+ style: recordDotFrameStyle,
3725
+ "aria-hidden": "true",
3726
+ children: /* @__PURE__ */ jsx6(
3727
+ "span",
3728
+ {
3729
+ className: "squisq-recorder-record-dot-center",
3730
+ style: recordDotStyle
3731
+ }
3732
+ )
3733
+ }
3734
+ ),
3735
+ "Record"
3736
+ ] }),
3737
+ (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx6(
3738
+ "button",
3739
+ {
3740
+ type: "button",
3741
+ style: btnDanger,
3742
+ onClick: () => void stage.recorder.stop(),
3743
+ children: "Stop"
3744
+ }
3745
+ ),
3746
+ stage.recorder.state === "processing" && /* @__PURE__ */ jsx6("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
3747
+ stage.recorder.state === "saving" && /* @__PURE__ */ jsx6("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
3748
+ stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3749
+ /* @__PURE__ */ jsx6("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
3750
+ /* @__PURE__ */ jsx6(
3751
+ "button",
3752
+ {
3753
+ type: "button",
3754
+ style: btnPrimary,
3755
+ onClick: () => void stage.handleSave(),
3756
+ children: "Save to document"
3757
+ }
3758
+ )
3759
+ ] })
3760
+ ] }),
3761
+ !narrationRecording && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3762
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx6(
3763
+ "button",
3764
+ {
3765
+ type: "button",
3766
+ style: btnPrimary,
3767
+ onClick: handleRequest,
3768
+ disabled: isBusy || !canCapture,
3769
+ children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
3770
+ }
3771
+ ),
3772
+ canRecord && /* @__PURE__ */ jsxs5("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
3773
+ /* @__PURE__ */ jsx6(
3774
+ "span",
3775
+ {
3776
+ className: "squisq-recorder-record-dot",
3777
+ style: recordDotFrameStyle,
3778
+ "aria-hidden": "true",
3779
+ children: /* @__PURE__ */ jsx6(
3780
+ "span",
3781
+ {
3782
+ className: "squisq-recorder-record-dot-center",
3783
+ style: recordDotStyle
3784
+ }
3785
+ )
3786
+ }
3787
+ ),
3788
+ "Record"
3789
+ ] }),
3790
+ canStop && /* @__PURE__ */ jsx6("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
3791
+ canSave && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3792
+ /* @__PURE__ */ jsx6(
3793
+ "button",
3794
+ {
3795
+ type: "button",
3796
+ style: btnSecondary,
3797
+ onClick: handleDiscard,
3798
+ disabled: isBusy,
3799
+ children: "Discard & re-record"
3800
+ }
3801
+ ),
3802
+ /* @__PURE__ */ jsx6(
3803
+ "button",
3804
+ {
3805
+ type: "button",
3806
+ style: btnPrimary,
3807
+ onClick: handleSave,
3808
+ disabled: isBusy,
3809
+ children: isSaving ? "Saving\u2026" : "Save to document"
3810
+ }
3811
+ )
3812
+ ] })
3813
+ ] })
3531
3814
  ] })
3532
- ] })
3533
- ] })
3534
- ] }),
3535
- narrationOn && narration && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
3815
+ ]
3816
+ }
3817
+ ),
3818
+ narrationOn && narration && /* @__PURE__ */ jsx6("div", { style: { ...rightColStyle, flex: "1 1 0" }, children: /* @__PURE__ */ jsx6(
3536
3819
  NarrationStage,
3537
3820
  {
3538
3821
  stage,
@@ -3544,7 +3827,7 @@ function RecorderModal({
3544
3827
  showReviewActions: false
3545
3828
  }
3546
3829
  ) }),
3547
- slidesOn && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
3830
+ slidesOn && /* @__PURE__ */ jsx6("div", { style: { ...rightColStyle, flex: "1 1 0" }, children: /* @__PURE__ */ jsx6(
3548
3831
  RecorderSlidesPanel,
3549
3832
  {
3550
3833
  slides: slideDeck,
@@ -3553,7 +3836,7 @@ function RecorderModal({
3553
3836
  viewport: slidesViewport,
3554
3837
  basePath: slides?.basePath,
3555
3838
  mediaProvider: slides?.mediaProvider ?? mediaProvider,
3556
- recording: recorder.state === "recording",
3839
+ recording: narrationRecording ? stage.recorder.state === "recording" : recorder.state === "recording",
3557
3840
  shownBlockIds
3558
3841
  }
3559
3842
  ) })
@@ -3568,7 +3851,7 @@ function RecorderModal({
3568
3851
  // src/recorder/RecorderPanel.tsx
3569
3852
  import { useCallback as useCallback5, useState as useState4 } from "react";
3570
3853
  import { createPortal } from "react-dom";
3571
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3854
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3572
3855
  function RecorderPanel({
3573
3856
  mediaProvider,
3574
3857
  container = null,
@@ -3594,8 +3877,8 @@ function RecorderPanel({
3594
3877
  [controlledOpen, onOpenChange]
3595
3878
  );
3596
3879
  const handleClose = useCallback5(() => setOpen(false), [setOpen]);
3597
- return /* @__PURE__ */ jsxs5(Fragment4, { children: [
3598
- showTrigger && /* @__PURE__ */ jsx6(
3880
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
3881
+ showTrigger && /* @__PURE__ */ jsx7(
3599
3882
  "button",
3600
3883
  {
3601
3884
  type: "button",
@@ -3604,11 +3887,11 @@ function RecorderPanel({
3604
3887
  "aria-label": tooltip,
3605
3888
  "aria-expanded": open,
3606
3889
  onClick: () => setOpen(!open),
3607
- children: /* @__PURE__ */ jsx6(Icon, { icon: "fa-solid fa-microphone" })
3890
+ children: /* @__PURE__ */ jsx7(Icon, { icon: "fa-solid fa-microphone" })
3608
3891
  }
3609
3892
  ),
3610
3893
  open && typeof document !== "undefined" && createPortal(
3611
- /* @__PURE__ */ jsx6(
3894
+ /* @__PURE__ */ jsx7(
3612
3895
  RecorderModal,
3613
3896
  {
3614
3897
  mediaProvider,