@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.
Files changed (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +20 -30
  3. package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  5. package/corpus/templates/analytics/app/lib/utils.ts +1 -1
  6. package/corpus/templates/analytics/app/pages/DataSources.tsx +2 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
  8. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueList.tsx +4 -2
  9. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -2
  10. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
  11. package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
  12. package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
  13. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
  14. package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
  15. package/corpus/templates/calendar/app/lib/utils.ts +1 -1
  16. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
  17. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
  18. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  19. package/corpus/templates/clips/app/lib/utils.ts +5 -6
  20. package/corpus/templates/clips/app/root.tsx +4 -1
  21. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -2
  22. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
  23. package/corpus/templates/clips/desktop/src/app.tsx +27 -13
  24. package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
  25. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
  26. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
  27. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
  28. package/corpus/templates/content/app/i18n-data.ts +10 -10
  29. package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
  30. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
  31. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
  32. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
  33. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
  34. package/corpus/templates/design/actions/get-design-system.ts +103 -3
  35. package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
  36. package/corpus/templates/design/actions/import-github.ts +32 -138
  37. package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
  38. package/corpus/templates/design/actions/present-design-variants.ts +93 -4
  39. package/corpus/templates/design/actions/run-design-audit.ts +229 -0
  40. package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
  41. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +2 -1
  42. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
  43. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
  44. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
  45. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
  46. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
  47. package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
  48. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  49. package/corpus/templates/design/app/i18n-data.ts +32 -1
  50. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
  51. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
  52. package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
  53. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
  54. package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
  60. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
  61. package/corpus/templates/design/shared/design-review.ts +1 -0
  62. package/corpus/templates/design/shared/html-integrity.ts +171 -0
  63. package/corpus/templates/mail/app/lib/utils.ts +1 -1
  64. package/corpus/templates/plan/app/pages/PlansPage.tsx +5 -2
  65. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
  66. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
  67. package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
  68. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  69. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
  70. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +58 -33
  71. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +28 -41
  72. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
  73. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
  74. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
  75. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
  76. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
  77. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
  78. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
  79. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
  80. package/corpus/templates/slides/app/global.css +24 -0
  81. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +5 -1
  82. package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
  83. package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
  84. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
  85. package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
  86. package/corpus/templates/slides/app/lib/image-download.ts +84 -0
  87. package/corpus/templates/slides/app/lib/utils.ts +1 -1
  88. package/corpus/templates/slides/app/pages/Index.tsx +25 -9
  89. package/corpus/templates/slides/netlify.toml +1 -0
  90. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
  91. package/dist/agent/production-agent.js +1 -1
  92. package/dist/brand-kit/types.d.ts +10 -2
  93. package/dist/cli/code.js +1 -1
  94. package/dist/cli/index.js +30 -1
  95. package/dist/cli/native-dependencies.d.ts +24 -0
  96. package/dist/cli/native-dependencies.js +264 -0
  97. package/dist/cli/plan-local.js +1 -1
  98. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  99. package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
  100. package/dist/cli/skills-content/help.d.ts +1 -1
  101. package/dist/cli/skills-content/help.js +2 -1
  102. package/dist/cli/skills.js +2 -1
  103. package/dist/cli/visualize-repo.js +1 -1
  104. package/dist/client/FeedbackButton.js +12 -12
  105. package/dist/client/NewWorkspaceAppFlow.js +4 -1
  106. package/dist/client/agent-chat-adapter.js +3 -4
  107. package/dist/client/agent-page/AgentTabsPage.d.ts +2 -2
  108. package/dist/client/agent-page/AgentTabsPage.js +12 -9
  109. package/dist/client/assistant-ui-recovery.d.ts +1 -1
  110. package/dist/client/assistant-ui-recovery.js +4 -0
  111. package/dist/client/chat/message-components.d.ts +11 -3
  112. package/dist/client/chat/message-components.js +31 -5
  113. package/dist/client/chat/tool-call-display.d.ts +2 -1
  114. package/dist/client/chat/tool-call-display.js +33 -15
  115. package/dist/client/db-admin/SqlEditor.js +1 -1
  116. package/dist/client/dev-overlay/DevOverlay.js +2 -2
  117. package/dist/client/extensions/ExtensionsSidebarSection.js +2 -1
  118. package/dist/client/extensions/portable-extension.js +4 -0
  119. package/dist/client/guided-questions.d.ts +8 -0
  120. package/dist/client/guided-questions.js +12 -3
  121. package/dist/client/onboarding/FirstRunOnboarding.js +2 -1
  122. package/dist/client/org/TeamPage.js +14 -3
  123. package/dist/client/settings/SettingsPanel.js +26 -1
  124. package/dist/client/sharing/ShareButton.js +16 -24
  125. package/dist/client/sse-event-processor.js +35 -15
  126. package/dist/client/tool-display.d.ts +9 -0
  127. package/dist/client/tool-display.js +39 -0
  128. package/dist/client/vite-dev-recovery-script.js +1 -1
  129. package/dist/extensions/html-shell.js +5 -0
  130. package/dist/integrations/catalog.js +9 -8
  131. package/dist/observability/routes.d.ts +2 -2
  132. package/dist/secrets/routes.d.ts +6 -6
  133. package/dist/server/agent-chat/prompt-resources.js +17 -16
  134. package/dist/server/agent-chat-plugin.d.ts +9 -0
  135. package/dist/server/agent-chat-plugin.js +31 -9
  136. package/dist/server/builder-design-systems.d.ts +33 -1
  137. package/dist/server/builder-design-systems.js +293 -11
  138. package/dist/server/design-token-utils.d.ts +11 -0
  139. package/dist/server/design-token-utils.js +68 -16
  140. package/dist/server/index.d.ts +1 -1
  141. package/dist/server/index.js +1 -1
  142. package/dist/server/onboarding-html.js +2 -1
  143. package/dist/shared/docs-url.d.ts +24 -0
  144. package/dist/shared/docs-url.js +39 -0
  145. package/dist/shared/index.d.ts +1 -0
  146. package/dist/shared/index.js +1 -0
  147. package/dist/templates/default/app/routes/_index.tsx +2 -1
  148. package/package.json +2 -2
  149. 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, "&amp;")
@@ -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 blob = await addSpeakerNotesToPptxBlob(initialBlob, slides);
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) {