@bendyline/squisq-video-react 2.3.5 → 2.4.1

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.
@@ -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,6 @@
1
1
  import {
2
2
  useFrameCapture
3
- } from "./chunk-UYJ7G34U.js";
3
+ } from "./chunk-4NI2QIFE.js";
4
4
  import {
5
5
  EXPORT_AUDIO_CHANNELS,
6
6
  EXPORT_AUDIO_SAMPLE_RATE,
@@ -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-UYJ7G34U.js";
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
- 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);
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 chooseSaveTarget(filename, format);
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 canvasToBlob(canvas, format, quality);
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
- downloadBlob(blob, filename);
132
+ downloadImageBlob(blob, filename);
173
133
  }
174
134
  capture.destroy();
175
135
  onClose();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useVideoExport
3
- } from "./chunk-DIWB5BH4.js";
3
+ } from "./chunk-BGS2NEYC.js";
4
4
 
5
5
  // src/VideoExportModal.tsx
6
6
  import { useState, useCallback, useId, useRef } from "react";
@@ -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 */
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  VideoExportButton,
3
3
  VideoExportModal
4
- } from "../chunk-J2O4TPEO.js";
4
+ } from "../chunk-JGSJQZGU.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "../chunk-ZMDTR472.js";
10
- import "../chunk-DIWB5BH4.js";
11
- import "../chunk-UYJ7G34U.js";
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 = 'png' | 'jpeg' | 'webp';
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-ZMDTR472.js";
6
- import "../chunk-UYJ7G34U.js";
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
+ };
@@ -3,10 +3,10 @@ import {
3
3
  resolveVideoCoverFramePlan,
4
4
  resolveVideoExportCover,
5
5
  useVideoExport
6
- } from "../chunk-DIWB5BH4.js";
6
+ } from "../chunk-BGS2NEYC.js";
7
7
  import {
8
8
  useFrameCapture
9
- } from "../chunk-UYJ7G34U.js";
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-J2O4TPEO.js";
4
+ } from "./chunk-JGSJQZGU.js";
5
5
  import {
6
6
  CoverImageExportModal,
7
7
  coverImageFilename,
8
8
  validateCoverImageDimensions
9
- } from "./chunk-ZMDTR472.js";
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-DIWB5BH4.js";
16
+ } from "./chunk-BGS2NEYC.js";
16
17
  import {
17
18
  useFrameCapture
18
- } from "./chunk-UYJ7G34U.js";
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.5",
3
+ "version": "2.4.1",
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.7.1",
73
- "@bendyline/squisq-video": "2.2.12",
74
- "@bendyline/squisq-react": "2.7.2",
76
+ "@bendyline/squisq": "2.9.0",
77
+ "@bendyline/squisq-video": "2.3.1",
78
+ "@bendyline/squisq-react": "2.9.0",
75
79
  "@ffmpeg/core": "0.12.9",
76
80
  "@ffmpeg/ffmpeg": "0.12.15",
77
81
  "@ffmpeg/util": "0.12.2",