@bendyline/squisq-video-react 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,23 @@
1
1
  import {
2
2
  createMp4Muxer
3
- } from "./chunk-6DKLHXX3.js";
3
+ } from "./chunk-2PGWBGAT.js";
4
4
 
5
5
  // src/VideoExportModal.tsx
6
- import { useState as useState2, useCallback as useCallback3 } from "react";
6
+ import { useState as useState2, useCallback as useCallback3, useId, useRef as useRef3 } from "react";
7
+ import { useModalDialog } from "@bendyline/squisq-react";
7
8
 
8
9
  // src/hooks/useVideoExport.ts
9
10
  import { useState, useRef as useRef2, useCallback as useCallback2, useEffect } from "react";
10
- import { resolveDimensions, computeAudioTimeline, QUALITY_PRESETS } from "@bendyline/squisq-video";
11
+ import {
12
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY,
13
+ fetchResourceBytes
14
+ } from "@bendyline/squisq/markdown";
15
+ import {
16
+ resolveDimensions,
17
+ computeAudioTimeline,
18
+ resolveFfmpegWasmLoad as resolveFfmpegWasmLoad3,
19
+ QUALITY_PRESETS
20
+ } from "@bendyline/squisq-video";
11
21
 
12
22
  // src/mainThreadEncoder.ts
13
23
  import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
@@ -43,14 +53,20 @@ function createEncoder(config) {
43
53
  ...config.audio ? { audio: config.audio } : {}
44
54
  });
45
55
  let closed = false;
56
+ let fatalError = null;
46
57
  const frameDuration = 1e6 / config.fps;
58
+ function fail(err) {
59
+ closed = true;
60
+ if (encoder.state !== "closed") encoder.close();
61
+ return fatalError ?? err;
62
+ }
47
63
  const encoder = new VideoEncoder({
48
64
  output(chunk, meta) {
49
65
  if (closed) return;
50
66
  muxer.addVideoChunk(chunk, meta ?? void 0);
51
67
  },
52
68
  error(err) {
53
- console.error("WebCodecs encoder error:", err.message);
69
+ fatalError ?? (fatalError = new Error(`WebCodecs encoder error: ${err.message}`));
54
70
  }
55
71
  });
