@agent-native/core 0.153.9 → 0.154.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +20 -30
- package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/lib/utils.ts +1 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
- package/corpus/templates/analytics/app/pages/monitoring/errors/IssueList.tsx +4 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
- package/corpus/templates/calendar/app/lib/utils.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/utils.ts +5 -6
- package/corpus/templates/clips/app/root.tsx +4 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -2
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
- package/corpus/templates/clips/desktop/src/app.tsx +27 -13
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
- package/corpus/templates/content/app/i18n-data.ts +10 -10
- package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
- package/corpus/templates/design/actions/get-design-system.ts +103 -3
- package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
- package/corpus/templates/design/actions/import-github.ts +32 -138
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
- package/corpus/templates/design/actions/present-design-variants.ts +93 -4
- package/corpus/templates/design/actions/run-design-audit.ts +229 -0
- package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/i18n-data.ts +32 -1
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
- package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
- package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +171 -0
- package/corpus/templates/mail/app/lib/utils.ts +1 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
- package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +58 -33
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +28 -41
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
- package/corpus/templates/slides/app/global.css +24 -0
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +5 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
- package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
- package/corpus/templates/slides/app/lib/image-download.ts +84 -0
- package/corpus/templates/slides/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +25 -9
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
- package/dist/agent/production-agent.js +1 -1
- package/dist/brand-kit/types.d.ts +10 -2
- package/dist/cli/code.js +1 -1
- package/dist/cli/index.js +30 -1
- package/dist/cli/native-dependencies.d.ts +24 -0
- package/dist/cli/native-dependencies.js +264 -0
- package/dist/cli/plan-local.js +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.js +2 -1
- package/dist/cli/skills.js +2 -1
- package/dist/cli/visualize-repo.js +1 -1
- package/dist/client/FeedbackButton.js +12 -12
- package/dist/client/NewWorkspaceAppFlow.js +4 -1
- package/dist/client/agent-chat-adapter.js +3 -4
- package/dist/client/agent-page/AgentTabsPage.d.ts +2 -2
- package/dist/client/agent-page/AgentTabsPage.js +12 -9
- package/dist/client/assistant-ui-recovery.d.ts +1 -1
- package/dist/client/assistant-ui-recovery.js +4 -0
- package/dist/client/chat/message-components.d.ts +11 -3
- package/dist/client/chat/message-components.js +31 -5
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.js +33 -15
- package/dist/client/db-admin/SqlEditor.js +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +2 -2
- package/dist/client/extensions/ExtensionsSidebarSection.js +2 -1
- package/dist/client/extensions/portable-extension.js +4 -0
- package/dist/client/guided-questions.d.ts +8 -0
- package/dist/client/guided-questions.js +12 -3
- package/dist/client/onboarding/FirstRunOnboarding.js +2 -1
- package/dist/client/org/TeamPage.js +14 -3
- package/dist/client/settings/SettingsPanel.js +26 -1
- package/dist/client/sharing/ShareButton.js +16 -24
- package/dist/client/sse-event-processor.js +35 -15
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +39 -0
- package/dist/client/vite-dev-recovery-script.js +1 -1
- package/dist/extensions/html-shell.js +5 -0
- package/dist/integrations/catalog.js +9 -8
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/prompt-resources.js +17 -16
- package/dist/server/agent-chat-plugin.d.ts +9 -0
- package/dist/server/agent-chat-plugin.js +31 -9
- package/dist/server/builder-design-systems.d.ts +33 -1
- package/dist/server/builder-design-systems.js +293 -11
- package/dist/server/design-token-utils.d.ts +11 -0
- package/dist/server/design-token-utils.js +68 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/shared/docs-url.d.ts +24 -0
- package/dist/shared/docs-url.js +39 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/default/app/routes/_index.tsx +2 -1
- package/package.json +2 -2
- package/src/templates/default/app/routes/_index.tsx +2 -1
|
@@ -27,6 +27,70 @@ function triggerBlobDownload(blob: Blob, filename: string) {
|
|
|
27
27
|
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const IMAGE_SETTLE_TIMEOUT_MS = 5_000;
|
|
31
|
+
|
|
32
|
+
function waitForImageToSettle(image: HTMLImageElement): Promise<void> {
|
|
33
|
+
return new Promise<void>((resolve, reject) => {
|
|
34
|
+
let decodeStarted = false;
|
|
35
|
+
let settled = false;
|
|
36
|
+
let timeoutId: number | undefined;
|
|
37
|
+
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
image.removeEventListener("error", handleLoad);
|
|
40
|
+
image.removeEventListener("load", handleLoad);
|
|
41
|
+
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
|
42
|
+
};
|
|
43
|
+
const finish = () => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
const fail = (error: unknown) => {
|
|
50
|
+
if (settled) return;
|
|
51
|
+
settled = true;
|
|
52
|
+
cleanup();
|
|
53
|
+
reject(error);
|
|
54
|
+
};
|
|
55
|
+
const decode = () => {
|
|
56
|
+
if (decodeStarted) return;
|
|
57
|
+
decodeStarted = true;
|
|
58
|
+
if (!image.complete || image.naturalWidth <= 0) {
|
|
59
|
+
finish();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
Promise.resolve()
|
|
63
|
+
.then(() => image.decode())
|
|
64
|
+
.then(finish, (error: unknown) => {
|
|
65
|
+
if (error instanceof Error && error.name === "EncodingError") {
|
|
66
|
+
finish();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
fail(error);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const handleLoad = () => decode();
|
|
73
|
+
|
|
74
|
+
timeoutId = window.setTimeout(finish, IMAGE_SETTLE_TIMEOUT_MS);
|
|
75
|
+
image.addEventListener("error", handleLoad);
|
|
76
|
+
image.addEventListener("load", handleLoad);
|
|
77
|
+
// The image can finish between the initial state read and listener setup.
|
|
78
|
+
if (image.complete) decode();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function waitForImagesToSettle(root: HTMLElement) {
|
|
83
|
+
const images = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
|
|
84
|
+
await Promise.all(images.map((image) => waitForImageToSettle(image)));
|
|
85
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
86
|
+
await new Promise<void>((resolve) => {
|
|
87
|
+
window.requestAnimationFrame(() =>
|
|
88
|
+
window.requestAnimationFrame(() => resolve()),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
30
94
|
function escapeXml(value: string) {
|
|
31
95
|
return value
|
|
32
96
|
.replace(/&/g, "&")
|
|
@@ -186,6 +250,11 @@ function createUnscaledExportClone(
|
|
|
186
250
|
source: HTMLElement,
|
|
187
251
|
dims: { width: number; height: number },
|
|
188
252
|
) {
|
|
253
|
+
const sourceRect = source.getBoundingClientRect();
|
|
254
|
+
const imageGeometry = collectImageGeometry(source);
|
|
255
|
+
const textGeometry = collectTextGeometry(source);
|
|
256
|
+
const positionedGeometry = collectPositionedGeometry(source);
|
|
257
|
+
|
|
189
258
|
const stage = document.createElement("div");
|
|
190
259
|
stage.setAttribute("aria-hidden", "true");
|
|
191
260
|
Object.assign(stage.style, {
|
|
@@ -215,9 +284,547 @@ function createUnscaledExportClone(
|
|
|
215
284
|
return {
|
|
216
285
|
element: clone,
|
|
217
286
|
cleanup: () => stage.remove(),
|
|
287
|
+
imageGeometry,
|
|
288
|
+
positionedGeometry,
|
|
289
|
+
textGeometry,
|
|
290
|
+
sourceRect,
|
|
218
291
|
};
|
|
219
292
|
}
|
|
220
293
|
|
|
294
|
+
interface ElementPathRecord {
|
|
295
|
+
path: number[];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface PositionedGeometryRecord extends ElementPathRecord {
|
|
299
|
+
rect: DOMRect;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface ImageGeometryRecord extends ElementPathRecord {
|
|
303
|
+
position: string;
|
|
304
|
+
rect: DOMRect;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface TextGeometryRecord extends ElementPathRecord {
|
|
308
|
+
fontSize: number;
|
|
309
|
+
heading: boolean;
|
|
310
|
+
letterSpacing: number;
|
|
311
|
+
lineHeight: number;
|
|
312
|
+
position: string;
|
|
313
|
+
rect: DOMRect;
|
|
314
|
+
scaleX: number;
|
|
315
|
+
scaleY: number;
|
|
316
|
+
singleLine: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getElementPath(root: HTMLElement, element: HTMLElement) {
|
|
320
|
+
const path: number[] = [];
|
|
321
|
+
let current: HTMLElement | null = element;
|
|
322
|
+
|
|
323
|
+
while (current && current !== root) {
|
|
324
|
+
const parent: HTMLElement | null = current.parentElement;
|
|
325
|
+
if (!parent) return null;
|
|
326
|
+
const index = Array.prototype.indexOf.call(parent.children, current);
|
|
327
|
+
if (index < 0) return null;
|
|
328
|
+
path.unshift(index);
|
|
329
|
+
current = parent;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return current === root ? path : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getElementAtPath(root: HTMLElement, path: number[]) {
|
|
336
|
+
let current: Element = root;
|
|
337
|
+
for (const index of path) {
|
|
338
|
+
const child = current.children[index];
|
|
339
|
+
if (!(child instanceof HTMLElement)) return null;
|
|
340
|
+
current = child;
|
|
341
|
+
}
|
|
342
|
+
return current instanceof HTMLElement ? current : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isPositionedElement(element: HTMLElement) {
|
|
346
|
+
const position = window.getComputedStyle(element).position;
|
|
347
|
+
return position === "absolute" || position === "fixed";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function hasPositionedAncestor(element: HTMLElement, root: HTMLElement) {
|
|
351
|
+
let parent = element.parentElement;
|
|
352
|
+
while (parent && parent !== root) {
|
|
353
|
+
if (isPositionedElement(parent)) return true;
|
|
354
|
+
parent = parent.parentElement;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isTextGeometryCandidate(element: HTMLElement) {
|
|
360
|
+
if (!element.textContent?.trim()) return false;
|
|
361
|
+
if (element.querySelector("img,svg,video,canvas")) return false;
|
|
362
|
+
if (element.closest('[aria-hidden="true"]')) return false;
|
|
363
|
+
if (
|
|
364
|
+
element.tagName === "LI" ||
|
|
365
|
+
element.tagName === "UL" ||
|
|
366
|
+
element.tagName === "OL"
|
|
367
|
+
) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const isHeading = /^H[1-3]$/.test(element.tagName);
|
|
371
|
+
const isTextBlock = element.tagName === "P";
|
|
372
|
+
const isLeafText =
|
|
373
|
+
element.tagName === "DIV" &&
|
|
374
|
+
(element.children.length === 0 ||
|
|
375
|
+
element.hasAttribute("data-slide-object-id"));
|
|
376
|
+
if (!isHeading && !isTextBlock && !isLeafText) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const style = window.getComputedStyle(element);
|
|
381
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
const rect = element.getBoundingClientRect();
|
|
385
|
+
return rect.width > 0 && rect.height > 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function computedLength(value: string, fallback: number) {
|
|
389
|
+
const parsed = Number.parseFloat(value);
|
|
390
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectImageGeometry(root: HTMLElement) {
|
|
394
|
+
return Array.from(root.querySelectorAll<HTMLImageElement>("img")).flatMap(
|
|
395
|
+
(element): ImageGeometryRecord[] => {
|
|
396
|
+
const style = window.getComputedStyle(element);
|
|
397
|
+
const rect = element.getBoundingClientRect();
|
|
398
|
+
const path = getElementPath(root, element);
|
|
399
|
+
if (
|
|
400
|
+
!path ||
|
|
401
|
+
style.display === "none" ||
|
|
402
|
+
style.visibility === "hidden" ||
|
|
403
|
+
!rect.width ||
|
|
404
|
+
!rect.height
|
|
405
|
+
) {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
return [{ path, position: style.position, rect }];
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function collectTextGeometry(root: HTMLElement) {
|
|
414
|
+
return Array.from(root.querySelectorAll<HTMLElement>("h1,h2,h3,p,div"))
|
|
415
|
+
.filter(isTextGeometryCandidate)
|
|
416
|
+
.flatMap((element): TextGeometryRecord[] => {
|
|
417
|
+
const path = getElementPath(root, element);
|
|
418
|
+
if (!path) return [];
|
|
419
|
+
|
|
420
|
+
const style = window.getComputedStyle(element);
|
|
421
|
+
const rect = element.getBoundingClientRect();
|
|
422
|
+
const layoutWidth = computedLength(style.width, rect.width);
|
|
423
|
+
const layoutHeight = computedLength(style.height, rect.height);
|
|
424
|
+
const fontSize = computedLength(style.fontSize, 16);
|
|
425
|
+
const lineHeight = computedLength(
|
|
426
|
+
style.lineHeight,
|
|
427
|
+
Math.max(16, fontSize * 1.2),
|
|
428
|
+
);
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
fontSize,
|
|
432
|
+
heading: /^H[1-3]$/.test(element.tagName),
|
|
433
|
+
letterSpacing: computedLength(style.letterSpacing, 0),
|
|
434
|
+
lineHeight,
|
|
435
|
+
path,
|
|
436
|
+
position: style.position,
|
|
437
|
+
rect,
|
|
438
|
+
scaleX: rect.width / Math.max(1, layoutWidth),
|
|
439
|
+
scaleY: rect.height / Math.max(1, layoutHeight),
|
|
440
|
+
singleLine: rect.height <= lineHeight * 1.35,
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The live slide is nested inside a positioned presentation wrapper, but the
|
|
448
|
+
* export clone is not. Record only the outermost positioned descendants so
|
|
449
|
+
* the clone can keep its source-space geometry without flattening nested
|
|
450
|
+
* objects or changing the editable object hierarchy.
|
|
451
|
+
*/
|
|
452
|
+
function collectPositionedGeometry(root: HTMLElement) {
|
|
453
|
+
return Array.from(root.querySelectorAll<HTMLElement>("*"))
|
|
454
|
+
.filter((element) => {
|
|
455
|
+
if (!isPositionedElement(element)) return false;
|
|
456
|
+
if (hasPositionedAncestor(element, root)) return false;
|
|
457
|
+
const rect = element.getBoundingClientRect();
|
|
458
|
+
return rect.width > 0 && rect.height > 0;
|
|
459
|
+
})
|
|
460
|
+
.flatMap((element): PositionedGeometryRecord[] => {
|
|
461
|
+
const path = getElementPath(root, element);
|
|
462
|
+
return path ? [{ path, rect: element.getBoundingClientRect() }] : [];
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function restorePositionedGeometry(
|
|
467
|
+
clone: HTMLElement,
|
|
468
|
+
sourceRect: DOMRect,
|
|
469
|
+
records: PositionedGeometryRecord[],
|
|
470
|
+
dims: { width: number; height: number },
|
|
471
|
+
) {
|
|
472
|
+
const cloneRect = clone.getBoundingClientRect();
|
|
473
|
+
if (
|
|
474
|
+
!sourceRect.width ||
|
|
475
|
+
!sourceRect.height ||
|
|
476
|
+
!cloneRect.width ||
|
|
477
|
+
!cloneRect.height
|
|
478
|
+
) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const scaleX = dims.width / sourceRect.width;
|
|
483
|
+
const scaleY = dims.height / sourceRect.height;
|
|
484
|
+
|
|
485
|
+
for (const record of records) {
|
|
486
|
+
const element = getElementAtPath(clone, record.path);
|
|
487
|
+
if (!element) continue;
|
|
488
|
+
|
|
489
|
+
const currentRect = element.getBoundingClientRect();
|
|
490
|
+
const desiredLeft = (record.rect.left - sourceRect.left) * scaleX;
|
|
491
|
+
const desiredTop = (record.rect.top - sourceRect.top) * scaleY;
|
|
492
|
+
const currentLeft = currentRect.left - cloneRect.left;
|
|
493
|
+
const currentTop = currentRect.top - cloneRect.top;
|
|
494
|
+
const currentStyle = window.getComputedStyle(element);
|
|
495
|
+
const currentCssLeft = Number.parseFloat(currentStyle.left);
|
|
496
|
+
const currentCssTop = Number.parseFloat(currentStyle.top);
|
|
497
|
+
|
|
498
|
+
element.style.position = "absolute";
|
|
499
|
+
element.style.right = "auto";
|
|
500
|
+
element.style.bottom = "auto";
|
|
501
|
+
element.style.left = `${(Number.isFinite(currentCssLeft)
|
|
502
|
+
? currentCssLeft + (desiredLeft - currentLeft)
|
|
503
|
+
: desiredLeft
|
|
504
|
+
).toFixed(3)}px`;
|
|
505
|
+
element.style.top = `${(Number.isFinite(currentCssTop)
|
|
506
|
+
? currentCssTop + (desiredTop - currentTop)
|
|
507
|
+
: desiredTop
|
|
508
|
+
).toFixed(3)}px`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function resetAutofitTransforms(root: HTMLElement) {
|
|
513
|
+
for (const layer of root.querySelectorAll<HTMLElement>(
|
|
514
|
+
"[data-fmd-autofit-content]",
|
|
515
|
+
)) {
|
|
516
|
+
layer.style.transform = "none";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function restoreTextGeometry(
|
|
521
|
+
clone: HTMLElement,
|
|
522
|
+
sourceRect: DOMRect,
|
|
523
|
+
records: TextGeometryRecord[],
|
|
524
|
+
dims: { width: number; height: number },
|
|
525
|
+
) {
|
|
526
|
+
if (!sourceRect.width || !sourceRect.height) return;
|
|
527
|
+
|
|
528
|
+
const scaleX = dims.width / sourceRect.width;
|
|
529
|
+
const scaleY = dims.height / sourceRect.height;
|
|
530
|
+
for (const record of records) {
|
|
531
|
+
const element = getElementAtPath(clone, record.path);
|
|
532
|
+
if (!element) continue;
|
|
533
|
+
|
|
534
|
+
let ancestor = element.parentElement;
|
|
535
|
+
while (ancestor && ancestor !== clone) {
|
|
536
|
+
if (window.getComputedStyle(ancestor).transform !== "none") {
|
|
537
|
+
ancestor.style.transform = "none";
|
|
538
|
+
}
|
|
539
|
+
ancestor = ancestor.parentElement;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const cloneRect = clone.getBoundingClientRect();
|
|
543
|
+
const desiredLeft = (record.rect.left - sourceRect.left) * scaleX;
|
|
544
|
+
const desiredTop = (record.rect.top - sourceRect.top) * scaleY;
|
|
545
|
+
const currentRect = element.getBoundingClientRect();
|
|
546
|
+
const currentLeft = currentRect.left - cloneRect.left;
|
|
547
|
+
const currentTop = currentRect.top - cloneRect.top;
|
|
548
|
+
const translateX = desiredLeft - currentLeft;
|
|
549
|
+
const translateY = desiredTop - currentTop;
|
|
550
|
+
|
|
551
|
+
if (record.position === "static" || record.position === "relative") {
|
|
552
|
+
element.dataset.exportTextGeometry = "true";
|
|
553
|
+
if (record.singleLine) element.style.whiteSpace = "nowrap";
|
|
554
|
+
element.style.left = `${translateX.toFixed(3)}px`;
|
|
555
|
+
element.style.position = "relative";
|
|
556
|
+
element.style.top = `${translateY.toFixed(3)}px`;
|
|
557
|
+
element.style.transform = "none";
|
|
558
|
+
if (Math.abs(record.scaleY - 1) >= 0.01) {
|
|
559
|
+
element.style.fontSize = `${Math.max(1, record.fontSize * record.scaleY)}px`;
|
|
560
|
+
element.style.letterSpacing = `${(record.letterSpacing * record.scaleX).toFixed(3)}px`;
|
|
561
|
+
element.style.lineHeight = record.lineHeight
|
|
562
|
+
? `${(record.lineHeight * record.scaleY).toFixed(3)}px`
|
|
563
|
+
: "normal";
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
element.dataset.exportTextGeometry = "true";
|
|
569
|
+
if (record.singleLine) element.style.whiteSpace = "nowrap";
|
|
570
|
+
element.style.boxSizing = "border-box";
|
|
571
|
+
element.style.bottom = "auto";
|
|
572
|
+
element.style.display = "block";
|
|
573
|
+
element.style.fontSize = `${Math.max(1, record.fontSize * record.scaleY)}px`;
|
|
574
|
+
element.style.height = `${Math.max(1, record.rect.height * scaleY)}px`;
|
|
575
|
+
element.style.left = `${desiredLeft.toFixed(3)}px`;
|
|
576
|
+
element.style.letterSpacing = `${(record.letterSpacing * record.scaleX).toFixed(3)}px`;
|
|
577
|
+
element.style.lineHeight = record.lineHeight
|
|
578
|
+
? `${(record.lineHeight * record.scaleY).toFixed(3)}px`
|
|
579
|
+
: "normal";
|
|
580
|
+
element.style.margin = "0";
|
|
581
|
+
element.style.maxHeight = "none";
|
|
582
|
+
element.style.maxWidth = "none";
|
|
583
|
+
element.style.minHeight = "0";
|
|
584
|
+
element.style.minWidth = "0";
|
|
585
|
+
element.style.position = "absolute";
|
|
586
|
+
element.style.right = "auto";
|
|
587
|
+
element.style.top = `${desiredTop.toFixed(3)}px`;
|
|
588
|
+
element.style.transform = "none";
|
|
589
|
+
element.style.width = `${Math.max(1, record.rect.width * scaleX)}px`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function restoreImageGeometry(
|
|
594
|
+
clone: HTMLElement,
|
|
595
|
+
sourceRect: DOMRect,
|
|
596
|
+
records: ImageGeometryRecord[],
|
|
597
|
+
dims: { width: number; height: number },
|
|
598
|
+
) {
|
|
599
|
+
if (!sourceRect.width || !sourceRect.height) return;
|
|
600
|
+
|
|
601
|
+
const cloneRect = clone.getBoundingClientRect();
|
|
602
|
+
const scaleX = dims.width / sourceRect.width;
|
|
603
|
+
const scaleY = dims.height / sourceRect.height;
|
|
604
|
+
for (const record of records) {
|
|
605
|
+
const element = getElementAtPath(
|
|
606
|
+
clone,
|
|
607
|
+
record.path,
|
|
608
|
+
) as HTMLImageElement | null;
|
|
609
|
+
if (!element) continue;
|
|
610
|
+
|
|
611
|
+
const desiredLeft = (record.rect.left - sourceRect.left) * scaleX;
|
|
612
|
+
const desiredTop = (record.rect.top - sourceRect.top) * scaleY;
|
|
613
|
+
const desiredWidth = record.rect.width * scaleX;
|
|
614
|
+
const desiredHeight = record.rect.height * scaleY;
|
|
615
|
+
element.style.boxSizing = "border-box";
|
|
616
|
+
element.style.height = `${Math.max(1, desiredHeight)}px`;
|
|
617
|
+
element.style.maxHeight = "none";
|
|
618
|
+
element.style.maxWidth = "none";
|
|
619
|
+
element.style.width = `${Math.max(1, desiredWidth)}px`;
|
|
620
|
+
|
|
621
|
+
if (record.position === "absolute" || record.position === "fixed") {
|
|
622
|
+
element.style.bottom = "auto";
|
|
623
|
+
element.style.left = `${desiredLeft.toFixed(3)}px`;
|
|
624
|
+
element.style.position = "absolute";
|
|
625
|
+
element.style.right = "auto";
|
|
626
|
+
element.style.top = `${desiredTop.toFixed(3)}px`;
|
|
627
|
+
element.style.transform = "none";
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const currentRect = element.getBoundingClientRect();
|
|
632
|
+
const currentLeft = currentRect.left - cloneRect.left;
|
|
633
|
+
const currentTop = currentRect.top - cloneRect.top;
|
|
634
|
+
element.style.transform = `translate(${(desiredLeft - currentLeft).toFixed(3)}px, ${(desiredTop - currentTop).toFixed(3)}px)`;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function normalizeSingleLineText(
|
|
639
|
+
clone: HTMLElement,
|
|
640
|
+
records: TextGeometryRecord[],
|
|
641
|
+
) {
|
|
642
|
+
const cloneRect = clone.getBoundingClientRect();
|
|
643
|
+
for (const record of records) {
|
|
644
|
+
if (!record.singleLine) continue;
|
|
645
|
+
const element = getElementAtPath(clone, record.path);
|
|
646
|
+
if (!element) continue;
|
|
647
|
+
|
|
648
|
+
const rect = element.getBoundingClientRect();
|
|
649
|
+
if (!rect.width || !rect.height) continue;
|
|
650
|
+
|
|
651
|
+
element.dataset.exportSingleLineText = "true";
|
|
652
|
+
if (element.dataset.exportTextGeometry === "true") continue;
|
|
653
|
+
element.style.boxSizing = "border-box";
|
|
654
|
+
element.style.whiteSpace = "nowrap";
|
|
655
|
+
if (record.heading) {
|
|
656
|
+
element.style.maxWidth = "none";
|
|
657
|
+
element.style.width = `${Math.max(1, Math.ceil(cloneRect.right - rect.left))}px`;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const buffer = Math.max(24, rect.width * 0.25);
|
|
662
|
+
const available = Math.max(rect.width, cloneRect.right - rect.left);
|
|
663
|
+
element.style.width = `${Math.max(
|
|
664
|
+
1,
|
|
665
|
+
Math.ceil(Math.min(rect.width + buffer, available)),
|
|
666
|
+
)}px`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* CSS markers live outside the LI box, so dom-to-pptx cannot infer the gap
|
|
672
|
+
* between a bullet and its text from getBoundingClientRect alone. Add that
|
|
673
|
+
* source-visible gap only on the export clone; the source DOM stays editable.
|
|
674
|
+
*/
|
|
675
|
+
function normalizeListsForPptx(root: HTMLElement) {
|
|
676
|
+
const bulletIndents: number[] = [];
|
|
677
|
+
|
|
678
|
+
for (const list of root.querySelectorAll<HTMLElement>("ul,ol")) {
|
|
679
|
+
const listStyle = window.getComputedStyle(list);
|
|
680
|
+
const listStyleType = listStyle.listStyleType || "disc";
|
|
681
|
+
const paddingLeft = Number.parseFloat(listStyle.paddingLeft) || 0;
|
|
682
|
+
if (
|
|
683
|
+
paddingLeft > 0 &&
|
|
684
|
+
listStyle.listStylePosition === "inside" &&
|
|
685
|
+
listStyle.transform === "none"
|
|
686
|
+
) {
|
|
687
|
+
list.style.transform = `translateX(${paddingLeft}px)`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const item of list.children) {
|
|
691
|
+
if (!(item instanceof HTMLElement) || item.tagName !== "LI") continue;
|
|
692
|
+
const itemStyle = window.getComputedStyle(item);
|
|
693
|
+
const currentMarginLeft = Number.parseFloat(itemStyle.marginLeft) || 0;
|
|
694
|
+
const markerStyle = window.getComputedStyle(item, "::marker");
|
|
695
|
+
const markerSize = Number.parseFloat(markerStyle.fontSize) || 20;
|
|
696
|
+
const markerGap = Math.max(24, markerSize * 1.2);
|
|
697
|
+
if (currentMarginLeft < markerGap) {
|
|
698
|
+
item.style.marginLeft = `${markerGap}px`;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (listStyleType === "none") continue;
|
|
703
|
+
const firstItem = Array.from(list.children).find(
|
|
704
|
+
(item): item is HTMLElement =>
|
|
705
|
+
item instanceof HTMLElement && item.tagName === "LI",
|
|
706
|
+
);
|
|
707
|
+
if (!firstItem) continue;
|
|
708
|
+
|
|
709
|
+
const listRect = list.getBoundingClientRect();
|
|
710
|
+
const itemRect = firstItem.getBoundingClientRect();
|
|
711
|
+
if (!listRect.width || !itemRect.width) continue;
|
|
712
|
+
|
|
713
|
+
const visualIndentPx = itemRect.left - listRect.left;
|
|
714
|
+
bulletIndents.push(Math.max(0, (visualIndentPx - paddingLeft) * 0.75));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return bulletIndents;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const EMU_PER_POINT = 12_700;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* dom-to-pptx prepends a bullet run, then PptxGenJS lets the following text
|
|
724
|
+
* run overwrite the paragraph properties. Restore the measured indent at the
|
|
725
|
+
* package boundary so Google Slides receives both the marker and its gap.
|
|
726
|
+
*/
|
|
727
|
+
function patchBulletIndentsInXml(xml: string, indentPoints: number[]) {
|
|
728
|
+
let result = "";
|
|
729
|
+
let cursor = 0;
|
|
730
|
+
let listIndex = 0;
|
|
731
|
+
|
|
732
|
+
while (true) {
|
|
733
|
+
const shapeStart = xml.indexOf("<p:sp>", cursor);
|
|
734
|
+
if (shapeStart < 0) {
|
|
735
|
+
result += xml.slice(cursor);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const shapeEnd = xml.indexOf("</p:sp>", shapeStart);
|
|
740
|
+
if (shapeEnd < 0) {
|
|
741
|
+
result += xml.slice(cursor);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
result += xml.slice(cursor, shapeStart);
|
|
746
|
+
let shapeXml = xml.slice(shapeStart, shapeEnd + "</p:sp>".length);
|
|
747
|
+
if (
|
|
748
|
+
shapeXml.includes("<a:buChar") &&
|
|
749
|
+
indentPoints[listIndex] !== undefined
|
|
750
|
+
) {
|
|
751
|
+
const indent = Math.max(
|
|
752
|
+
0,
|
|
753
|
+
Math.round(indentPoints[listIndex] * EMU_PER_POINT),
|
|
754
|
+
);
|
|
755
|
+
let patchedShape = "";
|
|
756
|
+
let shapeCursor = 0;
|
|
757
|
+
|
|
758
|
+
while (true) {
|
|
759
|
+
const bulletStart = shapeXml.indexOf("<a:buChar", shapeCursor);
|
|
760
|
+
if (bulletStart < 0) {
|
|
761
|
+
patchedShape += shapeXml.slice(shapeCursor);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const paragraphStart = shapeXml.lastIndexOf("<a:pPr", bulletStart);
|
|
766
|
+
const paragraphEnd = shapeXml.indexOf("</a:pPr>", bulletStart);
|
|
767
|
+
if (paragraphStart < shapeCursor || paragraphEnd < 0) {
|
|
768
|
+
patchedShape += shapeXml.slice(shapeCursor);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
patchedShape += shapeXml.slice(shapeCursor, paragraphStart);
|
|
773
|
+
const paragraphXml = shapeXml.slice(
|
|
774
|
+
paragraphStart,
|
|
775
|
+
paragraphEnd + "</a:pPr>".length,
|
|
776
|
+
);
|
|
777
|
+
const openingEnd = paragraphXml.indexOf(">");
|
|
778
|
+
const openingTag = paragraphXml
|
|
779
|
+
.slice(0, openingEnd)
|
|
780
|
+
.replace(/\s(?:marL|indent)="[^"]*"/g, "");
|
|
781
|
+
patchedShape +=
|
|
782
|
+
`${openingTag} marL="${indent}" indent="-${indent}"` +
|
|
783
|
+
paragraphXml.slice(openingEnd);
|
|
784
|
+
shapeCursor = paragraphEnd + "</a:pPr>".length;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
shapeXml = patchedShape;
|
|
788
|
+
listIndex += 1;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
result += shapeXml;
|
|
792
|
+
cursor = shapeEnd + "</p:sp>".length;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export async function patchBulletIndentsInPptxBlob(
|
|
799
|
+
blob: Blob,
|
|
800
|
+
slideBulletIndents: number[][],
|
|
801
|
+
) {
|
|
802
|
+
if (!slideBulletIndents.some((indents) => indents.length > 0)) return blob;
|
|
803
|
+
|
|
804
|
+
const { default: JSZip } = await importExportModule(() => import("jszip"));
|
|
805
|
+
const zip = await JSZip.loadAsync(blob);
|
|
806
|
+
|
|
807
|
+
for (let i = 0; i < slideBulletIndents.length; i++) {
|
|
808
|
+
const indents = slideBulletIndents[i];
|
|
809
|
+
if (!indents.length) continue;
|
|
810
|
+
|
|
811
|
+
const slideFile = zip.file(`ppt/slides/slide${i + 1}.xml`);
|
|
812
|
+
if (!slideFile) continue;
|
|
813
|
+
|
|
814
|
+
const slideXml = await slideFile.async("string");
|
|
815
|
+
zip.file(
|
|
816
|
+
`ppt/slides/slide${i + 1}.xml`,
|
|
817
|
+
patchBulletIndentsInXml(slideXml, indents),
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return zip.generateAsync({
|
|
822
|
+
type: "blob",
|
|
823
|
+
compression: "DEFLATE",
|
|
824
|
+
compressionOptions: { level: 6 },
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
221
828
|
function svgDataUrl(svg: SVGSVGElement) {
|
|
222
829
|
const copy = svg.cloneNode(true) as SVGSVGElement;
|
|
223
830
|
if (!copy.getAttribute("xmlns")) {
|
|
@@ -300,6 +907,7 @@ function widenNoWrapTextElements(root: HTMLElement) {
|
|
|
300
907
|
for (const element of elements) {
|
|
301
908
|
if (!element.textContent?.trim()) continue;
|
|
302
909
|
if (element.querySelector("img,svg,video,canvas")) continue;
|
|
910
|
+
if (element.dataset.exportSingleLineText === "true") continue;
|
|
303
911
|
const style = window.getComputedStyle(element);
|
|
304
912
|
if (style.whiteSpace !== "nowrap" && style.whiteSpace !== "pre") continue;
|
|
305
913
|
const rect = element.getBoundingClientRect();
|
|
@@ -419,20 +1027,44 @@ export async function buildDeckPptxBlob(
|
|
|
419
1027
|
element: HTMLElement;
|
|
420
1028
|
cleanup: () => void;
|
|
421
1029
|
}> = [];
|
|
1030
|
+
const slideBulletIndents: number[][] = [];
|
|
422
1031
|
|
|
423
1032
|
try {
|
|
424
1033
|
for (let i = 0; i < slides.length; i++) {
|
|
425
1034
|
const exportSlide = slides[i];
|
|
426
1035
|
const source = findSlideExportSource(exportSlide.id, i, slides.length);
|
|
1036
|
+
await waitForImagesToSettle(source);
|
|
427
1037
|
const clone = createUnscaledExportClone(source, {
|
|
428
1038
|
width: dims.width,
|
|
429
1039
|
height: dims.height,
|
|
430
1040
|
});
|
|
431
1041
|
exportClones.push(clone);
|
|
1042
|
+
await preloadImagesWithCors(clone.element);
|
|
1043
|
+
resetAutofitTransforms(clone.element);
|
|
1044
|
+
slideBulletIndents.push(normalizeListsForPptx(clone.element));
|
|
1045
|
+
restorePositionedGeometry(
|
|
1046
|
+
clone.element,
|
|
1047
|
+
clone.sourceRect,
|
|
1048
|
+
clone.positionedGeometry,
|
|
1049
|
+
dims,
|
|
1050
|
+
);
|
|
1051
|
+
restoreTextGeometry(
|
|
1052
|
+
clone.element,
|
|
1053
|
+
clone.sourceRect,
|
|
1054
|
+
clone.textGeometry,
|
|
1055
|
+
dims,
|
|
1056
|
+
);
|
|
1057
|
+
normalizeSingleLineText(clone.element, clone.textGeometry);
|
|
432
1058
|
materializeImportedBackgroundGrid(clone.element);
|
|
433
1059
|
widenNoWrapTextElements(clone.element);
|
|
434
1060
|
await replaceInlineSvgsWithImages(clone.element);
|
|
435
1061
|
await preloadImagesWithCors(clone.element);
|
|
1062
|
+
restoreImageGeometry(
|
|
1063
|
+
clone.element,
|
|
1064
|
+
clone.sourceRect,
|
|
1065
|
+
clone.imageGeometry,
|
|
1066
|
+
dims,
|
|
1067
|
+
);
|
|
436
1068
|
}
|
|
437
1069
|
|
|
438
1070
|
const initialBlob = await exportToPptx(
|
|
@@ -447,7 +1079,11 @@ export async function buildDeckPptxBlob(
|
|
|
447
1079
|
},
|
|
448
1080
|
);
|
|
449
1081
|
|
|
450
|
-
const
|
|
1082
|
+
const bulletPatchedBlob = await patchBulletIndentsInPptxBlob(
|
|
1083
|
+
initialBlob,
|
|
1084
|
+
slideBulletIndents,
|
|
1085
|
+
);
|
|
1086
|
+
const blob = await addSpeakerNotesToPptxBlob(bulletPatchedBlob, slides);
|
|
451
1087
|
return { blob, filename: safePptxName(deckTitle) };
|
|
452
1088
|
} finally {
|
|
453
1089
|
for (const clone of exportClones) {
|