@agent-native/core 0.163.4 → 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/progress/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/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -0
- package/dist/templates/chat/netlify.toml +2 -0
- 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,131 @@
|
|
|
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 { trace } from "@/components/design/design-trace";
|
|
8
|
+
import type { FrameGeometry } from "@/components/design/multi-screen/types";
|
|
9
|
+
import { prettyScreenName } from "@/lib/screen-names";
|
|
10
|
+
import {
|
|
11
|
+
blankScreenHtml,
|
|
12
|
+
nextBlankScreenFilename,
|
|
13
|
+
} from "@/pages/design-editor/canvas-primitive-insert";
|
|
14
|
+
import type { FileCreationHistoryEntry } from "@/pages/design-editor/history";
|
|
15
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
16
|
+
|
|
17
|
+
export interface CreateScreenFrameArgs {
|
|
18
|
+
canEditDesign: boolean;
|
|
19
|
+
canvasFrameGeometryById: CanvasFrameGeometryById;
|
|
20
|
+
createFileMutation: ReturnType<
|
|
21
|
+
typeof useActionMutation<undefined, undefined, "create-file">
|
|
22
|
+
>;
|
|
23
|
+
files: DesignFile[];
|
|
24
|
+
focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
|
|
25
|
+
id: string | undefined;
|
|
26
|
+
locallyPinnedHeightIdsRef: RefObject<Set<string>>;
|
|
27
|
+
optimisticallyInsertCreatedFile: (args: {
|
|
28
|
+
fileId: string;
|
|
29
|
+
filename: string;
|
|
30
|
+
fileType: DesignFile["fileType"];
|
|
31
|
+
content: string;
|
|
32
|
+
result?: Record<string, unknown> | null;
|
|
33
|
+
}) => void;
|
|
34
|
+
queryClient: QueryClient;
|
|
35
|
+
recordFileCreationHistoryEntry: (entry: FileCreationHistoryEntry) => void;
|
|
36
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
37
|
+
writeFrameGeometrySnapshot: (
|
|
38
|
+
geometryById: CanvasFrameGeometryById,
|
|
39
|
+
options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
|
|
40
|
+
) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runCreateScreenFrame(
|
|
44
|
+
{
|
|
45
|
+
canEditDesign,
|
|
46
|
+
canvasFrameGeometryById,
|
|
47
|
+
createFileMutation,
|
|
48
|
+
files,
|
|
49
|
+
focusCreatedScreen,
|
|
50
|
+
id,
|
|
51
|
+
locallyPinnedHeightIdsRef,
|
|
52
|
+
optimisticallyInsertCreatedFile,
|
|
53
|
+
queryClient,
|
|
54
|
+
recordFileCreationHistoryEntry,
|
|
55
|
+
t,
|
|
56
|
+
writeFrameGeometrySnapshot,
|
|
57
|
+
}: CreateScreenFrameArgs,
|
|
58
|
+
geometry: { x: number; y: number; width: number; height: number },
|
|
59
|
+
) {
|
|
60
|
+
if (!id || !canEditDesign) return;
|
|
61
|
+
const filename = nextBlankScreenFilename(files);
|
|
62
|
+
const content = blankScreenHtml(prettyScreenName(filename));
|
|
63
|
+
const nextGeometry = {
|
|
64
|
+
x: Math.round(geometry.x),
|
|
65
|
+
y: Math.round(geometry.y),
|
|
66
|
+
width: Math.max(64, Math.round(geometry.width)),
|
|
67
|
+
height: Math.max(64, Math.round(geometry.height)),
|
|
68
|
+
};
|
|
69
|
+
createFileMutation.mutate(
|
|
70
|
+
{
|
|
71
|
+
designId: id,
|
|
72
|
+
filename,
|
|
73
|
+
content,
|
|
74
|
+
fileType: "html",
|
|
75
|
+
} as any,
|
|
76
|
+
{
|
|
77
|
+
onSuccess: (result: any) => {
|
|
78
|
+
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
79
|
+
if (nextId) {
|
|
80
|
+
optimisticallyInsertCreatedFile({
|
|
81
|
+
fileId: nextId,
|
|
82
|
+
filename,
|
|
83
|
+
fileType: "html",
|
|
84
|
+
content,
|
|
85
|
+
result,
|
|
86
|
+
});
|
|
87
|
+
locallyPinnedHeightIdsRef.current.add(nextId);
|
|
88
|
+
trace("screen", "pin-drawn-height", {
|
|
89
|
+
fileId: nextId,
|
|
90
|
+
height: nextGeometry.height,
|
|
91
|
+
why: "a drawn size is deliberate; without this the device floor and content-fit pass override it",
|
|
92
|
+
});
|
|
93
|
+
// The drawn height is a deliberate size, so pin it: otherwise
|
|
94
|
+
// the device floor and content-fit pass immediately override it.
|
|
95
|
+
writeFrameGeometrySnapshot(
|
|
96
|
+
{
|
|
97
|
+
...canvasFrameGeometryById,
|
|
98
|
+
[nextId]: nextGeometry,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
syncViewportFrameIds: [nextId],
|
|
102
|
+
pinHeightFrameIds: [nextId],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
focusCreatedScreen(nextId, nextGeometry);
|
|
106
|
+
recordFileCreationHistoryEntry({
|
|
107
|
+
filename,
|
|
108
|
+
content,
|
|
109
|
+
fileType: "html",
|
|
110
|
+
geometry: nextGeometry,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Refetch only when there is no created id to insert optimistically:
|
|
114
|
+
// a whole-design refetch re-downloads every screen's HTML, which is
|
|
115
|
+
// what made adding a frame feel slow.
|
|
116
|
+
if (!nextId) {
|
|
117
|
+
queryClient.invalidateQueries({
|
|
118
|
+
queryKey: ["action", "get-design"],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
onError: (error) => {
|
|
123
|
+
toast.error(
|
|
124
|
+
error instanceof Error
|
|
125
|
+
? error.message
|
|
126
|
+
: t("designEditor.toasts.screenDuplicateError"),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import type { CodeLayerNode, CodeLayerTreeNode } from "@shared/code-layer";
|
|
2
|
+
import {
|
|
3
|
+
applyVisualEdit,
|
|
4
|
+
buildCodeLayerProjection,
|
|
5
|
+
moveNodeBetweenDocuments,
|
|
6
|
+
} from "@shared/code-layer";
|
|
7
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
|
|
10
|
+
import { trace } from "@/components/design/design-trace";
|
|
11
|
+
import { dndHostLog } from "@/components/design/dnd-debug";
|
|
12
|
+
import { validateCrossScreenSourceHtmlSnapshot } from "@/components/design/multi-screen/cross-screen-drop";
|
|
13
|
+
import { getPrimaryIframeId } from "@/components/design/multi-screen/iframe-targeting";
|
|
14
|
+
import type {
|
|
15
|
+
ElementInfo,
|
|
16
|
+
PortableStyleSnapshot,
|
|
17
|
+
RuntimeStructureInsertRequest,
|
|
18
|
+
} from "@/components/design/types";
|
|
19
|
+
import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
|
|
20
|
+
import {
|
|
21
|
+
bridgeSourceIdForCodeLayerNode,
|
|
22
|
+
codeLayerPatchMessage,
|
|
23
|
+
elementInfoFromCodeLayerNode,
|
|
24
|
+
resolveCodeLayerNodeFromBridge,
|
|
25
|
+
} from "@/pages/design-editor/code-layer-state";
|
|
26
|
+
import { adaptAutoTextColorForCrossScreenNode } from "@/pages/design-editor/cross-screen-text-color";
|
|
27
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
28
|
+
import { DESIGN_EDITOR_DEBUG_LOGS } from "@/pages/design-editor/editor-constants";
|
|
29
|
+
import type { ContentHistoryEntry } from "@/pages/design-editor/history";
|
|
30
|
+
import {
|
|
31
|
+
removeAbsolutePositioningFromNodeInHtml,
|
|
32
|
+
setAbsolutePositioningForNodeInHtml,
|
|
33
|
+
} from "@/pages/design-editor/html-layer-positioning";
|
|
34
|
+
import { resolveOverviewScreenSourceType } from "@/pages/design-editor/pending-edits";
|
|
35
|
+
import { applyPortableStyleSnapshotToHtml } from "@/pages/design-editor/portable-style";
|
|
36
|
+
import { resolveRuntimeStructureMoveExecutionMode } from "@/pages/design-editor/react-semantic-handoff";
|
|
37
|
+
|
|
38
|
+
export interface CrossScreenElementDropArgs {
|
|
39
|
+
applyFileContentUpdate: (
|
|
40
|
+
fileId: string,
|
|
41
|
+
nextContent: string,
|
|
42
|
+
options?: {
|
|
43
|
+
refreshPreview?: boolean;
|
|
44
|
+
skipPreview?: boolean;
|
|
45
|
+
forcePreviewFullDocument?: boolean;
|
|
46
|
+
persist?: boolean;
|
|
47
|
+
recordHistory?: boolean;
|
|
48
|
+
updatedAt?: string;
|
|
49
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
50
|
+
},
|
|
51
|
+
) => void;
|
|
52
|
+
boardFileId: string | undefined;
|
|
53
|
+
canEditDesign: boolean;
|
|
54
|
+
clearPendingOverviewLayerSelectionTimer: () => void;
|
|
55
|
+
codeLayerOwnerByNodeIdRef: RefObject<
|
|
56
|
+
Map<
|
|
57
|
+
string,
|
|
58
|
+
{
|
|
59
|
+
fileId: string;
|
|
60
|
+
node: CodeLayerNode;
|
|
61
|
+
tree: CodeLayerTreeNode[];
|
|
62
|
+
runtimeOnly: boolean;
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
>;
|
|
66
|
+
designSourceType: "inline" | "localhost" | "fusion";
|
|
67
|
+
getScreenContent: (screenId: string) => string;
|
|
68
|
+
id: string | undefined;
|
|
69
|
+
overviewScreens: OverviewScreen[];
|
|
70
|
+
pendingOverviewLayerSelectionRef: RefObject<string | null>;
|
|
71
|
+
pendingOverviewScreenSelectionRef: RefObject<string | null>;
|
|
72
|
+
recordContentHistoryEntry: (entry: ContentHistoryEntry) => void;
|
|
73
|
+
runtimeStructureInsertRevisionRef: RefObject<number>;
|
|
74
|
+
sendRuntimeLayerMoveSemanticHandoff: (
|
|
75
|
+
subjectLayerId: string,
|
|
76
|
+
targetLayerId: string,
|
|
77
|
+
placement: "before" | "after" | "inside",
|
|
78
|
+
) => boolean;
|
|
79
|
+
setActiveFileId: Dispatch<SetStateAction<string | null>>;
|
|
80
|
+
setCreatedOverviewLayerSelection: Dispatch<
|
|
81
|
+
SetStateAction<{ screenId: string; layerId: string } | null>
|
|
82
|
+
>;
|
|
83
|
+
setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
|
|
84
|
+
setRuntimeStructureInsertRequest: Dispatch<
|
|
85
|
+
SetStateAction<
|
|
86
|
+
(RuntimeStructureInsertRequest & { screenId: string }) | null
|
|
87
|
+
>
|
|
88
|
+
>;
|
|
89
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
90
|
+
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
91
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
92
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function runCrossScreenElementDrop(
|
|
96
|
+
{
|
|
97
|
+
applyFileContentUpdate,
|
|
98
|
+
boardFileId,
|
|
99
|
+
canEditDesign,
|
|
100
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
101
|
+
codeLayerOwnerByNodeIdRef,
|
|
102
|
+
designSourceType,
|
|
103
|
+
getScreenContent,
|
|
104
|
+
id,
|
|
105
|
+
overviewScreens,
|
|
106
|
+
pendingOverviewLayerSelectionRef,
|
|
107
|
+
pendingOverviewScreenSelectionRef,
|
|
108
|
+
recordContentHistoryEntry,
|
|
109
|
+
runtimeStructureInsertRevisionRef,
|
|
110
|
+
sendRuntimeLayerMoveSemanticHandoff,
|
|
111
|
+
setActiveFileId,
|
|
112
|
+
setCreatedOverviewLayerSelection,
|
|
113
|
+
setOverviewSelectedScreenIds,
|
|
114
|
+
setRuntimeStructureInsertRequest,
|
|
115
|
+
setSelectedElement,
|
|
116
|
+
setSelectedLayerIdsState,
|
|
117
|
+
t,
|
|
118
|
+
viewModeRef,
|
|
119
|
+
}: CrossScreenElementDropArgs,
|
|
120
|
+
{
|
|
121
|
+
sourceSelector,
|
|
122
|
+
sourceNodeId,
|
|
123
|
+
sourceScreenId,
|
|
124
|
+
targetScreenId,
|
|
125
|
+
targetAnchorNodeId,
|
|
126
|
+
targetAnchorPendingNodeId,
|
|
127
|
+
targetAnchorSelector,
|
|
128
|
+
targetAnchorPlacement,
|
|
129
|
+
targetDropMode,
|
|
130
|
+
targetAnchorRect,
|
|
131
|
+
targetLocalPoint,
|
|
132
|
+
sourcePointerOffset,
|
|
133
|
+
sourceHtmlSnapshot,
|
|
134
|
+
styleSnapshot,
|
|
135
|
+
}: {
|
|
136
|
+
sourceSelector: string;
|
|
137
|
+
sourceNodeId?: string;
|
|
138
|
+
sourceScreenId: string;
|
|
139
|
+
targetScreenId: string;
|
|
140
|
+
targetAnchorNodeId?: string;
|
|
141
|
+
targetAnchorPendingNodeId?: string;
|
|
142
|
+
targetAnchorSelector?: string;
|
|
143
|
+
targetAnchorPlacement?: "before" | "after" | "inside";
|
|
144
|
+
targetDropMode?: "flow-insert" | "absolute-container";
|
|
145
|
+
targetAnchorRect?: {
|
|
146
|
+
left: number;
|
|
147
|
+
top: number;
|
|
148
|
+
width: number;
|
|
149
|
+
height: number;
|
|
150
|
+
};
|
|
151
|
+
targetCanvasPoint?: { x: number; y: number };
|
|
152
|
+
targetLocalPoint?: { x: number; y: number };
|
|
153
|
+
sourcePointerOffset?: { x: number; y: number };
|
|
154
|
+
sourceHtmlSnapshot?: string;
|
|
155
|
+
styleSnapshot?: PortableStyleSnapshot;
|
|
156
|
+
},
|
|
157
|
+
) {
|
|
158
|
+
dndHostLog("persist:cross-screen", {
|
|
159
|
+
sourceScreenId,
|
|
160
|
+
targetScreenId,
|
|
161
|
+
targetAnchorPlacement,
|
|
162
|
+
targetDropMode,
|
|
163
|
+
});
|
|
164
|
+
trace("drop", "cross-screen-persist", {
|
|
165
|
+
from: sourceScreenId,
|
|
166
|
+
to: targetScreenId,
|
|
167
|
+
mode: targetDropMode,
|
|
168
|
+
placement: targetAnchorPlacement,
|
|
169
|
+
anchor: targetAnchorNodeId ?? targetAnchorSelector ?? null,
|
|
170
|
+
node: sourceNodeId ?? sourceSelector,
|
|
171
|
+
blocked: !canEditDesign
|
|
172
|
+
? "read-only design"
|
|
173
|
+
: sourceScreenId === targetScreenId
|
|
174
|
+
? "same screen — nothing to move"
|
|
175
|
+
: null,
|
|
176
|
+
});
|
|
177
|
+
if (!canEditDesign) return;
|
|
178
|
+
if (sourceScreenId === targetScreenId) return;
|
|
179
|
+
|
|
180
|
+
const findLayerOwner = (
|
|
181
|
+
screenId: string,
|
|
182
|
+
nodeId: string | undefined,
|
|
183
|
+
selector: string | undefined,
|
|
184
|
+
) =>
|
|
185
|
+
Array.from(codeLayerOwnerByNodeIdRef.current.entries()).find(
|
|
186
|
+
([candidateId, owner]) =>
|
|
187
|
+
owner.fileId === screenId &&
|
|
188
|
+
((nodeId &&
|
|
189
|
+
(candidateId === nodeId ||
|
|
190
|
+
bridgeSourceIdForCodeLayerNode(owner.node) === nodeId)) ||
|
|
191
|
+
(selector && owner.node.selector === selector)),
|
|
192
|
+
);
|
|
193
|
+
const sourceOwnerEntry = findLayerOwner(
|
|
194
|
+
sourceScreenId,
|
|
195
|
+
sourceNodeId,
|
|
196
|
+
sourceSelector,
|
|
197
|
+
);
|
|
198
|
+
const targetOwnerEntry = findLayerOwner(
|
|
199
|
+
targetScreenId,
|
|
200
|
+
targetAnchorNodeId,
|
|
201
|
+
targetAnchorSelector,
|
|
202
|
+
);
|
|
203
|
+
// A live localhost destination has no editable stored document — its
|
|
204
|
+
// stored "content" is the bridge URL — so the source-edit path below
|
|
205
|
+
// would write a whole HTML document over that URL and never reach the
|
|
206
|
+
// running app. Key off the destination SCREEN's source type: a live
|
|
207
|
+
// anchor normally has no stored layer owner at all, so both runtimeOnly
|
|
208
|
+
// flags read false and the drop looks like an ordinary source move.
|
|
209
|
+
const crossScreenExecutionMode = resolveRuntimeStructureMoveExecutionMode({
|
|
210
|
+
subjectRuntimeOnly: Boolean(sourceOwnerEntry?.[1].runtimeOnly),
|
|
211
|
+
targetRuntimeOnly: Boolean(targetOwnerEntry?.[1].runtimeOnly),
|
|
212
|
+
sourceScreenId,
|
|
213
|
+
targetScreenId,
|
|
214
|
+
// Only a board primitive may be reinterpreted as an insert; a real
|
|
215
|
+
// screen's element dropped into a live app is a move, and inserting it
|
|
216
|
+
// would leave a duplicate behind in its own screen.
|
|
217
|
+
sourceScreenIsBoard: Boolean(boardFileId) && sourceScreenId === boardFileId,
|
|
218
|
+
targetScreenIsLive: (() => {
|
|
219
|
+
// overviewScreens deliberately excludes the board file, and
|
|
220
|
+
// resolveOverviewScreenSourceType answers with the DESIGN-level
|
|
221
|
+
// fallback for an unknown screen. Trusting that fallback would
|
|
222
|
+
// route a live→board drop into the board's own preview DOM, where
|
|
223
|
+
// nothing is persisted and the node disappears on next render.
|
|
224
|
+
// An unresolved screen is not a live screen.
|
|
225
|
+
const targetScreen = overviewScreens.find(
|
|
226
|
+
(screen) => screen.id === targetScreenId,
|
|
227
|
+
);
|
|
228
|
+
return (
|
|
229
|
+
Boolean(targetScreen) &&
|
|
230
|
+
resolveOverviewScreenSourceType(targetScreen, designSourceType) ===
|
|
231
|
+
"localhost"
|
|
232
|
+
);
|
|
233
|
+
})(),
|
|
234
|
+
});
|
|
235
|
+
if (crossScreenExecutionMode === "screen-bridge-insert") {
|
|
236
|
+
const boardContent = getScreenContent(sourceScreenId);
|
|
237
|
+
if (!boardContent) return;
|
|
238
|
+
const boardProjection = buildCodeLayerProjection(boardContent);
|
|
239
|
+
const subjectNode = sourceNodeId
|
|
240
|
+
? (boardProjection.nodes.find(
|
|
241
|
+
(node) =>
|
|
242
|
+
node.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
|
|
243
|
+
node.id === sourceNodeId,
|
|
244
|
+
) ??
|
|
245
|
+
resolveCodeLayerNodeFromBridge(
|
|
246
|
+
boardProjection,
|
|
247
|
+
sourceSelector,
|
|
248
|
+
sourceNodeId,
|
|
249
|
+
))
|
|
250
|
+
: resolveCodeLayerNodeFromBridge(boardProjection, sourceSelector);
|
|
251
|
+
const subjectNodeId =
|
|
252
|
+
subjectNode?.dataAttributes["data-agent-native-node-id"];
|
|
253
|
+
const validatedSourceHtmlSnapshot =
|
|
254
|
+
subjectNodeId && sourceHtmlSnapshot
|
|
255
|
+
? validateCrossScreenSourceHtmlSnapshot(
|
|
256
|
+
sourceHtmlSnapshot,
|
|
257
|
+
subjectNodeId,
|
|
258
|
+
)
|
|
259
|
+
: undefined;
|
|
260
|
+
if (sourceHtmlSnapshot && !validatedSourceHtmlSnapshot) {
|
|
261
|
+
toast.error(t("designEditor.toasts.layerMoveFailed"), {
|
|
262
|
+
duration: 4000,
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Reuse the stored-document transforms instead of slicing the source
|
|
267
|
+
// span: they already own absolute/flow semantics. The portable style
|
|
268
|
+
// snapshot is always inlined (no sourceContent argument) — a live app
|
|
269
|
+
// never shares the board's stylesheet head, so the node would land
|
|
270
|
+
// unstyled otherwise.
|
|
271
|
+
const insertedHtml = subjectNodeId
|
|
272
|
+
? (() => {
|
|
273
|
+
const styled = applyPortableStyleSnapshotToHtml(
|
|
274
|
+
validatedSourceHtmlSnapshot ?? boardContent,
|
|
275
|
+
subjectNodeId,
|
|
276
|
+
styleSnapshot,
|
|
277
|
+
);
|
|
278
|
+
const positioned =
|
|
279
|
+
targetDropMode === "absolute-container" &&
|
|
280
|
+
targetLocalPoint &&
|
|
281
|
+
targetAnchorRect
|
|
282
|
+
? setAbsolutePositioningForNodeInHtml(
|
|
283
|
+
styled,
|
|
284
|
+
subjectNodeId,
|
|
285
|
+
{
|
|
286
|
+
x: targetLocalPoint.x - targetAnchorRect.left,
|
|
287
|
+
y: targetLocalPoint.y - targetAnchorRect.top,
|
|
288
|
+
},
|
|
289
|
+
sourcePointerOffset,
|
|
290
|
+
)
|
|
291
|
+
: removeAbsolutePositioningFromNodeInHtml(styled, subjectNodeId);
|
|
292
|
+
return new DOMParser()
|
|
293
|
+
.parseFromString(positioned, "text/html")
|
|
294
|
+
.querySelector(
|
|
295
|
+
`[data-agent-native-node-id="${CSS.escape(subjectNodeId)}"]`,
|
|
296
|
+
)?.outerHTML;
|
|
297
|
+
})()
|
|
298
|
+
: undefined;
|
|
299
|
+
if (!insertedHtml) {
|
|
300
|
+
toast.error(t("designEditor.toasts.layerMoveFailed"), {
|
|
301
|
+
duration: 4000,
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
runtimeStructureInsertRevisionRef.current += 1;
|
|
306
|
+
setRuntimeStructureInsertRequest({
|
|
307
|
+
requestId: runtimeStructureInsertRevisionRef.current,
|
|
308
|
+
screenId: targetScreenId,
|
|
309
|
+
html: insertedHtml,
|
|
310
|
+
anchor: {
|
|
311
|
+
selector: targetAnchorSelector ?? "",
|
|
312
|
+
sourceId: targetAnchorNodeId,
|
|
313
|
+
pendingNodeId: targetAnchorPendingNodeId,
|
|
314
|
+
},
|
|
315
|
+
placement: targetAnchorPlacement ?? "inside",
|
|
316
|
+
});
|
|
317
|
+
// The board keeps its copy until the pending live edit is applied.
|
|
318
|
+
// Removing it here would commit the board file immediately while the
|
|
319
|
+
// destination is still only a pending live edit, and undo pops whichever
|
|
320
|
+
// stack is newer — one Cmd+Z would revert half the gesture, and a drop
|
|
321
|
+
// that is never applied would lose the primitive entirely.
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (crossScreenExecutionMode === "semantic-handoff") {
|
|
325
|
+
if (!sourceOwnerEntry || !targetOwnerEntry) {
|
|
326
|
+
// A runtime/source cross-screen drop without an exact target (for
|
|
327
|
+
// example, dropping on the bare screen root) cannot satisfy the
|
|
328
|
+
// semantic handoff's two-anchor contract. Do not fall through to a
|
|
329
|
+
// selector guess or mutate stored wrapper HTML that does not own the
|
|
330
|
+
// runtime React node.
|
|
331
|
+
toast.error(t("designEditor.toasts.reactSourceAnchorsLoading"));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
sendRuntimeLayerMoveSemanticHandoff(
|
|
335
|
+
sourceOwnerEntry[0],
|
|
336
|
+
targetOwnerEntry[0],
|
|
337
|
+
targetAnchorPlacement ?? "inside",
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const sourceContent = getScreenContent(sourceScreenId);
|
|
343
|
+
const rawDestContent = getScreenContent(targetScreenId);
|
|
344
|
+
if (!sourceContent || !rawDestContent) return;
|
|
345
|
+
|
|
346
|
+
// Id-on-demand handshake (two-step, mirroring the element-select
|
|
347
|
+
// persist-on-select path above): AI-generated/duplicated screens often
|
|
348
|
+
// carry ZERO data-agent-native-node-id attributes, so the hit-test
|
|
349
|
+
// bridge can't return an anchor id — it mints a pendingNodeId (stamped
|
|
350
|
+
// on the LIVE dest DOM as data-an-pending-node-id) plus a
|
|
351
|
+
// source-equivalent structural anchorSelector. Persist that pending id
|
|
352
|
+
// as the anchor's real node id in the STORED dest document first, then
|
|
353
|
+
// resolve the drop against it — otherwise every flow-insert into an
|
|
354
|
+
// id-less screen silently degrades to absolute placement even though
|
|
355
|
+
// the hit-test found a valid before/after/inside slot. applyVisualEdit
|
|
356
|
+
// resolves the selector STRICTLY (unique match or conflict), so a
|
|
357
|
+
// selector that can't be honestly mapped to one source element (e.g.
|
|
358
|
+
// Alpine template instances) leaves the absolute fallback untouched
|
|
359
|
+
// rather than ever stamping the wrong node.
|
|
360
|
+
let destContent = rawDestContent;
|
|
361
|
+
let effectiveAnchorNodeId = targetAnchorNodeId;
|
|
362
|
+
if (
|
|
363
|
+
!targetAnchorNodeId &&
|
|
364
|
+
targetAnchorPendingNodeId &&
|
|
365
|
+
targetAnchorSelector
|
|
366
|
+
) {
|
|
367
|
+
const stamped = applyVisualEdit(
|
|
368
|
+
rawDestContent,
|
|
369
|
+
{
|
|
370
|
+
kind: "attribute",
|
|
371
|
+
target: { selector: targetAnchorSelector },
|
|
372
|
+
name: "data-agent-native-node-id",
|
|
373
|
+
value: targetAnchorPendingNodeId,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
source: {
|
|
377
|
+
kind: "design-file",
|
|
378
|
+
designId: id,
|
|
379
|
+
fileId: targetScreenId,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
if (
|
|
384
|
+
stamped.result.status === "applied" &&
|
|
385
|
+
stamped.content !== rawDestContent
|
|
386
|
+
) {
|
|
387
|
+
destContent = stamped.content;
|
|
388
|
+
effectiveAnchorNodeId = targetAnchorPendingNodeId;
|
|
389
|
+
} else {
|
|
390
|
+
// Silent degradation: the hit-test found a valid before/after/
|
|
391
|
+
// inside slot, but the pending anchor id couldn't be honestly
|
|
392
|
+
// persisted (e.g. targetAnchorSelector resolved to an Alpine
|
|
393
|
+
// template instance or no longer matches uniquely) — this drop
|
|
394
|
+
// falls through to absolute placement below with no anchor at
|
|
395
|
+
// all. Surface it instead of failing quietly; fallback behavior
|
|
396
|
+
// itself is intentionally unchanged. Dev-only: this is a known,
|
|
397
|
+
// handled degraded path (not a correctness bug), so keep
|
|
398
|
+
// production consoles quiet — see DESIGN_EDITOR_DEBUG_LOGS.
|
|
399
|
+
if (DESIGN_EDITOR_DEBUG_LOGS) {
|
|
400
|
+
console.warn(
|
|
401
|
+
"[design] cross-screen drop: could not stamp pending anchor node id — falling back to absolute placement",
|
|
402
|
+
{
|
|
403
|
+
targetScreenId,
|
|
404
|
+
targetAnchorSelector,
|
|
405
|
+
targetAnchorPendingNodeId,
|
|
406
|
+
status: stamped.result.status,
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Resolve the data-agent-native-node-id that moveNodeBetweenDocuments
|
|
414
|
+
// uses as a stable key. Prefer the bridge-supplied sourceNodeId when it
|
|
415
|
+
// looks like a node-attr id; otherwise look up via selector projection.
|
|
416
|
+
const sourceProjection = buildCodeLayerProjection(sourceContent);
|
|
417
|
+
const resolvedSourceNode = sourceNodeId
|
|
418
|
+
? (sourceProjection.nodes.find(
|
|
419
|
+
(n) =>
|
|
420
|
+
n.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
|
|
421
|
+
n.id === sourceNodeId,
|
|
422
|
+
) ??
|
|
423
|
+
resolveCodeLayerNodeFromBridge(
|
|
424
|
+
sourceProjection,
|
|
425
|
+
sourceSelector,
|
|
426
|
+
sourceNodeId,
|
|
427
|
+
))
|
|
428
|
+
: resolveCodeLayerNodeFromBridge(sourceProjection, sourceSelector);
|
|
429
|
+
const nodeAttrId =
|
|
430
|
+
resolvedSourceNode?.dataAttributes["data-agent-native-node-id"] ??
|
|
431
|
+
sourceNodeId ??
|
|
432
|
+
sourceSelector;
|
|
433
|
+
const destProjection = buildCodeLayerProjection(destContent);
|
|
434
|
+
const resolvedTargetAnchor = effectiveAnchorNodeId
|
|
435
|
+
? resolveCodeLayerNodeFromBridge(
|
|
436
|
+
destProjection,
|
|
437
|
+
undefined,
|
|
438
|
+
effectiveAnchorNodeId,
|
|
439
|
+
)
|
|
440
|
+
: null;
|
|
441
|
+
const targetAnchorAttrId =
|
|
442
|
+
resolvedTargetAnchor?.dataAttributes["data-agent-native-node-id"];
|
|
443
|
+
|
|
444
|
+
// Use hit-test anchor when the canvas supplied one; fall back to
|
|
445
|
+
// top-level body append ("inside" with no anchor = existing behaviour).
|
|
446
|
+
const result = moveNodeBetweenDocuments(sourceContent, destContent, {
|
|
447
|
+
nodeId: nodeAttrId,
|
|
448
|
+
...(targetAnchorAttrId
|
|
449
|
+
? {
|
|
450
|
+
anchorNodeId: targetAnchorAttrId,
|
|
451
|
+
placement: targetAnchorPlacement ?? "inside",
|
|
452
|
+
}
|
|
453
|
+
: { placement: "inside" }),
|
|
454
|
+
});
|
|
455
|
+
if (result.status !== "applied") {
|
|
456
|
+
toast.error(
|
|
457
|
+
codeLayerPatchMessage(
|
|
458
|
+
result.message,
|
|
459
|
+
t("designEditor.toasts.layerMoveFailed"),
|
|
460
|
+
),
|
|
461
|
+
{ duration: 4000 },
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Finding 8: see the same-screen move's identical handling above —
|
|
466
|
+
// the anchor placement was redirected out of a <template> interior to
|
|
467
|
+
// right after the enclosing template's close instead of failing or
|
|
468
|
+
// teleporting to doc end.
|
|
469
|
+
if (result.anchorRedirected) {
|
|
470
|
+
toast(t("designEditor.toasts.layerMoveRedirected"), {
|
|
471
|
+
duration: 4000,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Hit-test anchors are emitted only for auto-layout insertion targets. If
|
|
476
|
+
// there is no anchor, preserve absolute mode and rebase left/top to the
|
|
477
|
+
// release point so screen↔board moves behave like Figma absolute layers.
|
|
478
|
+
const destNodeAttrId = result.movedNodeId ?? nodeAttrId;
|
|
479
|
+
const styleSnapshotDest = applyPortableStyleSnapshotToHtml(
|
|
480
|
+
result.destHtml,
|
|
481
|
+
destNodeAttrId,
|
|
482
|
+
styleSnapshot,
|
|
483
|
+
sourceContent,
|
|
484
|
+
);
|
|
485
|
+
// Finding 8: board/screen text carrying the auto-applied white default
|
|
486
|
+
// (see BOARD_TEXT_AUTO_COLOR_MARKER / defaultCanvasTextColor) must not
|
|
487
|
+
// keep that forced white when it lands cross-screen in a light
|
|
488
|
+
// destination — otherwise it renders invisible white-on-white. The
|
|
489
|
+
// in-screen drag path already adapts via the bridge's
|
|
490
|
+
// adaptAutoTextColorForNest; this is the cross-screen mirror, applied
|
|
491
|
+
// host-side now that the node has actually been re-parented into
|
|
492
|
+
// destContent.
|
|
493
|
+
const liveDestIframe = document.querySelector<HTMLIFrameElement>(
|
|
494
|
+
`[data-screen-iframe-id="${CSS.escape(getPrimaryIframeId(targetScreenId))}"]`,
|
|
495
|
+
);
|
|
496
|
+
const stylePreservedDest = adaptAutoTextColorForCrossScreenNode(
|
|
497
|
+
styleSnapshotDest,
|
|
498
|
+
destNodeAttrId,
|
|
499
|
+
liveDestIframe?.contentDocument ?? null,
|
|
500
|
+
);
|
|
501
|
+
const nextDestContent = targetAnchorAttrId
|
|
502
|
+
? targetDropMode === "absolute-container"
|
|
503
|
+
? targetLocalPoint && targetAnchorRect
|
|
504
|
+
? setAbsolutePositioningForNodeInHtml(
|
|
505
|
+
stylePreservedDest,
|
|
506
|
+
destNodeAttrId,
|
|
507
|
+
{
|
|
508
|
+
x: targetLocalPoint.x - targetAnchorRect.left,
|
|
509
|
+
y: targetLocalPoint.y - targetAnchorRect.top,
|
|
510
|
+
},
|
|
511
|
+
sourcePointerOffset,
|
|
512
|
+
)
|
|
513
|
+
: stylePreservedDest
|
|
514
|
+
: removeAbsolutePositioningFromNodeInHtml(
|
|
515
|
+
stylePreservedDest,
|
|
516
|
+
destNodeAttrId,
|
|
517
|
+
)
|
|
518
|
+
: targetLocalPoint
|
|
519
|
+
? setAbsolutePositioningForNodeInHtml(
|
|
520
|
+
stylePreservedDest,
|
|
521
|
+
destNodeAttrId,
|
|
522
|
+
targetLocalPoint,
|
|
523
|
+
sourcePointerOffset,
|
|
524
|
+
)
|
|
525
|
+
: stylePreservedDest;
|
|
526
|
+
|
|
527
|
+
recordContentHistoryEntry({
|
|
528
|
+
changes: [
|
|
529
|
+
{
|
|
530
|
+
fileId: sourceScreenId,
|
|
531
|
+
before: sourceContent,
|
|
532
|
+
after: result.sourceHtml,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
fileId: targetScreenId,
|
|
536
|
+
before: destContent,
|
|
537
|
+
after: nextDestContent,
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
applyFileContentUpdate(sourceScreenId, result.sourceHtml, {
|
|
543
|
+
recordHistory: false,
|
|
544
|
+
refreshPreview: false,
|
|
545
|
+
forcePreviewFullDocument: true,
|
|
546
|
+
});
|
|
547
|
+
applyFileContentUpdate(targetScreenId, nextDestContent, {
|
|
548
|
+
recordHistory: false,
|
|
549
|
+
refreshPreview: false,
|
|
550
|
+
forcePreviewFullDocument: true,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Switch active screen to the target and select the moved node; viewMode
|
|
554
|
+
// stays "overview" (no setViewMode call).
|
|
555
|
+
pendingOverviewScreenSelectionRef.current =
|
|
556
|
+
targetScreenId === boardFileId ? null : targetScreenId;
|
|
557
|
+
pendingOverviewLayerSelectionRef.current = destNodeAttrId;
|
|
558
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
559
|
+
setActiveFileId(targetScreenId);
|
|
560
|
+
const finalProjection = buildCodeLayerProjection(nextDestContent);
|
|
561
|
+
const movedNodeFinal = finalProjection.nodes.find(
|
|
562
|
+
(n) => n.dataAttributes["data-agent-native-node-id"] === destNodeAttrId,
|
|
563
|
+
);
|
|
564
|
+
if (movedNodeFinal) {
|
|
565
|
+
setCreatedOverviewLayerSelection({
|
|
566
|
+
screenId: targetScreenId,
|
|
567
|
+
layerId: movedNodeFinal.id,
|
|
568
|
+
});
|
|
569
|
+
setSelectedLayerIdsState([movedNodeFinal.id]);
|
|
570
|
+
setSelectedElement(elementInfoFromCodeLayerNode(movedNodeFinal));
|
|
571
|
+
if (viewModeRef.current === "overview") {
|
|
572
|
+
setOverviewSelectedScreenIds(
|
|
573
|
+
targetScreenId === boardFileId ? [] : [targetScreenId],
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|