@agent-native/core 0.163.3 → 0.164.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
- package/corpus/templates/analytics/netlify.toml +2 -0
- package/corpus/templates/assets/netlify.toml +2 -0
- package/corpus/templates/brain/netlify.toml +2 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
- package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
- package/corpus/templates/calendar/netlify.toml +2 -0
- package/corpus/templates/chat/netlify.toml +2 -0
- package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
- package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
- package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_index.tsx +10 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
- package/corpus/templates/clips/netlify.toml +2 -0
- package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
- package/corpus/templates/content/netlify.toml +2 -0
- package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
- package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
- package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
- package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
- package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
- package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
- package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
- package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
- package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
- package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
- package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
- package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
- package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
- package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
- package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
- package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
- package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
- package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
- package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
- package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
- package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
- package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
- package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
- package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
- package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
- package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
- package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
- package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
- package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
- package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
- package/corpus/templates/design/netlify.toml +2 -0
- package/corpus/templates/design/shared/board-file.ts +2 -1
- package/corpus/templates/dispatch/netlify.toml +2 -0
- package/corpus/templates/forms/netlify.toml +2 -0
- package/corpus/templates/mail/netlify.toml +2 -0
- package/corpus/templates/plan/netlify.toml +2 -0
- package/corpus/templates/slides/netlify.toml +2 -0
- package/dist/agent/engine/builder-engine.d.ts +6 -4
- package/dist/agent/engine/builder-engine.js +91 -26
- package/dist/agent/engine/registry.js +16 -0
- package/dist/cli/code-agent-agent-tools.d.ts +2 -0
- package/dist/cli/code-agent-agent-tools.js +188 -0
- package/dist/cli/code-agent-collaboration.d.ts +34 -0
- package/dist/cli/code-agent-collaboration.js +104 -0
- package/dist/cli/code-agent-executor.js +6 -0
- package/dist/cli/code-agent-schedules.d.ts +54 -0
- package/dist/cli/code-agent-schedules.js +198 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -2
- package/dist/client/EnvironmentBadge.js +6 -5
- package/dist/client/chat/run-recovery.js +3 -2
- package/dist/client/chat/tool-call-display.js +2 -3
- package/dist/client/components/CodeRequiredDialog.js +4 -2
- package/dist/client/i18n.js +5 -4
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +6 -1
- package/dist/client/settings/useBuilderStatus.js +22 -33
- package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
- package/dist/code-agents/index.d.ts +2 -0
- package/dist/code-agents/index.js +2 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/localization/shared.d.ts +2 -0
- package/dist/localization/shared.js +5 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +50 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/auth-marketing-locales.d.ts +7 -0
- package/dist/server/auth-marketing-locales.js +1322 -0
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +35 -6
- package/dist/server/auth.js +4 -17
- package/dist/server/builder-browser.d.ts +22 -0
- package/dist/server/builder-browser.js +77 -1
- package/dist/server/builder-oauth.d.ts +50 -0
- package/dist/server/builder-oauth.js +263 -0
- package/dist/server/core-routes-plugin.d.ts +11 -0
- package/dist/server/core-routes-plugin.js +291 -415
- package/dist/server/credential-provider.d.ts +1 -1
- package/dist/server/credential-provider.js +1 -1
- package/dist/server/csrf.js +2 -2
- package/dist/server/onboarding-html.js +52 -120
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -0
- package/dist/templates/chat/netlify.toml +2 -0
- package/dist/vite/client.js +35 -37
- package/docs/content/deployment.mdx +2 -0
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/netlify.toml +2 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
2
|
+
import type { CodeLayerNode } from "@shared/code-layer";
|
|
3
|
+
import { buildCodeLayerProjection } from "@shared/code-layer";
|
|
4
|
+
import type { RefObject } from "react";
|
|
5
|
+
|
|
6
|
+
import { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
|
|
7
|
+
import type { FrameGeometry } from "@/components/design/multi-screen/types";
|
|
8
|
+
import type { DesignHotkeyDistributeAxis } from "@/hooks/useDesignHotkeys";
|
|
9
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
10
|
+
import {
|
|
11
|
+
cloneCanvasFrameGeometry,
|
|
12
|
+
getCanvasFrameGeometry,
|
|
13
|
+
} from "@/pages/design-editor/design-data-geometry-utils";
|
|
14
|
+
import type { AlignableRect } from "@/pages/design-editor/layout-operations";
|
|
15
|
+
import { computeDistributedPositions } from "@/pages/design-editor/layout-operations";
|
|
16
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
17
|
+
|
|
18
|
+
export interface DistributeSelectionArgs {
|
|
19
|
+
activeFile: DesignFile;
|
|
20
|
+
boardFileId: string | undefined;
|
|
21
|
+
boardFrameGeometry: FrameGeometry | undefined;
|
|
22
|
+
canEditDesign: boolean;
|
|
23
|
+
commitNodePositions: (
|
|
24
|
+
baseContent: string,
|
|
25
|
+
positions: ReadonlyMap<string, { x: number; y: number }>,
|
|
26
|
+
) => boolean;
|
|
27
|
+
designDataJsonRef: RefObject<Record<string, unknown>>;
|
|
28
|
+
getActiveFileSelectedNodeIds: (content: string) => string[];
|
|
29
|
+
getFreshActiveContent: () => string;
|
|
30
|
+
handleGeometryCommit: (
|
|
31
|
+
before: CanvasFrameGeometryById,
|
|
32
|
+
after: CanvasFrameGeometryById,
|
|
33
|
+
options?: { source?: "pointer" | "keyboard" },
|
|
34
|
+
) => void;
|
|
35
|
+
overviewScreens: OverviewScreen[];
|
|
36
|
+
overviewSelectedScreenIds: string[];
|
|
37
|
+
rectFromCodeLayerNode: (node: CodeLayerNode) => AlignableRect;
|
|
38
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function runDistributeSelection(
|
|
42
|
+
{
|
|
43
|
+
activeFile,
|
|
44
|
+
boardFileId,
|
|
45
|
+
boardFrameGeometry,
|
|
46
|
+
canEditDesign,
|
|
47
|
+
commitNodePositions,
|
|
48
|
+
designDataJsonRef,
|
|
49
|
+
getActiveFileSelectedNodeIds,
|
|
50
|
+
getFreshActiveContent,
|
|
51
|
+
handleGeometryCommit,
|
|
52
|
+
overviewScreens,
|
|
53
|
+
overviewSelectedScreenIds,
|
|
54
|
+
rectFromCodeLayerNode,
|
|
55
|
+
viewModeRef,
|
|
56
|
+
}: DistributeSelectionArgs,
|
|
57
|
+
axis: DesignHotkeyDistributeAxis,
|
|
58
|
+
) {
|
|
59
|
+
if (!canEditDesign) return;
|
|
60
|
+
|
|
61
|
+
if (viewModeRef.current === "overview") {
|
|
62
|
+
if (overviewSelectedScreenIds.length < 3) return;
|
|
63
|
+
const before = getCanvasFrameGeometry(designDataJsonRef.current);
|
|
64
|
+
const screenRects: AlignableRect[] = [];
|
|
65
|
+
overviewSelectedScreenIds.forEach((screenId) => {
|
|
66
|
+
const screenIndex = overviewScreens.findIndex(
|
|
67
|
+
(screen) => screen.id === screenId,
|
|
68
|
+
);
|
|
69
|
+
const screen =
|
|
70
|
+
screenIndex >= 0 ? overviewScreens[screenIndex] : undefined;
|
|
71
|
+
const fallbackGeometry =
|
|
72
|
+
screenIndex >= 0
|
|
73
|
+
? getInitialFrameGeometry(screenIndex, {
|
|
74
|
+
width: screen?.width ?? 1280,
|
|
75
|
+
height: screen?.height ?? 2560,
|
|
76
|
+
})
|
|
77
|
+
: boardFileId === screenId
|
|
78
|
+
? boardFrameGeometry
|
|
79
|
+
: undefined;
|
|
80
|
+
if (!fallbackGeometry) return;
|
|
81
|
+
const geometry = { ...fallbackGeometry, ...before[screenId] };
|
|
82
|
+
screenRects.push({
|
|
83
|
+
id: screenId,
|
|
84
|
+
x: geometry.x,
|
|
85
|
+
y: geometry.y,
|
|
86
|
+
width: geometry.width,
|
|
87
|
+
height: geometry.height,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
if (screenRects.length < 3) return;
|
|
91
|
+
const positions = computeDistributedPositions(screenRects, axis);
|
|
92
|
+
if (positions.size === 0) return;
|
|
93
|
+
const after = cloneCanvasFrameGeometry(before);
|
|
94
|
+
positions.forEach((position, screenId) => {
|
|
95
|
+
after[screenId] = { ...after[screenId]!, ...position };
|
|
96
|
+
});
|
|
97
|
+
handleGeometryCommit(before, after);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!activeFile) return;
|
|
102
|
+
const baseContent = getFreshActiveContent();
|
|
103
|
+
const nodeIds = getActiveFileSelectedNodeIds(baseContent);
|
|
104
|
+
if (nodeIds.length < 3) return;
|
|
105
|
+
const projection = buildCodeLayerProjection(baseContent);
|
|
106
|
+
const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
|
|
107
|
+
const selectedNodes = nodeIds
|
|
108
|
+
.map((nodeId) => nodesById.get(nodeId))
|
|
109
|
+
.filter((node): node is CodeLayerNode => Boolean(node));
|
|
110
|
+
if (selectedNodes.length < 3) return;
|
|
111
|
+
const selectedRects = selectedNodes.map(rectFromCodeLayerNode);
|
|
112
|
+
const positions = computeDistributedPositions(selectedRects, axis);
|
|
113
|
+
commitNodePositions(baseContent, positions);
|
|
114
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
2
|
+
import { normalizeDesignSourceType } from "@shared/source-mode";
|
|
3
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
|
|
6
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
7
|
+
import type { RasterPdfPage } from "@/pages/design-editor/export-capture";
|
|
8
|
+
import {
|
|
9
|
+
PDF_MIN_PRINT_RASTER_SCALE,
|
|
10
|
+
createMultiPageRasterPdf,
|
|
11
|
+
} from "@/pages/design-editor/export-capture";
|
|
12
|
+
import {
|
|
13
|
+
PngCaptureError,
|
|
14
|
+
cropCanvasToRect,
|
|
15
|
+
renderExportDocumentCanvas,
|
|
16
|
+
} from "@/pages/design-editor/png-export-render";
|
|
17
|
+
|
|
18
|
+
export interface DownloadAllScreensPdfArgs {
|
|
19
|
+
activeCanvasSourceType: "inline" | "localhost" | "fusion";
|
|
20
|
+
canEditDesign: boolean;
|
|
21
|
+
canvasFrameGeometryById: CanvasFrameGeometryById;
|
|
22
|
+
fallbackExportName: (extension: string, suffix?: string) => string;
|
|
23
|
+
overviewScreens: OverviewScreen[];
|
|
24
|
+
pngExportingRef: RefObject<boolean>;
|
|
25
|
+
setPngExporting: Dispatch<SetStateAction<boolean>>;
|
|
26
|
+
showRasterCaptureError: (error: unknown, format?: "png" | "pdf") => void;
|
|
27
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
28
|
+
triggerBlobDownload: (blob: Blob, filename: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runDownloadAllScreensPdf({
|
|
32
|
+
activeCanvasSourceType,
|
|
33
|
+
canEditDesign,
|
|
34
|
+
canvasFrameGeometryById,
|
|
35
|
+
fallbackExportName,
|
|
36
|
+
overviewScreens,
|
|
37
|
+
pngExportingRef,
|
|
38
|
+
setPngExporting,
|
|
39
|
+
showRasterCaptureError,
|
|
40
|
+
t,
|
|
41
|
+
triggerBlobDownload,
|
|
42
|
+
}: DownloadAllScreensPdfArgs) {
|
|
43
|
+
if (pngExportingRef.current) return;
|
|
44
|
+
if (overviewScreens.length < 2) return;
|
|
45
|
+
pngExportingRef.current = true;
|
|
46
|
+
setPngExporting(true);
|
|
47
|
+
try {
|
|
48
|
+
const html2canvas = (await import("html2canvas")).default;
|
|
49
|
+
const pages: RasterPdfPage[] = [];
|
|
50
|
+
for (const screen of overviewScreens) {
|
|
51
|
+
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
52
|
+
`iframe[data-design-preview-iframe][data-screen-iframe-id="${CSS.escape(screen.id)}"]`,
|
|
53
|
+
);
|
|
54
|
+
if (!iframe) throw new PngCaptureError("no-preview");
|
|
55
|
+
let doc: Document | null = null;
|
|
56
|
+
try {
|
|
57
|
+
doc = iframe.contentDocument;
|
|
58
|
+
if (!doc?.documentElement) doc = null;
|
|
59
|
+
} catch {
|
|
60
|
+
doc = null;
|
|
61
|
+
}
|
|
62
|
+
if (!doc) {
|
|
63
|
+
const sourceType =
|
|
64
|
+
normalizeDesignSourceType(iframe.dataset.designSourceType) ??
|
|
65
|
+
activeCanvasSourceType;
|
|
66
|
+
if (sourceType !== "inline") {
|
|
67
|
+
throw new PngCaptureError("external-preview");
|
|
68
|
+
}
|
|
69
|
+
if (!canEditDesign) {
|
|
70
|
+
throw new PngCaptureError("read-only-preview");
|
|
71
|
+
}
|
|
72
|
+
throw new PngCaptureError("no-preview");
|
|
73
|
+
}
|
|
74
|
+
const geometry = canvasFrameGeometryById[screen.id] ?? {};
|
|
75
|
+
const pageWidth = Math.max(
|
|
76
|
+
1,
|
|
77
|
+
geometry.width ?? screen.width ?? iframe.clientWidth,
|
|
78
|
+
);
|
|
79
|
+
const pageHeight = Math.max(
|
|
80
|
+
1,
|
|
81
|
+
geometry.height ?? screen.height ?? iframe.clientHeight,
|
|
82
|
+
);
|
|
83
|
+
const rendered = await renderExportDocumentCanvas({
|
|
84
|
+
doc,
|
|
85
|
+
iframe,
|
|
86
|
+
// Same print-quality floor as the single-page path: a 1x capture
|
|
87
|
+
// stretched to fill a fixed physical page size reads as blurry.
|
|
88
|
+
exportScale: PDF_MIN_PRINT_RASTER_SCALE,
|
|
89
|
+
render: html2canvas,
|
|
90
|
+
});
|
|
91
|
+
const view = doc.defaultView;
|
|
92
|
+
const viewportCanvas = cropCanvasToRect(
|
|
93
|
+
rendered.canvas,
|
|
94
|
+
{
|
|
95
|
+
x: view?.scrollX ?? 0,
|
|
96
|
+
y: view?.scrollY ?? 0,
|
|
97
|
+
width: Math.max(1, iframe.clientWidth),
|
|
98
|
+
height: Math.max(1, iframe.clientHeight),
|
|
99
|
+
},
|
|
100
|
+
rendered.scale,
|
|
101
|
+
);
|
|
102
|
+
const dataUrl = (viewportCanvas ?? rendered.canvas).toDataURL(
|
|
103
|
+
"image/png",
|
|
104
|
+
);
|
|
105
|
+
pages.push({ dataUrl, width: pageWidth, height: pageHeight });
|
|
106
|
+
}
|
|
107
|
+
const pdf = await createMultiPageRasterPdf(pages);
|
|
108
|
+
triggerBlobDownload(pdf, fallbackExportName("pdf", "all-screens"));
|
|
109
|
+
toast.success(t("designEditor.toasts.pdfAllScreensDownloaded"));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
showRasterCaptureError(error, "pdf");
|
|
112
|
+
} finally {
|
|
113
|
+
pngExportingRef.current = false;
|
|
114
|
+
setPngExporting(false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
|
|
4
|
+
import type { ExportSettingsValue } from "@/components/design/inspector";
|
|
5
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
6
|
+
import {
|
|
7
|
+
PDF_MIN_PRINT_RASTER_SCALE,
|
|
8
|
+
createSinglePageRasterPdf,
|
|
9
|
+
} from "@/pages/design-editor/export-capture";
|
|
10
|
+
import type { PngCaptureScope } from "@/pages/design-editor/png-export-render";
|
|
11
|
+
import { resolveExportCropRect } from "@/pages/design-editor/png-export-render";
|
|
12
|
+
|
|
13
|
+
export interface DownloadPdfArgs {
|
|
14
|
+
fallbackExportName: (extension: string, suffix?: string) => string;
|
|
15
|
+
pngExportingRef: RefObject<boolean>;
|
|
16
|
+
renderPngBlob: (arg0: {
|
|
17
|
+
scope: PngCaptureScope;
|
|
18
|
+
settings?: Partial<ExportSettingsValue>;
|
|
19
|
+
format?: "png" | "jpg" | "webp";
|
|
20
|
+
}) => Promise<Blob>;
|
|
21
|
+
resolvePngCaptureTarget: (scope: PngCaptureScope) => {
|
|
22
|
+
cropSelection: ElementInfo | readonly ElementInfo[] | null;
|
|
23
|
+
doc: Document;
|
|
24
|
+
iframe: HTMLIFrameElement;
|
|
25
|
+
};
|
|
26
|
+
setPngExporting: Dispatch<SetStateAction<boolean>>;
|
|
27
|
+
showRasterCaptureError: (error: unknown, format?: "png" | "pdf") => void;
|
|
28
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
29
|
+
triggerBlobDownload: (blob: Blob, filename: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runDownloadPdf(
|
|
33
|
+
{
|
|
34
|
+
fallbackExportName,
|
|
35
|
+
pngExportingRef,
|
|
36
|
+
renderPngBlob,
|
|
37
|
+
resolvePngCaptureTarget,
|
|
38
|
+
setPngExporting,
|
|
39
|
+
showRasterCaptureError,
|
|
40
|
+
t,
|
|
41
|
+
triggerBlobDownload,
|
|
42
|
+
}: DownloadPdfArgs,
|
|
43
|
+
settings?: Partial<ExportSettingsValue>,
|
|
44
|
+
) {
|
|
45
|
+
if (pngExportingRef.current) return;
|
|
46
|
+
pngExportingRef.current = true;
|
|
47
|
+
setPngExporting(true);
|
|
48
|
+
try {
|
|
49
|
+
const { cropSelection, doc, iframe } = resolvePngCaptureTarget("document");
|
|
50
|
+
const crop = resolveExportCropRect(doc, cropSelection);
|
|
51
|
+
const pageWidth = Math.max(
|
|
52
|
+
1,
|
|
53
|
+
crop?.width ??
|
|
54
|
+
Math.max(
|
|
55
|
+
doc.documentElement.scrollWidth,
|
|
56
|
+
doc.body?.scrollWidth ?? 0,
|
|
57
|
+
iframe.clientWidth,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
const pageHeight = Math.max(
|
|
61
|
+
1,
|
|
62
|
+
crop?.height ??
|
|
63
|
+
Math.max(
|
|
64
|
+
doc.documentElement.scrollHeight,
|
|
65
|
+
doc.body?.scrollHeight ?? 0,
|
|
66
|
+
iframe.clientHeight,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
// Force a print-quality raster floor: the PDF page renders at a fixed
|
|
70
|
+
// physical size (see createSinglePageRasterPdf), so a 1x capture —
|
|
71
|
+
// the export panel's ordinary default — embeds only ~96 DPI, which
|
|
72
|
+
// looks visibly soft once printed. Still honor an explicit higher
|
|
73
|
+
// user-selected scale (3x/4x).
|
|
74
|
+
const pdfScale = Math.max(
|
|
75
|
+
PDF_MIN_PRINT_RASTER_SCALE,
|
|
76
|
+
settings?.scale ?? PDF_MIN_PRINT_RASTER_SCALE,
|
|
77
|
+
);
|
|
78
|
+
const png = await renderPngBlob({
|
|
79
|
+
scope: "document",
|
|
80
|
+
settings: { ...settings, scale: pdfScale },
|
|
81
|
+
format: "png",
|
|
82
|
+
});
|
|
83
|
+
const dataUrl = await new Promise<string>((resolve, reject) => {
|
|
84
|
+
const reader = new FileReader();
|
|
85
|
+
reader.onerror = () =>
|
|
86
|
+
reject(reader.error ?? new Error("PDF read failed"));
|
|
87
|
+
reader.onload = () => resolve(String(reader.result));
|
|
88
|
+
reader.readAsDataURL(png);
|
|
89
|
+
});
|
|
90
|
+
const pdf = await createSinglePageRasterPdf({
|
|
91
|
+
dataUrl,
|
|
92
|
+
width: pageWidth,
|
|
93
|
+
height: pageHeight,
|
|
94
|
+
});
|
|
95
|
+
triggerBlobDownload(pdf, fallbackExportName("pdf", settings?.suffix));
|
|
96
|
+
toast.success(t("designEditor.toasts.pdfDownloaded"));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
showRasterCaptureError(error, "pdf");
|
|
99
|
+
} finally {
|
|
100
|
+
pngExportingRef.current = false;
|
|
101
|
+
setPngExporting(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Dispatch, SetStateAction } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
|
|
4
|
+
import type { ExportSettingsValue } from "@/components/design/inspector";
|
|
5
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
6
|
+
import {
|
|
7
|
+
buildStaticForeignObjectSvg,
|
|
8
|
+
stripNonStaticXmlAttributes,
|
|
9
|
+
waitForExportReady,
|
|
10
|
+
} from "@/pages/design-editor/export-capture";
|
|
11
|
+
import {
|
|
12
|
+
removeEditorChromeOverlays,
|
|
13
|
+
resolveExportCropRect,
|
|
14
|
+
sanitizeSerializedXmlForSvg,
|
|
15
|
+
} from "@/pages/design-editor/png-export-render";
|
|
16
|
+
import type { DesignData } from "@/pages/design-editor/types";
|
|
17
|
+
|
|
18
|
+
export interface DownloadSvgArgs {
|
|
19
|
+
design: DesignData | null;
|
|
20
|
+
fallbackExportName: (extension: string, suffix?: string) => string;
|
|
21
|
+
selectedElement: ElementInfo | null;
|
|
22
|
+
setSvgExporting: Dispatch<SetStateAction<boolean>>;
|
|
23
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
24
|
+
triggerBlobDownload: (blob: Blob, filename: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runDownloadSvg(
|
|
28
|
+
{
|
|
29
|
+
design,
|
|
30
|
+
fallbackExportName,
|
|
31
|
+
selectedElement,
|
|
32
|
+
setSvgExporting,
|
|
33
|
+
t,
|
|
34
|
+
triggerBlobDownload,
|
|
35
|
+
}: DownloadSvgArgs,
|
|
36
|
+
settings?: Partial<ExportSettingsValue>,
|
|
37
|
+
) {
|
|
38
|
+
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
39
|
+
"iframe[data-design-preview-iframe]",
|
|
40
|
+
);
|
|
41
|
+
const doc = iframe?.contentDocument;
|
|
42
|
+
if (!doc?.documentElement) {
|
|
43
|
+
toast.error(t("designEditor.toasts.openScreenSvg"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setSvgExporting(true);
|
|
48
|
+
try {
|
|
49
|
+
// Same readiness race as the PNG path (see waitForExportReady):
|
|
50
|
+
// without this, an SVG downloaded right after a screen loads inlines
|
|
51
|
+
// whatever partial/empty CSSOM exists at that instant, producing an
|
|
52
|
+
// unstyled export for no reason a user could tell from the UI.
|
|
53
|
+
await waitForExportReady(doc);
|
|
54
|
+
const width = Math.max(
|
|
55
|
+
doc.documentElement.scrollWidth,
|
|
56
|
+
doc.body?.scrollWidth ?? 0,
|
|
57
|
+
iframe?.clientWidth ?? 0,
|
|
58
|
+
);
|
|
59
|
+
const height = Math.max(
|
|
60
|
+
doc.documentElement.scrollHeight,
|
|
61
|
+
doc.body?.scrollHeight ?? 0,
|
|
62
|
+
iframe?.clientHeight ?? 0,
|
|
63
|
+
);
|
|
64
|
+
const clone = doc.documentElement.cloneNode(true) as HTMLElement;
|
|
65
|
+
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
66
|
+
const stylesheetLinks = Array.from(
|
|
67
|
+
doc.querySelectorAll<HTMLLinkElement>('link[rel~="stylesheet"]'),
|
|
68
|
+
);
|
|
69
|
+
const clonedStylesheetLinks = Array.from(
|
|
70
|
+
clone.querySelectorAll<HTMLLinkElement>('link[rel~="stylesheet"]'),
|
|
71
|
+
);
|
|
72
|
+
const stylesheets = Array.from(doc.styleSheets);
|
|
73
|
+
|
|
74
|
+
stylesheetLinks.forEach((link, index) => {
|
|
75
|
+
const sheet = stylesheets.find(
|
|
76
|
+
(candidate) =>
|
|
77
|
+
(candidate as StyleSheet & { ownerNode?: Node | null }).ownerNode ===
|
|
78
|
+
link,
|
|
79
|
+
) as CSSStyleSheet | undefined;
|
|
80
|
+
let cssText = "";
|
|
81
|
+
try {
|
|
82
|
+
cssText = Array.from(sheet?.cssRules ?? [])
|
|
83
|
+
.map((rule) => rule.cssText)
|
|
84
|
+
.join("\n");
|
|
85
|
+
} catch {
|
|
86
|
+
// Cross-origin stylesheets cannot be read. Leave the original link in
|
|
87
|
+
// place instead of failing the whole export.
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!cssText.trim()) return;
|
|
91
|
+
const style = doc.createElement("style");
|
|
92
|
+
style.setAttribute(
|
|
93
|
+
"data-agent-native-inlined-stylesheet",
|
|
94
|
+
link.getAttribute("href") ?? "",
|
|
95
|
+
);
|
|
96
|
+
style.textContent = cssText;
|
|
97
|
+
clonedStylesheetLinks[index]?.replaceWith(style);
|
|
98
|
+
});
|
|
99
|
+
clone.querySelectorAll("script").forEach((node) => node.remove());
|
|
100
|
+
// Strip the editor's selection outline / handles so the SVG shows only
|
|
101
|
+
// the design, not the editor chrome.
|
|
102
|
+
removeEditorChromeOverlays(clone);
|
|
103
|
+
// The live DOM can contain Alpine/Vue-style directive attributes such
|
|
104
|
+
// as `@click`, `:class`, and `x-bind:class`. They are valid HTML but
|
|
105
|
+
// invalid/unbound XML names, and the old serializer emitted a download
|
|
106
|
+
// that xmllint/Figma rejected at the first directive. This is a static
|
|
107
|
+
// snapshot (scripts are removed just above), so strip executable and
|
|
108
|
+
// otherwise XML-unsafe attributes from the clone before serialization.
|
|
109
|
+
stripNonStaticXmlAttributes(clone);
|
|
110
|
+
clone.style.width = `${width}px`;
|
|
111
|
+
clone.style.minHeight = `${height}px`;
|
|
112
|
+
|
|
113
|
+
const body = clone.querySelector("body") as HTMLElement | null;
|
|
114
|
+
if (body) {
|
|
115
|
+
body.style.margin = body.style.margin || "0";
|
|
116
|
+
body.style.width = `${width}px`;
|
|
117
|
+
body.style.minHeight = `${height}px`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const serializedHtml = sanitizeSerializedXmlForSvg(
|
|
121
|
+
new XMLSerializer().serializeToString(clone),
|
|
122
|
+
);
|
|
123
|
+
const safeTitle =
|
|
124
|
+
design?.title
|
|
125
|
+
?.replace(/&/g, "&")
|
|
126
|
+
.replace(/"/g, """)
|
|
127
|
+
.replace(/</g, "<")
|
|
128
|
+
.replace(/>/g, ">") || t("designEditor.designExport");
|
|
129
|
+
const exportScale = Math.max(0.1, Math.min(4, settings?.scale ?? 1));
|
|
130
|
+
// When an element is selected, crop to just that frame by narrowing the
|
|
131
|
+
// SVG viewBox to its document-space rect. The foreignObject still holds
|
|
132
|
+
// the full document so layout and inherited styles stay intact; the
|
|
133
|
+
// viewBox clips the visible region to the selection.
|
|
134
|
+
const cropRect = resolveExportCropRect(doc, selectedElement);
|
|
135
|
+
const svg = buildStaticForeignObjectSvg({
|
|
136
|
+
documentWidth: width,
|
|
137
|
+
documentHeight: height,
|
|
138
|
+
cropRect,
|
|
139
|
+
scale: exportScale,
|
|
140
|
+
safeTitle,
|
|
141
|
+
serializedHtml,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
triggerBlobDownload(
|
|
145
|
+
new Blob([svg], { type: "image/svg+xml;charset=utf-8" }),
|
|
146
|
+
fallbackExportName("svg", settings?.suffix),
|
|
147
|
+
);
|
|
148
|
+
toast.success(t("designEditor.toasts.svgDownloaded"));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("SVG export failed:", error);
|
|
151
|
+
toast.error(
|
|
152
|
+
error instanceof Error
|
|
153
|
+
? error.message
|
|
154
|
+
: t("designEditor.toasts.svgExportError"),
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
setSvgExporting(false);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
3
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { RefObject } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
import { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
|
|
8
|
+
import type { FrameGeometry } from "@/components/design/multi-screen/types";
|
|
9
|
+
import {
|
|
10
|
+
nextDuplicatedFilename,
|
|
11
|
+
normalizedDesignFileType,
|
|
12
|
+
reassignDuplicatedNodeIds,
|
|
13
|
+
} from "@/pages/design-editor/canvas-primitive-insert";
|
|
14
|
+
import type { DesignDataOperation } from "@/pages/design-editor/data-operations";
|
|
15
|
+
import { applyDesignDataOperations } from "@/pages/design-editor/data-operations";
|
|
16
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
17
|
+
import {
|
|
18
|
+
getCanvasFrameGeometry,
|
|
19
|
+
getDesignDataRecord,
|
|
20
|
+
} from "@/pages/design-editor/design-data-geometry-utils";
|
|
21
|
+
import type { FileCreationHistoryEntry } from "@/pages/design-editor/history";
|
|
22
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
23
|
+
|
|
24
|
+
export interface DuplicateScreenArgs {
|
|
25
|
+
canEditDesign: boolean;
|
|
26
|
+
createFileAsync: ReturnType<
|
|
27
|
+
typeof useActionMutation<undefined, undefined, "create-file">
|
|
28
|
+
>["mutateAsync"];
|
|
29
|
+
designDataJsonRef: RefObject<Record<string, unknown>>;
|
|
30
|
+
files: DesignFile[];
|
|
31
|
+
focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
|
|
32
|
+
id: string | undefined;
|
|
33
|
+
liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
|
|
34
|
+
optimisticallyInsertCreatedFile: (args: {
|
|
35
|
+
fileId: string;
|
|
36
|
+
filename: string;
|
|
37
|
+
fileType: DesignFile["fileType"];
|
|
38
|
+
content: string;
|
|
39
|
+
result?: Record<string, unknown> | null;
|
|
40
|
+
}) => void;
|
|
41
|
+
overviewScreens: OverviewScreen[];
|
|
42
|
+
queryClient: QueryClient;
|
|
43
|
+
recordFileCreationHistoryEntry: (entry: FileCreationHistoryEntry) => void;
|
|
44
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
45
|
+
updateDesignAsync: ReturnType<
|
|
46
|
+
typeof useActionMutation<undefined, undefined, "update-design">
|
|
47
|
+
>["mutateAsync"];
|
|
48
|
+
writeFrameGeometrySnapshot: (
|
|
49
|
+
geometryById: CanvasFrameGeometryById,
|
|
50
|
+
options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
|
|
51
|
+
) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runDuplicateScreen(
|
|
55
|
+
{
|
|
56
|
+
canEditDesign,
|
|
57
|
+
createFileAsync,
|
|
58
|
+
designDataJsonRef,
|
|
59
|
+
files,
|
|
60
|
+
focusCreatedScreen,
|
|
61
|
+
id,
|
|
62
|
+
liveFrameGeometryRef,
|
|
63
|
+
optimisticallyInsertCreatedFile,
|
|
64
|
+
overviewScreens,
|
|
65
|
+
queryClient,
|
|
66
|
+
recordFileCreationHistoryEntry,
|
|
67
|
+
t,
|
|
68
|
+
updateDesignAsync,
|
|
69
|
+
writeFrameGeometrySnapshot,
|
|
70
|
+
}: DuplicateScreenArgs,
|
|
71
|
+
screenId: string,
|
|
72
|
+
request?: {
|
|
73
|
+
canvasPosition?: { x: number; y: number };
|
|
74
|
+
},
|
|
75
|
+
) {
|
|
76
|
+
if (!id || !canEditDesign) return;
|
|
77
|
+
const source = files.find((file) => file.id === screenId);
|
|
78
|
+
if (!source) return;
|
|
79
|
+
const filename = nextDuplicatedFilename(files, source.filename);
|
|
80
|
+
const content = reassignDuplicatedNodeIds(source.content);
|
|
81
|
+
const fileType = normalizedDesignFileType(source.fileType);
|
|
82
|
+
const sourceOverviewScreen = overviewScreens.find(
|
|
83
|
+
(screen) => screen.id === screenId,
|
|
84
|
+
);
|
|
85
|
+
const fallbackGeometry = getInitialFrameGeometry(overviewScreens.length, {
|
|
86
|
+
width: sourceOverviewScreen?.width ?? 1280,
|
|
87
|
+
height: sourceOverviewScreen?.height ?? 2560,
|
|
88
|
+
});
|
|
89
|
+
const createdGeometry: FrameGeometry = request?.canvasPosition
|
|
90
|
+
? {
|
|
91
|
+
...fallbackGeometry,
|
|
92
|
+
...liveFrameGeometryRef.current[screenId],
|
|
93
|
+
x: request.canvasPosition.x,
|
|
94
|
+
y: request.canvasPosition.y,
|
|
95
|
+
}
|
|
96
|
+
: fallbackGeometry;
|
|
97
|
+
|
|
98
|
+
// Per-call mutate callbacks, not a promise, would silently strand every
|
|
99
|
+
// duplicate but the last: a second mutate() detaches the observer from
|
|
100
|
+
// the first mutation, so only the newest call's onSuccess ever runs.
|
|
101
|
+
void createFileAsync({
|
|
102
|
+
designId: id,
|
|
103
|
+
filename,
|
|
104
|
+
content,
|
|
105
|
+
fileType,
|
|
106
|
+
} as any)
|
|
107
|
+
.then((result: any) => {
|
|
108
|
+
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
109
|
+
// Refetch only when there is no created id to insert optimistically:
|
|
110
|
+
// a whole-design refetch re-downloads every screen's HTML, which is
|
|
111
|
+
// what made adding a frame feel slow.
|
|
112
|
+
if (!nextId) {
|
|
113
|
+
queryClient.invalidateQueries({
|
|
114
|
+
queryKey: ["action", "get-design"],
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
optimisticallyInsertCreatedFile({
|
|
118
|
+
fileId: nextId,
|
|
119
|
+
filename,
|
|
120
|
+
fileType,
|
|
121
|
+
content,
|
|
122
|
+
result,
|
|
123
|
+
});
|
|
124
|
+
// Optimistic geometry keeps frame, selection, and camera agreeing
|
|
125
|
+
// before the refetch. Base it on the map writeFrameGeometrySnapshot
|
|
126
|
+
// diffs against, or a sibling duplicate's placement is deleted.
|
|
127
|
+
writeFrameGeometrySnapshot({
|
|
128
|
+
...getCanvasFrameGeometry(designDataJsonRef.current),
|
|
129
|
+
[nextId]: createdGeometry,
|
|
130
|
+
});
|
|
131
|
+
focusCreatedScreen(nextId, createdGeometry);
|
|
132
|
+
recordFileCreationHistoryEntry({
|
|
133
|
+
filename,
|
|
134
|
+
content,
|
|
135
|
+
fileType,
|
|
136
|
+
geometry: createdGeometry,
|
|
137
|
+
});
|
|
138
|
+
// A duplicated localhost/fusion screen stays URL-backed only if its
|
|
139
|
+
// metadata comes along, and the carry must be path-addressed or it
|
|
140
|
+
// replaces a peer's metadata for every other screen.
|
|
141
|
+
const sourceMetadataById = getDesignDataRecord(
|
|
142
|
+
designDataJsonRef.current,
|
|
143
|
+
"screenMetadata",
|
|
144
|
+
);
|
|
145
|
+
const sourceMetadata = getDesignDataRecord(
|
|
146
|
+
sourceMetadataById,
|
|
147
|
+
screenId,
|
|
148
|
+
);
|
|
149
|
+
const sourceType = sourceMetadata.sourceType;
|
|
150
|
+
if (sourceType === "localhost" || sourceType === "fusion") {
|
|
151
|
+
const dataOperations: DesignDataOperation[] = [
|
|
152
|
+
{
|
|
153
|
+
op: "set",
|
|
154
|
+
path: ["screenMetadata", nextId],
|
|
155
|
+
value: { ...sourceMetadata },
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const sourceLocalhostScreensById = getDesignDataRecord(
|
|
159
|
+
designDataJsonRef.current,
|
|
160
|
+
"localhostScreens",
|
|
161
|
+
);
|
|
162
|
+
const sourceLocalhostScreen = getDesignDataRecord(
|
|
163
|
+
sourceLocalhostScreensById,
|
|
164
|
+
screenId,
|
|
165
|
+
);
|
|
166
|
+
if (Object.keys(sourceLocalhostScreen).length > 0) {
|
|
167
|
+
dataOperations.push({
|
|
168
|
+
op: "set",
|
|
169
|
+
path: ["localhostScreens", nextId],
|
|
170
|
+
value: { ...sourceLocalhostScreen },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const nextData = applyDesignDataOperations(
|
|
174
|
+
designDataJsonRef.current,
|
|
175
|
+
dataOperations,
|
|
176
|
+
);
|
|
177
|
+
designDataJsonRef.current = nextData;
|
|
178
|
+
queryClient.setQueryData(
|
|
179
|
+
["action", "get-design", { id }],
|
|
180
|
+
(old: any) => {
|
|
181
|
+
if (!old || typeof old !== "object") return old;
|
|
182
|
+
return { ...old, data: JSON.stringify(nextData) };
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
void updateDesignAsync({ id, dataOperations } as any).catch(() => {
|
|
186
|
+
queryClient.invalidateQueries({
|
|
187
|
+
queryKey: ["action", "get-design"],
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
toast.success(t("designEditor.toasts.screenDuplicated"));
|
|
193
|
+
})
|
|
194
|
+
.catch((error: unknown) => {
|
|
195
|
+
toast.error(
|
|
196
|
+
error instanceof Error
|
|
197
|
+
? error.message
|
|
198
|
+
: t("designEditor.toasts.screenDuplicateError"),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
}
|