@agent-native/core 0.80.11 → 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 +47 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +419 -19
- 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/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/actions/open-asset-picker.ts +9 -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/routes/library.tsx +90 -16
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- 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 +63 -20
- package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
- 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/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +5 -19
- 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 +10 -1
- 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-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/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 +53 -4
- 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 +230 -3308
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
- package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
- 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 +21 -0
- package/corpus/templates/design/app/i18n-data.ts +358 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
- 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-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/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/motion-compiler.ts +58 -7
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- 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/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/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/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 +2 -2
- 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/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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board object types shared between the frontend, actions, and agent skills.
|
|
3
|
+
*
|
|
4
|
+
* Board objects are persisted canvas primitives stored inside designs.data under
|
|
5
|
+
* the "boardObjects" key. Unlike code-layer nodes (which live inside screen HTML
|
|
6
|
+
* files), board objects float on the infinite canvas surface and are not bound
|
|
7
|
+
* to any screen iframe.
|
|
8
|
+
*
|
|
9
|
+
* The kind set mirrors DraftPrimitiveKind in MultiScreenCanvas.tsx exactly so
|
|
10
|
+
* the same shape-rendering logic (DraftPrimitiveContent / canvasPrimitiveReactStyle)
|
|
11
|
+
* can be reused for the visual output in BoardObjectLayer.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type CanvasPrimitiveKindLike =
|
|
15
|
+
| "frame"
|
|
16
|
+
| "rectangle"
|
|
17
|
+
| "ellipse"
|
|
18
|
+
| "polygon"
|
|
19
|
+
| "star"
|
|
20
|
+
| "line"
|
|
21
|
+
| "arrow"
|
|
22
|
+
| "text"
|
|
23
|
+
| "path";
|
|
24
|
+
|
|
25
|
+
export interface BoardObjectEntry {
|
|
26
|
+
id: string;
|
|
27
|
+
kind: CanvasPrimitiveKindLike;
|
|
28
|
+
/**
|
|
29
|
+
* Bounding-box geometry in canvas units (same coordinate space as
|
|
30
|
+
* FrameGeometry — offset from the canvas origin before SURFACE_PADDING is
|
|
31
|
+
* applied). z is the stacking order; rotation is in degrees (clockwise).
|
|
32
|
+
*/
|
|
33
|
+
geometry: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
rotation?: number;
|
|
39
|
+
z?: number;
|
|
40
|
+
};
|
|
41
|
+
/** CSS fill colour (background). Defaults to theme primary tint when absent. */
|
|
42
|
+
fill?: string;
|
|
43
|
+
/** CSS stroke colour (border). Defaults to theme primary stroke when absent. */
|
|
44
|
+
stroke?: string;
|
|
45
|
+
/** Stroke width in pixels. */
|
|
46
|
+
strokeWidth?: number;
|
|
47
|
+
/** Text content — only meaningful when kind === "text". */
|
|
48
|
+
text?: string;
|
|
49
|
+
/**
|
|
50
|
+
* SVG path data string — only meaningful when kind is "path", "line", or "arrow".
|
|
51
|
+
* Falls back to a pointsToPath conversion when absent and `points` is set.
|
|
52
|
+
*/
|
|
53
|
+
pathData?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Raw point list for polyline/polygon kinds without explicit pathData.
|
|
56
|
+
* Each entry is a canvas-space {x, y} point.
|
|
57
|
+
*/
|
|
58
|
+
points?: Array<{ x: number; y: number }>;
|
|
59
|
+
/**
|
|
60
|
+
* When true the text box grows horizontally to fit its content
|
|
61
|
+
* (single-line auto-size mode).
|
|
62
|
+
*/
|
|
63
|
+
autoSize?: boolean;
|
|
64
|
+
/** Human-readable label shown in the layers panel. */
|
|
65
|
+
name?: string;
|
|
66
|
+
/** ISO 8601 creation timestamp. */
|
|
67
|
+
createdAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Valid kind set for runtime validation
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const VALID_KINDS = new Set<string>([
|
|
75
|
+
"frame",
|
|
76
|
+
"rectangle",
|
|
77
|
+
"ellipse",
|
|
78
|
+
"polygon",
|
|
79
|
+
"star",
|
|
80
|
+
"line",
|
|
81
|
+
"arrow",
|
|
82
|
+
"text",
|
|
83
|
+
"path",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// parseBoardObjects
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse the boardObjects map from an unknown value (e.g. from designs.data JSON).
|
|
92
|
+
*
|
|
93
|
+
* Accepts:
|
|
94
|
+
* - A Record<string, BoardObjectEntry> already deserialized from JSON.
|
|
95
|
+
* - A JSON string that encodes such a record.
|
|
96
|
+
* - null / undefined / any other value → returns an empty record.
|
|
97
|
+
*
|
|
98
|
+
* Individual entries that are missing the required `id`, `kind`, or `geometry`
|
|
99
|
+
* fields are silently dropped so a partially-corrupt payload never prevents
|
|
100
|
+
* the canvas from rendering.
|
|
101
|
+
*/
|
|
102
|
+
export function parseBoardObjects(
|
|
103
|
+
value: unknown,
|
|
104
|
+
): Record<string, BoardObjectEntry> {
|
|
105
|
+
let raw: unknown = value;
|
|
106
|
+
|
|
107
|
+
// Attempt to parse JSON strings.
|
|
108
|
+
if (typeof raw === "string") {
|
|
109
|
+
try {
|
|
110
|
+
raw = JSON.parse(raw);
|
|
111
|
+
} catch {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result: Record<string, BoardObjectEntry> = {};
|
|
121
|
+
for (const [key, entry] of Object.entries(raw as Record<string, unknown>)) {
|
|
122
|
+
if (!isValidEntry(entry)) continue;
|
|
123
|
+
result[key] = entry as BoardObjectEntry;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// draftToBoardObjectEntry
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Minimal structural type matching DraftPrimitive in MultiScreenCanvas.tsx.
|
|
134
|
+
* Defined here so shared/board-objects.ts stays free of React/frontend imports.
|
|
135
|
+
*/
|
|
136
|
+
export interface DraftPrimitiveLike {
|
|
137
|
+
id: string;
|
|
138
|
+
kind: CanvasPrimitiveKindLike;
|
|
139
|
+
geometry: {
|
|
140
|
+
x: number;
|
|
141
|
+
y: number;
|
|
142
|
+
width: number;
|
|
143
|
+
height: number;
|
|
144
|
+
rotation?: number;
|
|
145
|
+
z?: number;
|
|
146
|
+
};
|
|
147
|
+
points?: Array<{ x: number; y: number }>;
|
|
148
|
+
pathData?: string;
|
|
149
|
+
text?: string;
|
|
150
|
+
fill?: string;
|
|
151
|
+
stroke?: string;
|
|
152
|
+
strokeWidth?: number;
|
|
153
|
+
autoSize?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert a DraftPrimitive (in-progress canvas shape) to a BoardObjectEntry
|
|
158
|
+
* ready for persistence.
|
|
159
|
+
*
|
|
160
|
+
* Called by MultiScreenCanvas when a drawn primitive is outside all frames
|
|
161
|
+
* (single-screen designs always absorb into the frame instead).
|
|
162
|
+
*/
|
|
163
|
+
export function draftToBoardObjectEntry(
|
|
164
|
+
draft: DraftPrimitiveLike,
|
|
165
|
+
): BoardObjectEntry {
|
|
166
|
+
const entry: BoardObjectEntry = {
|
|
167
|
+
id: draft.id,
|
|
168
|
+
kind: draft.kind,
|
|
169
|
+
geometry: { ...draft.geometry },
|
|
170
|
+
createdAt: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
if (draft.fill !== undefined) entry.fill = draft.fill;
|
|
173
|
+
if (draft.stroke !== undefined) entry.stroke = draft.stroke;
|
|
174
|
+
if (draft.strokeWidth !== undefined) entry.strokeWidth = draft.strokeWidth;
|
|
175
|
+
if (draft.text !== undefined) entry.text = draft.text;
|
|
176
|
+
if (draft.pathData !== undefined) entry.pathData = draft.pathData;
|
|
177
|
+
if (draft.points !== undefined) entry.points = draft.points;
|
|
178
|
+
if (draft.autoSize !== undefined) entry.autoSize = draft.autoSize;
|
|
179
|
+
return entry;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Board-object resize helpers
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Geometry shape for resize calculations (same as BoardObjectEntry.geometry). */
|
|
187
|
+
export interface BoardObjectGeometry {
|
|
188
|
+
x: number;
|
|
189
|
+
y: number;
|
|
190
|
+
width: number;
|
|
191
|
+
height: number;
|
|
192
|
+
rotation?: number;
|
|
193
|
+
z?: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Apply a resize delta to a board object's geometry given the resize handle
|
|
198
|
+
* (one of "nw", "ne", "se", "sw") and the mouse delta (dx, dy) in canvas units.
|
|
199
|
+
*
|
|
200
|
+
* - "nw": move top-left corner → adjusts x, y, width, height.
|
|
201
|
+
* - "ne": move top-right corner → adjusts y, width, height.
|
|
202
|
+
* - "se": move bottom-right corner → adjusts width, height.
|
|
203
|
+
* - "sw": move bottom-left corner → adjusts x, width, height.
|
|
204
|
+
*
|
|
205
|
+
* Width and height are clamped to a minimum of 4 pixels.
|
|
206
|
+
*/
|
|
207
|
+
export function applyBoardObjectResize(
|
|
208
|
+
origin: BoardObjectGeometry,
|
|
209
|
+
handle: string,
|
|
210
|
+
dx: number,
|
|
211
|
+
dy: number,
|
|
212
|
+
): BoardObjectGeometry {
|
|
213
|
+
const MIN_SIZE = 4;
|
|
214
|
+
let { x, y, width, height } = origin;
|
|
215
|
+
|
|
216
|
+
switch (handle) {
|
|
217
|
+
case "nw":
|
|
218
|
+
x = origin.x + dx;
|
|
219
|
+
y = origin.y + dy;
|
|
220
|
+
width = Math.max(MIN_SIZE, origin.width - dx);
|
|
221
|
+
height = Math.max(MIN_SIZE, origin.height - dy);
|
|
222
|
+
// Keep anchor at bottom-right
|
|
223
|
+
if (origin.width - dx < MIN_SIZE) x = origin.x + origin.width - MIN_SIZE;
|
|
224
|
+
if (origin.height - dy < MIN_SIZE)
|
|
225
|
+
y = origin.y + origin.height - MIN_SIZE;
|
|
226
|
+
break;
|
|
227
|
+
case "ne":
|
|
228
|
+
y = origin.y + dy;
|
|
229
|
+
width = Math.max(MIN_SIZE, origin.width + dx);
|
|
230
|
+
height = Math.max(MIN_SIZE, origin.height - dy);
|
|
231
|
+
if (origin.height - dy < MIN_SIZE)
|
|
232
|
+
y = origin.y + origin.height - MIN_SIZE;
|
|
233
|
+
break;
|
|
234
|
+
case "se":
|
|
235
|
+
width = Math.max(MIN_SIZE, origin.width + dx);
|
|
236
|
+
height = Math.max(MIN_SIZE, origin.height + dy);
|
|
237
|
+
break;
|
|
238
|
+
case "sw":
|
|
239
|
+
x = origin.x + dx;
|
|
240
|
+
width = Math.max(MIN_SIZE, origin.width - dx);
|
|
241
|
+
height = Math.max(MIN_SIZE, origin.height + dy);
|
|
242
|
+
if (origin.width - dx < MIN_SIZE) x = origin.x + origin.width - MIN_SIZE;
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { ...origin, x, y, width, height };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Map a resize handle string to the CSS cursor value shown while dragging.
|
|
253
|
+
*/
|
|
254
|
+
export function getBoardObjectResizeCursor(handle: string): string {
|
|
255
|
+
switch (handle) {
|
|
256
|
+
case "nw":
|
|
257
|
+
case "se":
|
|
258
|
+
return "nwse-resize";
|
|
259
|
+
case "ne":
|
|
260
|
+
case "sw":
|
|
261
|
+
return "nesw-resize";
|
|
262
|
+
default:
|
|
263
|
+
return "se-resize";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isValidEntry(entry: unknown): entry is BoardObjectEntry {
|
|
268
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const e = entry as Record<string, unknown>;
|
|
272
|
+
if (typeof e["id"] !== "string" || !e["id"]) return false;
|
|
273
|
+
if (typeof e["kind"] !== "string" || !VALID_KINDS.has(e["kind"])) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (typeof e["createdAt"] !== "string") return false;
|
|
277
|
+
const geo = e["geometry"];
|
|
278
|
+
if (geo == null || typeof geo !== "object" || Array.isArray(geo)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const g = geo as Record<string, unknown>;
|
|
282
|
+
return (
|
|
283
|
+
typeof g["x"] === "number" &&
|
|
284
|
+
typeof g["y"] === "number" &&
|
|
285
|
+
typeof g["width"] === "number" &&
|
|
286
|
+
typeof g["height"] === "number"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -1659,6 +1659,17 @@ function layerNameFor(
|
|
|
1659
1659
|
}
|
|
1660
1660
|
|
|
1661
1661
|
function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
|
|
1662
|
+
// Canvas primitives (drawn shapes / board objects) carry their kind via
|
|
1663
|
+
// data-an-primitive so the layers panel shows a true shape/text/frame icon
|
|
1664
|
+
// instead of the generic code glyph. The marker wins over tag heuristics:
|
|
1665
|
+
// these primitives are <div>s, which would otherwise classify as "element".
|
|
1666
|
+
const primitiveKind = node.dataAttributes["data-an-primitive"];
|
|
1667
|
+
if (primitiveKind) {
|
|
1668
|
+
if (primitiveKind === "text") return "text";
|
|
1669
|
+
if (primitiveKind === "frame") return "frame";
|
|
1670
|
+
if (primitiveKind === "image") return "image";
|
|
1671
|
+
return "shape";
|
|
1672
|
+
}
|
|
1662
1673
|
if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
|
|
1663
1674
|
if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
|
|
1664
1675
|
if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
|
|
@@ -3371,6 +3382,47 @@ export function applyVisualEdit(
|
|
|
3371
3382
|
};
|
|
3372
3383
|
}
|
|
3373
3384
|
|
|
3385
|
+
/**
|
|
3386
|
+
* Attributes injected by the editor at runtime that must NOT appear in
|
|
3387
|
+
* on-disk source files. These are stripped before any write-back so that
|
|
3388
|
+
* the saved file stays clean and matches what a developer would author.
|
|
3389
|
+
*
|
|
3390
|
+
* - `data-agent-native-node-id` — stable selection id stamped by the editor.
|
|
3391
|
+
* - `data-agent-native-layer-name` is intentionally kept: it is a
|
|
3392
|
+
* developer-authored attribute (the canonical layer-name hint) and is
|
|
3393
|
+
* useful in committed source. Only ephemeral runtime stamps are removed.
|
|
3394
|
+
*/
|
|
3395
|
+
const EDITOR_ONLY_ATTRIBUTES: readonly string[] = ["data-agent-native-node-id"];
|
|
3396
|
+
|
|
3397
|
+
/**
|
|
3398
|
+
* Strip editor-only runtime attributes from an HTML string, returning clean
|
|
3399
|
+
* source suitable for writing back to disk.
|
|
3400
|
+
*
|
|
3401
|
+
* Currently removes `data-agent-native-node-id` (and any future attributes
|
|
3402
|
+
* listed in EDITOR_ONLY_ATTRIBUTES). The function operates on the raw HTML
|
|
3403
|
+
* string with a regex that handles both quoted forms and unquoted values, and
|
|
3404
|
+
* is safe to apply to already-clean source (idempotent).
|
|
3405
|
+
*
|
|
3406
|
+
* @param html The raw HTML string, potentially containing editor stamps.
|
|
3407
|
+
* @returns A new string with all editor-only attributes removed.
|
|
3408
|
+
*/
|
|
3409
|
+
export function stripEditorOnlyAttributes(html: string): string {
|
|
3410
|
+
if (!html || typeof html !== "string") return html ?? "";
|
|
3411
|
+
let result = html;
|
|
3412
|
+
for (const attr of EDITOR_ONLY_ATTRIBUTES) {
|
|
3413
|
+
// Match the attribute with optional surrounding whitespace. The value may
|
|
3414
|
+
// be double-quoted, single-quoted, or unquoted (no spaces / > chars).
|
|
3415
|
+
// A leading \s+ is required so we only strip the attribute name+value pair
|
|
3416
|
+
// and leave surrounding markup intact.
|
|
3417
|
+
const re = new RegExp(
|
|
3418
|
+
`\\s+${attr.replace(/-/g, "\\-")}\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s"'=><\`]+)`,
|
|
3419
|
+
"gi",
|
|
3420
|
+
);
|
|
3421
|
+
result = result.replace(re, "");
|
|
3422
|
+
}
|
|
3423
|
+
return result;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3374
3426
|
export interface MoveNodeBetweenDocumentsOptions {
|
|
3375
3427
|
nodeId: string;
|
|
3376
3428
|
anchorNodeId?: string;
|
|
@@ -125,6 +125,7 @@ export function compile(timeline: MotionTimeline): CompileResult {
|
|
|
125
125
|
*/
|
|
126
126
|
export function parse(css: string): MotionTrack[] {
|
|
127
127
|
const tracks: MotionTrack[] = [];
|
|
128
|
+
const targetByAnimationName = parseAnimationTargets(css);
|
|
128
129
|
const kfRe = /@keyframes\s+(an-motion-[^\s{]+)\s*\{/g;
|
|
129
130
|
let m: RegExpExecArray | null;
|
|
130
131
|
|
|
@@ -138,15 +139,33 @@ export function parse(css: string): MotionTrack[] {
|
|
|
138
139
|
if (body === null) continue;
|
|
139
140
|
|
|
140
141
|
tracks.push({
|
|
141
|
-
targetNodeId: decoded.targetNodeId,
|
|
142
|
+
targetNodeId: targetByAnimationName.get(fullName) ?? decoded.targetNodeId,
|
|
142
143
|
property: decoded.property,
|
|
143
|
-
keyframes: parseKeyframeBody(body),
|
|
144
|
+
keyframes: parseKeyframeBody(body, decoded.property),
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
return tracks;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Extract the CSS body from a managed `<style data-agent-native-motion>` block
|
|
153
|
+
* inside an HTML document. Returns `null` when the document has no managed block
|
|
154
|
+
* or the block is malformed.
|
|
155
|
+
*/
|
|
156
|
+
export function extractManagedMotionCss(html: string): string | null {
|
|
157
|
+
const openRe = /<style\b(?=[^>]*\bdata-agent-native-motion\b)[^>]*>/i;
|
|
158
|
+
const openMatch = openRe.exec(html);
|
|
159
|
+
if (!openMatch) return null;
|
|
160
|
+
|
|
161
|
+
const bodyStart = openMatch.index + openMatch[0].length;
|
|
162
|
+
const afterOpen = html.slice(bodyStart);
|
|
163
|
+
const closeMatch = /<\s*\/\s*style\b[^>]*>/i.exec(afterOpen);
|
|
164
|
+
if (!closeMatch) return null;
|
|
165
|
+
|
|
166
|
+
return afterOpen.slice(0, closeMatch.index).trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
150
169
|
/**
|
|
151
170
|
* Return the djb2 hash of a CSS string — useful for verifying stored
|
|
152
171
|
* `compiled_hash` values without re-compiling a full timeline.
|
|
@@ -292,6 +311,30 @@ function escAttr(value: string): string {
|
|
|
292
311
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
293
312
|
}
|
|
294
313
|
|
|
314
|
+
function unescAttr(value: string): string {
|
|
315
|
+
return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseAnimationTargets(css: string): Map<string, string> {
|
|
319
|
+
const targets = new Map<string, string>();
|
|
320
|
+
const ruleRe =
|
|
321
|
+
/\[data-agent-native-node-id="((?:\\.|[^"\\])*)"\]\s*\{([^}]*)\}/g;
|
|
322
|
+
let m: RegExpExecArray | null;
|
|
323
|
+
|
|
324
|
+
while ((m = ruleRe.exec(css)) !== null) {
|
|
325
|
+
const targetNodeId = unescAttr(m[1]);
|
|
326
|
+
const body = m[2];
|
|
327
|
+
const nameMatch = body.match(/animation-name\s*:\s*([^;]+)/);
|
|
328
|
+
if (!nameMatch) continue;
|
|
329
|
+
for (const name of nameMatch[1].split(",")) {
|
|
330
|
+
const trimmed = name.trim();
|
|
331
|
+
if (trimmed) targets.set(trimmed, targetNodeId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return targets;
|
|
336
|
+
}
|
|
337
|
+
|
|
295
338
|
/**
|
|
296
339
|
* Find the content of the CSS block that starts just after position `start`
|
|
297
340
|
* (i.e., just after the opening `{`). Returns `null` on unbalanced braces.
|
|
@@ -309,10 +352,14 @@ function extractBlock(css: string, start: number): string | null {
|
|
|
309
352
|
}
|
|
310
353
|
|
|
311
354
|
/** Parse the interior of a `@keyframes` block into `MotionKeyframe[]`. */
|
|
312
|
-
function parseKeyframeBody(body: string): MotionKeyframe[] {
|
|
355
|
+
function parseKeyframeBody(body: string, property: string): MotionKeyframe[] {
|
|
313
356
|
const frames: MotionKeyframe[] = [];
|
|
314
357
|
const stopRe = /([\d.]+%|from|to)\s*\{([^}]*)\}/g;
|
|
315
358
|
let m: RegExpExecArray | null;
|
|
359
|
+
const propRe = new RegExp(
|
|
360
|
+
`^\\s*${escapeRegExp(property)}\\s*:\\s*([^;]+)`,
|
|
361
|
+
"m",
|
|
362
|
+
);
|
|
316
363
|
|
|
317
364
|
while ((m = stopRe.exec(body)) !== null) {
|
|
318
365
|
const pctStr = m[1];
|
|
@@ -323,10 +370,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
|
|
|
323
370
|
const easeMatch = content.match(/animation-timing-function\s*:\s*([^;]+)/);
|
|
324
371
|
const ease = easeMatch ? (easeMatch[1].trim() as MotionEase) : undefined;
|
|
325
372
|
|
|
326
|
-
// Extract the
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
);
|
|
373
|
+
// Extract the animated property's value. The compiler emits the same
|
|
374
|
+
// property for every stop, so parsing by the decoded property avoids
|
|
375
|
+
// confusing `animation-timing-function` with the animated value.
|
|
376
|
+
const propMatch = content.match(propRe);
|
|
330
377
|
const value = propMatch ? propMatch[1].trim() : "";
|
|
331
378
|
|
|
332
379
|
frames.push({ t, value, ...(ease !== undefined ? { ease } : {}) });
|
|
@@ -335,6 +382,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
|
|
|
335
382
|
return frames;
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
function escapeRegExp(value: string): string {
|
|
386
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
387
|
+
}
|
|
388
|
+
|
|
338
389
|
/**
|
|
339
390
|
* djb2 string hash — deterministic, no crypto dependency.
|
|
340
391
|
* Returns a 32-bit unsigned integer as a decimal string.
|
|
@@ -138,8 +138,8 @@ export const MOTION_PROPERTY_PRESETS: MotionPropertyPreset[] = [
|
|
|
138
138
|
/**
|
|
139
139
|
* Build a brand-new {@link MotionTrack} for a target node + property, seeded
|
|
140
140
|
* with two keyframes (start/end) so it is immediately valid for both the live
|
|
141
|
-
* preview bridge and the `apply-motion-edit`
|
|
142
|
-
* MotionDock "create first track" path.
|
|
141
|
+
* preview bridge and the `apply-motion-edit` managed CSS persist path. Used by
|
|
142
|
+
* the MotionDock "create first track" path.
|
|
143
143
|
*
|
|
144
144
|
* When `preset` is omitted, a neutral 0 → 1 opacity-style pair is used so the
|
|
145
145
|
* track still compiles; callers normally pass a {@link MotionPropertyPreset}.
|
|
@@ -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
|