@agent-native/core 0.80.10 → 0.81.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/core/CHANGELOG.md +53 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +419 -19
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
- package/corpus/core/src/client/chat-view-transition.ts +33 -2
- package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
- package/corpus/core/src/db/client.ts +29 -2
- package/corpus/core/src/provider-api/index.ts +29 -0
- package/corpus/core/src/server/core-routes-plugin.ts +58 -20
- package/corpus/core/src/styles/agent-native.css +54 -0
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
- package/corpus/templates/analytics/app/global.css +8 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/global.css +15 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +233 -52
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
- package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
- package/corpus/templates/design/AGENTS.md +27 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
- package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
- package/corpus/templates/design/actions/capture-design-state.ts +27 -4
- package/corpus/templates/design/actions/connect-localhost.ts +59 -22
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +21 -26
- package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
- package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
- package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
- package/corpus/templates/design/actions/navigate.ts +18 -3
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
- package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
- package/corpus/templates/design/actions/provider-api-request.ts +3 -3
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
- package/corpus/templates/design/actions/run-design-audit.ts +74 -10
- package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/actions/write-local-file.ts +213 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
- package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
- package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
- package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
- package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/global.css +7 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +369 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
- package/corpus/templates/design/app/pages/Index.tsx +5 -1
- package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/e2e/helpers.ts +168 -65
- package/corpus/templates/design/package.json +3 -1
- package/corpus/templates/design/server/db/schema.ts +23 -0
- package/corpus/templates/design/server/handlers/uploads.ts +22 -1
- package/corpus/templates/design/server/lib/provider-api.ts +7 -4
- package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
- package/corpus/templates/design/server/plugins/db.ts +39 -0
- package/corpus/templates/design/server/register-secrets.ts +38 -0
- package/corpus/templates/design/shared/board-file.ts +228 -0
- package/corpus/templates/design/shared/board-objects.ts +288 -0
- package/corpus/templates/design/shared/code-layer.ts +52 -0
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +137 -15
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/corpus/templates/design/tsconfig.json +2 -1
- package/corpus/templates/slides/AGENTS.md +9 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
- package/corpus/templates/slides/app/vite-env.d.ts +18 -0
- package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/vite.config.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +25 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +357 -18
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -0
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +5 -5
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/chat-view-transition.d.ts +6 -0
- package/dist/client/chat-view-transition.d.ts.map +1 -1
- package/dist/client/chat-view-transition.js +23 -2
- package/dist/client/chat-view-transition.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +3 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +11 -4
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -3
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +20 -6
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +28 -2
- package/dist/db/client.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/index.d.ts +5 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -0
- package/dist/provider-api/index.js.map +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +43 -18
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +54 -0
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -30,7 +30,12 @@ export type TweakSelections = Record<string, string | number | boolean>;
|
|
|
30
30
|
*
|
|
31
31
|
* Tweaks without a `cssVar` are skipped (they don't map to a property).
|
|
32
32
|
* A missing selection falls back to the tweak's `defaultValue`.
|
|
33
|
+
* Selection keys that are themselves safe CSS custom properties are also
|
|
34
|
+
* emitted directly, which lets token edits persist vars that were not part of
|
|
35
|
+
* the generated tweak definition list.
|
|
33
36
|
*/
|
|
37
|
+
const CSS_CUSTOM_PROPERTY_NAME = /^--[-_a-zA-Z0-9]+$/;
|
|
38
|
+
|
|
34
39
|
export function resolveTweaksToCssVars(
|
|
35
40
|
tweaks: TweakDefinition[],
|
|
36
41
|
selections: TweakSelections,
|
|
@@ -39,19 +44,37 @@ export function resolveTweaksToCssVars(
|
|
|
39
44
|
for (const t of tweaks) {
|
|
40
45
|
if (!t.cssVar) continue;
|
|
41
46
|
const v = selections[t.id] ?? t.defaultValue;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
out[t.cssVar] = String(v);
|
|
50
|
-
}
|
|
47
|
+
out[t.cssVar] = stringifyCssVarValue(t.cssVar, v, t.unit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const [key, value] of Object.entries(selections)) {
|
|
51
|
+
if (!isDirectCssVarSelectionKey(key)) continue;
|
|
52
|
+
out[key] = stringifyCssVarValue(key, value);
|
|
51
53
|
}
|
|
54
|
+
|
|
52
55
|
return out;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
export function isDirectCssVarSelectionKey(key: string): boolean {
|
|
59
|
+
return CSS_CUSTOM_PROPERTY_NAME.test(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stringifyCssVarValue(
|
|
63
|
+
cssVar: string,
|
|
64
|
+
value: string | number | boolean,
|
|
65
|
+
unit?: string,
|
|
66
|
+
): string {
|
|
67
|
+
if (typeof value === "boolean") {
|
|
68
|
+
return value ? "1" : "0";
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === "number") {
|
|
71
|
+
const resolvedUnit =
|
|
72
|
+
unit ?? (cssVar.toLowerCase().includes("radius") ? "px" : "");
|
|
73
|
+
return `${value}${resolvedUnit}`;
|
|
74
|
+
}
|
|
75
|
+
return String(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/**
|
|
56
79
|
* Render resolved CSS vars as a `:root { ... }` block. Used by the
|
|
57
80
|
* coding-handoff bundle so external agents inherit the user's tuned tokens
|
|
@@ -54,6 +54,15 @@ extending the editor's save path, enqueue a granular op (`patch-slide`,
|
|
|
54
54
|
- `navigate` moves the UI to decks, slides, imports, and exports.
|
|
55
55
|
- Use app actions for full deck/slide data instead of relying on ambient context.
|
|
56
56
|
|
|
57
|
+
## Export Behavior
|
|
58
|
+
|
|
59
|
+
- Browser PowerPoint export uses the rendered slide DOM to generate native,
|
|
60
|
+
editable PPTX text/shapes/images. Do not replace it with full-slide images
|
|
61
|
+
unless the user explicitly asks for non-editable visual snapshots.
|
|
62
|
+
- Google Slides export is a PPTX import workflow: generate the same editable
|
|
63
|
+
PPTX and have the user import it into Google Slides. Creating a native Google
|
|
64
|
+
Slides file directly requires a separate Google Slides API batchUpdate path.
|
|
65
|
+
|
|
57
66
|
## Skills
|
|
58
67
|
|
|
59
68
|
Read the relevant skill before deeper work:
|
|
@@ -129,6 +129,8 @@ interface EditorToolbarProps {
|
|
|
129
129
|
onDuplicateDeck?: () => void;
|
|
130
130
|
/** Export the deck as PDF */
|
|
131
131
|
onExportPdf?: () => void;
|
|
132
|
+
/** Export the deck as PPTX */
|
|
133
|
+
onExportPptx?: () => Promise<void> | void;
|
|
132
134
|
/** Active deck aspect ratio (defaults to 16:9 when omitted) */
|
|
133
135
|
aspectRatio?: AspectRatio;
|
|
134
136
|
/** Change the deck's aspect ratio */
|
|
@@ -251,6 +253,7 @@ export default function EditorToolbar({
|
|
|
251
253
|
onTogglePinMode,
|
|
252
254
|
onDuplicateDeck,
|
|
253
255
|
onExportPdf,
|
|
256
|
+
onExportPptx,
|
|
254
257
|
aspectRatio,
|
|
255
258
|
onSetAspectRatio,
|
|
256
259
|
designSystemTitle,
|
|
@@ -929,6 +932,7 @@ graph TD
|
|
|
929
932
|
deckTitle={deckTitle}
|
|
930
933
|
onDuplicate={onDuplicateDeck ?? (() => {})}
|
|
931
934
|
onExportPdf={onExportPdf ?? (() => {})}
|
|
935
|
+
onExportPptx={onExportPptx ?? (() => {})}
|
|
932
936
|
/>
|
|
933
937
|
</div>
|
|
934
938
|
|
|
@@ -23,6 +23,7 @@ interface ExportMenuProps {
|
|
|
23
23
|
deckTitle: string;
|
|
24
24
|
onDuplicate: () => void;
|
|
25
25
|
onExportPdf: () => void;
|
|
26
|
+
onExportPptx: () => Promise<void> | void;
|
|
26
27
|
onShareLink?: () => void;
|
|
27
28
|
onShareTeam?: () => void;
|
|
28
29
|
}
|
|
@@ -32,6 +33,7 @@ export function ExportMenu({
|
|
|
32
33
|
deckTitle,
|
|
33
34
|
onDuplicate,
|
|
34
35
|
onExportPdf,
|
|
36
|
+
onExportPptx,
|
|
35
37
|
onShareLink,
|
|
36
38
|
onShareTeam,
|
|
37
39
|
}: ExportMenuProps) {
|
|
@@ -66,30 +68,9 @@ export function ExportMenu({
|
|
|
66
68
|
}
|
|
67
69
|
};
|
|
68
70
|
|
|
69
|
-
const fetchPptxExport = async () => {
|
|
70
|
-
const res = await fetch(`${appBasePath()}/api/exports/pptx`, {
|
|
71
|
-
method: "POST",
|
|
72
|
-
headers: { "Content-Type": "application/json" },
|
|
73
|
-
body: JSON.stringify({ deckId }),
|
|
74
|
-
});
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
await readErrorMessage(res, t("editorExport.pptxFailed")),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
blob: await res.blob(),
|
|
82
|
-
filename: filenameFromDisposition(
|
|
83
|
-
res.headers.get("content-disposition"),
|
|
84
|
-
".pptx",
|
|
85
|
-
),
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
|
|
89
71
|
const handleExportPptx = async () => {
|
|
90
72
|
try {
|
|
91
|
-
|
|
92
|
-
triggerBlobDownload(blob, filename);
|
|
73
|
+
await onExportPptx();
|
|
93
74
|
} catch (err) {
|
|
94
75
|
console.error("Export failed:", err);
|
|
95
76
|
toast({
|
|
@@ -105,8 +86,7 @@ export function ExportMenu({
|
|
|
105
86
|
|
|
106
87
|
const handleExportGoogleSlides = async () => {
|
|
107
88
|
try {
|
|
108
|
-
|
|
109
|
-
triggerBlobDownload(blob, filename);
|
|
89
|
+
await onExportPptx();
|
|
110
90
|
toast({
|
|
111
91
|
title: t("editorExport.googleSlidesDownloaded"),
|
|
112
92
|
description: t("editorExport.googleSlidesImportHint"),
|
|
@@ -20,7 +20,7 @@ import { type AspectRatio, getAspectRatioDims } from "./aspect-ratios";
|
|
|
20
20
|
* setting the attribute and re-assigning the same src. This is the root
|
|
21
21
|
* cause of the "blank images in exported PDF" bug Rochkind reported.
|
|
22
22
|
*/
|
|
23
|
-
async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
|
|
23
|
+
export async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
|
|
24
24
|
const imgs = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
|
|
25
25
|
await Promise.all(
|
|
26
26
|
imgs.map(async (img) => {
|
|
@@ -60,6 +60,33 @@ async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
|
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function findSlideExportSource(
|
|
64
|
+
slideId: string,
|
|
65
|
+
slideIndex: number,
|
|
66
|
+
slideCount: number,
|
|
67
|
+
): HTMLElement {
|
|
68
|
+
const candidates = Array.from(
|
|
69
|
+
document.querySelectorAll<HTMLElement>(
|
|
70
|
+
`[data-slide-canvas="${CSS.escape(slideId)}"]`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
// Don't silently drop missing slides — a collapsed sidebar (mobile
|
|
74
|
+
// default) would otherwise produce a partial export with no warning.
|
|
75
|
+
if (candidates.length === 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Slide ${slideIndex + 1} of ${slideCount} is not currently rendered. Open the slide sidebar and try again.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A given slide can appear multiple times (sidebar thumbnail + active
|
|
82
|
+
// editor canvas); pick the one with the largest natural width so we
|
|
83
|
+
// capture full-resolution pixels even when the visible copy is scaled
|
|
84
|
+
// down via CSS transform.
|
|
85
|
+
return candidates.reduce((best, el) =>
|
|
86
|
+
el.offsetWidth > best.offsetWidth ? el : best,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
export async function exportDeckAsPdf(
|
|
64
91
|
deckTitle: string,
|
|
65
92
|
slideIds: string[],
|
|
@@ -99,25 +126,7 @@ export async function exportDeckAsPdf(
|
|
|
99
126
|
|
|
100
127
|
for (let i = 0; i < slideIds.length; i++) {
|
|
101
128
|
const slideId = slideIds[i];
|
|
102
|
-
|
|
103
|
-
// editor canvas); pick the one with the largest natural width so we
|
|
104
|
-
// capture full-resolution pixels even when the visible copy is scaled
|
|
105
|
-
// down via CSS transform.
|
|
106
|
-
const candidates = Array.from(
|
|
107
|
-
document.querySelectorAll<HTMLElement>(
|
|
108
|
-
`[data-slide-canvas="${CSS.escape(slideId)}"]`,
|
|
109
|
-
),
|
|
110
|
-
);
|
|
111
|
-
// Don't silently drop missing slides — a collapsed sidebar (mobile
|
|
112
|
-
// default) would otherwise produce a partial PDF with no warning.
|
|
113
|
-
if (candidates.length === 0) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Slide ${i + 1} of ${slideIds.length} is not currently rendered. Open the slide sidebar and try again.`,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const source = candidates.reduce((best, el) =>
|
|
119
|
-
el.offsetWidth > best.offsetWidth ? el : best,
|
|
120
|
-
);
|
|
129
|
+
const source = findSlideExportSource(slideId, i, slideIds.length);
|
|
121
130
|
|
|
122
131
|
// Force CORS-enabled re-fetch on every cross-origin <img> before
|
|
123
132
|
// capture — otherwise the canvas tainting check inside modern-screenshot
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { type AspectRatio, getAspectRatioDims } from "./aspect-ratios";
|
|
2
|
+
import {
|
|
3
|
+
findSlideExportSource,
|
|
4
|
+
preloadImagesWithCors,
|
|
5
|
+
} from "./export-pdf-client";
|
|
6
|
+
|
|
7
|
+
interface PptxExportSlide {
|
|
8
|
+
id: string;
|
|
9
|
+
notes?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safePptxName(title: string) {
|
|
13
|
+
const safeName = title.replace(/[^a-zA-Z0-9]/g, "-") || "deck";
|
|
14
|
+
return `${safeName}.pptx`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function triggerBlobDownload(blob: Blob, filename: string) {
|
|
18
|
+
const url = URL.createObjectURL(blob);
|
|
19
|
+
const a = document.createElement("a");
|
|
20
|
+
a.href = url;
|
|
21
|
+
a.download = filename;
|
|
22
|
+
a.rel = "noopener";
|
|
23
|
+
document.body.appendChild(a);
|
|
24
|
+
a.click();
|
|
25
|
+
a.remove();
|
|
26
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeXml(value: string) {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/&/g, "&")
|
|
32
|
+
.replace(/</g, "<")
|
|
33
|
+
.replace(/>/g, ">")
|
|
34
|
+
.replace(/"/g, """)
|
|
35
|
+
.replace(/'/g, "'");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addRelationship(xml: string, relationship: string) {
|
|
39
|
+
if (xml.includes(relationship)) return xml;
|
|
40
|
+
return xml.replace("</Relationships>", `${relationship}</Relationships>`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addContentTypeOverride(xml: string, partName: string, type: string) {
|
|
44
|
+
if (xml.includes(`PartName="${partName}"`)) return xml;
|
|
45
|
+
return xml.replace(
|
|
46
|
+
"</Types>",
|
|
47
|
+
`<Override PartName="${partName}" ContentType="${type}"/></Types>`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nextRelationshipId(xml: string) {
|
|
52
|
+
const ids = Array.from(xml.matchAll(/\bId="rId(\d+)"/g)).map((match) =>
|
|
53
|
+
Number(match[1]),
|
|
54
|
+
);
|
|
55
|
+
return `rId${Math.max(0, ...ids) + 1}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notesTextBody(notes: string) {
|
|
59
|
+
const lines = notes.split(/\r?\n/);
|
|
60
|
+
return lines
|
|
61
|
+
.map(
|
|
62
|
+
(line) =>
|
|
63
|
+
`<a:p><a:r><a:rPr lang="en-US" dirty="0"/><a:t>${escapeXml(line)}</a:t></a:r><a:endParaRPr lang="en-US" dirty="0"/></a:p>`,
|
|
64
|
+
)
|
|
65
|
+
.join("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function notesSlideXml(notes: string, slideNumber: number) {
|
|
69
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
70
|
+
<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>${notesTextBody(notes)}</p:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="4" name="Slide Number Placeholder 3"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldNum" sz="quarter" idx="10"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:fld id="{F7021451-1387-4CA6-816F-3879F97B5CBC}" type="slidenum"><a:rPr lang="en-US"/><a:t>${slideNumber}</a:t></a:fld><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const NOTES_MASTER_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
74
|
+
<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/><p:notesStyle><a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr></p:notesStyle></p:notesMaster>`;
|
|
75
|
+
|
|
76
|
+
const NOTES_MASTER_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
77
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/></Relationships>`;
|
|
78
|
+
|
|
79
|
+
export async function addSpeakerNotesToPptxBlob(
|
|
80
|
+
blob: Blob,
|
|
81
|
+
slides: PptxExportSlide[],
|
|
82
|
+
): Promise<Blob> {
|
|
83
|
+
const hasNotes = slides.some((slide) => slide.notes?.trim());
|
|
84
|
+
if (!hasNotes) return blob;
|
|
85
|
+
|
|
86
|
+
const { default: JSZip } = await import("jszip");
|
|
87
|
+
const zip = await JSZip.loadAsync(blob);
|
|
88
|
+
|
|
89
|
+
const contentTypesFile = zip.file("[Content_Types].xml");
|
|
90
|
+
const presentationFile = zip.file("ppt/presentation.xml");
|
|
91
|
+
const presentationRelsFile = zip.file("ppt/_rels/presentation.xml.rels");
|
|
92
|
+
|
|
93
|
+
if (!contentTypesFile || !presentationFile || !presentationRelsFile) {
|
|
94
|
+
return blob;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let contentTypes = await contentTypesFile.async("string");
|
|
98
|
+
let presentationXml = await presentationFile.async("string");
|
|
99
|
+
let presentationRels = await presentationRelsFile.async("string");
|
|
100
|
+
|
|
101
|
+
if (!zip.file("ppt/notesMasters/notesMaster1.xml")) {
|
|
102
|
+
zip.file("ppt/notesMasters/notesMaster1.xml", NOTES_MASTER_XML);
|
|
103
|
+
}
|
|
104
|
+
if (!zip.file("ppt/notesMasters/_rels/notesMaster1.xml.rels")) {
|
|
105
|
+
zip.file(
|
|
106
|
+
"ppt/notesMasters/_rels/notesMaster1.xml.rels",
|
|
107
|
+
NOTES_MASTER_RELS_XML,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
contentTypes = addContentTypeOverride(
|
|
112
|
+
contentTypes,
|
|
113
|
+
"/ppt/notesMasters/notesMaster1.xml",
|
|
114
|
+
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!presentationRels.includes("relationships/notesMaster")) {
|
|
118
|
+
const relId = nextRelationshipId(presentationRels);
|
|
119
|
+
presentationRels = addRelationship(
|
|
120
|
+
presentationRels,
|
|
121
|
+
`<Relationship Id="${relId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="notesMasters/notesMaster1.xml"/>`,
|
|
122
|
+
);
|
|
123
|
+
if (!presentationXml.includes("<p:notesMasterIdLst>")) {
|
|
124
|
+
presentationXml = presentationXml.replace(
|
|
125
|
+
"</p:sldIdLst>",
|
|
126
|
+
`</p:sldIdLst><p:notesMasterIdLst><p:notesMasterId r:id="${relId}"/></p:notesMasterIdLst>`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!presentationXml.includes("<p:notesSz")) {
|
|
132
|
+
presentationXml = presentationXml.replace(
|
|
133
|
+
"<p:defaultTextStyle>",
|
|
134
|
+
'<p:notesSz cx="6858000" cy="12192000"/><p:defaultTextStyle>',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < slides.length; i++) {
|
|
139
|
+
const notes = slides[i].notes?.trim();
|
|
140
|
+
if (!notes) continue;
|
|
141
|
+
|
|
142
|
+
const slideNumber = i + 1;
|
|
143
|
+
const slideRelsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
144
|
+
const slideRelsFile = zip.file(slideRelsPath);
|
|
145
|
+
if (!slideRelsFile) continue;
|
|
146
|
+
|
|
147
|
+
let slideRels = await slideRelsFile.async("string");
|
|
148
|
+
if (!slideRels.includes("relationships/notesSlide")) {
|
|
149
|
+
const relId = nextRelationshipId(slideRels);
|
|
150
|
+
slideRels = addRelationship(
|
|
151
|
+
slideRels,
|
|
152
|
+
`<Relationship Id="${relId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide${slideNumber}.xml"/>`,
|
|
153
|
+
);
|
|
154
|
+
zip.file(slideRelsPath, slideRels);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
zip.file(
|
|
158
|
+
`ppt/notesSlides/notesSlide${slideNumber}.xml`,
|
|
159
|
+
notesSlideXml(notes, slideNumber),
|
|
160
|
+
);
|
|
161
|
+
zip.file(
|
|
162
|
+
`ppt/notesSlides/_rels/notesSlide${slideNumber}.xml.rels`,
|
|
163
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
164
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide${slideNumber}.xml"/></Relationships>`,
|
|
165
|
+
);
|
|
166
|
+
contentTypes = addContentTypeOverride(
|
|
167
|
+
contentTypes,
|
|
168
|
+
`/ppt/notesSlides/notesSlide${slideNumber}.xml`,
|
|
169
|
+
"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
zip.file("[Content_Types].xml", contentTypes);
|
|
174
|
+
zip.file("ppt/presentation.xml", presentationXml);
|
|
175
|
+
zip.file("ppt/_rels/presentation.xml.rels", presentationRels);
|
|
176
|
+
|
|
177
|
+
return zip.generateAsync({
|
|
178
|
+
type: "blob",
|
|
179
|
+
compression: "DEFLATE",
|
|
180
|
+
compressionOptions: { level: 6 },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createUnscaledExportClone(
|
|
185
|
+
source: HTMLElement,
|
|
186
|
+
dims: { width: number; height: number },
|
|
187
|
+
) {
|
|
188
|
+
const stage = document.createElement("div");
|
|
189
|
+
stage.setAttribute("aria-hidden", "true");
|
|
190
|
+
Object.assign(stage.style, {
|
|
191
|
+
height: `${dims.height}px`,
|
|
192
|
+
left: "-100000px",
|
|
193
|
+
overflow: "hidden",
|
|
194
|
+
pointerEvents: "none",
|
|
195
|
+
position: "fixed",
|
|
196
|
+
top: "0",
|
|
197
|
+
width: `${dims.width}px`,
|
|
198
|
+
zIndex: "-1",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const clone = source.cloneNode(true) as HTMLElement;
|
|
202
|
+
Object.assign(clone.style, {
|
|
203
|
+
height: `${dims.height}px`,
|
|
204
|
+
maxHeight: `${dims.height}px`,
|
|
205
|
+
maxWidth: `${dims.width}px`,
|
|
206
|
+
position: "relative",
|
|
207
|
+
transform: "none",
|
|
208
|
+
width: `${dims.width}px`,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
stage.appendChild(clone);
|
|
212
|
+
document.body.appendChild(stage);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
element: clone,
|
|
216
|
+
cleanup: () => stage.remove(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function svgDataUrl(svg: SVGSVGElement) {
|
|
221
|
+
const copy = svg.cloneNode(true) as SVGSVGElement;
|
|
222
|
+
if (!copy.getAttribute("xmlns")) {
|
|
223
|
+
copy.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
224
|
+
}
|
|
225
|
+
const serialized = new XMLSerializer().serializeToString(copy);
|
|
226
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serialized)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function rasterizeSvgElement(
|
|
230
|
+
svg: SVGSVGElement,
|
|
231
|
+
width: number,
|
|
232
|
+
height: number,
|
|
233
|
+
) {
|
|
234
|
+
const fallback = svgDataUrl(svg);
|
|
235
|
+
const canvas = document.createElement("canvas");
|
|
236
|
+
const ctx = canvas.getContext("2d");
|
|
237
|
+
if (!ctx || typeof Image === "undefined") return fallback;
|
|
238
|
+
|
|
239
|
+
const scale = Math.max(2, window.devicePixelRatio || 1);
|
|
240
|
+
canvas.width = Math.max(1, Math.ceil(width * scale));
|
|
241
|
+
canvas.height = Math.max(1, Math.ceil(height * scale));
|
|
242
|
+
|
|
243
|
+
const image = new Image();
|
|
244
|
+
const loaded = new Promise<void>((resolve, reject) => {
|
|
245
|
+
image.onload = () => resolve();
|
|
246
|
+
image.onerror = () => reject(new Error("Could not rasterize SVG"));
|
|
247
|
+
});
|
|
248
|
+
image.src = fallback;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await loaded;
|
|
252
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
253
|
+
return canvas.toDataURL("image/png");
|
|
254
|
+
} catch {
|
|
255
|
+
return fallback;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function replaceInlineSvgsWithImages(root: HTMLElement) {
|
|
260
|
+
const svgs = Array.from(root.querySelectorAll<SVGSVGElement>("svg"));
|
|
261
|
+
for (const svg of svgs) {
|
|
262
|
+
const rect = svg.getBoundingClientRect();
|
|
263
|
+
const viewBox = svg.viewBox?.baseVal;
|
|
264
|
+
const width =
|
|
265
|
+
rect.width || Number(svg.getAttribute("width")) || viewBox?.width || 1;
|
|
266
|
+
const height =
|
|
267
|
+
rect.height || Number(svg.getAttribute("height")) || viewBox?.height || 1;
|
|
268
|
+
const dataUrl = await rasterizeSvgElement(svg, width, height);
|
|
269
|
+
const img = document.createElement("img");
|
|
270
|
+
const style = window.getComputedStyle(svg);
|
|
271
|
+
img.src = dataUrl;
|
|
272
|
+
img.alt = svg.getAttribute("aria-label") ?? "";
|
|
273
|
+
Object.assign(img.style, {
|
|
274
|
+
alignSelf: style.alignSelf,
|
|
275
|
+
display: style.display === "inline" ? "inline-block" : style.display,
|
|
276
|
+
flex: style.flex,
|
|
277
|
+
height: `${height}px`,
|
|
278
|
+
justifySelf: style.justifySelf,
|
|
279
|
+
left: style.left,
|
|
280
|
+
marginBottom: style.marginBottom,
|
|
281
|
+
marginLeft: style.marginLeft,
|
|
282
|
+
marginRight: style.marginRight,
|
|
283
|
+
marginTop: style.marginTop,
|
|
284
|
+
objectFit: "contain",
|
|
285
|
+
opacity: style.opacity,
|
|
286
|
+
position: style.position,
|
|
287
|
+
right: style.right,
|
|
288
|
+
top: style.top,
|
|
289
|
+
transform: style.transform === "none" ? "" : style.transform,
|
|
290
|
+
width: `${width}px`,
|
|
291
|
+
zIndex: style.zIndex,
|
|
292
|
+
});
|
|
293
|
+
svg.replaceWith(img);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function widenNoWrapTextElements(root: HTMLElement) {
|
|
298
|
+
const elements = Array.from(root.querySelectorAll<HTMLElement>("*"));
|
|
299
|
+
for (const element of elements) {
|
|
300
|
+
if (!element.textContent?.trim()) continue;
|
|
301
|
+
if (element.querySelector("img,svg,video,canvas")) continue;
|
|
302
|
+
const style = window.getComputedStyle(element);
|
|
303
|
+
if (style.whiteSpace !== "nowrap" && style.whiteSpace !== "pre") continue;
|
|
304
|
+
const rect = element.getBoundingClientRect();
|
|
305
|
+
if (!rect.width || !rect.height) continue;
|
|
306
|
+
const buffer = Math.max(24, rect.width * 0.25);
|
|
307
|
+
element.style.boxSizing = "border-box";
|
|
308
|
+
if (style.display === "inline") {
|
|
309
|
+
element.style.display = "inline-block";
|
|
310
|
+
}
|
|
311
|
+
element.style.width = `${Math.ceil(rect.width + buffer)}px`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function exportDeckAsPptx(
|
|
316
|
+
deckTitle: string,
|
|
317
|
+
slides: PptxExportSlide[],
|
|
318
|
+
aspectRatio?: AspectRatio,
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
const { exportToPptx } = await import("dom-to-pptx");
|
|
321
|
+
|
|
322
|
+
if (typeof document !== "undefined" && document.fonts?.ready) {
|
|
323
|
+
await document.fonts.ready;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const dims = getAspectRatioDims(aspectRatio);
|
|
327
|
+
const exportClones: Array<{
|
|
328
|
+
element: HTMLElement;
|
|
329
|
+
cleanup: () => void;
|
|
330
|
+
}> = [];
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
for (let i = 0; i < slides.length; i++) {
|
|
334
|
+
const exportSlide = slides[i];
|
|
335
|
+
const source = findSlideExportSource(exportSlide.id, i, slides.length);
|
|
336
|
+
const clone = createUnscaledExportClone(source, {
|
|
337
|
+
width: dims.width,
|
|
338
|
+
height: dims.height,
|
|
339
|
+
});
|
|
340
|
+
exportClones.push(clone);
|
|
341
|
+
widenNoWrapTextElements(clone.element);
|
|
342
|
+
await replaceInlineSvgsWithImages(clone.element);
|
|
343
|
+
await preloadImagesWithCors(clone.element);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const initialBlob = await exportToPptx(
|
|
347
|
+
exportClones.map((clone) => clone.element),
|
|
348
|
+
{
|
|
349
|
+
autoEmbedFonts: true,
|
|
350
|
+
fileName: safePptxName(deckTitle),
|
|
351
|
+
height: dims.pptxInches.h,
|
|
352
|
+
skipDownload: true,
|
|
353
|
+
svgAsVector: false,
|
|
354
|
+
width: dims.pptxInches.w,
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const blob = await addSpeakerNotesToPptxBlob(initialBlob, slides);
|
|
359
|
+
triggerBlobDownload(blob, safePptxName(deckTitle));
|
|
360
|
+
} finally {
|
|
361
|
+
for (const clone of exportClones) {
|
|
362
|
+
clone.cleanup();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -65,6 +65,7 @@ import { toast } from "@/hooks/use-toast";
|
|
|
65
65
|
import type { AspectRatio } from "@/lib/aspect-ratios";
|
|
66
66
|
import { getPreset } from "@/lib/design-systems";
|
|
67
67
|
import { exportDeckAsPdf } from "@/lib/export-pdf-client";
|
|
68
|
+
import { exportDeckAsPptx } from "@/lib/export-pptx-client";
|
|
68
69
|
import {
|
|
69
70
|
shouldClearNewDeckGeneratingState,
|
|
70
71
|
shouldShowNewDeckGeneratingOverlay,
|
|
@@ -81,7 +82,6 @@ const Pinpoint = lazy<ComponentType<PinpointProps>>(() =>
|
|
|
81
82
|
);
|
|
82
83
|
|
|
83
84
|
function MissingDeckAccessPane({
|
|
84
|
-
deckId,
|
|
85
85
|
hasTeamJoinOption,
|
|
86
86
|
orgLoading,
|
|
87
87
|
orgError,
|
|
@@ -89,7 +89,6 @@ function MissingDeckAccessPane({
|
|
|
89
89
|
onRetry,
|
|
90
90
|
onBack,
|
|
91
91
|
}: {
|
|
92
|
-
deckId: string | undefined;
|
|
93
92
|
hasTeamJoinOption: boolean;
|
|
94
93
|
orgLoading: boolean;
|
|
95
94
|
orgError: boolean;
|
|
@@ -116,7 +115,7 @@ function MissingDeckAccessPane({
|
|
|
116
115
|
: t("deckEditor.deckUnavailableDescription");
|
|
117
116
|
|
|
118
117
|
return (
|
|
119
|
-
<div className="flex
|
|
118
|
+
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
|
|
120
119
|
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-sm">
|
|
121
120
|
<div className="mb-4 flex items-center gap-3">
|
|
122
121
|
<div className="flex size-10 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
|
|
@@ -124,11 +123,6 @@ function MissingDeckAccessPane({
|
|
|
124
123
|
</div>
|
|
125
124
|
<div className="min-w-0">
|
|
126
125
|
<h1 className="text-base font-semibold text-foreground">{title}</h1>
|
|
127
|
-
{deckId && (
|
|
128
|
-
<p className="truncate text-xs text-muted-foreground">
|
|
129
|
-
Deck ID: {deckId}
|
|
130
|
-
</p>
|
|
131
|
-
)}
|
|
132
126
|
</div>
|
|
133
127
|
</div>
|
|
134
128
|
<p className="text-sm leading-6 text-muted-foreground">{description}</p>
|
|
@@ -723,7 +717,6 @@ export default function DeckEditor() {
|
|
|
723
717
|
if (!deck || !id) {
|
|
724
718
|
return (
|
|
725
719
|
<MissingDeckAccessPane
|
|
726
|
-
deckId={id}
|
|
727
720
|
hasTeamJoinOption={hasTeamJoinOption}
|
|
728
721
|
orgLoading={orgLoading}
|
|
729
722
|
orgError={orgError}
|
|
@@ -846,6 +839,16 @@ export default function DeckEditor() {
|
|
|
846
839
|
});
|
|
847
840
|
}
|
|
848
841
|
}}
|
|
842
|
+
onExportPptx={async () => {
|
|
843
|
+
const slides = deck.slides.map((s) => ({
|
|
844
|
+
id: s.id,
|
|
845
|
+
notes: s.notes,
|
|
846
|
+
}));
|
|
847
|
+
if (slides.length === 0) {
|
|
848
|
+
throw new Error(t("deckEditor.deckHasNoSlides"));
|
|
849
|
+
}
|
|
850
|
+
await exportDeckAsPptx(deck.title, slides, deck.aspectRatio);
|
|
851
|
+
}}
|
|
849
852
|
aspectRatio={deck.aspectRatio}
|
|
850
853
|
designSystemTitle={designSystemTitle}
|
|
851
854
|
onSetAspectRatio={(ratio: AspectRatio) => {
|