@bendyline/squisq-editor-react 2.11.14 → 2.11.16

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.
@@ -21,6 +21,114 @@ import {
21
21
  useStreamPreview
22
22
  } from "./chunk-34FRFDQ6.js";
23
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
+ }
131
+
24
132
  // src/recorder/sources/screenStream.ts
25
133
  function mixAudioTracks(streams) {
26
134
  const sources = streams.map((s) => s.getAudioTracks()).flat().filter((t) => t.readyState === "live");
@@ -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(
@@ -2059,6 +2179,134 @@ import {
2059
2179
  recordSlideShown
2060
2180
  } from "@bendyline/squisq/narration";
2061
2181
 
2182
+ // src/recorder/RecorderScreenSettings.tsx
2183
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2184
+ var field = { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 };
2185
+ var input = {
2186
+ width: "100%",
2187
+ boxSizing: "border-box",
2188
+ padding: "6px 8px",
2189
+ font: "inherit",
2190
+ color: "var(--squisq-recorder-text)",
2191
+ background: "var(--squisq-recorder-input)",
2192
+ border: "1px solid var(--squisq-recorder-border)"
2193
+ };
2194
+ function RecorderScreenSettings({
2195
+ value,
2196
+ onChange,
2197
+ disabled,
2198
+ onChoose,
2199
+ hasPreview
2200
+ }) {
2201
+ const crop = value.crop;
2202
+ return /* @__PURE__ */ jsxs4(
2203
+ "fieldset",
2204
+ {
2205
+ disabled,
2206
+ style: {
2207
+ margin: "0 0 12px",
2208
+ padding: 12,
2209
+ border: "1px solid var(--squisq-recorder-border)",
2210
+ fontSize: 13
2211
+ },
2212
+ children: [
2213
+ /* @__PURE__ */ jsx5("legend", { children: "What to record" }),
2214
+ /* @__PURE__ */ jsxs4(
2215
+ "div",
2216
+ {
2217
+ style: {
2218
+ display: "grid",
2219
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
2220
+ gap: 10
2221
+ },
2222
+ children: [
2223
+ /* @__PURE__ */ jsxs4("label", { style: field, children: [
2224
+ "Preferred surface",
2225
+ /* @__PURE__ */ jsxs4(
2226
+ "select",
2227
+ {
2228
+ style: input,
2229
+ value: value.displaySurface ?? "",
2230
+ onChange: (event) => onChange({ ...value, displaySurface: event.target.value || void 0 }),
2231
+ children: [
2232
+ /* @__PURE__ */ jsx5("option", { value: "", children: "Any available surface" }),
2233
+ /* @__PURE__ */ jsx5("option", { value: "monitor", children: "Entire screen" }),
2234
+ /* @__PURE__ */ jsx5("option", { value: "window", children: "Application window" }),
2235
+ /* @__PURE__ */ jsx5("option", { value: "browser", children: "Browser tab (when available)" })
2236
+ ]
2237
+ }
2238
+ )
2239
+ ] }),
2240
+ /* @__PURE__ */ jsxs4("label", { style: field, children: [
2241
+ "Capture area",
2242
+ /* @__PURE__ */ jsxs4(
2243
+ "select",
2244
+ {
2245
+ style: input,
2246
+ value: value.crop ? "region" : "full",
2247
+ onChange: (event) => onChange({
2248
+ ...value,
2249
+ crop: event.target.value === "region" ? { x: 0, y: 0, width: 1280, height: 720 } : void 0
2250
+ }),
2251
+ children: [
2252
+ /* @__PURE__ */ jsx5("option", { value: "full", children: "Whole selected surface" }),
2253
+ /* @__PURE__ */ jsx5("option", { value: "region", children: "Region by coordinates" })
2254
+ ]
2255
+ }
2256
+ )
2257
+ ] }),
2258
+ crop && /* @__PURE__ */ jsx5(
2259
+ "div",
2260
+ {
2261
+ style: {
2262
+ gridColumn: "1 / -1",
2263
+ display: "grid",
2264
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
2265
+ gap: 10
2266
+ },
2267
+ children: ["x", "y", "width", "height"].map((key) => /* @__PURE__ */ jsxs4("label", { style: field, children: [
2268
+ {
2269
+ x: "Left (px)",
2270
+ y: "Top (px)",
2271
+ width: "Region width (px)",
2272
+ height: "Region height (px)"
2273
+ }[key],
2274
+ /* @__PURE__ */ jsx5(
2275
+ "input",
2276
+ {
2277
+ style: input,
2278
+ type: "number",
2279
+ min: key === "x" || key === "y" ? 0 : 1,
2280
+ max: 16384,
2281
+ step: 1,
2282
+ value: Number.isFinite(crop[key]) ? crop[key] : "",
2283
+ onChange: (event) => onChange({ ...value, crop: { ...crop, [key]: event.target.valueAsNumber } })
2284
+ }
2285
+ )
2286
+ ] }, key))
2287
+ }
2288
+ )
2289
+ ]
2290
+ }
2291
+ ),
2292
+ /* @__PURE__ */ jsxs4("p", { style: { margin: "8px 0 0", color: "var(--squisq-recorder-muted)" }, children: [
2293
+ "Choose a screen, window or tab in the picker, then check the preview. Available choices depend on your browser or app.",
2294
+ value.crop && " Coordinates start at the selected surface\u2019s top-left, in captured pixels. Only this region is saved; audio is unchanged."
2295
+ ] }),
2296
+ /* @__PURE__ */ jsx5(
2297
+ "button",
2298
+ {
2299
+ type: "button",
2300
+ style: { ...input, marginTop: 10, width: "auto", cursor: "pointer" },
2301
+ onClick: onChoose,
2302
+ children: hasPreview ? "Change screen or window" : "Choose screen or window"
2303
+ }
2304
+ )
2305
+ ]
2306
+ }
2307
+ );
2308
+ }
2309
+
2062
2310
  // src/modal/useModalDialog.ts
2063
2311
  import { useModalDialog } from "@bendyline/squisq-react";
2064
2312
 
@@ -2085,7 +2333,7 @@ function narrationCaptureSummary(withCamera) {
2085
2333
  }
2086
2334
 
2087
2335
  // src/recorder/RecorderModal.tsx
2088
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2336
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2089
2337
  var overlayStyle = {
2090
2338
  position: "fixed",
2091
2339
  inset: 0,
@@ -2494,6 +2742,7 @@ function RecorderModal({
2494
2742
  audioConstraints,
2495
2743
  videoConstraints: cameraConstraints,
2496
2744
  screenVideoConstraints: screenConstraints,
2745
+ screenRegion: deviceSettings.screen.crop,
2497
2746
  screenAudioConstraints,
2498
2747
  mimeType: (source === "mic" ? encoding.audioMimeType : encoding.videoMimeType).trim() || void 0,
2499
2748
  bitsPerSecond: recorderBitsPerSecond(encoding.bitsPerSecond),
@@ -3082,14 +3331,14 @@ function RecorderModal({
3082
3331
  };
3083
3332
  }
3084
3333
  };
3085
- return /* @__PURE__ */ jsx5(
3334
+ return /* @__PURE__ */ jsx6(
3086
3335
  "div",
3087
3336
  {
3088
3337
  ref: overlayRef,
3089
3338
  className: "squisq-editor-shell squisq-recorder-overlay",
3090
3339
  "data-theme": colorScheme,
3091
3340
  style: { ...overlayStyle, ...recorderThemeStyle(colorScheme) },
3092
- children: /* @__PURE__ */ jsxs4(
3341
+ children: /* @__PURE__ */ jsxs5(
3093
3342
  "div",
3094
3343
  {
3095
3344
  ref: dialogRef,
@@ -3108,15 +3357,15 @@ function RecorderModal({
3108
3357
  "aria-labelledby": headingId,
3109
3358
  tabIndex: -1,
3110
3359
  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) => {
3360
+ /* @__PURE__ */ jsx6("h2", { id: headingId, style: titleStyle, children: title }),
3361
+ /* @__PURE__ */ jsxs5("div", { style: expanded ? bodyRowStyle : void 0, children: [
3362
+ /* @__PURE__ */ jsxs5("div", { style: expanded ? leftColStyle : void 0, children: [
3363
+ /* @__PURE__ */ jsx6("div", { style: toggleRowStyle, role: "group", "aria-label": "Capture sources", children: TOGGLE_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxs5(Fragment2, { children: [
3364
+ groupIndex > 0 && /* @__PURE__ */ jsx6("div", { "aria-hidden": "true", style: groupDividerStyle }),
3365
+ /* @__PURE__ */ jsx6("div", { style: toggleGroupStyle, children: group.toggles.map((t) => {
3117
3366
  if (t.key === "systemAudio" && !canIncludeSystemAudio) return null;
3118
3367
  const props = narrationOn ? narrationToggleFor(t.key) : simpleToggleProps(t.key);
3119
- return /* @__PURE__ */ jsx5(
3368
+ return /* @__PURE__ */ jsx6(
3120
3369
  "button",
3121
3370
  {
3122
3371
  type: "button",
@@ -3131,9 +3380,9 @@ function RecorderModal({
3131
3380
  );
3132
3381
  }) })
3133
3382
  ] }, 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(
3383
+ /* @__PURE__ */ jsx6("p", { style: summaryStyle, children: narrationOn ? narrationCaptureSummary(stage.recorder.withCamera) : captureSummary(micOn, cameraOn, screenOn, includeSystemAudio) }),
3384
+ narrationAvailable && /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3385
+ /* @__PURE__ */ jsx6(
3137
3386
  "input",
3138
3387
  {
3139
3388
  type: "checkbox",
@@ -3146,8 +3395,8 @@ function RecorderModal({
3146
3395
  ),
3147
3396
  "Show narration mode"
3148
3397
  ] }),
3149
- slidesAvailable && /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3150
- /* @__PURE__ */ jsx5(
3398
+ slidesAvailable && /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3399
+ /* @__PURE__ */ jsx6(
3151
3400
  "input",
3152
3401
  {
3153
3402
  type: "checkbox",
@@ -3160,7 +3409,7 @@ function RecorderModal({
3160
3409
  ),
3161
3410
  "Show slides mode"
3162
3411
  ] }),
3163
- /* @__PURE__ */ jsx5(
3412
+ /* @__PURE__ */ jsx6(
3164
3413
  RecorderDeviceQuickPicks,
3165
3414
  {
3166
3415
  devices: mediaDevices,
@@ -3179,7 +3428,20 @@ function RecorderModal({
3179
3428
  disabled: deviceSettingsLocked
3180
3429
  }
3181
3430
  ),
3182
- /* @__PURE__ */ jsx5(
3431
+ !narrationOn && screenOn && /* @__PURE__ */ jsx6(
3432
+ RecorderScreenSettings,
3433
+ {
3434
+ value: deviceSettings.screen,
3435
+ onChange: (screen) => handleDeviceSettingsChange({ ...deviceSettings, screen }),
3436
+ disabled: deviceSettingsLocked,
3437
+ hasPreview: recorder.state === "ready",
3438
+ onChoose: () => {
3439
+ recorder.cancel();
3440
+ void handleRequest();
3441
+ }
3442
+ }
3443
+ ),
3444
+ /* @__PURE__ */ jsx6(
3183
3445
  RecorderDeviceSettingsPanel,
3184
3446
  {
3185
3447
  value: deviceSettings,
@@ -3194,12 +3456,12 @@ function RecorderModal({
3194
3456
  cameraStream: activeCameraStream
3195
3457
  }
3196
3458
  ),
3197
- !narrationOn && recorder.error && /* @__PURE__ */ jsx5("div", { style: errorStyle, children: recorder.error.message }),
3198
- !narrationOn && saveError && /* @__PURE__ */ jsxs4("div", { role: "alert", style: errorStyle, children: [
3459
+ !narrationOn && recorder.error && /* @__PURE__ */ jsx6("div", { style: errorStyle, children: recorder.error.message }),
3460
+ !narrationOn && saveError && /* @__PURE__ */ jsxs5("div", { role: "alert", style: errorStyle, children: [
3199
3461
  saveError,
3200
- /* @__PURE__ */ jsx5("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
3462
+ /* @__PURE__ */ jsx6("p", { style: { marginBottom: 0 }, children: "Your recording is still available below. Download a copy before closing, or try saving again." })
3201
3463
  ] }),
3202
- /* @__PURE__ */ jsxs4("p", { style: summaryStyle, children: [
3464
+ /* @__PURE__ */ jsxs5("p", { style: summaryStyle, children: [
3203
3465
  "Recording size:",
3204
3466
  " ",
3205
3467
  formatRecordingBytes(
@@ -3213,9 +3475,9 @@ function RecorderModal({
3213
3475
  " ",
3214
3476
  "total. Final encoding may add more data."
3215
3477
  ] }),
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(
3478
+ !narrationOn && 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." }),
3479
+ narrationOn && (stage.recorder.error ?? stage.controller.mic.error) && /* @__PURE__ */ jsx6("div", { style: errorStyle, children: (stage.recorder.error ?? stage.controller.mic.error)?.message }),
3480
+ narrationOn && narrationCameraStream && /* @__PURE__ */ jsx6("div", { style: previewBoxStyle, children: /* @__PURE__ */ jsx6(
3219
3481
  "video",
3220
3482
  {
3221
3483
  ref: previewRef,
@@ -3225,10 +3487,10 @@ function RecorderModal({
3225
3487
  style: { width: "100%", height: "100%", objectFit: "contain" }
3226
3488
  }
3227
3489
  ) }),
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(
3490
+ narrationOn && !narrationCameraStream && narrationTakeDone && /* @__PURE__ */ jsx6("div", { style: audioMeterStyle, children: stage.recorder.take ? `\u2713 Recorded ${formatDurationMs(stage.recorder.take.durationSec * 1e3)}` : "\u25CF Processing take\u2026" }),
3491
+ narrationOn && !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." }) }),
3492
+ narrationOn && !narrationCameraStream && !narrationTakeDone && !stage.recorder.withCamera && /* @__PURE__ */ jsxs5("div", { style: { ...audioMeterStyle, position: "relative", overflow: "hidden" }, children: [
3493
+ /* @__PURE__ */ jsx6(
3232
3494
  "div",
3233
3495
  {
3234
3496
  "aria-hidden": "true",
@@ -3238,11 +3500,11 @@ function RecorderModal({
3238
3500
  }
3239
3501
  }
3240
3502
  ),
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." })
3503
+ /* @__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." })
3242
3504
  ] }),
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(
3505
+ !narrationOn && !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." }) }),
3506
+ !narrationOn && showPreview && recorder.state !== "stopped" && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: isDual ? { ...previewBoxStyle, position: "relative" } : previewBoxStyle, children: [
3507
+ /* @__PURE__ */ jsx6(
3246
3508
  "video",
3247
3509
  {
3248
3510
  ref: previewRef,
@@ -3252,7 +3514,7 @@ function RecorderModal({
3252
3514
  style: { width: "100%", height: "100%", objectFit: "contain" }
3253
3515
  }
3254
3516
  ),
3255
- isDual && /* @__PURE__ */ jsx5(
3517
+ isDual && /* @__PURE__ */ jsx6(
3256
3518
  "video",
3257
3519
  {
3258
3520
  ref: cameraPreviewRef,
@@ -3273,12 +3535,12 @@ function RecorderModal({
3273
3535
  }
3274
3536
  )
3275
3537
  ] }),
3276
- !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx5("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
3538
+ !narrationOn && showPreview && recorder.state !== "stopped" && isAudioOnly && /* @__PURE__ */ jsx6("div", { style: audioMeterStyle, children: recorder.state === "recording" ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
3277
3539
  "\u25CF Recording ",
3278
3540
  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(
3541
+ ] }) : /* @__PURE__ */ jsx6(Fragment3, { children: "Microphone ready" }) }),
3542
+ !narrationOn && recorder.state === "stopped" && playbackUrl && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: { ...previewBoxStyle, position: "relative" }, children: [
3543
+ /* @__PURE__ */ jsx6(
3282
3544
  "video",
3283
3545
  {
3284
3546
  src: playbackUrl,
@@ -3290,7 +3552,7 @@ function RecorderModal({
3290
3552
  style: { width: "100%", height: "100%", objectFit: "contain" }
3291
3553
  }
3292
3554
  ),
3293
- /* @__PURE__ */ jsxs4(
3555
+ /* @__PURE__ */ jsxs5(
3294
3556
  "div",
3295
3557
  {
3296
3558
  role: "timer",
@@ -3304,9 +3566,9 @@ function RecorderModal({
3304
3566
  }
3305
3567
  )
3306
3568
  ] }),
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(
3569
+ !narrationOn && recorder.state === "stopped" && isDual && cameraPlaybackUrl && /* @__PURE__ */ jsxs5("div", { style: { marginBottom: 12 }, children: [
3570
+ /* @__PURE__ */ jsx6("div", { style: summaryStyle, children: "Camera (picture-in-picture)" }),
3571
+ /* @__PURE__ */ jsx6(
3310
3572
  "video",
3311
3573
  {
3312
3574
  src: cameraPlaybackUrl,
@@ -3322,14 +3584,14 @@ function RecorderModal({
3322
3584
  }
3323
3585
  )
3324
3586
  ] }),
3325
- !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: { marginBottom: 12 }, children: [
3326
- /* @__PURE__ */ jsxs4("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
3587
+ !narrationOn && recorder.state === "stopped" && playbackUrl && isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: { marginBottom: 12 }, children: [
3588
+ /* @__PURE__ */ jsxs5("div", { style: { ...audioMeterStyle, marginBottom: 8 }, children: [
3327
3589
  "\u2713 Recorded ",
3328
3590
  formatDurationMs(recorder.durationMs)
3329
3591
  ] }),
3330
- /* @__PURE__ */ jsx5("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
3592
+ /* @__PURE__ */ jsx6("audio", { src: playbackUrl, controls: true, style: { width: "100%" } })
3331
3593
  ] }),
3332
- !narrationOn && canSave && playbackUrl && /* @__PURE__ */ jsxs4(
3594
+ !narrationOn && canSave && playbackUrl && /* @__PURE__ */ jsxs5(
3333
3595
  "div",
3334
3596
  {
3335
3597
  style: {
@@ -3339,8 +3601,8 @@ function RecorderModal({
3339
3601
  marginBottom: 12
3340
3602
  },
3341
3603
  children: [
3342
- /* @__PURE__ */ jsx5("a", { href: playbackUrl, download: downloadFilename, style: btnSecondary, children: isDual ? "Download screen recording" : "Download recording" }),
3343
- isDual && cameraPlaybackUrl && /* @__PURE__ */ jsx5(
3604
+ /* @__PURE__ */ jsx6("a", { href: playbackUrl, download: downloadFilename, style: btnSecondary, children: isDual ? "Download screen recording" : "Download recording" }),
3605
+ isDual && cameraPlaybackUrl && /* @__PURE__ */ jsx6(
3344
3606
  "a",
3345
3607
  {
3346
3608
  href: cameraPlaybackUrl,
@@ -3354,7 +3616,7 @@ function RecorderModal({
3354
3616
  children: "Download camera recording"
3355
3617
  }
3356
3618
  ),
3357
- timingDownloadUrl && /* @__PURE__ */ jsx5(
3619
+ timingDownloadUrl && /* @__PURE__ */ jsx6(
3358
3620
  "a",
3359
3621
  {
3360
3622
  href: timingDownloadUrl,
@@ -3366,9 +3628,9 @@ function RecorderModal({
3366
3628
  ]
3367
3629
  }
3368
3630
  ),
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(
3631
+ !narrationOn && !slidesTimingCapture && source === "mic" && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3632
+ /* @__PURE__ */ jsx6("label", { style: labelStyle, htmlFor: "recorder-source-text", children: "Script (used to auto-match this narration to a block)" }),
3633
+ /* @__PURE__ */ jsx6(
3372
3634
  "textarea",
3373
3635
  {
3374
3636
  id: "recorder-source-text",
@@ -3380,8 +3642,8 @@ function RecorderModal({
3380
3642
  }
3381
3643
  )
3382
3644
  ] }),
3383
- /* @__PURE__ */ jsx5("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
3384
- /* @__PURE__ */ jsx5(
3645
+ /* @__PURE__ */ jsx6("label", { style: labelStyle, htmlFor: "recorder-basename", children: "Filename (optional)" }),
3646
+ /* @__PURE__ */ jsx6(
3385
3647
  "input",
3386
3648
  {
3387
3649
  id: "recorder-basename",
@@ -3393,13 +3655,13 @@ function RecorderModal({
3393
3655
  disabled: narrationOn ? !narrationRecorderIdle : recorder.state === "recording"
3394
3656
  }
3395
3657
  ),
3396
- !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs4("div", { style: recordingStatusStyle, children: [
3658
+ !narrationOn && recorder.state === "recording" && !isAudioOnly && /* @__PURE__ */ jsxs5("div", { style: recordingStatusStyle, children: [
3397
3659
  "\u25CF Recording ",
3398
3660
  formatDurationMs(recorder.durationMs)
3399
3661
  ] }),
3400
- showTimingCheckbox && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3401
- /* @__PURE__ */ jsxs4("label", { style: checkboxRowStyle, children: [
3402
- /* @__PURE__ */ jsx5(
3662
+ showTimingCheckbox && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3663
+ /* @__PURE__ */ jsxs5("label", { style: checkboxRowStyle, children: [
3664
+ /* @__PURE__ */ jsx6(
3403
3665
  "input",
3404
3666
  {
3405
3667
  type: "checkbox",
@@ -3411,10 +3673,10 @@ function RecorderModal({
3411
3673
  ),
3412
3674
  SLIDE_TIMING_CHECKBOX_LABEL
3413
3675
  ] }),
3414
- applySlideTimings && slideCoverageWarning && /* @__PURE__ */ jsx5("p", { style: summaryStyle, children: slideCoverageWarning })
3676
+ applySlideTimings && slideCoverageWarning && /* @__PURE__ */ jsx6("p", { style: summaryStyle, children: slideCoverageWarning })
3415
3677
  ] }),
3416
- /* @__PURE__ */ jsxs4("div", { style: buttonRowStyle, children: [
3417
- /* @__PURE__ */ jsx5(
3678
+ /* @__PURE__ */ jsxs5("div", { style: buttonRowStyle, children: [
3679
+ /* @__PURE__ */ jsx6(
3418
3680
  "button",
3419
3681
  {
3420
3682
  type: "button",
@@ -3424,8 +3686,8 @@ function RecorderModal({
3424
3686
  children: "Close"
3425
3687
  }
3426
3688
  ),
3427
- narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3428
- narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx5(
3689
+ narrationOn && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3690
+ narrationRecorderIdle && !narrationMicLive && /* @__PURE__ */ jsx6(
3429
3691
  "button",
3430
3692
  {
3431
3693
  type: "button",
@@ -3435,14 +3697,14 @@ function RecorderModal({
3435
3697
  children: narrationRequesting ? "Requesting\u2026" : "Start preview"
3436
3698
  }
3437
3699
  ),
3438
- narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
3439
- /* @__PURE__ */ jsx5(
3700
+ narrationRecorderIdle && narrationMicLive && /* @__PURE__ */ jsxs5("button", { type: "button", style: btnRecord, onClick: handleNarrationRecord, children: [
3701
+ /* @__PURE__ */ jsx6(
3440
3702
  "span",
3441
3703
  {
3442
3704
  className: "squisq-recorder-record-dot",
3443
3705
  style: recordDotFrameStyle,
3444
3706
  "aria-hidden": "true",
3445
- children: /* @__PURE__ */ jsx5(
3707
+ children: /* @__PURE__ */ jsx6(
3446
3708
  "span",
3447
3709
  {
3448
3710
  className: "squisq-recorder-record-dot-center",
@@ -3453,7 +3715,7 @@ function RecorderModal({
3453
3715
  ),
3454
3716
  "Record"
3455
3717
  ] }),
3456
- (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx5(
3718
+ (stage.recorder.state === "recording" || stage.recorder.state === "starting") && /* @__PURE__ */ jsx6(
3457
3719
  "button",
3458
3720
  {
3459
3721
  type: "button",
@@ -3462,11 +3724,11 @@ function RecorderModal({
3462
3724
  children: "Stop"
3463
3725
  }
3464
3726
  ),
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(
3727
+ stage.recorder.state === "processing" && /* @__PURE__ */ jsx6("span", { style: recordingStatusStyle, children: "Aligning take\u2026" }),
3728
+ stage.recorder.state === "saving" && /* @__PURE__ */ jsx6("span", { style: recordingStatusStyle, children: "Saving\u2026" }),
3729
+ stage.recorder.state === "review" && stage.recorder.take && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3730
+ /* @__PURE__ */ jsx6("button", { type: "button", style: btnSecondary, onClick: stage.handleRetake, children: "Discard & re-record" }),
3731
+ /* @__PURE__ */ jsx6(
3470
3732
  "button",
3471
3733
  {
3472
3734
  type: "button",
@@ -3477,8 +3739,8 @@ function RecorderModal({
3477
3739
  )
3478
3740
  ] })
3479
3741
  ] }),
3480
- !narrationOn && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3481
- (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx5(
3742
+ !narrationOn && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3743
+ (recorder.state === "idle" || recorder.state === "error" || recorder.state === "requesting") && /* @__PURE__ */ jsx6(
3482
3744
  "button",
3483
3745
  {
3484
3746
  type: "button",
@@ -3488,14 +3750,14 @@ function RecorderModal({
3488
3750
  children: recorder.state === "requesting" ? "Requesting\u2026" : "Start preview"
3489
3751
  }
3490
3752
  ),
3491
- canRecord && /* @__PURE__ */ jsxs4("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
3492
- /* @__PURE__ */ jsx5(
3753
+ canRecord && /* @__PURE__ */ jsxs5("button", { type: "button", style: btnRecord, onClick: handleStart, disabled: isBusy, children: [
3754
+ /* @__PURE__ */ jsx6(
3493
3755
  "span",
3494
3756
  {
3495
3757
  className: "squisq-recorder-record-dot",
3496
3758
  style: recordDotFrameStyle,
3497
3759
  "aria-hidden": "true",
3498
- children: /* @__PURE__ */ jsx5(
3760
+ children: /* @__PURE__ */ jsx6(
3499
3761
  "span",
3500
3762
  {
3501
3763
  className: "squisq-recorder-record-dot-center",
@@ -3506,9 +3768,9 @@ function RecorderModal({
3506
3768
  ),
3507
3769
  "Record"
3508
3770
  ] }),
3509
- canStop && /* @__PURE__ */ jsx5("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
3510
- canSave && /* @__PURE__ */ jsxs4(Fragment3, { children: [
3511
- /* @__PURE__ */ jsx5(
3771
+ canStop && /* @__PURE__ */ jsx6("button", { type: "button", style: btnDanger, onClick: handleStop, disabled: isBusy, children: "Stop" }),
3772
+ canSave && /* @__PURE__ */ jsxs5(Fragment3, { children: [
3773
+ /* @__PURE__ */ jsx6(
3512
3774
  "button",
3513
3775
  {
3514
3776
  type: "button",
@@ -3518,7 +3780,7 @@ function RecorderModal({
3518
3780
  children: "Discard & re-record"
3519
3781
  }
3520
3782
  ),
3521
- /* @__PURE__ */ jsx5(
3783
+ /* @__PURE__ */ jsx6(
3522
3784
  "button",
3523
3785
  {
3524
3786
  type: "button",
@@ -3532,7 +3794,7 @@ function RecorderModal({
3532
3794
  ] })
3533
3795
  ] })
3534
3796
  ] }),
3535
- narrationOn && narration && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
3797
+ narrationOn && narration && /* @__PURE__ */ jsx6("div", { style: rightColStyle, children: /* @__PURE__ */ jsx6(
3536
3798
  NarrationStage,
3537
3799
  {
3538
3800
  stage,
@@ -3544,7 +3806,7 @@ function RecorderModal({
3544
3806
  showReviewActions: false
3545
3807
  }
3546
3808
  ) }),
3547
- slidesOn && /* @__PURE__ */ jsx5("div", { style: rightColStyle, children: /* @__PURE__ */ jsx5(
3809
+ slidesOn && /* @__PURE__ */ jsx6("div", { style: rightColStyle, children: /* @__PURE__ */ jsx6(
3548
3810
  RecorderSlidesPanel,
3549
3811
  {
3550
3812
  slides: slideDeck,
@@ -3568,7 +3830,7 @@ function RecorderModal({
3568
3830
  // src/recorder/RecorderPanel.tsx
3569
3831
  import { useCallback as useCallback5, useState as useState4 } from "react";
3570
3832
  import { createPortal } from "react-dom";
3571
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3833
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3572
3834
  function RecorderPanel({
3573
3835
  mediaProvider,
3574
3836
  container = null,
@@ -3594,8 +3856,8 @@ function RecorderPanel({
3594
3856
  [controlledOpen, onOpenChange]
3595
3857
  );
3596
3858
  const handleClose = useCallback5(() => setOpen(false), [setOpen]);
3597
- return /* @__PURE__ */ jsxs5(Fragment4, { children: [
3598
- showTrigger && /* @__PURE__ */ jsx6(
3859
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
3860
+ showTrigger && /* @__PURE__ */ jsx7(
3599
3861
  "button",
3600
3862
  {
3601
3863
  type: "button",
@@ -3604,11 +3866,11 @@ function RecorderPanel({
3604
3866
  "aria-label": tooltip,
3605
3867
  "aria-expanded": open,
3606
3868
  onClick: () => setOpen(!open),
3607
- children: /* @__PURE__ */ jsx6(Icon, { icon: "fa-solid fa-microphone" })
3869
+ children: /* @__PURE__ */ jsx7(Icon, { icon: "fa-solid fa-microphone" })
3608
3870
  }
3609
3871
  ),
3610
3872
  open && typeof document !== "undefined" && createPortal(
3611
- /* @__PURE__ */ jsx6(
3873
+ /* @__PURE__ */ jsx7(
3612
3874
  RecorderModal,
3613
3875
  {
3614
3876
  mediaProvider,