@bendyline/squisq-video-react 2.2.11 → 2.3.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/README.md +14 -4
- package/dist/{chunk-KJ5RKG67.js → chunk-32QCPFXE.js} +4 -2
- package/dist/{chunk-I4SXMCDF.js → chunk-5MFQMJ5Z.js} +90 -11
- package/dist/{chunk-KW5BBYKP.js → chunk-CRXJOZSH.js} +1 -1
- package/dist/chunk-NTRCMTCF.js +401 -0
- package/dist/chunk-U3RSDWQL.js +978 -0
- package/dist/{chunk-F2XUI32B.js → chunk-YTEDBL6F.js} +69 -945
- package/dist/components/index.d.ts +4 -2
- package/dist/components/index.js +14 -5
- package/dist/cover-image/index.d.ts +23 -0
- package/dist/cover-image/index.js +11 -0
- package/dist/encoder/index.d.ts +1 -1
- package/dist/encoder/index.js +2 -2
- package/dist/hooks/index.d.ts +8 -15
- package/dist/hooks/index.js +8 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +18 -6
- package/dist/{mainThreadEncoder-CiVsL1Bf.d.ts → mainThreadEncoder-BVCLjlfj.d.ts} +7 -0
- package/dist/{useVideoExport-CM5XiM6Z.d.ts → useVideoExport-B2eVCvTX.d.ts} +29 -5
- package/dist/workers/encode.worker.js +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -76,10 +76,15 @@ function App() {
|
|
|
76
76
|
|
|
77
77
|
## Components
|
|
78
78
|
|
|
79
|
-
| Component
|
|
80
|
-
|
|
|
81
|
-
| `VideoExportModal`
|
|
82
|
-
| `VideoExportButton`
|
|
79
|
+
| Component | Description |
|
|
80
|
+
| ----------------------- | ---------------------------------------------------------------------------------- |
|
|
81
|
+
| `VideoExportModal` | Full modal UI — configure MP4/GIF, captions, motion, quality, fps, and orientation |
|
|
82
|
+
| `VideoExportButton` | Drop-in button that opens the export modal via portal |
|
|
83
|
+
| `CoverImageExportModal` | Save the managed cover as PNG, JPEG, or WebP with bounded resolution controls |
|
|
84
|
+
|
|
85
|
+
Import `CoverImageExportModal` from
|
|
86
|
+
`@bendyline/squisq-video-react/cover-image` when a surface only needs cover
|
|
87
|
+
capture. This entry point excludes the MP4/GIF encoder worker graph.
|
|
83
88
|
|
|
84
89
|
## Hooks
|
|
85
90
|
|
|
@@ -99,6 +104,11 @@ The `VideoExportModal` lets users configure:
|
|
|
99
104
|
- **Captions:** off, standard, or social
|
|
100
105
|
- **Animations & transitions:** enabled by default for MP4 and disabled by default for GIF
|
|
101
106
|
|
|
107
|
+
Managed covers inherit `squisq-cover-duration` and `squisq-cover-playback`
|
|
108
|
+
from document frontmatter. `preroll` adds the cover before story frame zero
|
|
109
|
+
and shifts audio; `overlay` keeps the exported duration unchanged while the
|
|
110
|
+
story and audio advance underneath the visible cover.
|
|
111
|
+
|
|
102
112
|
## Using the Hook Directly
|
|
103
113
|
|
|
104
114
|
For custom export UIs, use `useVideoExport` directly:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_MP4_SPILL_THRESHOLD_BYTES,
|
|
2
3
|
applyWebCodecsBackpressure,
|
|
3
4
|
createMp4Muxer,
|
|
4
5
|
resolveWebCodecsQueueLimit
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-5MFQMJ5Z.js";
|
|
6
7
|
|
|
7
8
|
// src/mainThreadEncoder.ts
|
|
8
9
|
import { bitrateForQuality, validateVideoExportOptions } from "@bendyline/squisq-video";
|
|
@@ -43,7 +44,8 @@ function createEncoder(config) {
|
|
|
43
44
|
width: config.width,
|
|
44
45
|
height: config.height,
|
|
45
46
|
fps: config.fps,
|
|
46
|
-
...config.audio ? { audio: config.audio } : {}
|
|
47
|
+
...config.audio ? { audio: config.audio } : {},
|
|
48
|
+
...config.spillOutputToBlob ? { spillToBlobThresholdBytes: DEFAULT_MP4_SPILL_THRESHOLD_BYTES } : {}
|
|
47
49
|
});
|
|
48
50
|
let closed = false;
|
|
49
51
|
let fatalError = null;
|
|
@@ -1905,9 +1905,13 @@ ensureNotFinalized_fn = function() {
|
|
|
1905
1905
|
};
|
|
1906
1906
|
|
|
1907
1907
|
// src/mp4Mux.ts
|
|
1908
|
+
var DEFAULT_MP4_SPILL_THRESHOLD_BYTES = 32 * 1024 * 1024;
|
|
1908
1909
|
var ChunkedMp4Output = class {
|
|
1909
|
-
constructor() {
|
|
1910
|
+
constructor(spillThresholdBytes = null) {
|
|
1911
|
+
this.spillThresholdBytes = spillThresholdBytes;
|
|
1910
1912
|
this.writes = [];
|
|
1913
|
+
this.spilled = [];
|
|
1914
|
+
this.bufferedBytes = 0;
|
|
1911
1915
|
this.length = 0;
|
|
1912
1916
|
}
|
|
1913
1917
|
write(data, position) {
|
|
@@ -1915,7 +1919,9 @@ var ChunkedMp4Output = class {
|
|
|
1915
1919
|
const end = position + owned.byteLength;
|
|
1916
1920
|
if (position >= this.length) {
|
|
1917
1921
|
this.writes.push({ position, data: owned });
|
|
1922
|
+
this.bufferedBytes += owned.byteLength;
|
|
1918
1923
|
this.length = end;
|
|
1924
|
+
this.maybeSpill();
|
|
1919
1925
|
return;
|
|
1920
1926
|
}
|
|
1921
1927
|
const updated = [];
|
|
@@ -1941,34 +1947,106 @@ var ChunkedMp4Output = class {
|
|
|
1941
1947
|
updated.push({ position, data: owned });
|
|
1942
1948
|
updated.sort((left, right) => left.position - right.position);
|
|
1943
1949
|
this.writes = updated;
|
|
1950
|
+
this.bufferedBytes = updated.reduce((sum, write) => sum + write.data.byteLength, 0);
|
|
1944
1951
|
this.length = Math.max(this.length, end);
|
|
1952
|
+
this.maybeSpill();
|
|
1953
|
+
}
|
|
1954
|
+
overlapsSpilled(write) {
|
|
1955
|
+
const end = write.position + write.data.byteLength;
|
|
1956
|
+
return this.spilled.some(
|
|
1957
|
+
(part) => part.position < end && part.position + part.size > write.position
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Consolidate buffered writes into Blob parts once they exceed the
|
|
1962
|
+
* threshold. Writes overlapping an already-spilled region are patches over
|
|
1963
|
+
* Blob bytes; they stay in memory (they are tiny) and win at assembly.
|
|
1964
|
+
*/
|
|
1965
|
+
maybeSpill() {
|
|
1966
|
+
if (this.spillThresholdBytes === null || this.bufferedBytes < this.spillThresholdBytes) return;
|
|
1967
|
+
const spillable = this.writes.filter((write) => !this.overlapsSpilled(write)).sort((left, right) => left.position - right.position);
|
|
1968
|
+
if (spillable.length === 0) return;
|
|
1969
|
+
const keep = new Set(spillable);
|
|
1970
|
+
let run = [];
|
|
1971
|
+
const flushRun = () => {
|
|
1972
|
+
if (run.length === 0) return;
|
|
1973
|
+
const position = run[0].position;
|
|
1974
|
+
const size = run.reduce((sum, write) => sum + write.data.byteLength, 0);
|
|
1975
|
+
this.spilled.push({
|
|
1976
|
+
position,
|
|
1977
|
+
size,
|
|
1978
|
+
blob: new Blob(run.map((write) => write.data))
|
|
1979
|
+
});
|
|
1980
|
+
run = [];
|
|
1981
|
+
};
|
|
1982
|
+
for (const write of spillable) {
|
|
1983
|
+
const previous = run[run.length - 1];
|
|
1984
|
+
if (previous && previous.position + previous.data.byteLength !== write.position) flushRun();
|
|
1985
|
+
run.push(write);
|
|
1986
|
+
}
|
|
1987
|
+
flushRun();
|
|
1988
|
+
this.spilled.sort((left, right) => left.position - right.position);
|
|
1989
|
+
this.writes = this.writes.filter((write) => !keep.has(write));
|
|
1990
|
+
this.bufferedBytes = this.writes.reduce((sum, write) => sum + write.data.byteLength, 0);
|
|
1991
|
+
}
|
|
1992
|
+
/** Regions in position order; in-memory writes take precedence over Blobs. */
|
|
1993
|
+
assembleParts() {
|
|
1994
|
+
const boundaries = /* @__PURE__ */ new Set([0, this.length]);
|
|
1995
|
+
for (const write of this.writes) {
|
|
1996
|
+
boundaries.add(write.position);
|
|
1997
|
+
boundaries.add(write.position + write.data.byteLength);
|
|
1998
|
+
}
|
|
1999
|
+
for (const part of this.spilled) {
|
|
2000
|
+
boundaries.add(part.position);
|
|
2001
|
+
boundaries.add(part.position + part.size);
|
|
2002
|
+
}
|
|
2003
|
+
const sorted = [...boundaries].sort((left, right) => left - right);
|
|
2004
|
+
const parts = [];
|
|
2005
|
+
for (let i = 0; i + 1 < sorted.length; i++) {
|
|
2006
|
+
const start = sorted[i];
|
|
2007
|
+
const end = sorted[i + 1];
|
|
2008
|
+
if (end <= start) continue;
|
|
2009
|
+
const write = this.writes.find(
|
|
2010
|
+
(candidate) => candidate.position <= start && candidate.position + candidate.data.byteLength >= end
|
|
2011
|
+
);
|
|
2012
|
+
if (write) {
|
|
2013
|
+
parts.push(write.data.subarray(start - write.position, end - write.position));
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const part = this.spilled.find(
|
|
2017
|
+
(candidate) => candidate.position <= start && candidate.position + candidate.size >= end
|
|
2018
|
+
);
|
|
2019
|
+
if (part) {
|
|
2020
|
+
parts.push(part.blob.slice(start - part.position, end - part.position));
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
parts.push(new Uint8Array(end - start));
|
|
2024
|
+
}
|
|
2025
|
+
return parts;
|
|
1945
2026
|
}
|
|
1946
2027
|
toArrayBuffer() {
|
|
2028
|
+
if (this.spilled.length > 0) {
|
|
2029
|
+
throw new Error("Spilled MP4 output can only finalize to a Blob");
|
|
2030
|
+
}
|
|
1947
2031
|
const output = new Uint8Array(this.length);
|
|
1948
2032
|
for (const write of this.writes) output.set(write.data, write.position);
|
|
1949
2033
|
this.release();
|
|
1950
2034
|
return output.buffer;
|
|
1951
2035
|
}
|
|
1952
2036
|
toBlob() {
|
|
1953
|
-
const
|
|
1954
|
-
let position = 0;
|
|
1955
|
-
for (const write of this.writes) {
|
|
1956
|
-
if (write.position > position) parts.push(new Uint8Array(write.position - position));
|
|
1957
|
-
parts.push(write.data);
|
|
1958
|
-
position = write.position + write.data.byteLength;
|
|
1959
|
-
}
|
|
1960
|
-
if (position < this.length) parts.push(new Uint8Array(this.length - position));
|
|
1961
|
-
const blob = new Blob(parts, { type: "video/mp4" });
|
|
2037
|
+
const blob = new Blob(this.assembleParts(), { type: "video/mp4" });
|
|
1962
2038
|
this.release();
|
|
1963
2039
|
return blob;
|
|
1964
2040
|
}
|
|
1965
2041
|
release() {
|
|
1966
2042
|
this.writes = [];
|
|
2043
|
+
this.spilled = [];
|
|
2044
|
+
this.bufferedBytes = 0;
|
|
1967
2045
|
this.length = 0;
|
|
1968
2046
|
}
|
|
1969
2047
|
};
|
|
1970
2048
|
function createMp4Muxer(options) {
|
|
1971
|
-
const output = new ChunkedMp4Output();
|
|
2049
|
+
const output = new ChunkedMp4Output(options.spillToBlobThresholdBytes ?? null);
|
|
1972
2050
|
const target = new StreamTarget({
|
|
1973
2051
|
onData: (data, position) => output.write(data, position)
|
|
1974
2052
|
});
|
|
@@ -2048,6 +2126,7 @@ function shouldEncodeFfmpegBatch(frameCount, byteLength, fps) {
|
|
|
2048
2126
|
}
|
|
2049
2127
|
|
|
2050
2128
|
export {
|
|
2129
|
+
DEFAULT_MP4_SPILL_THRESHOLD_BYTES,
|
|
2051
2130
|
createMp4Muxer,
|
|
2052
2131
|
resolveWebCodecsQueueLimit,
|
|
2053
2132
|
applyWebCodecsBackpressure,
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useFrameCapture
|
|
3
|
+
} from "./chunk-YTEDBL6F.js";
|
|
4
|
+
|
|
5
|
+
// src/CoverImageExportModal.tsx
|
|
6
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
7
|
+
import { useModalDialog } from "@bendyline/squisq-react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var MIN_DIMENSION = 64;
|
|
10
|
+
var MAX_DIMENSION = 7680;
|
|
11
|
+
var MAX_PIXELS = 33177600;
|
|
12
|
+
var COVER_IMAGE_SIZE_PRESETS = [{ label: "YouTube cover", width: 1280, height: 720 }];
|
|
13
|
+
var FORMAT_DETAILS = {
|
|
14
|
+
png: { extension: "png", mime: "image/png", label: "PNG \u2014 lossless" },
|
|
15
|
+
jpeg: { extension: "jpg", mime: "image/jpeg", label: "JPEG \u2014 smaller file" },
|
|
16
|
+
webp: { extension: "webp", mime: "image/webp", label: "WebP \u2014 compact" }
|
|
17
|
+
};
|
|
18
|
+
function validateCoverImageDimensions(width, height) {
|
|
19
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {
|
|
20
|
+
return "Width and height must be whole numbers.";
|
|
21
|
+
}
|
|
22
|
+
if (width < MIN_DIMENSION || height < MIN_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
23
|
+
return `Width and height must be between ${MIN_DIMENSION} and ${MAX_DIMENSION} pixels.`;
|
|
24
|
+
}
|
|
25
|
+
if (width * height > MAX_PIXELS) {
|
|
26
|
+
return "Resolution is too large. Choose a size at or below 33 megapixels.";
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function coverImageFilename(requestedName, format) {
|
|
31
|
+
const extension = FORMAT_DETAILS[format].extension;
|
|
32
|
+
const base = requestedName?.replace(/\.[^.]+$/, "").replace(/[<>:"/\\|?*]/g, "-").split("").map((character) => character.charCodeAt(0) < 32 ? "-" : character).join("").trim().replace(/[. ]+$/g, "") || "document";
|
|
33
|
+
return `${base}-cover.${extension}`;
|
|
34
|
+
}
|
|
35
|
+
function canvasToBlob(canvas, format, quality) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
canvas.toBlob(
|
|
38
|
+
(blob) => {
|
|
39
|
+
if (blob) resolve(blob);
|
|
40
|
+
else reject(new Error("The browser could not encode the cover image."));
|
|
41
|
+
},
|
|
42
|
+
FORMAT_DETAILS[format].mime,
|
|
43
|
+
format === "png" ? void 0 : quality
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function chooseSaveTarget(filename, format) {
|
|
48
|
+
const picker = window.showSaveFilePicker;
|
|
49
|
+
if (!picker) return void 0;
|
|
50
|
+
const details = FORMAT_DETAILS[format];
|
|
51
|
+
try {
|
|
52
|
+
return await picker.call(window, {
|
|
53
|
+
suggestedName: filename,
|
|
54
|
+
types: [
|
|
55
|
+
{
|
|
56
|
+
description: `${details.label.split(" \u2014")[0]} image`,
|
|
57
|
+
accept: { [details.mime]: [`.${details.extension}`] }
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
} catch (caught) {
|
|
62
|
+
if (caught instanceof DOMException && caught.name === "AbortError") return null;
|
|
63
|
+
throw caught;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function downloadBlob(blob, filename) {
|
|
67
|
+
const url = URL.createObjectURL(blob);
|
|
68
|
+
const anchor = document.createElement("a");
|
|
69
|
+
anchor.href = url;
|
|
70
|
+
anchor.download = filename;
|
|
71
|
+
document.body.appendChild(anchor);
|
|
72
|
+
anchor.click();
|
|
73
|
+
anchor.remove();
|
|
74
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
75
|
+
}
|
|
76
|
+
var overlayStyle = {
|
|
77
|
+
position: "fixed",
|
|
78
|
+
inset: 0,
|
|
79
|
+
zIndex: 1e4,
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
background: "rgba(0, 0, 0, 0.55)"
|
|
84
|
+
};
|
|
85
|
+
var rowStyle = {
|
|
86
|
+
display: "grid",
|
|
87
|
+
gridTemplateColumns: "1fr 1fr",
|
|
88
|
+
gap: 12
|
|
89
|
+
};
|
|
90
|
+
var labelStyle = {
|
|
91
|
+
display: "grid",
|
|
92
|
+
gap: 5,
|
|
93
|
+
marginBottom: 12,
|
|
94
|
+
fontSize: 13,
|
|
95
|
+
fontWeight: 600
|
|
96
|
+
};
|
|
97
|
+
function CoverImageExportModal({
|
|
98
|
+
doc,
|
|
99
|
+
mediaProvider,
|
|
100
|
+
theme,
|
|
101
|
+
coverSlideTemplate,
|
|
102
|
+
defaultWidth = 1920,
|
|
103
|
+
defaultHeight = 1080,
|
|
104
|
+
defaultFileName,
|
|
105
|
+
colorScheme = "light",
|
|
106
|
+
saveOutput,
|
|
107
|
+
onClose
|
|
108
|
+
}) {
|
|
109
|
+
const overlayRef = useRef(null);
|
|
110
|
+
const dialogRef = useRef(null);
|
|
111
|
+
const titleId = useId();
|
|
112
|
+
const capture = useFrameCapture();
|
|
113
|
+
const [format, setFormat] = useState("png");
|
|
114
|
+
const [width, setWidth] = useState(defaultWidth);
|
|
115
|
+
const [height, setHeight] = useState(defaultHeight);
|
|
116
|
+
const [quality, setQuality] = useState(0.92);
|
|
117
|
+
const [busy, setBusy] = useState(false);
|
|
118
|
+
const [error, setError] = useState(null);
|
|
119
|
+
const dark = colorScheme === "dark";
|
|
120
|
+
const surface = dark ? "#111827" : "#ffffff";
|
|
121
|
+
const control = dark ? "#0f172a" : "#ffffff";
|
|
122
|
+
const text = dark ? "#f8fafc" : "#1f2937";
|
|
123
|
+
const muted = dark ? "#94a3b8" : "#6b7280";
|
|
124
|
+
const border = dark ? "#475569" : "#cbd5e1";
|
|
125
|
+
const filename = coverImageFilename(defaultFileName, format);
|
|
126
|
+
const dimensionError = validateCoverImageDimensions(width, height);
|
|
127
|
+
const handleClose = useCallback(() => {
|
|
128
|
+
if (busy) return;
|
|
129
|
+
capture.destroy();
|
|
130
|
+
onClose();
|
|
131
|
+
}, [busy, capture, onClose]);
|
|
132
|
+
useModalDialog({
|
|
133
|
+
rootRef: overlayRef,
|
|
134
|
+
dialogRef,
|
|
135
|
+
closeOnEscape: !busy,
|
|
136
|
+
onClose: handleClose
|
|
137
|
+
});
|
|
138
|
+
const handleExport = useCallback(async () => {
|
|
139
|
+
if (dimensionError || !doc.startBlock) return;
|
|
140
|
+
setError(null);
|
|
141
|
+
try {
|
|
142
|
+
const saveTarget = saveOutput ? void 0 : await chooseSaveTarget(filename, format);
|
|
143
|
+
if (saveTarget === null) return;
|
|
144
|
+
setBusy(true);
|
|
145
|
+
await capture.init(
|
|
146
|
+
doc,
|
|
147
|
+
{
|
|
148
|
+
width,
|
|
149
|
+
height,
|
|
150
|
+
animationsEnabled: false,
|
|
151
|
+
theme,
|
|
152
|
+
mediaProvider: mediaProvider ?? void 0,
|
|
153
|
+
showCoverSlide: true,
|
|
154
|
+
coverSlideTemplate
|
|
155
|
+
},
|
|
156
|
+
"off"
|
|
157
|
+
);
|
|
158
|
+
await capture.setCoverVisible(true);
|
|
159
|
+
const canvas = await capture.captureCanvasFrame(0);
|
|
160
|
+
const blob = await canvasToBlob(canvas, format, quality);
|
|
161
|
+
if (saveOutput) {
|
|
162
|
+
const saved = await saveOutput(blob, filename);
|
|
163
|
+
if (saved === false) {
|
|
164
|
+
capture.destroy();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
} else if (saveTarget) {
|
|
168
|
+
const writable = await saveTarget.createWritable();
|
|
169
|
+
await writable.write(blob);
|
|
170
|
+
await writable.close();
|
|
171
|
+
} else {
|
|
172
|
+
downloadBlob(blob, filename);
|
|
173
|
+
}
|
|
174
|
+
capture.destroy();
|
|
175
|
+
onClose();
|
|
176
|
+
} catch (caught) {
|
|
177
|
+
capture.destroy();
|
|
178
|
+
setError(caught instanceof Error ? caught.message : "The cover image could not be exported.");
|
|
179
|
+
} finally {
|
|
180
|
+
setBusy(false);
|
|
181
|
+
}
|
|
182
|
+
}, [
|
|
183
|
+
capture,
|
|
184
|
+
coverSlideTemplate,
|
|
185
|
+
dimensionError,
|
|
186
|
+
doc,
|
|
187
|
+
filename,
|
|
188
|
+
format,
|
|
189
|
+
height,
|
|
190
|
+
mediaProvider,
|
|
191
|
+
onClose,
|
|
192
|
+
quality,
|
|
193
|
+
saveOutput,
|
|
194
|
+
theme,
|
|
195
|
+
width
|
|
196
|
+
]);
|
|
197
|
+
const fieldStyle = {
|
|
198
|
+
boxSizing: "border-box",
|
|
199
|
+
width: "100%",
|
|
200
|
+
minHeight: 34,
|
|
201
|
+
border: `1px solid ${border}`,
|
|
202
|
+
borderRadius: 4,
|
|
203
|
+
background: control,
|
|
204
|
+
color: text,
|
|
205
|
+
padding: "6px 8px",
|
|
206
|
+
colorScheme
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ jsx(
|
|
209
|
+
"div",
|
|
210
|
+
{
|
|
211
|
+
ref: overlayRef,
|
|
212
|
+
style: overlayStyle,
|
|
213
|
+
"data-color-scheme": colorScheme,
|
|
214
|
+
onClick: (event) => event.stopPropagation(),
|
|
215
|
+
children: /* @__PURE__ */ jsxs(
|
|
216
|
+
"div",
|
|
217
|
+
{
|
|
218
|
+
ref: dialogRef,
|
|
219
|
+
role: "dialog",
|
|
220
|
+
"aria-modal": "true",
|
|
221
|
+
"aria-labelledby": titleId,
|
|
222
|
+
tabIndex: -1,
|
|
223
|
+
style: {
|
|
224
|
+
position: "relative",
|
|
225
|
+
boxSizing: "border-box",
|
|
226
|
+
width: "min(440px, calc(100vw - 32px))",
|
|
227
|
+
padding: 24,
|
|
228
|
+
border: `1px solid ${border}`,
|
|
229
|
+
borderRadius: 8,
|
|
230
|
+
background: surface,
|
|
231
|
+
color: text,
|
|
232
|
+
boxShadow: "0 18px 48px rgba(0, 0, 0, 0.28)",
|
|
233
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
234
|
+
colorScheme
|
|
235
|
+
},
|
|
236
|
+
onClick: (event) => event.stopPropagation(),
|
|
237
|
+
children: [
|
|
238
|
+
/* @__PURE__ */ jsx("h2", { id: titleId, style: { margin: "0 36px 18px 0", fontSize: 19 }, children: "Export cover slide" }),
|
|
239
|
+
/* @__PURE__ */ jsx(
|
|
240
|
+
"button",
|
|
241
|
+
{
|
|
242
|
+
type: "button",
|
|
243
|
+
"aria-label": "Close cover image export",
|
|
244
|
+
disabled: busy,
|
|
245
|
+
onClick: handleClose,
|
|
246
|
+
style: {
|
|
247
|
+
position: "absolute",
|
|
248
|
+
top: 12,
|
|
249
|
+
right: 12,
|
|
250
|
+
width: 32,
|
|
251
|
+
height: 32,
|
|
252
|
+
border: 0,
|
|
253
|
+
background: "transparent",
|
|
254
|
+
color: text,
|
|
255
|
+
fontSize: 24,
|
|
256
|
+
cursor: busy ? "default" : "pointer"
|
|
257
|
+
},
|
|
258
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
259
|
+
}
|
|
260
|
+
),
|
|
261
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
262
|
+
"Format",
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"select",
|
|
265
|
+
{
|
|
266
|
+
"aria-label": "Image format",
|
|
267
|
+
value: format,
|
|
268
|
+
disabled: busy,
|
|
269
|
+
onChange: (event) => setFormat(event.target.value),
|
|
270
|
+
style: fieldStyle,
|
|
271
|
+
children: Object.entries(FORMAT_DETAILS).map(([value, details]) => /* @__PURE__ */ jsx("option", { value, children: details.label }, value))
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }),
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
|
|
276
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
277
|
+
"Width",
|
|
278
|
+
/* @__PURE__ */ jsx(
|
|
279
|
+
"input",
|
|
280
|
+
{
|
|
281
|
+
"aria-label": "Image width",
|
|
282
|
+
type: "number",
|
|
283
|
+
min: MIN_DIMENSION,
|
|
284
|
+
max: MAX_DIMENSION,
|
|
285
|
+
step: 1,
|
|
286
|
+
value: width,
|
|
287
|
+
disabled: busy,
|
|
288
|
+
onChange: (event) => setWidth(Number(event.target.value)),
|
|
289
|
+
style: fieldStyle
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
294
|
+
"Height",
|
|
295
|
+
/* @__PURE__ */ jsx(
|
|
296
|
+
"input",
|
|
297
|
+
{
|
|
298
|
+
"aria-label": "Image height",
|
|
299
|
+
type: "number",
|
|
300
|
+
min: MIN_DIMENSION,
|
|
301
|
+
max: MAX_DIMENSION,
|
|
302
|
+
step: 1,
|
|
303
|
+
value: height,
|
|
304
|
+
disabled: busy,
|
|
305
|
+
onChange: (event) => setHeight(Number(event.target.value)),
|
|
306
|
+
style: fieldStyle
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] })
|
|
310
|
+
] }),
|
|
311
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, margin: "-2px 0 14px" }, children: [
|
|
312
|
+
/* @__PURE__ */ jsx(
|
|
313
|
+
"button",
|
|
314
|
+
{
|
|
315
|
+
type: "button",
|
|
316
|
+
disabled: busy,
|
|
317
|
+
onClick: () => {
|
|
318
|
+
setWidth(defaultWidth);
|
|
319
|
+
setHeight(defaultHeight);
|
|
320
|
+
},
|
|
321
|
+
children: "1\xD7"
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
/* @__PURE__ */ jsx(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
disabled: busy,
|
|
329
|
+
onClick: () => {
|
|
330
|
+
setWidth(defaultWidth * 2);
|
|
331
|
+
setHeight(defaultHeight * 2);
|
|
332
|
+
},
|
|
333
|
+
children: "2\xD7"
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
COVER_IMAGE_SIZE_PRESETS.map((preset) => /* @__PURE__ */ jsx(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
disabled: busy,
|
|
341
|
+
title: `${preset.width} \xD7 ${preset.height} pixels`,
|
|
342
|
+
onClick: () => {
|
|
343
|
+
setWidth(preset.width);
|
|
344
|
+
setHeight(preset.height);
|
|
345
|
+
},
|
|
346
|
+
children: preset.label
|
|
347
|
+
},
|
|
348
|
+
preset.label
|
|
349
|
+
)),
|
|
350
|
+
/* @__PURE__ */ jsxs("span", { style: { alignSelf: "center", color: muted, fontSize: 12 }, children: [
|
|
351
|
+
width.toLocaleString(),
|
|
352
|
+
" \xD7 ",
|
|
353
|
+
height.toLocaleString(),
|
|
354
|
+
" pixels"
|
|
355
|
+
] })
|
|
356
|
+
] }),
|
|
357
|
+
format !== "png" && /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
358
|
+
"Quality: ",
|
|
359
|
+
Math.round(quality * 100),
|
|
360
|
+
"%",
|
|
361
|
+
/* @__PURE__ */ jsx(
|
|
362
|
+
"input",
|
|
363
|
+
{
|
|
364
|
+
"aria-label": "Image quality",
|
|
365
|
+
type: "range",
|
|
366
|
+
min: 0.5,
|
|
367
|
+
max: 1,
|
|
368
|
+
step: 0.05,
|
|
369
|
+
value: quality,
|
|
370
|
+
disabled: busy,
|
|
371
|
+
onChange: (event) => setQuality(Number(event.target.value))
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
] }),
|
|
375
|
+
(dimensionError || error) && /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: "0 0 12px", color: dark ? "#fca5a5" : "#b91c1c" }, children: dimensionError ?? error }),
|
|
376
|
+
!doc.startBlock && /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: "0 0 12px", color: dark ? "#fca5a5" : "#b91c1c" }, children: "This document does not have a cover slide to export." }),
|
|
377
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
|
|
378
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: handleClose, children: "Cancel" }),
|
|
379
|
+
/* @__PURE__ */ jsx(
|
|
380
|
+
"button",
|
|
381
|
+
{
|
|
382
|
+
type: "button",
|
|
383
|
+
disabled: busy || !!dimensionError || !doc.startBlock,
|
|
384
|
+
onClick: () => void handleExport(),
|
|
385
|
+
style: { minWidth: 132 },
|
|
386
|
+
children: busy ? "Rendering\u2026" : "Choose location\u2026"
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
] })
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export {
|
|
398
|
+
validateCoverImageDimensions,
|
|
399
|
+
coverImageFilename,
|
|
400
|
+
CoverImageExportModal
|
|
401
|
+
};
|