@bendyline/squisq-video-react 2.3.4 → 2.4.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/chunk-3NJDHK62.js +60 -0
- package/dist/{chunk-UYJ7G34U.js → chunk-4NI2QIFE.js} +12 -0
- package/dist/{chunk-DIWB5BH4.js → chunk-BGS2NEYC.js} +1 -1
- package/dist/{chunk-ZMDTR472.js → chunk-BRE3IRIM.js} +13 -53
- package/dist/{chunk-J2O4TPEO.js → chunk-JGSJQZGU.js} +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +5 -4
- package/dist/cover-image/index.d.ts +2 -1
- package/dist/cover-image/index.js +3 -2
- package/dist/dashboard-image/index.d.ts +29 -0
- package/dist/dashboard-image/index.js +422 -0
- package/dist/hooks/index.js +2 -2
- package/dist/imageExportShared-B6nqWtE-.d.ts +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -4
- package/package.json +8 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/imageExportShared.ts
|
|
2
|
+
var IMAGE_FORMAT_DETAILS = {
|
|
3
|
+
png: { extension: "png", mime: "image/png", label: "PNG \u2014 lossless" },
|
|
4
|
+
jpeg: { extension: "jpg", mime: "image/jpeg", label: "JPEG \u2014 smaller file" },
|
|
5
|
+
webp: { extension: "webp", mime: "image/webp", label: "WebP \u2014 compact" }
|
|
6
|
+
};
|
|
7
|
+
function imageExportFilename(requestedName, suffix, format) {
|
|
8
|
+
const extension = IMAGE_FORMAT_DETAILS[format].extension;
|
|
9
|
+
const base = requestedName?.replace(/\.[^.]+$/, "").replace(/[<>:"/\\|?*]/g, "-").split("").map((character) => character.charCodeAt(0) < 32 ? "-" : character).join("").trim().replace(/[. ]+$/g, "") || "document";
|
|
10
|
+
return `${base}-${suffix}.${extension}`;
|
|
11
|
+
}
|
|
12
|
+
function canvasToImageBlob(canvas, format, quality) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
canvas.toBlob(
|
|
15
|
+
(blob) => {
|
|
16
|
+
if (blob) resolve(blob);
|
|
17
|
+
else reject(new Error("The browser could not encode the image."));
|
|
18
|
+
},
|
|
19
|
+
IMAGE_FORMAT_DETAILS[format].mime,
|
|
20
|
+
format === "png" ? void 0 : quality
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function chooseImageSaveTarget(filename, format) {
|
|
25
|
+
const picker = window.showSaveFilePicker;
|
|
26
|
+
if (!picker) return void 0;
|
|
27
|
+
const details = IMAGE_FORMAT_DETAILS[format];
|
|
28
|
+
try {
|
|
29
|
+
return await picker.call(window, {
|
|
30
|
+
suggestedName: filename,
|
|
31
|
+
types: [
|
|
32
|
+
{
|
|
33
|
+
description: `${details.label.split(" \u2014")[0]} image`,
|
|
34
|
+
accept: { [details.mime]: [`.${details.extension}`] }
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
} catch (caught) {
|
|
39
|
+
if (caught instanceof DOMException && caught.name === "AbortError") return null;
|
|
40
|
+
throw caught;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function downloadImageBlob(blob, filename) {
|
|
44
|
+
const url = URL.createObjectURL(blob);
|
|
45
|
+
const anchor = document.createElement("a");
|
|
46
|
+
anchor.href = url;
|
|
47
|
+
anchor.download = filename;
|
|
48
|
+
document.body.appendChild(anchor);
|
|
49
|
+
anchor.click();
|
|
50
|
+
anchor.remove();
|
|
51
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
IMAGE_FORMAT_DETAILS,
|
|
56
|
+
imageExportFilename,
|
|
57
|
+
canvasToImageBlob,
|
|
58
|
+
chooseImageSaveTarget,
|
|
59
|
+
downloadImageBlob
|
|
60
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createElement } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { useRef, useCallback, useMemo } from "react";
|
|
5
|
+
import { resolveDashboardStyleId } from "@bendyline/squisq/doc";
|
|
5
6
|
import { DocPlayer, MediaContext } from "@bendyline/squisq-react";
|
|
6
7
|
import html2canvas from "html2canvas";
|
|
7
8
|
var MIME_MAP = {
|
|
@@ -993,6 +994,17 @@ function useFrameCapture() {
|
|
|
993
994
|
autoPlay: false,
|
|
994
995
|
forceViewport: { width, height, name: "export" },
|
|
995
996
|
theme: renderOptions.theme,
|
|
997
|
+
// Undefined preserves DocPlayer's default; 'dashboard' mounts the
|
|
998
|
+
// one-canvas dashboard rendition for single-frame capture.
|
|
999
|
+
displayMode: renderOptions.displayMode,
|
|
1000
|
+
dashboardLayout: renderOptions.dashboard?.layout,
|
|
1001
|
+
dashboardShowTitle: renderOptions.dashboard?.title,
|
|
1002
|
+
// RenderHtmlOptions types the style as a plain string (it is serialized
|
|
1003
|
+
// into a script for the standalone path), so narrow it the same way the
|
|
1004
|
+
// CLI does. An unrecognized value yields undefined, which defers to the
|
|
1005
|
+
// doc's own squisq-dashboard-style rather than forcing 'basic'.
|
|
1006
|
+
dashboardStyle: resolveDashboardStyleId(renderOptions.dashboard?.style),
|
|
1007
|
+
dashboardDocumentTitle: renderOptions.dashboard?.documentTitle,
|
|
996
1008
|
videoPresentation: renderOptions.videoPresentation,
|
|
997
1009
|
pipSize: renderOptions.pipSize,
|
|
998
1010
|
pipShape: renderOptions.pipShape,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMAGE_FORMAT_DETAILS,
|
|
3
|
+
canvasToImageBlob,
|
|
4
|
+
chooseImageSaveTarget,
|
|
5
|
+
downloadImageBlob,
|
|
6
|
+
imageExportFilename
|
|
7
|
+
} from "./chunk-3NJDHK62.js";
|
|
1
8
|
import {
|
|
2
9
|
useFrameCapture
|
|
3
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-4NI2QIFE.js";
|
|
4
11
|
|
|
5
12
|
// src/CoverImageExportModal.tsx
|
|
6
13
|
import { useCallback, useId, useRef, useState } from "react";
|
|
@@ -10,11 +17,7 @@ var MIN_DIMENSION = 64;
|
|
|
10
17
|
var MAX_DIMENSION = 7680;
|
|
11
18
|
var MAX_PIXELS = 33177600;
|
|
12
19
|
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
|
-
};
|
|
20
|
+
var FORMAT_DETAILS = IMAGE_FORMAT_DETAILS;
|
|
18
21
|
function validateCoverImageDimensions(width, height) {
|
|
19
22
|
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {
|
|
20
23
|
return "Width and height must be whole numbers.";
|
|
@@ -28,50 +31,7 @@ function validateCoverImageDimensions(width, height) {
|
|
|
28
31
|
return null;
|
|
29
32
|
}
|
|
30
33
|
function coverImageFilename(requestedName, format) {
|
|
31
|
-
|
|
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);
|
|
34
|
+
return imageExportFilename(requestedName, "cover", format);
|
|
75
35
|
}
|
|
76
36
|
var overlayStyle = {
|
|
77
37
|
position: "fixed",
|
|
@@ -139,7 +99,7 @@ function CoverImageExportModal({
|
|
|
139
99
|
if (dimensionError || !doc.startBlock) return;
|
|
140
100
|
setError(null);
|
|
141
101
|
try {
|
|
142
|
-
const saveTarget = saveOutput ? void 0 : await
|
|
102
|
+
const saveTarget = saveOutput ? void 0 : await chooseImageSaveTarget(filename, format);
|
|
143
103
|
if (saveTarget === null) return;
|
|
144
104
|
setBusy(true);
|
|
145
105
|
await capture.init(
|
|
@@ -157,7 +117,7 @@ function CoverImageExportModal({
|
|
|
157
117
|
);
|
|
158
118
|
await capture.setCoverVisible(true);
|
|
159
119
|
const canvas = await capture.captureCanvasFrame(0);
|
|
160
|
-
const blob = await
|
|
120
|
+
const blob = await canvasToImageBlob(canvas, format, quality);
|
|
161
121
|
if (saveOutput) {
|
|
162
122
|
const saved = await saveOutput(blob, filename);
|
|
163
123
|
if (saved === false) {
|
|
@@ -169,7 +129,7 @@ function CoverImageExportModal({
|
|
|
169
129
|
await writable.write(blob);
|
|
170
130
|
await writable.close();
|
|
171
131
|
} else {
|
|
172
|
-
|
|
132
|
+
downloadImageBlob(blob, filename);
|
|
173
133
|
}
|
|
174
134
|
capture.destroy();
|
|
175
135
|
onClose();
|
|
@@ -7,6 +7,7 @@ import '@bendyline/squisq/doc';
|
|
|
7
7
|
import '@bendyline/squisq-video';
|
|
8
8
|
import '@bendyline/squisq-react';
|
|
9
9
|
import '../mainThreadEncoder-BVCLjlfj.js';
|
|
10
|
+
import '../imageExportShared-B6nqWtE-.js';
|
|
10
11
|
|
|
11
12
|
interface VideoExportModalProps {
|
|
12
13
|
/** The document to export */
|
package/dist/components/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VideoExportButton,
|
|
3
3
|
VideoExportModal
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-JGSJQZGU.js";
|
|
5
5
|
import {
|
|
6
6
|
CoverImageExportModal,
|
|
7
7
|
coverImageFilename,
|
|
8
8
|
validateCoverImageDimensions
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-
|
|
11
|
-
import "../chunk-
|
|
9
|
+
} from "../chunk-BRE3IRIM.js";
|
|
10
|
+
import "../chunk-3NJDHK62.js";
|
|
11
|
+
import "../chunk-BGS2NEYC.js";
|
|
12
|
+
import "../chunk-4NI2QIFE.js";
|
|
12
13
|
import "../chunk-32QCPFXE.js";
|
|
13
14
|
import "../chunk-5MFQMJ5Z.js";
|
|
14
15
|
export {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { Doc, MediaProvider, Theme } from '@bendyline/squisq/schemas';
|
|
3
3
|
import { CoverSlideTemplate } from '@bendyline/squisq/doc';
|
|
4
|
+
import { I as ImageExportFormat } from '../imageExportShared-B6nqWtE-.js';
|
|
4
5
|
|
|
5
|
-
type CoverImageExportFormat =
|
|
6
|
+
type CoverImageExportFormat = ImageExportFormat;
|
|
6
7
|
interface CoverImageExportModalProps {
|
|
7
8
|
doc: Doc;
|
|
8
9
|
mediaProvider?: MediaProvider | null;
|
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
CoverImageExportModal,
|
|
3
3
|
coverImageFilename,
|
|
4
4
|
validateCoverImageDimensions
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-BRE3IRIM.js";
|
|
6
|
+
import "../chunk-3NJDHK62.js";
|
|
7
|
+
import "../chunk-4NI2QIFE.js";
|
|
7
8
|
export {
|
|
8
9
|
CoverImageExportModal,
|
|
9
10
|
coverImageFilename,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Doc, MediaProvider, Theme } from '@bendyline/squisq/schemas';
|
|
3
|
+
import { DashboardStyleId } from '@bendyline/squisq/doc';
|
|
4
|
+
import { DashboardResolutionId } from '@bendyline/squisq-video';
|
|
5
|
+
export { DASHBOARD_RESOLUTIONS, DEFAULT_DASHBOARD_RESOLUTION, DashboardResolutionId, DashboardResolutionPreset, validateDashboardImageDimensions } from '@bendyline/squisq-video';
|
|
6
|
+
import { I as ImageExportFormat } from '../imageExportShared-B6nqWtE-.js';
|
|
7
|
+
|
|
8
|
+
type DashboardImageExportFormat = ImageExportFormat;
|
|
9
|
+
interface DashboardImageExportModalProps {
|
|
10
|
+
doc: Doc;
|
|
11
|
+
mediaProvider?: MediaProvider | null;
|
|
12
|
+
theme?: Theme;
|
|
13
|
+
/** Initially selected resolution preset (default `'fhd'`). */
|
|
14
|
+
defaultResolution?: DashboardResolutionId;
|
|
15
|
+
/** Initially selected layout id or `'auto'`. */
|
|
16
|
+
defaultLayout?: string;
|
|
17
|
+
/** Initial title-band state (default true — the projection default). */
|
|
18
|
+
defaultShowTitle?: boolean;
|
|
19
|
+
/** Initially selected cell style variant (default `'basic'`). */
|
|
20
|
+
defaultStyle?: DashboardStyleId;
|
|
21
|
+
defaultFileName?: string;
|
|
22
|
+
colorScheme?: 'light' | 'dark';
|
|
23
|
+
/** Optional host save flow. Return false when the user cancels. */
|
|
24
|
+
saveOutput?: (blob: Blob, filename: string) => boolean | void | Promise<boolean | void>;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
}
|
|
27
|
+
declare function DashboardImageExportModal({ doc, mediaProvider, theme, defaultResolution, defaultLayout, defaultShowTitle, defaultStyle, defaultFileName, colorScheme, saveOutput, onClose, }: DashboardImageExportModalProps): react_jsx_runtime.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export { type DashboardImageExportFormat, DashboardImageExportModal, type DashboardImageExportModalProps };
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMAGE_FORMAT_DETAILS,
|
|
3
|
+
canvasToImageBlob,
|
|
4
|
+
chooseImageSaveTarget,
|
|
5
|
+
downloadImageBlob,
|
|
6
|
+
imageExportFilename
|
|
7
|
+
} from "../chunk-3NJDHK62.js";
|
|
8
|
+
import {
|
|
9
|
+
useFrameCapture
|
|
10
|
+
} from "../chunk-4NI2QIFE.js";
|
|
11
|
+
|
|
12
|
+
// src/DashboardImageExportModal.tsx
|
|
13
|
+
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
DASHBOARD_AUTO_LAYOUT_ID,
|
|
16
|
+
DASHBOARD_STYLES,
|
|
17
|
+
DEFAULT_DASHBOARD_STYLE,
|
|
18
|
+
listDashboardLayouts
|
|
19
|
+
} from "@bendyline/squisq/doc";
|
|
20
|
+
import {
|
|
21
|
+
DASHBOARD_RESOLUTIONS,
|
|
22
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
23
|
+
MAX_DASHBOARD_IMAGE_DIMENSION,
|
|
24
|
+
MIN_DASHBOARD_IMAGE_DIMENSION,
|
|
25
|
+
validateDashboardImageDimensions
|
|
26
|
+
} from "@bendyline/squisq-video";
|
|
27
|
+
import { useModalDialog } from "@bendyline/squisq-react";
|
|
28
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
+
var CUSTOM_RESOLUTION = "custom";
|
|
30
|
+
function documentTitleFromFileName(fileName) {
|
|
31
|
+
if (!fileName) return "";
|
|
32
|
+
const base = fileName.split(/[\\/]/).pop() ?? "";
|
|
33
|
+
return base.replace(/\.[^.]+$/, "").trim();
|
|
34
|
+
}
|
|
35
|
+
var overlayStyle = {
|
|
36
|
+
position: "fixed",
|
|
37
|
+
inset: 0,
|
|
38
|
+
zIndex: 1e4,
|
|
39
|
+
display: "flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
background: "rgba(0, 0, 0, 0.55)"
|
|
43
|
+
};
|
|
44
|
+
var rowStyle = {
|
|
45
|
+
display: "grid",
|
|
46
|
+
gridTemplateColumns: "1fr 1fr",
|
|
47
|
+
gap: 12
|
|
48
|
+
};
|
|
49
|
+
var labelStyle = {
|
|
50
|
+
display: "grid",
|
|
51
|
+
gap: 5,
|
|
52
|
+
marginBottom: 12,
|
|
53
|
+
fontSize: 13,
|
|
54
|
+
fontWeight: 600
|
|
55
|
+
};
|
|
56
|
+
function DashboardImageExportModal({
|
|
57
|
+
doc,
|
|
58
|
+
mediaProvider,
|
|
59
|
+
theme,
|
|
60
|
+
defaultResolution = DEFAULT_DASHBOARD_RESOLUTION,
|
|
61
|
+
defaultLayout = DASHBOARD_AUTO_LAYOUT_ID,
|
|
62
|
+
defaultShowTitle = true,
|
|
63
|
+
defaultStyle = DEFAULT_DASHBOARD_STYLE,
|
|
64
|
+
defaultFileName,
|
|
65
|
+
colorScheme = "light",
|
|
66
|
+
saveOutput,
|
|
67
|
+
onClose
|
|
68
|
+
}) {
|
|
69
|
+
const overlayRef = useRef(null);
|
|
70
|
+
const dialogRef = useRef(null);
|
|
71
|
+
const titleId = useId();
|
|
72
|
+
const capture = useFrameCapture();
|
|
73
|
+
const [format, setFormat] = useState("png");
|
|
74
|
+
const [resolution, setResolution] = useState(defaultResolution);
|
|
75
|
+
const defaultPreset = DASHBOARD_RESOLUTIONS.find((preset) => preset.id === defaultResolution) ?? DASHBOARD_RESOLUTIONS[1];
|
|
76
|
+
const [customWidth, setCustomWidth] = useState(defaultPreset.width);
|
|
77
|
+
const [customHeight, setCustomHeight] = useState(defaultPreset.height);
|
|
78
|
+
const [layout, setLayout] = useState(defaultLayout);
|
|
79
|
+
const [showTitle, setShowTitle] = useState(defaultShowTitle);
|
|
80
|
+
const [style, setStyle] = useState(defaultStyle);
|
|
81
|
+
const [quality, setQuality] = useState(0.92);
|
|
82
|
+
const [busy, setBusy] = useState(false);
|
|
83
|
+
const [error, setError] = useState(null);
|
|
84
|
+
const dark = colorScheme === "dark";
|
|
85
|
+
const surface = dark ? "#111827" : "#ffffff";
|
|
86
|
+
const control = dark ? "#0f172a" : "#ffffff";
|
|
87
|
+
const text = dark ? "#f8fafc" : "#1f2937";
|
|
88
|
+
const muted = dark ? "#94a3b8" : "#6b7280";
|
|
89
|
+
const border = dark ? "#475569" : "#cbd5e1";
|
|
90
|
+
const layoutOptions = useMemo(() => listDashboardLayouts(doc), [doc]);
|
|
91
|
+
const isCustom = resolution === CUSTOM_RESOLUTION;
|
|
92
|
+
const activePreset = DASHBOARD_RESOLUTIONS.find((preset) => preset.id === resolution);
|
|
93
|
+
const width = isCustom ? customWidth : activePreset?.width ?? defaultPreset.width;
|
|
94
|
+
const height = isCustom ? customHeight : activePreset?.height ?? defaultPreset.height;
|
|
95
|
+
const dimensionError = isCustom ? validateDashboardImageDimensions(width, height) : null;
|
|
96
|
+
const filename = imageExportFilename(defaultFileName, "dashboard", format);
|
|
97
|
+
const handleClose = useCallback(() => {
|
|
98
|
+
if (busy) return;
|
|
99
|
+
capture.destroy();
|
|
100
|
+
onClose();
|
|
101
|
+
}, [busy, capture, onClose]);
|
|
102
|
+
useModalDialog({
|
|
103
|
+
rootRef: overlayRef,
|
|
104
|
+
dialogRef,
|
|
105
|
+
closeOnEscape: !busy,
|
|
106
|
+
onClose: handleClose
|
|
107
|
+
});
|
|
108
|
+
const handleExport = useCallback(async () => {
|
|
109
|
+
if (dimensionError) return;
|
|
110
|
+
setError(null);
|
|
111
|
+
try {
|
|
112
|
+
const saveTarget = saveOutput ? void 0 : await chooseImageSaveTarget(filename, format);
|
|
113
|
+
if (saveTarget === null) return;
|
|
114
|
+
setBusy(true);
|
|
115
|
+
await capture.init(
|
|
116
|
+
doc,
|
|
117
|
+
{
|
|
118
|
+
width,
|
|
119
|
+
height,
|
|
120
|
+
animationsEnabled: false,
|
|
121
|
+
theme,
|
|
122
|
+
mediaProvider: mediaProvider ?? void 0,
|
|
123
|
+
displayMode: "dashboard",
|
|
124
|
+
dashboard: {
|
|
125
|
+
layout,
|
|
126
|
+
title: showTitle,
|
|
127
|
+
style,
|
|
128
|
+
documentTitle: documentTitleFromFileName(defaultFileName)
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"off"
|
|
132
|
+
);
|
|
133
|
+
const canvas = await capture.captureCanvasFrame(0);
|
|
134
|
+
const blob = await canvasToImageBlob(canvas, format, quality);
|
|
135
|
+
if (saveOutput) {
|
|
136
|
+
const saved = await saveOutput(blob, filename);
|
|
137
|
+
if (saved === false) {
|
|
138
|
+
capture.destroy();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
} else if (saveTarget) {
|
|
142
|
+
const writable = await saveTarget.createWritable();
|
|
143
|
+
await writable.write(blob);
|
|
144
|
+
await writable.close();
|
|
145
|
+
} else {
|
|
146
|
+
downloadImageBlob(blob, filename);
|
|
147
|
+
}
|
|
148
|
+
capture.destroy();
|
|
149
|
+
onClose();
|
|
150
|
+
} catch (caught) {
|
|
151
|
+
capture.destroy();
|
|
152
|
+
setError(
|
|
153
|
+
caught instanceof Error ? caught.message : "The dashboard image could not be exported."
|
|
154
|
+
);
|
|
155
|
+
} finally {
|
|
156
|
+
setBusy(false);
|
|
157
|
+
}
|
|
158
|
+
}, [
|
|
159
|
+
capture,
|
|
160
|
+
defaultFileName,
|
|
161
|
+
dimensionError,
|
|
162
|
+
doc,
|
|
163
|
+
filename,
|
|
164
|
+
format,
|
|
165
|
+
height,
|
|
166
|
+
layout,
|
|
167
|
+
mediaProvider,
|
|
168
|
+
onClose,
|
|
169
|
+
quality,
|
|
170
|
+
saveOutput,
|
|
171
|
+
showTitle,
|
|
172
|
+
style,
|
|
173
|
+
theme,
|
|
174
|
+
width
|
|
175
|
+
]);
|
|
176
|
+
const fieldStyle = {
|
|
177
|
+
boxSizing: "border-box",
|
|
178
|
+
width: "100%",
|
|
179
|
+
minHeight: 34,
|
|
180
|
+
border: `1px solid ${border}`,
|
|
181
|
+
borderRadius: 4,
|
|
182
|
+
background: control,
|
|
183
|
+
color: text,
|
|
184
|
+
padding: "6px 8px",
|
|
185
|
+
colorScheme
|
|
186
|
+
};
|
|
187
|
+
const builtInLayouts = layoutOptions.filter((option) => !option.custom);
|
|
188
|
+
const customLayouts = layoutOptions.filter((option) => option.custom);
|
|
189
|
+
return /* @__PURE__ */ jsx(
|
|
190
|
+
"div",
|
|
191
|
+
{
|
|
192
|
+
ref: overlayRef,
|
|
193
|
+
style: overlayStyle,
|
|
194
|
+
"data-color-scheme": colorScheme,
|
|
195
|
+
onClick: (event) => event.stopPropagation(),
|
|
196
|
+
children: /* @__PURE__ */ jsxs(
|
|
197
|
+
"div",
|
|
198
|
+
{
|
|
199
|
+
ref: dialogRef,
|
|
200
|
+
role: "dialog",
|
|
201
|
+
"aria-modal": "true",
|
|
202
|
+
"aria-labelledby": titleId,
|
|
203
|
+
tabIndex: -1,
|
|
204
|
+
style: {
|
|
205
|
+
position: "relative",
|
|
206
|
+
boxSizing: "border-box",
|
|
207
|
+
width: "min(440px, calc(100vw - 32px))",
|
|
208
|
+
padding: 24,
|
|
209
|
+
border: `1px solid ${border}`,
|
|
210
|
+
borderRadius: 8,
|
|
211
|
+
background: surface,
|
|
212
|
+
color: text,
|
|
213
|
+
boxShadow: "0 18px 48px rgba(0, 0, 0, 0.28)",
|
|
214
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
215
|
+
colorScheme
|
|
216
|
+
},
|
|
217
|
+
onClick: (event) => event.stopPropagation(),
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsx("h2", { id: titleId, style: { margin: "0 36px 18px 0", fontSize: 19 }, children: "Export dashboard as image" }),
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
"button",
|
|
222
|
+
{
|
|
223
|
+
type: "button",
|
|
224
|
+
"aria-label": "Close dashboard image export",
|
|
225
|
+
disabled: busy,
|
|
226
|
+
onClick: handleClose,
|
|
227
|
+
style: {
|
|
228
|
+
position: "absolute",
|
|
229
|
+
top: 12,
|
|
230
|
+
right: 12,
|
|
231
|
+
width: 32,
|
|
232
|
+
height: 32,
|
|
233
|
+
border: 0,
|
|
234
|
+
background: "transparent",
|
|
235
|
+
color: text,
|
|
236
|
+
fontSize: 24,
|
|
237
|
+
cursor: busy ? "default" : "pointer"
|
|
238
|
+
},
|
|
239
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
240
|
+
}
|
|
241
|
+
),
|
|
242
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
243
|
+
"Format",
|
|
244
|
+
/* @__PURE__ */ jsx(
|
|
245
|
+
"select",
|
|
246
|
+
{
|
|
247
|
+
"aria-label": "Image format",
|
|
248
|
+
value: format,
|
|
249
|
+
disabled: busy,
|
|
250
|
+
onChange: (event) => setFormat(event.target.value),
|
|
251
|
+
style: fieldStyle,
|
|
252
|
+
children: Object.entries(IMAGE_FORMAT_DETAILS).map(([value, details]) => /* @__PURE__ */ jsx("option", { value, children: details.label }, value))
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
] }),
|
|
256
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
257
|
+
"Resolution",
|
|
258
|
+
/* @__PURE__ */ jsxs(
|
|
259
|
+
"select",
|
|
260
|
+
{
|
|
261
|
+
"aria-label": "Image resolution",
|
|
262
|
+
value: resolution,
|
|
263
|
+
disabled: busy,
|
|
264
|
+
onChange: (event) => setResolution(event.target.value),
|
|
265
|
+
style: fieldStyle,
|
|
266
|
+
children: [
|
|
267
|
+
DASHBOARD_RESOLUTIONS.map((preset) => /* @__PURE__ */ jsx("option", { value: preset.id, children: preset.label }, preset.id)),
|
|
268
|
+
/* @__PURE__ */ jsx("option", { value: CUSTOM_RESOLUTION, children: "Custom\u2026" })
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
] }),
|
|
273
|
+
isCustom && /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
|
|
274
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
275
|
+
"Width",
|
|
276
|
+
/* @__PURE__ */ jsx(
|
|
277
|
+
"input",
|
|
278
|
+
{
|
|
279
|
+
"aria-label": "Image width",
|
|
280
|
+
type: "number",
|
|
281
|
+
min: MIN_DASHBOARD_IMAGE_DIMENSION,
|
|
282
|
+
max: MAX_DASHBOARD_IMAGE_DIMENSION,
|
|
283
|
+
step: 1,
|
|
284
|
+
value: customWidth,
|
|
285
|
+
disabled: busy,
|
|
286
|
+
onChange: (event) => setCustomWidth(Number(event.target.value)),
|
|
287
|
+
style: fieldStyle
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
] }),
|
|
291
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
292
|
+
"Height",
|
|
293
|
+
/* @__PURE__ */ jsx(
|
|
294
|
+
"input",
|
|
295
|
+
{
|
|
296
|
+
"aria-label": "Image height",
|
|
297
|
+
type: "number",
|
|
298
|
+
min: MIN_DASHBOARD_IMAGE_DIMENSION,
|
|
299
|
+
max: MAX_DASHBOARD_IMAGE_DIMENSION,
|
|
300
|
+
step: 1,
|
|
301
|
+
value: customHeight,
|
|
302
|
+
disabled: busy,
|
|
303
|
+
onChange: (event) => setCustomHeight(Number(event.target.value)),
|
|
304
|
+
style: fieldStyle
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
] })
|
|
308
|
+
] }),
|
|
309
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
310
|
+
"Layout",
|
|
311
|
+
/* @__PURE__ */ jsxs(
|
|
312
|
+
"select",
|
|
313
|
+
{
|
|
314
|
+
"aria-label": "Dashboard layout",
|
|
315
|
+
value: layout,
|
|
316
|
+
disabled: busy,
|
|
317
|
+
onChange: (event) => setLayout(event.target.value),
|
|
318
|
+
style: fieldStyle,
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx("option", { value: DASHBOARD_AUTO_LAYOUT_ID, children: "Auto" }),
|
|
321
|
+
builtInLayouts.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: `${option.label} (${option.capacity})` }, option.id)),
|
|
322
|
+
customLayouts.length > 0 && /* @__PURE__ */ jsx("optgroup", { label: "Custom", children: customLayouts.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: `${option.label} (${option.capacity})` }, option.id)) })
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
328
|
+
"Style",
|
|
329
|
+
/* @__PURE__ */ jsx(
|
|
330
|
+
"select",
|
|
331
|
+
{
|
|
332
|
+
"aria-label": "Dashboard cell style",
|
|
333
|
+
value: style,
|
|
334
|
+
disabled: busy,
|
|
335
|
+
onChange: (event) => setStyle(event.target.value),
|
|
336
|
+
style: fieldStyle,
|
|
337
|
+
children: DASHBOARD_STYLES.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, title: option.description, children: option.label }, option.id))
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
] }),
|
|
341
|
+
/* @__PURE__ */ jsxs(
|
|
342
|
+
"label",
|
|
343
|
+
{
|
|
344
|
+
style: {
|
|
345
|
+
display: "flex",
|
|
346
|
+
alignItems: "center",
|
|
347
|
+
gap: 8,
|
|
348
|
+
marginBottom: 12,
|
|
349
|
+
fontSize: 13,
|
|
350
|
+
fontWeight: 600
|
|
351
|
+
},
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ jsx(
|
|
354
|
+
"input",
|
|
355
|
+
{
|
|
356
|
+
type: "checkbox",
|
|
357
|
+
checked: showTitle,
|
|
358
|
+
disabled: busy,
|
|
359
|
+
onChange: (event) => setShowTitle(event.target.checked)
|
|
360
|
+
}
|
|
361
|
+
),
|
|
362
|
+
"Include document title"
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
),
|
|
366
|
+
format !== "png" && /* @__PURE__ */ jsxs("label", { style: labelStyle, children: [
|
|
367
|
+
"Quality: ",
|
|
368
|
+
Math.round(quality * 100),
|
|
369
|
+
"%",
|
|
370
|
+
/* @__PURE__ */ jsx(
|
|
371
|
+
"input",
|
|
372
|
+
{
|
|
373
|
+
"aria-label": "Image quality",
|
|
374
|
+
type: "range",
|
|
375
|
+
min: 0.5,
|
|
376
|
+
max: 1,
|
|
377
|
+
step: 0.05,
|
|
378
|
+
value: quality,
|
|
379
|
+
disabled: busy,
|
|
380
|
+
onChange: (event) => setQuality(Number(event.target.value))
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
] }),
|
|
384
|
+
/* @__PURE__ */ jsxs("p", { style: { margin: "0 0 12px", color: muted, fontSize: 12 }, children: [
|
|
385
|
+
width.toLocaleString(),
|
|
386
|
+
" \xD7 ",
|
|
387
|
+
height.toLocaleString(),
|
|
388
|
+
" pixels"
|
|
389
|
+
] }),
|
|
390
|
+
(dimensionError || error) && /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: "0 0 12px", color: dark ? "#fca5a5" : "#b91c1c" }, children: dimensionError ?? error }),
|
|
391
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [
|
|
392
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: handleClose, children: "Cancel" }),
|
|
393
|
+
/* @__PURE__ */ jsx(
|
|
394
|
+
"button",
|
|
395
|
+
{
|
|
396
|
+
type: "button",
|
|
397
|
+
disabled: busy || !!dimensionError,
|
|
398
|
+
onClick: () => void handleExport(),
|
|
399
|
+
style: { minWidth: 132 },
|
|
400
|
+
children: busy ? "Rendering\u2026" : "Choose location\u2026"
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
] })
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/entries/dashboard-image.ts
|
|
412
|
+
import {
|
|
413
|
+
DASHBOARD_RESOLUTIONS as DASHBOARD_RESOLUTIONS2,
|
|
414
|
+
DEFAULT_DASHBOARD_RESOLUTION as DEFAULT_DASHBOARD_RESOLUTION2,
|
|
415
|
+
validateDashboardImageDimensions as validateDashboardImageDimensions2
|
|
416
|
+
} from "@bendyline/squisq-video";
|
|
417
|
+
export {
|
|
418
|
+
DASHBOARD_RESOLUTIONS2 as DASHBOARD_RESOLUTIONS,
|
|
419
|
+
DEFAULT_DASHBOARD_RESOLUTION2 as DEFAULT_DASHBOARD_RESOLUTION,
|
|
420
|
+
DashboardImageExportModal,
|
|
421
|
+
validateDashboardImageDimensions2 as validateDashboardImageDimensions
|
|
422
|
+
};
|
package/dist/hooks/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
resolveVideoCoverFramePlan,
|
|
4
4
|
resolveVideoExportCover,
|
|
5
5
|
useVideoExport
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-BGS2NEYC.js";
|
|
7
7
|
import {
|
|
8
8
|
useFrameCapture
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-4NI2QIFE.js";
|
|
10
10
|
import "../chunk-32QCPFXE.js";
|
|
11
11
|
import "../chunk-5MFQMJ5Z.js";
|
|
12
12
|
export {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared single-frame image-export helpers used by both raster export
|
|
3
|
+
* dialogs (CoverImageExportModal, DashboardImageExportModal): format
|
|
4
|
+
* metadata, canvas encoding, the File System Access save flow, and the
|
|
5
|
+
* download fallback. Pure module-level functions with no component state.
|
|
6
|
+
*/
|
|
7
|
+
type ImageExportFormat = 'png' | 'jpeg' | 'webp';
|
|
8
|
+
|
|
9
|
+
export type { ImageExportFormat as I };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,6 @@ export { supportsWebCodecsAac } from './encoder/index.js';
|
|
|
8
8
|
import 'react/jsx-runtime';
|
|
9
9
|
import '@bendyline/squisq/schemas';
|
|
10
10
|
import '@bendyline/squisq/doc';
|
|
11
|
+
import './imageExportShared-B6nqWtE-.js';
|
|
11
12
|
import '@bendyline/squisq/markdown';
|
|
12
13
|
import '@bendyline/squisq-react';
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VideoExportButton,
|
|
3
3
|
VideoExportModal
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JGSJQZGU.js";
|
|
5
5
|
import {
|
|
6
6
|
CoverImageExportModal,
|
|
7
7
|
coverImageFilename,
|
|
8
8
|
validateCoverImageDimensions
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-BRE3IRIM.js";
|
|
10
|
+
import "./chunk-3NJDHK62.js";
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
12
13
|
resolveVideoCoverFramePlan,
|
|
13
14
|
resolveVideoExportCover,
|
|
14
15
|
useVideoExport
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-BGS2NEYC.js";
|
|
16
17
|
import {
|
|
17
18
|
useFrameCapture
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-4NI2QIFE.js";
|
|
19
20
|
import {
|
|
20
21
|
createEncoder,
|
|
21
22
|
supportsWebCodecs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-video-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/cover-image/index.d.ts",
|
|
43
43
|
"import": "./dist/cover-image/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./dashboard-image": {
|
|
46
|
+
"types": "./dist/dashboard-image/index.d.ts",
|
|
47
|
+
"import": "./dist/dashboard-image/index.js"
|
|
48
|
+
},
|
|
45
49
|
"./hooks": {
|
|
46
50
|
"types": "./dist/hooks/index.d.ts",
|
|
47
51
|
"import": "./dist/hooks/index.js"
|
|
@@ -69,9 +73,9 @@
|
|
|
69
73
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
70
74
|
},
|
|
71
75
|
"dependencies": {
|
|
72
|
-
"@bendyline/squisq": "2.
|
|
73
|
-
"@bendyline/squisq-video": "2.
|
|
74
|
-
"@bendyline/squisq-react": "2.
|
|
76
|
+
"@bendyline/squisq": "2.8.0",
|
|
77
|
+
"@bendyline/squisq-video": "2.3.0",
|
|
78
|
+
"@bendyline/squisq-react": "2.8.0",
|
|
75
79
|
"@ffmpeg/core": "0.12.9",
|
|
76
80
|
"@ffmpeg/ffmpeg": "0.12.15",
|
|
77
81
|
"@ffmpeg/util": "0.12.2",
|