56
72
  encoder.configure({
@@ -66,6 +82,10 @@ function createEncoder(config) {
66
82
  });
67
83
  return {
68
84
  async encodeFrame(bitmap, frameIndex) {
85
+ if (fatalError) {
86
+ bitmap.close();
87
+ throw fail(fatalError);
88
+ }
69
89
  if (closed) {
70
90
  bitmap.close();
71
91
  throw new Error("Encoder already closed");
@@ -83,7 +103,13 @@ function createEncoder(config) {
83
103
  },
84
104
  async finalize() {
85
105
  if (closed) throw new Error("Encoder already closed");
86
- await encoder.flush();
106
+ if (fatalError) throw fail(fatalError);
107
+ try {
108
+ await encoder.flush();
109
+ } catch (err) {
110
+ throw fail(err instanceof Error ? err : new Error(String(err)));
111
+ }
112
+ if (fatalError) throw fail(fatalError);
87
113
  encoder.close();
88
114
  closed = true;
89
115
  return muxer.finalize();
@@ -177,6 +203,7 @@ function createWorkerEncoder(config) {
177
203
  height: config.height,
178
204
  fps: config.fps,
179
205
  quality: config.quality,
206
+ ...config.totalFrames !== void 0 ? { totalFrames: config.totalFrames } : {},
180
207
  ...config.ffmpegWasm ? { ffmpegWasm: config.ffmpegWasm } : {}
181
208
  });
182
209
  return {
@@ -236,7 +263,8 @@ function createWorkerEncoder(config) {
236
263
 
237
264
  // src/audioTrack.ts
238
265
  import {
239
- ffmpegAudioMuxArgs
266
+ ffmpegAudioMuxArgs,
267
+ resolveFfmpegWasmLoad
240
268
  } from "@bendyline/squisq-video";
241
269
  var EXPORT_AUDIO_SAMPLE_RATE = 48e3;
242
270
  var EXPORT_AUDIO_CHANNELS = 2;
@@ -382,13 +410,13 @@ function audioBufferToWav(buffer) {
382
410
  return new Uint8Array(out);
383
411
  }
384
412
  async function muxAudioWithFfmpegWasm(videoMp4, wav, audioBitrate, loadConfig) {
413
+ const load = resolveFfmpegWasmLoad(loadConfig, "ffmpeg.wasm audio muxing", {
414
+ classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
415
+ });
385
416
  const { FFmpeg } = await import("@ffmpeg/ffmpeg");
386
417
  const ffmpeg = new FFmpeg();
387
418
  try {
388
- await ffmpeg.load({
389
- ...loadConfig,
390
- classWorkerURL: loadConfig?.classWorkerURL ?? new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
391
- });
419
+ await ffmpeg.load(load);
392
420
  await ffmpeg.writeFile("video.mp4", videoMp4);
393
421
  await ffmpeg.writeFile("audio.wav", wav);
394
422
  const exitCode = await ffmpeg.exec([
@@ -413,7 +441,8 @@ async function muxAudioWithFfmpegWasm(videoMp4, wav, audioBitrate, loadConfig) {
413
441
 
414
442
  // src/gifTranscode.ts
415
443
  import {
416
- ffmpegGifOutputArgs
444
+ ffmpegGifOutputArgs,
445
+ resolveFfmpegWasmLoad as resolveFfmpegWasmLoad2
417
446
  } from "@bendyline/squisq-video";
418
447
  function buildGifFfmpegArgs(options) {
419
448
  return ["-y", "-i", "video.mp4", ...ffmpegGifOutputArgs(options), "out.gif"];
@@ -425,6 +454,9 @@ async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, si
425
454
  if (signal?.aborted) {
426
455
  throw new DOMException("Animated GIF export was cancelled.", "AbortError");
427
456
  }
457
+ const load = resolveFfmpegWasmLoad2(loadConfig, "Animated GIF export", {
458
+ classWorkerURL: new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
459
+ });
428
460
  const args = buildGifFfmpegArgs(options);
429
461
  const { FFmpeg } = await import("@ffmpeg/ffmpeg");
430
462
  const ffmpeg = new FFmpeg();
@@ -437,10 +469,7 @@ async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, si
437
469
  const handleAbort = () => terminate();
438
470
  signal?.addEventListener("abort", handleAbort, { once: true });
439
471
  try {
440
- await ffmpeg.load({
441
- ...loadConfig,
442
- classWorkerURL: loadConfig?.classWorkerURL ?? new URL("./workers/ffmpeg.class-worker.js", import.meta.url).href
443
- });
472
+ await ffmpeg.load(load);
444
473
  if (signal?.aborted) {
445
474
  throw new DOMException("Animated GIF export was cancelled.", "AbortError");
446
475
  }
@@ -479,33 +508,24 @@ var MIME_MAP = {
479
508
  bmp: "image/bmp",
480
509
  avif: "image/avif"
481
510
  };
482
- function arrayBufferToDataUrl(buffer, mime) {
483
- const bytes = new Uint8Array(buffer);
484
- const chunks = [];
485
- const CHUNK = 8192;
486
- for (let i = 0; i < bytes.length; i += CHUNK) {
487
- chunks.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
488
- }
489
- return `data:${mime};base64,${btoa(chunks.join(""))}`;
490
- }
491
511
  function createInlineProvider(images) {
492
- const dataUrls = /* @__PURE__ */ new Map();
512
+ const blobUrls = /* @__PURE__ */ new Map();
493
513
  const mimeTypes = /* @__PURE__ */ new Map();
494
514
  for (const [path, buffer] of images) {
495
515
  const ext = path.split(".").pop()?.toLowerCase() ?? "";
496
516
  const mime = MIME_MAP[ext] ?? "application/octet-stream";
497
- dataUrls.set(path, arrayBufferToDataUrl(buffer, mime));
517
+ blobUrls.set(path, URL.createObjectURL(new Blob([buffer], { type: mime })));
498
518
  mimeTypes.set(path, mime);
499
519
  }
500
520
  return {
501
521
  async resolveUrl(relativePath) {
502
- return dataUrls.get(relativePath) ?? relativePath;
522
+ return blobUrls.get(relativePath) ?? relativePath;
503
523
  },
504
524
  async listMedia() {
505
- return [...dataUrls.keys()].map((name) => ({
525
+ return [...blobUrls.keys()].map((name) => ({
506
526
  name,
507
527
  mimeType: mimeTypes.get(name) ?? "application/octet-stream",
508
- size: 0
528
+ size: images.get(name)?.byteLength ?? 0
509
529
  }));
510
530
  },
511
531
  async addMedia() {
@@ -515,6 +535,8 @@ function createInlineProvider(images) {
515
535
  throw new Error("Read-only");
516
536
  },
517
537
  dispose() {
538
+ blobUrls.forEach((url) => URL.revokeObjectURL(url));
539
+ blobUrls.clear();
518
540
  }
519
541
  };
520
542
  }
@@ -522,19 +544,23 @@ function useFrameCapture() {
522
544
  const containerRef = useRef(null);
523
545
  const rootRef = useRef(null);
524
546
  const renderAPIRef = useRef(null);
547
+ const mediaProviderRef = useRef(null);
525
548
  const dimensionsRef = useRef({ width: 1920, height: 1080 });
526
549
  const init = useCallback(
527
550
  async (doc, renderOptions, captionMode) => {
528
- if (rootRef.current || containerRef.current) {
551
+ if (rootRef.current || containerRef.current || mediaProviderRef.current) {
529
552
  const oldRoot = rootRef.current;
530
553
  const oldContainer = containerRef.current;
554
+ const oldMediaProvider = mediaProviderRef.current;
531
555
  rootRef.current = null;
532
556
  containerRef.current = null;
533
557
  renderAPIRef.current = null;
558
+ mediaProviderRef.current = null;
534
559
  await new Promise((resolve) => {
535
560
  setTimeout(() => {
536
561
  if (oldRoot) oldRoot.unmount();
537
562
  if (oldContainer) oldContainer.remove();
563
+ oldMediaProvider?.dispose();
538
564
  resolve();
539
565
  }, 0);
540
566
  });
@@ -552,6 +578,7 @@ function useFrameCapture() {
552
578
  renderRoot.style.cssText = `width:${width}px;height:${height}px;`;
553
579
  container.appendChild(renderRoot);
554
580
  const mediaProvider = renderOptions.images ? createInlineProvider(renderOptions.images) : null;
581
+ mediaProviderRef.current = mediaProvider;
555
582
  const root = createRoot(renderRoot);
556
583
  rootRef.current = root;
557
584
  const captionsEnabled = captionMode !== void 0 && captionMode !== "off";
@@ -639,12 +666,40 @@ function useFrameCapture() {
639
666
  containerRef.current.remove();
640
667
  containerRef.current = null;
641
668
  }
669
+ mediaProviderRef.current?.dispose();
670
+ mediaProviderRef.current = null;
642
671
  renderAPIRef.current = null;
643
672
  }, []);
644
673
  return useMemo(() => ({ init, captureFrame, destroy }), [init, captureFrame, destroy]);
645
674
  }
646
675
 
647
676
  // src/hooks/useVideoExport.ts
677
+ var MAX_EXPORT_MEDIA_FILES = 256;
678
+ var MAX_EXPORT_MEDIA_FILE_BYTES = 64 * 1024 * 1024;
679
+ var MAX_EXPORT_MEDIA_TOTAL_BYTES = 256 * 1024 * 1024;
680
+ function toArrayBuffer(bytes) {
681
+ return bytes.slice().buffer;
682
+ }
683
+ function collectDocumentMediaReferences(doc) {
684
+ const references = /* @__PURE__ */ new Set();
685
+ const seen = /* @__PURE__ */ new WeakSet();
686
+ const visit = (value) => {
687
+ if (typeof value === "string") {
688
+ references.add(value);
689
+ if (value.startsWith("./")) references.add(value.slice(2));
690
+ return;
691
+ }
692
+ if (!value || typeof value !== "object" || seen.has(value)) return;
693
+ seen.add(value);
694
+ if (Array.isArray(value)) {
695
+ value.forEach(visit);
696
+ return;
697
+ }
698
+ Object.values(value).forEach(visit);
699
+ };
700
+ visit(doc);
701
+ return references;
702
+ }
648
703
  async function resolveAudioBuffers(clips, sources) {
649
704
  const srcs = new Set(clips.map((c) => c.src));
650
705
  const out = /* @__PURE__ */ new Map();
@@ -653,8 +708,10 @@ async function resolveAudioBuffers(clips, sources) {
653
708
  if (!data && sources.mediaProvider) {
654
709
  try {
655
710
  const url = await sources.mediaProvider.resolveUrl(src);
656
- const res = await fetch(url);
657
- if (res.ok) data = await res.arrayBuffer();
711
+ const resource = await fetchResourceBytes(url, {
712
+ policy: sources.resourcePolicy
713
+ });
714
+ data = toArrayBuffer(resource.bytes);
658
715
  } catch {
659
716
  }
660
717
  }
@@ -755,6 +812,7 @@ function useVideoExport() {
755
812
  const fps = config.fps ?? (effectiveOutputFormat === "gif" ? 10 : 30);
756
813
  const orientation = config.orientation ?? "landscape";
757
814
  const animationsEnabled = config.animationsEnabled ?? effectiveOutputFormat === "mp4";
815
+ const audioPolicy = config.audioPolicy ?? "require";
758
816
  setOutputFormat(effectiveOutputFormat);
759
817
  try {
760
818
  const gifDefaults = orientation === "portrait" ? { width: 540, height: 960 } : { width: 960, height: 540 };
@@ -772,6 +830,9 @@ function useVideoExport() {
772
830
  "Animated GIF export requires ffmpeg.wasm and SharedArrayBuffer (Cross-Origin-Isolation headers)."
773
831
  );
774
832
  }
833
+ if (effectiveOutputFormat === "gif") {
834
+ resolveFfmpegWasmLoad3(config.ffmpegWasm, "Animated GIF export");
835
+ }
775
836
  if (!webCodecsAvailable && !sharedArrayBufferAvailable) {
776
837
  throw new Error(
777
838
  "No video encoder available. WebCodecs requires Chrome 94+ / Edge 94+, and the ffmpeg.wasm fallback requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
@@ -791,13 +852,38 @@ function useVideoExport() {
791
852
  if (!images && config.mediaProvider) {
792
853
  images = /* @__PURE__ */ new Map();
793
854
  const entries = await config.mediaProvider.listMedia();
794
- for (const entry of entries) {
855
+ const references = collectDocumentMediaReferences(doc);
856
+ const neededEntries = entries.filter(
857
+ (entry) => references.has(entry.name) || references.has(`./${entry.name}`)
858
+ );
859
+ if (neededEntries.length > MAX_EXPORT_MEDIA_FILES) {
860
+ throw new Error(
861
+ `Document references ${neededEntries.length} media files; browser export supports at most ${MAX_EXPORT_MEDIA_FILES}.`
862
+ );
863
+ }
864
+ let totalMediaBytes = 0;
865
+ for (const entry of neededEntries) {
866
+ if (cancelledRef.current) return;
867
+ if (entry.size > MAX_EXPORT_MEDIA_FILE_BYTES) {
868
+ throw new Error(`Media file "${entry.name}" is too large for browser video export.`);
869
+ }
795
870
  const url2 = await config.mediaProvider.resolveUrl(entry.name);
796
- const res = await fetch(url2);
797
- if (res.ok) {
798
- const data = await res.arrayBuffer();
799
- images.set(entry.name, data);
871
+ const resource = await fetchResourceBytes(url2, {
872
+ policy: {
873
+ ...DEFAULT_INTERACTIVE_RESOURCE_POLICY,
874
+ ...config.resourcePolicy,
875
+ maxBytes: Math.min(
876
+ config.resourcePolicy?.maxBytes ?? MAX_EXPORT_MEDIA_FILE_BYTES,
877
+ MAX_EXPORT_MEDIA_FILE_BYTES
878
+ )
879
+ }
880
+ });
881
+ const data = toArrayBuffer(resource.bytes);
882
+ totalMediaBytes += data.byteLength;
883
+ if (totalMediaBytes > MAX_EXPORT_MEDIA_TOTAL_BYTES) {
884
+ throw new Error("Referenced media exceeds the browser video export memory limit.");
800
885
  }
886
+ images.set(entry.name, data);
801
887
  }
802
888
  }
803
889
  const docDuration = await frameCapture.init(
@@ -810,11 +896,12 @@ function useVideoExport() {
810
896
  if (docDuration <= 0) {
811
897
  throw new Error("Document has zero duration \u2014 nothing to export");
812
898
  }
899
+ const totalFrames = Math.ceil(docDuration * fps);
813
900
  setPhase("Starting encoder\u2026");
814
901
  setProgress(5);
815
902
  const canUseWebCodecs = webCodecsAvailable && await supportsWebCodecsH264({ width, height, fps, quality });
816
903
  const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
817
- const timeline = effectiveOutputFormat === "mp4" ? computeAudioTimeline(doc, 0) : [];
904
+ const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, 0) : [];
818
905
  const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
819
906
  const tierDecision = selectAudioTier({
820
907
  hasClips: timeline.length > 0,
@@ -825,16 +912,27 @@ function useVideoExport() {
825
912
  let renderedAudio = null;
826
913
  let audioIncludedLocal = false;
827
914
  let audioReasonLocal = tierDecision.reason;
915
+ if (timeline.length > 0 && tierDecision.tier === 3 && audioPolicy === "require") {
916
+ throw new Error(tierDecision.reason ?? "This browser cannot include the document audio.");
917
+ }
828
918
  if (tierDecision.tier === 1 || tierDecision.tier === 2) {
829
919
  setPhase("Preparing audio\u2026");
830
920
  try {
831
921
  const buffers = await resolveAudioBuffers(timeline, {
832
922
  audio: config.audio,
833
923
  images,
834
- mediaProvider: config.mediaProvider
924
+ mediaProvider: config.mediaProvider,
925
+ resourcePolicy: config.resourcePolicy
835
926
  });
927
+ const missingSources = [...new Set(timeline.map((clip) => clip.src))].filter(
928
+ (src) => !buffers.has(src)
929
+ );
930
+ if (missingSources.length > 0) {
931
+ audioReasonLocal = `Audio files could not be loaded: ${missingSources.join(", ")}`;
932
+ if (audioPolicy === "require") throw new Error(audioReasonLocal);
933
+ }
836
934
  if (buffers.size === 0) {
837
- audioReasonLocal = "Audio files for this document could not be loaded.";
935
+ audioReasonLocal ?? (audioReasonLocal = "Audio files for this document could not be loaded.");
838
936
  } else {
839
937
  const totalAudioDur = timeline.reduce(
840
938
  (max, c) => Math.max(max, c.startSec + c.durationSec),
@@ -850,6 +948,7 @@ function useVideoExport() {
850
948
  } catch (audioErr) {
851
949
  renderedAudio = null;
852
950
  audioReasonLocal = `Audio could not be prepared: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
951
+ if (audioPolicy === "require") throw new Error(audioReasonLocal);
853
952
  }
854
953
  }
855
954
  const useInlineAudio = renderedAudio !== null && tierDecision.tier === 1;
@@ -876,6 +975,7 @@ function useVideoExport() {
876
975
  height,
877
976
  fps,
878
977
  quality,
978
+ totalFrames,
879
979
  ffmpegWasm: config.ffmpegWasm
880
980
  });
881
981
  encoder = workerEncoder;
@@ -892,7 +992,6 @@ function useVideoExport() {
892
992
  encoderRef.current = encoder;
893
993
  if (cancelledRef.current) return;
894
994
  setState("capturing");
895
- const totalFrames = Math.ceil(docDuration * fps);
896
995
  const captureStartTime = performance.now();
897
996
  const UI_UPDATE_INTERVAL = 10;
898
997
  for (let i = 0; i < totalFrames; i++) {
@@ -930,6 +1029,7 @@ function useVideoExport() {
930
1029
  } catch (audioErr) {
931
1030
  audioIncludedLocal = false;
932
1031
  audioReasonLocal = `Audio encoding failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
1032
+ if (audioPolicy === "require") throw new Error(audioReasonLocal);
933
1033
  }
934
1034
  }
935
1035
  setState("encoding");
@@ -968,6 +1068,7 @@ function useVideoExport() {
968
1068
  } catch (audioErr) {
969
1069
  audioIncludedLocal = false;
970
1070
  audioReasonLocal = `Audio muxing failed: ${audioErr instanceof Error ? audioErr.message : String(audioErr)}`;
1071
+ if (audioPolicy === "require") throw new Error(audioReasonLocal);
971
1072
  }
972
1073
  }
973
1074
  if (cancelledRef.current) return;
@@ -1053,6 +1154,7 @@ var VIDEO_EXPORT_PALETTES = {
1053
1154
  secondary: "#E8DFC6",
1054
1155
  primary: "#8B6914",
1055
1156
  primaryBorder: "#7a5c10",
1157
+ primaryText: "#ffffff",
1056
1158
  success: "#2d6a10",
1057
1159
  danger: "#a03020"
1058
1160
  },
@@ -1068,6 +1170,7 @@ var VIDEO_EXPORT_PALETTES = {
1068
1170
  secondary: "#1e293b",
1069
1171
  primary: "#9a7416",
1070
1172
  primaryBorder: "#d1a73b",
1173
+ primaryText: "#ffffff",
1071
1174
  success: "#86efac",
1072
1175
  danger: "#fca5a5"
1073
1176
  }
@@ -1113,7 +1216,6 @@ var btnPrimary = {
1113
1216
  fontFamily: "inherit",
1114
1217
  fontWeight: 500,
1115
1218
  cursor: "pointer",
1116
- color: "#fff",
1117
1219
  borderRadius: 0
1118
1220
  };
1119
1221
  var btnSecondary = {
@@ -1145,8 +1247,12 @@ function VideoExportModal({
1145
1247
  audio,
1146
1248
  defaultConfig,
1147
1249
  colorScheme = "light",
1250
+ uiPalette,
1148
1251
  onClose
1149
1252
  }) {
1253
+ const overlayRef = useRef3(null);
1254
+ const dialogRef = useRef3(null);
1255
+ const titleId = useId();
1150
1256
  const initialOutputFormat = defaultConfig?.outputFormat ?? "mp4";
1151
1257
  const [outputFormat, setOutputFormat] = useState2(initialOutputFormat);
1152
1258
  const [quality, setQuality] = useState2(defaultConfig?.quality ?? "normal");
@@ -1158,7 +1264,13 @@ function VideoExportModal({
1158
1264
  const [animationsEnabled, setAnimationsEnabled] = useState2(
1159
1265
  defaultConfig?.animationsEnabled ?? initialOutputFormat === "mp4"
1160
1266
  );
1161
- const palette = VIDEO_EXPORT_PALETTES[colorScheme];
1267
+ const [audioPolicy, setAudioPolicy] = useState2(
1268
+ defaultConfig?.audioPolicy ?? "require"
1269
+ );
1270
+ const palette = {
1271
+ ...VIDEO_EXPORT_PALETTES[colorScheme],
1272
+ ...uiPalette
1273
+ };
1162
1274
  const themedModalStyle = {
1163
1275
  ...modalStyle,
1164
1276
  background: palette.surface,
@@ -1178,7 +1290,8 @@ function VideoExportModal({
1178
1290
  const themedPrimaryButtonStyle = {
1179
1291
  ...btnPrimary,
1180
1292
  background: palette.primary,
1181
- border: `1px solid ${palette.primaryBorder}`
1293
+ border: `1px solid ${palette.primaryBorder}`,
1294
+ color: palette.primaryText
1182
1295
  };
1183
1296
  const themedSecondaryButtonStyle = {
1184
1297
  ...btnSecondary,
@@ -1223,6 +1336,7 @@ function VideoExportModal({
1223
1336
  fps,
1224
1337
  orientation,
1225
1338
  captionMode,
1339
+ audioPolicy,
1226
1340
  images,
1227
1341
  audio,
1228
1342
  mediaProvider,
@@ -1238,6 +1352,7 @@ function VideoExportModal({
1238
1352
  fps,
1239
1353
  orientation,
1240
1354
  captionMode,
1355
+ audioPolicy,
1241
1356
  images,
1242
1357
  audio,
1243
1358
  mediaProvider,
@@ -1263,202 +1378,247 @@ function VideoExportModal({
1263
1378
  onClose();
1264
1379
  }, [state, cancelExport, resetExport, onClose]);
1265
1380
  const isExporting = state === "preparing" || state === "capturing" || state === "encoding";
1381
+ useModalDialog({ rootRef: overlayRef, dialogRef, onClose: handleClose });
1266
1382
  return /* @__PURE__ */ jsx(
1267
1383
  "div",
1268
1384
  {
1385
+ ref: overlayRef,
1269
1386
  style: { ...overlayStyle, background: palette.overlay },
1270
1387
  "data-color-scheme": colorScheme,
1271
1388
  onClick: handleClose,
1272
- children: /* @__PURE__ */ jsxs("div", { style: themedModalStyle, onClick: (e) => e.stopPropagation(), children: [
1273
- /* @__PURE__ */ jsx("h2", { style: themedTitleStyle, children: outputFormat === "gif" ? "Export Animated GIF" : "Export Video" }),
1274
- state === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
1275
- /* @__PURE__ */ jsxs("div", { children: [
1276
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Format" }),
1277
- /* @__PURE__ */ jsxs(
1278
- "select",
1279
- {
1280
- "aria-label": "Format",
1281
- style: themedSelectStyle,
1282
- value: outputFormat,
1283
- onChange: (e) => handleOutputFormatChange(e.target.value),
1284
- children: [
1285
- /* @__PURE__ */ jsx("option", { value: "mp4", children: "MP4 video" }),
1286
- /* @__PURE__ */ jsx("option", { value: "gif", children: "Animated GIF" })
1287
- ]
1288
- }
1289
- )
1290
- ] }),
1291
- /* @__PURE__ */ jsxs("div", { children: [
1292
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Quality" }),
1293
- /* @__PURE__ */ jsxs(
1294
- "select",
1295
- {
1296
- "aria-label": "Quality",
1297
- style: themedSelectStyle,
1298
- value: quality,
1299
- onChange: (e) => setQuality(e.target.value),
1300
- children: [
1301
- /* @__PURE__ */ jsx("option", { value: "draft", children: "Draft \u2014 fast, lower quality" }),
1302
- /* @__PURE__ */ jsx("option", { value: "normal", children: "Normal \u2014 balanced" }),
1303
- /* @__PURE__ */ jsx("option", { value: "high", children: "High \u2014 best quality, slower" })
1304
- ]
1305
- }
1306
- )
1307
- ] }),
1308
- /* @__PURE__ */ jsxs("div", { children: [
1309
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Frame Rate" }),
1310
- /* @__PURE__ */ jsxs(
1311
- "select",
1312
- {
1313
- "aria-label": "Frame Rate",
1314
- style: themedSelectStyle,
1315
- value: fps,
1316
- onChange: (e) => setFps(Number(e.target.value)),
1317
- children: [
1318
- /* @__PURE__ */ jsx("option", { value: 10, children: "10 fps \u2014 recommended for GIF" }),
1319
- /* @__PURE__ */ jsx("option", { value: 15, children: "15 fps \u2014 fast export" }),
1320
- /* @__PURE__ */ jsx("option", { value: 24, children: "24 fps \u2014 cinematic" }),
1321
- /* @__PURE__ */ jsx("option", { value: 30, children: "30 fps \u2014 smooth" })
1322
- ]
1323
- }
1324
- )
1325
- ] }),
1326
- /* @__PURE__ */ jsxs("div", { children: [
1327
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Orientation" }),
1328
- /* @__PURE__ */ jsxs(
1329
- "select",
1330
- {
1331
- "aria-label": "Orientation",
1332
- style: themedSelectStyle,
1333
- value: orientation,
1334
- onChange: (e) => setOrientation(e.target.value),
1335
- children: [
1336
- /* @__PURE__ */ jsxs("option", { value: "landscape", children: [
1337
- "Landscape (",
1338
- outputFormat === "gif" ? "960 \xD7 540" : "1920 \xD7 1080",
1339
- ")"
1340
- ] }),
1341
- /* @__PURE__ */ jsxs("option", { value: "portrait", children: [
1342
- "Portrait (",
1343
- outputFormat === "gif" ? "540 \xD7 960" : "1080 \xD7 1920",
1344
- ")"
1345
- ] })
1346
- ]
1347
- }
1348
- )
1349
- ] }),
1350
- /* @__PURE__ */ jsxs("div", { children: [
1351
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Captions" }),
1352
- /* @__PURE__ */ jsxs(
1353
- "select",
1354
- {
1355
- "aria-label": "Captions",
1356
- style: themedSelectStyle,
1357
- value: captionMode,
1358
- onChange: (e) => setCaptionMode(e.target.value),
1359
- children: [
1360
- /* @__PURE__ */ jsx("option", { value: "off", children: "None" }),
1361
- /* @__PURE__ */ jsx("option", { value: "standard", children: "Standard (top bar)" }),
1362
- /* @__PURE__ */ jsx("option", { value: "social", children: "Social media (large words)" })
1363
- ]
1364
- }
1365
- )
1366
- ] }),
1367
- /* @__PURE__ */ jsxs("div", { children: [
1368
- /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Animations & transitions" }),
1369
- /* @__PURE__ */ jsxs(
1370
- "select",
1371
- {
1372
- "aria-label": "Animations and transitions",
1373
- style: themedSelectStyle,
1374
- value: animationsEnabled ? "enabled" : "disabled",
1375
- onChange: (e) => setAnimationsEnabled(e.target.value === "enabled"),
1376
- children: [
1377
- /* @__PURE__ */ jsx("option", { value: "enabled", children: "Enabled" }),
1378
- /* @__PURE__ */ jsx("option", { value: "disabled", children: "Disabled \u2014 smaller files" })
1379
- ]
1380
- }
1381
- ),
1382
- outputFormat === "gif" && animationsEnabled && /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "-6px 0 12px" }, children: "Disabling motion is recommended for much smaller GIFs." })
1383
- ] }),
1384
- /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1385
- /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Cancel" }),
1386
- /* @__PURE__ */ jsx("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: outputFormat === "gif" ? "Export GIF" : "Export Video" })
1387
- ] })
1388
- ] }),
1389
- isExporting && /* @__PURE__ */ jsxs(Fragment, { children: [
1390
- backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 8px 0" }, children: [
1391
- "Encoder: ",
1392
- encoderLabel(completedOutputFormat, backend)
1393
- ] }),
1394
- /* @__PURE__ */ jsx("div", { style: { ...progressBarOuterStyle, background: palette.secondary }, children: /* @__PURE__ */ jsx(
1395
- "div",
1396
- {
1397
- style: {
1398
- width: `${progress}%`,
1399
- height: "100%",
1400
- background: palette.primary,
1401
- transition: "width 0.3s ease"
1402
- }
1403
- }
1404
- ) }),
1405
- /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, margin: "0 0 4px 0" }, children: [
1406
- progress,
1407
- "% complete"
1408
- ] }),
1409
- /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: 0 }, children: [
1410
- formatDuration(elapsed),
1411
- " elapsed",
1412
- estimatedRemaining > 0 && ` \xB7 ~${formatDuration(estimatedRemaining)} remaining`
1413
- ] }),
1414
- /* @__PURE__ */ jsx("div", { style: footerStyle, children: /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: cancelExport, children: "Cancel" }) })
1415
- ] }),
1416
- state === "complete" && /* @__PURE__ */ jsxs(Fragment, { children: [
1417
- /* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.success }, children: "Export complete!" }),
1418
- /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, color: palette.label, margin: "0 0 4px 0" }, children: [
1419
- "File size: ",
1420
- (fileSize / (1024 * 1024)).toFixed(1),
1421
- " MB"
1422
- ] }),
1423
- completedOutputFormat === "gif" ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: "Animated GIF does not include audio." }) : audioIncluded ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.success, margin: "0 0 4px 0" }, children: "Audio included \u2713" }) : /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: [
1424
- "Video only",
1425
- audioSkippedReason ? ` \u2014 ${audioSkippedReason}` : ""
1426
- ] }),
1427
- backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 12px 0" }, children: [
1428
- "Encoded with ",
1429
- encoderLabel(completedOutputFormat, backend)
1430
- ] }),
1431
- /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1432
- /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
1433
- /* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleDownload, children: [
1434
- "Download ",
1435
- completedOutputFormat.toUpperCase()
1436
- ] })
1437
- ] })
1438
- ] }),
1439
- state === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
1440
- /* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.danger }, children: "Export failed" }),
1441
- /* @__PURE__ */ jsx(
1442
- "p",
1443
- {
1444
- style: {
1445
- fontSize: 13,
1446
- color: palette.label,
1447
- margin: "0 0 12px 0",
1448
- wordBreak: "break-word"
1449
- },
1450
- children: error
1451
- }
1452
- ),
1453
- /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1454
- /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
1455
- /* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: [
1456
- "Retry ",
1457
- outputFormat === "gif" ? "GIF" : "video"
1389
+ children: /* @__PURE__ */ jsxs(
1390
+ "div",
1391
+ {
1392
+ ref: dialogRef,
1393
+ role: "dialog",
1394
+ "aria-modal": "true",
1395
+ "aria-labelledby": titleId,
1396
+ tabIndex: -1,
1397
+ style: themedModalStyle,
1398
+ "data-squisq-video-export-modal": true,
1399
+ onClick: (e) => e.stopPropagation(),
1400
+ children: [
1401
+ /* @__PURE__ */ jsx("h2", { id: titleId, style: themedTitleStyle, children: outputFormat === "gif" ? "Export Animated GIF" : "Export Video" }),
1402
+ state === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
1403
+ /* @__PURE__ */ jsxs("div", { children: [
1404
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Format" }),
1405
+ /* @__PURE__ */ jsxs(
1406
+ "select",
1407
+ {
1408
+ "aria-label": "Format",
1409
+ style: themedSelectStyle,
1410
+ value: outputFormat,
1411
+ onChange: (e) => handleOutputFormatChange(e.target.value),
1412
+ children: [
1413
+ /* @__PURE__ */ jsx("option", { value: "mp4", children: "MP4 video" }),
1414
+ /* @__PURE__ */ jsx("option", { value: "gif", children: "Animated GIF" })
1415
+ ]
1416
+ }
1417
+ )
1418
+ ] }),
1419
+ /* @__PURE__ */ jsxs("div", { children: [
1420
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Quality" }),
1421
+ /* @__PURE__ */ jsxs(
1422
+ "select",
1423
+ {
1424
+ "aria-label": "Quality",
1425
+ style: themedSelectStyle,
1426
+ value: quality,
1427
+ onChange: (e) => setQuality(e.target.value),
1428
+ children: [
1429
+ /* @__PURE__ */ jsx("option", { value: "draft", children: "Draft \u2014 fast, lower quality" }),
1430
+ /* @__PURE__ */ jsx("option", { value: "normal", children: "Normal \u2014 balanced" }),
1431
+ /* @__PURE__ */ jsx("option", { value: "high", children: "High \u2014 best quality, slower" })
1432
+ ]
1433
+ }
1434
+ )
1435
+ ] }),
1436
+ /* @__PURE__ */ jsxs("div", { children: [
1437
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Frame Rate" }),
1438
+ /* @__PURE__ */ jsxs(
1439
+ "select",
1440
+ {
1441
+ "aria-label": "Frame Rate",
1442
+ style: themedSelectStyle,
1443
+ value: fps,
1444
+ onChange: (e) => setFps(Number(e.target.value)),
1445
+ children: [
1446
+ /* @__PURE__ */ jsx("option", { value: 10, children: "10 fps \u2014 recommended for GIF" }),
1447
+ /* @__PURE__ */ jsx("option", { value: 15, children: "15 fps \u2014 fast export" }),
1448
+ /* @__PURE__ */ jsx("option", { value: 24, children: "24 fps \u2014 cinematic" }),
1449
+ /* @__PURE__ */ jsx("option", { value: 30, children: "30 fps \u2014 smooth" })
1450
+ ]
1451
+ }
1452
+ )
1453
+ ] }),
1454
+ /* @__PURE__ */ jsxs("div", { children: [
1455
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Orientation" }),
1456
+ /* @__PURE__ */ jsxs(
1457
+ "select",
1458
+ {
1459
+ "aria-label": "Orientation",
1460
+ style: themedSelectStyle,
1461
+ value: orientation,
1462
+ onChange: (e) => setOrientation(e.target.value),
1463
+ children: [
1464
+ /* @__PURE__ */ jsxs("option", { value: "landscape", children: [
1465
+ "Landscape (",
1466
+ outputFormat === "gif" ? "960 \xD7 540" : "1920 \xD7 1080",
1467
+ ")"
1468
+ ] }),
1469
+ /* @__PURE__ */ jsxs("option", { value: "portrait", children: [
1470
+ "Portrait (",
1471
+ outputFormat === "gif" ? "540 \xD7 960" : "1080 \xD7 1920",
1472
+ ")"
1473
+ ] })
1474
+ ]
1475
+ }
1476
+ )
1477
+ ] }),
1478
+ /* @__PURE__ */ jsxs("div", { children: [
1479
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Captions" }),
1480
+ /* @__PURE__ */ jsxs(
1481
+ "select",
1482
+ {
1483
+ "aria-label": "Captions",
1484
+ style: themedSelectStyle,
1485
+ value: captionMode,
1486
+ onChange: (e) => setCaptionMode(e.target.value),
1487
+ children: [
1488
+ /* @__PURE__ */ jsx("option", { value: "off", children: "None" }),
1489
+ /* @__PURE__ */ jsx("option", { value: "standard", children: "Standard (top bar)" }),
1490
+ /* @__PURE__ */ jsx("option", { value: "social", children: "Social media (large words)" })
1491
+ ]
1492
+ }
1493
+ )
1494
+ ] }),
1495
+ outputFormat === "mp4" && /* @__PURE__ */ jsxs("div", { children: [
1496
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Audio" }),
1497
+ /* @__PURE__ */ jsxs(
1498
+ "select",
1499
+ {
1500
+ "aria-label": "Audio handling",
1501
+ style: themedSelectStyle,
1502
+ value: audioPolicy,
1503
+ onChange: (e) => setAudioPolicy(e.target.value),
1504
+ children: [
1505
+ /* @__PURE__ */ jsx("option", { value: "require", children: "Require document audio" }),
1506
+ /* @__PURE__ */ jsx("option", { value: "best-effort", children: "Best effort \u2014 allow video-only fallback" }),
1507
+ /* @__PURE__ */ jsx("option", { value: "omit", children: "Omit audio intentionally" })
1508
+ ]
1509
+ }
1510
+ )
1511
+ ] }),
1512
+ /* @__PURE__ */ jsxs("div", { children: [
1513
+ /* @__PURE__ */ jsx("label", { style: themedLabelStyle, children: "Animations & transitions" }),
1514
+ /* @__PURE__ */ jsxs(
1515
+ "select",
1516
+ {
1517
+ "aria-label": "Animations and transitions",
1518
+ style: themedSelectStyle,
1519
+ value: animationsEnabled ? "enabled" : "disabled",
1520
+ onChange: (e) => setAnimationsEnabled(e.target.value === "enabled"),
1521
+ children: [
1522
+ /* @__PURE__ */ jsx("option", { value: "enabled", children: "Enabled" }),
1523
+ /* @__PURE__ */ jsx("option", { value: "disabled", children: "Disabled \u2014 smaller files" })
1524
+ ]
1525
+ }
1526
+ ),
1527
+ outputFormat === "gif" && animationsEnabled && /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "-6px 0 12px" }, children: "Disabling motion is recommended for much smaller GIFs." })
1528
+ ] }),
1529
+ /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1530
+ /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Cancel" }),
1531
+ /* @__PURE__ */ jsx("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: outputFormat === "gif" ? "Export GIF" : "Export Video" })
1532
+ ] })
1533
+ ] }),
1534
+ isExporting && /* @__PURE__ */ jsxs(Fragment, { children: [
1535
+ backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 8px 0" }, children: [
1536
+ "Encoder: ",
1537
+ encoderLabel(completedOutputFormat, backend)
1538
+ ] }),
1539
+ /* @__PURE__ */ jsx(
1540
+ "div",
1541
+ {
1542
+ role: "progressbar",
1543
+ "aria-label": "Video export progress",
1544
+ "aria-valuemin": 0,
1545
+ "aria-valuemax": 100,
1546
+ "aria-valuenow": progress,
1547
+ style: { ...progressBarOuterStyle, background: palette.secondary },
1548
+ "data-squisq-video-export-progress-track": true,
1549
+ children: /* @__PURE__ */ jsx(
1550
+ "div",
1551
+ {
1552
+ style: {
1553
+ width: `${progress}%`,
1554
+ height: "100%",
1555
+ background: palette.primary,
1556
+ transition: "width 0.3s ease"
1557
+ },
1558
+ "data-squisq-video-export-progress": true
1559
+ }
1560
+ )
1561
+ }
1562
+ ),
1563
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, margin: "0 0 4px 0" }, children: [
1564
+ progress,
1565
+ "% complete"
1566
+ ] }),
1567
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: 0 }, children: [
1568
+ formatDuration(elapsed),
1569
+ " elapsed",
1570
+ estimatedRemaining > 0 && ` \xB7 ~${formatDuration(estimatedRemaining)} remaining`
1571
+ ] }),
1572
+ /* @__PURE__ */ jsx("div", { style: footerStyle, children: /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: cancelExport, children: "Cancel" }) })
1573
+ ] }),
1574
+ state === "complete" && /* @__PURE__ */ jsxs(Fragment, { children: [
1575
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.success }, children: "Export complete!" }),
1576
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, color: palette.label, margin: "0 0 4px 0" }, children: [
1577
+ "File size: ",
1578
+ (fileSize / (1024 * 1024)).toFixed(1),
1579
+ " MB"
1580
+ ] }),
1581
+ completedOutputFormat === "gif" ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: "Animated GIF does not include audio." }) : audioIncluded ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, color: palette.success, margin: "0 0 4px 0" }, children: "Audio included \u2713" }) : /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 4px 0" }, children: [
1582
+ "Video only",
1583
+ audioSkippedReason ? ` \u2014 ${audioSkippedReason}` : ""
1584
+ ] }),
1585
+ backend && /* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: "0 0 12px 0" }, children: [
1586
+ "Encoded with ",
1587
+ encoderLabel(completedOutputFormat, backend)
1588
+ ] }),
1589
+ /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1590
+ /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
1591
+ /* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleDownload, children: [
1592
+ "Download ",
1593
+ completedOutputFormat.toUpperCase()
1594
+ ] })
1595
+ ] })
1596
+ ] }),
1597
+ state === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
1598
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 14, margin: "0 0 8px 0", color: palette.danger }, children: "Export failed" }),
1599
+ /* @__PURE__ */ jsx(
1600
+ "p",
1601
+ {
1602
+ style: {
1603
+ fontSize: 13,
1604
+ color: palette.label,
1605
+ margin: "0 0 12px 0",
1606
+ wordBreak: "break-word"
1607
+ },
1608
+ children: error
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsxs("div", { style: footerStyle, children: [
1612
+ /* @__PURE__ */ jsx("button", { style: themedSecondaryButtonStyle, onClick: handleClose, children: "Close" }),
1613
+ /* @__PURE__ */ jsxs("button", { style: themedPrimaryButtonStyle, onClick: handleExport, children: [
1614
+ "Retry ",
1615
+ outputFormat === "gif" ? "GIF" : "video"
1616
+ ] })
1617
+ ] })
1458
1618
  ] })
1459
- ] })
1460
- ] })
1461
- ] })
1619
+ ]
1620
+ }
1621
+ )
1462
1622
  }
1463
1623
  );
1464
1624
  }
@@ -1475,6 +1635,7 @@ function VideoExportButton({
1475
1635
  audio,
1476
1636
  defaultConfig,
1477
1637
  colorScheme,
1638
+ uiPalette,
1478
1639
  label,
1479
1640
  style,
1480
1641
  disabled
@@ -1496,6 +1657,7 @@ function VideoExportButton({
1496
1657
  audio,
1497
1658
  defaultConfig,
1498
1659
  colorScheme,
1660
+ uiPalette,
1499
1661
  onClose: handleClose
1500
1662
  }
1501
1663
  ),
@@ -1513,4 +1675,3 @@ export {
1513
1675
  useFrameCapture,
1514
1676
  useVideoExport
1515
1677
  };
1516
- //# sourceMappingURL=index.js.map