@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,200 @@
|
|
|
1
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
2
|
+
import { normalizeDesignSourceType } from "@shared/source-mode";
|
|
3
|
+
|
|
4
|
+
import type { ExportSettingsValue } from "@/components/design/inspector";
|
|
5
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
6
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
7
|
+
import {
|
|
8
|
+
getExportCompositeBounds,
|
|
9
|
+
resolveRasterExportScale,
|
|
10
|
+
} from "@/pages/design-editor/export-capture";
|
|
11
|
+
import type { PngCaptureScope } from "@/pages/design-editor/png-export-render";
|
|
12
|
+
import {
|
|
13
|
+
PngCaptureError,
|
|
14
|
+
cropCanvasToRect,
|
|
15
|
+
renderExportDocumentCanvas,
|
|
16
|
+
resolveExportCropRect,
|
|
17
|
+
} from "@/pages/design-editor/png-export-render";
|
|
18
|
+
|
|
19
|
+
export interface RenderPngBlobArgs {
|
|
20
|
+
activeCanvasSourceType: "inline" | "localhost" | "fusion";
|
|
21
|
+
canEditDesign: boolean;
|
|
22
|
+
canvasFrameGeometryById: CanvasFrameGeometryById;
|
|
23
|
+
overviewScreens: OverviewScreen[];
|
|
24
|
+
resolvePngCaptureTarget: (scope: PngCaptureScope) => {
|
|
25
|
+
cropSelection: ElementInfo | readonly ElementInfo[] | null;
|
|
26
|
+
doc: Document;
|
|
27
|
+
iframe: HTMLIFrameElement;
|
|
28
|
+
};
|
|
29
|
+
selectedScreenIds: string[];
|
|
30
|
+
viewMode: "single" | "overview";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runRenderPngBlob(
|
|
34
|
+
{
|
|
35
|
+
activeCanvasSourceType,
|
|
36
|
+
canEditDesign,
|
|
37
|
+
canvasFrameGeometryById,
|
|
38
|
+
overviewScreens,
|
|
39
|
+
resolvePngCaptureTarget,
|
|
40
|
+
selectedScreenIds,
|
|
41
|
+
viewMode,
|
|
42
|
+
}: RenderPngBlobArgs,
|
|
43
|
+
{
|
|
44
|
+
scope,
|
|
45
|
+
settings,
|
|
46
|
+
format = "png",
|
|
47
|
+
}: {
|
|
48
|
+
scope: PngCaptureScope;
|
|
49
|
+
settings?: Partial<ExportSettingsValue>;
|
|
50
|
+
format?: "png" | "jpg" | "webp";
|
|
51
|
+
},
|
|
52
|
+
): Promise<Blob> {
|
|
53
|
+
const html2canvas = (await import("html2canvas")).default;
|
|
54
|
+
const requestedExportScale =
|
|
55
|
+
settings?.scale ?? Math.max(2, window.devicePixelRatio || 1);
|
|
56
|
+
let outputCanvas: HTMLCanvasElement;
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
scope === "screens" &&
|
|
60
|
+
viewMode === "overview" &&
|
|
61
|
+
selectedScreenIds.length > 0
|
|
62
|
+
) {
|
|
63
|
+
const captures = selectedScreenIds
|
|
64
|
+
.map((screenId, order) => {
|
|
65
|
+
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
66
|
+
`iframe[data-design-preview-iframe][data-screen-iframe-id="${CSS.escape(screenId)}"]`,
|
|
67
|
+
);
|
|
68
|
+
if (!iframe) throw new PngCaptureError("no-preview");
|
|
69
|
+
let doc: Document | null = null;
|
|
70
|
+
try {
|
|
71
|
+
doc = iframe.contentDocument;
|
|
72
|
+
if (!doc?.documentElement) doc = null;
|
|
73
|
+
} catch {
|
|
74
|
+
doc = null;
|
|
75
|
+
}
|
|
76
|
+
if (!doc) {
|
|
77
|
+
const sourceType =
|
|
78
|
+
normalizeDesignSourceType(iframe.dataset.designSourceType) ??
|
|
79
|
+
activeCanvasSourceType;
|
|
80
|
+
if (sourceType !== "inline") {
|
|
81
|
+
throw new PngCaptureError("external-preview");
|
|
82
|
+
}
|
|
83
|
+
if (!canEditDesign) {
|
|
84
|
+
throw new PngCaptureError("read-only-preview");
|
|
85
|
+
}
|
|
86
|
+
throw new PngCaptureError("no-preview");
|
|
87
|
+
}
|
|
88
|
+
const screen = overviewScreens.find(
|
|
89
|
+
(candidate) => candidate.id === screenId,
|
|
90
|
+
);
|
|
91
|
+
const geometry = canvasFrameGeometryById[screenId] ?? {};
|
|
92
|
+
return {
|
|
93
|
+
doc,
|
|
94
|
+
frame: {
|
|
95
|
+
x: geometry.x ?? order * ((screen?.width ?? 1440) + 80),
|
|
96
|
+
y: geometry.y ?? 0,
|
|
97
|
+
width: Math.max(
|
|
98
|
+
1,
|
|
99
|
+
geometry.width ?? screen?.width ?? iframe.clientWidth,
|
|
100
|
+
),
|
|
101
|
+
height: Math.max(
|
|
102
|
+
1,
|
|
103
|
+
geometry.height ?? screen?.height ?? iframe.clientHeight,
|
|
104
|
+
),
|
|
105
|
+
rotation: geometry.rotation ?? 0,
|
|
106
|
+
},
|
|
107
|
+
iframe,
|
|
108
|
+
order,
|
|
109
|
+
z: geometry.z ?? order,
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
.sort((left, right) => left.z - right.z || left.order - right.order);
|
|
113
|
+
const bounds = getExportCompositeBounds(
|
|
114
|
+
captures.map((capture) => capture.frame),
|
|
115
|
+
);
|
|
116
|
+
if (!bounds) throw new PngCaptureError("no-preview");
|
|
117
|
+
const exportScale = resolveRasterExportScale({
|
|
118
|
+
width: bounds.width,
|
|
119
|
+
height: bounds.height,
|
|
120
|
+
requestedScale: requestedExportScale,
|
|
121
|
+
});
|
|
122
|
+
outputCanvas = document.createElement("canvas");
|
|
123
|
+
outputCanvas.width = Math.max(1, Math.ceil(bounds.width * exportScale));
|
|
124
|
+
outputCanvas.height = Math.max(1, Math.ceil(bounds.height * exportScale));
|
|
125
|
+
const context = outputCanvas.getContext("2d");
|
|
126
|
+
if (!context) throw new PngCaptureError("blob-failed");
|
|
127
|
+
|
|
128
|
+
for (const capture of captures) {
|
|
129
|
+
const rendered = await renderExportDocumentCanvas({
|
|
130
|
+
doc: capture.doc,
|
|
131
|
+
iframe: capture.iframe,
|
|
132
|
+
exportScale,
|
|
133
|
+
render: html2canvas,
|
|
134
|
+
});
|
|
135
|
+
const view = capture.doc.defaultView;
|
|
136
|
+
const viewportCanvas = cropCanvasToRect(
|
|
137
|
+
rendered.canvas,
|
|
138
|
+
{
|
|
139
|
+
x: view?.scrollX ?? 0,
|
|
140
|
+
y: view?.scrollY ?? 0,
|
|
141
|
+
width: Math.max(1, capture.iframe.clientWidth),
|
|
142
|
+
height: Math.max(1, capture.iframe.clientHeight),
|
|
143
|
+
},
|
|
144
|
+
rendered.scale,
|
|
145
|
+
);
|
|
146
|
+
const frame = capture.frame;
|
|
147
|
+
context.save();
|
|
148
|
+
context.translate(
|
|
149
|
+
(frame.x + frame.width / 2 - bounds.x) * exportScale,
|
|
150
|
+
(frame.y + frame.height / 2 - bounds.y) * exportScale,
|
|
151
|
+
);
|
|
152
|
+
context.rotate(((frame.rotation ?? 0) * Math.PI) / 180);
|
|
153
|
+
context.drawImage(
|
|
154
|
+
viewportCanvas ?? rendered.canvas,
|
|
155
|
+
(-frame.width / 2) * exportScale,
|
|
156
|
+
(-frame.height / 2) * exportScale,
|
|
157
|
+
frame.width * exportScale,
|
|
158
|
+
frame.height * exportScale,
|
|
159
|
+
);
|
|
160
|
+
context.restore();
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
const { cropSelection, doc, iframe } = resolvePngCaptureTarget(scope);
|
|
164
|
+
const rendered = await renderExportDocumentCanvas({
|
|
165
|
+
doc,
|
|
166
|
+
iframe,
|
|
167
|
+
exportScale: requestedExportScale,
|
|
168
|
+
render: html2canvas,
|
|
169
|
+
});
|
|
170
|
+
const cropRect = resolveExportCropRect(doc, cropSelection);
|
|
171
|
+
const cropped = cropRect
|
|
172
|
+
? cropCanvasToRect(rendered.canvas, cropRect, rendered.scale)
|
|
173
|
+
: null;
|
|
174
|
+
// An element capture that silently widens to the whole document is a
|
|
175
|
+
// preview of something the user did not ask to export, and nothing
|
|
176
|
+
// downstream can tell it apart from a real one.
|
|
177
|
+
if (scope === "element" && !cropped) {
|
|
178
|
+
throw new PngCaptureError("no-preview");
|
|
179
|
+
}
|
|
180
|
+
// Render the whole page first, then crop, so ancestor backgrounds show
|
|
181
|
+
// through every selected frame exactly as they do on screen.
|
|
182
|
+
outputCanvas = cropped ?? rendered.canvas;
|
|
183
|
+
}
|
|
184
|
+
const mimeType =
|
|
185
|
+
format === "jpg"
|
|
186
|
+
? "image/jpeg"
|
|
187
|
+
: format === "webp"
|
|
188
|
+
? "image/webp"
|
|
189
|
+
: "image/png";
|
|
190
|
+
return await new Promise<Blob>((resolve, reject) => {
|
|
191
|
+
outputCanvas.toBlob(
|
|
192
|
+
(blob) => {
|
|
193
|
+
if (blob) resolve(blob);
|
|
194
|
+
else reject(new PngCaptureError("blob-failed"));
|
|
195
|
+
},
|
|
196
|
+
mimeType,
|
|
197
|
+
mimeType === "image/png" ? undefined : 0.95,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { sourceContentHash } from "@shared/source-workspace";
|
|
3
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
import type { DesignSaveOutboxEntry } from "@/lib/design-save-outbox";
|
|
8
|
+
import { updateFileResultPersistedContent } from "@/lib/design-save-outbox";
|
|
9
|
+
import type { PatchProofState } from "@/pages/design-editor/command-types";
|
|
10
|
+
import type { FileContentSaveRequest } from "@/pages/design-editor/editor-state";
|
|
11
|
+
import { shouldClearLatestUnloadSave } from "@/pages/design-editor/editor-state";
|
|
12
|
+
import {
|
|
13
|
+
classifyDesignSaveFailure,
|
|
14
|
+
designSaveErrorMessage,
|
|
15
|
+
} from "@/pages/design-editor/save-failure";
|
|
16
|
+
|
|
17
|
+
export interface SaveFileContentArgs {
|
|
18
|
+
acknowledgeOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<void>;
|
|
19
|
+
canEditDesignRef: RefObject<boolean>;
|
|
20
|
+
createFileSaveOutboxEntry: (
|
|
21
|
+
pending: FileContentSaveRequest,
|
|
22
|
+
expectedVersionHash?: string | undefined,
|
|
23
|
+
) => DesignSaveOutboxEntry | null;
|
|
24
|
+
fileSaveChainsRef: RefObject<Record<string, Promise<void>>>;
|
|
25
|
+
journalOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
|
|
26
|
+
lastAckedFileContentHashRef: RefObject<Record<string, string>>;
|
|
27
|
+
latestFileSaveForUnloadRef: RefObject<Record<string, FileContentSaveRequest>>;
|
|
28
|
+
markPendingLocalFileContent: (
|
|
29
|
+
fileId: string,
|
|
30
|
+
content: string,
|
|
31
|
+
baseUpdatedAt?: string | null,
|
|
32
|
+
) => void;
|
|
33
|
+
queryClient: QueryClient;
|
|
34
|
+
setPatchProof: Dispatch<SetStateAction<PatchProofState | null>>;
|
|
35
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
36
|
+
updateFileMutation: ReturnType<
|
|
37
|
+
typeof useActionMutation<undefined, undefined, "update-file">
|
|
38
|
+
>;
|
|
39
|
+
warnChangesWillRetry: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runSaveFileContent(
|
|
43
|
+
{
|
|
44
|
+
acknowledgeOutboxEntry,
|
|
45
|
+
canEditDesignRef,
|
|
46
|
+
createFileSaveOutboxEntry,
|
|
47
|
+
fileSaveChainsRef,
|
|
48
|
+
journalOutboxEntry,
|
|
49
|
+
lastAckedFileContentHashRef,
|
|
50
|
+
latestFileSaveForUnloadRef,
|
|
51
|
+
markPendingLocalFileContent,
|
|
52
|
+
queryClient,
|
|
53
|
+
setPatchProof,
|
|
54
|
+
t,
|
|
55
|
+
updateFileMutation,
|
|
56
|
+
warnChangesWillRetry,
|
|
57
|
+
}: SaveFileContentArgs,
|
|
58
|
+
pending: FileContentSaveRequest,
|
|
59
|
+
) {
|
|
60
|
+
if (!canEditDesignRef.current) return;
|
|
61
|
+
markPendingLocalFileContent(pending.id, pending.content);
|
|
62
|
+
// Stamp the CURRENTLY-known acked hash onto the object kept for the
|
|
63
|
+
// pagehide/unload keepalive only — that path has no "resolve at send
|
|
64
|
+
// time" luxury (unload can fire the instant this runs), so best-effort
|
|
65
|
+
// now is all it gets. Does NOT feed the real mutateAsync call below,
|
|
66
|
+
// which still re-resolves the hash fresh at send time; see that call's
|
|
67
|
+
// comment for why an already-set pending.expectedVersionHash must not
|
|
68
|
+
// be allowed to shadow a fresher ref read there.
|
|
69
|
+
latestFileSaveForUnloadRef.current[pending.id] =
|
|
70
|
+
pending.expectedVersionHash !== undefined
|
|
71
|
+
? pending
|
|
72
|
+
: {
|
|
73
|
+
...pending,
|
|
74
|
+
...(lastAckedFileContentHashRef.current[pending.id]
|
|
75
|
+
? {
|
|
76
|
+
expectedVersionHash:
|
|
77
|
+
lastAckedFileContentHashRef.current[pending.id],
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
const queuedOutboxEntry = createFileSaveOutboxEntry(
|
|
82
|
+
latestFileSaveForUnloadRef.current[pending.id],
|
|
83
|
+
);
|
|
84
|
+
if (queuedOutboxEntry) void journalOutboxEntry(queuedOutboxEntry);
|
|
85
|
+
const previous = fileSaveChainsRef.current[pending.id] ?? Promise.resolve();
|
|
86
|
+
const current = previous
|
|
87
|
+
.catch(() => {})
|
|
88
|
+
.then(async () => {
|
|
89
|
+
try {
|
|
90
|
+
// Resolve the optimistic-concurrency hash at SEND time (after any
|
|
91
|
+
// earlier chained save for this file has landed and refreshed the
|
|
92
|
+
// acked hash), not at queue time — the freshest ref value always
|
|
93
|
+
// wins here regardless of anything queue-time code may have
|
|
94
|
+
// stamped onto `pending` for the unload path above. Attached
|
|
95
|
+
// whenever a hash is known, on BOTH syncCollab true and false
|
|
96
|
+
// saves — see lastAckedFileContentHashRef's doc comment. Never
|
|
97
|
+
// invent a hash when one isn't known yet; omit as before.
|
|
98
|
+
const expectedVersionHash =
|
|
99
|
+
lastAckedFileContentHashRef.current[pending.id] ??
|
|
100
|
+
pending.expectedVersionHash;
|
|
101
|
+
const outboxEntry = createFileSaveOutboxEntry(
|
|
102
|
+
pending,
|
|
103
|
+
expectedVersionHash,
|
|
104
|
+
);
|
|
105
|
+
if (outboxEntry) await journalOutboxEntry(outboxEntry);
|
|
106
|
+
const result = await updateFileMutation.mutateAsync({
|
|
107
|
+
id: pending.id,
|
|
108
|
+
content: pending.content,
|
|
109
|
+
syncCollab: pending.syncCollab,
|
|
110
|
+
operationSource: pending.operationSource,
|
|
111
|
+
operationRevision: pending.operationRevision,
|
|
112
|
+
...(expectedVersionHash ? { expectedVersionHash } : {}),
|
|
113
|
+
} as any);
|
|
114
|
+
const resultInfo = result as
|
|
115
|
+
| {
|
|
116
|
+
skippedStaleMirror?: boolean;
|
|
117
|
+
skippedStaleOperation?: boolean;
|
|
118
|
+
versionHash?: string;
|
|
119
|
+
}
|
|
120
|
+
| undefined;
|
|
121
|
+
const skippedStaleMirror = Boolean(resultInfo?.skippedStaleMirror);
|
|
122
|
+
// skippedStaleMirror: the server intentionally left the SQL
|
|
123
|
+
// mirror column untouched because our expectedVersionHash no
|
|
124
|
+
// longer matched the live collab text (a live editor moved past
|
|
125
|
+
// the base this write was computed from). The mirror was NOT
|
|
126
|
+
// updated to pending.content, so recording pending.content's hash
|
|
127
|
+
// as "acked" here would be wrong — it would make a later guarded
|
|
128
|
+
// save believe the server holds content it doesn't. Leave the
|
|
129
|
+
// previously-acked hash in place instead; the DB-reconcile effect
|
|
130
|
+
// (activeFile watcher) will pick up the true live content and
|
|
131
|
+
// refresh the acked hash from that.
|
|
132
|
+
if (!skippedStaleMirror) {
|
|
133
|
+
lastAckedFileContentHashRef.current[pending.id] =
|
|
134
|
+
resultInfo?.versionHash ?? sourceContentHash(pending.content);
|
|
135
|
+
}
|
|
136
|
+
const persistedContentMatches = updateFileResultPersistedContent(
|
|
137
|
+
resultInfo,
|
|
138
|
+
pending.content,
|
|
139
|
+
);
|
|
140
|
+
if (persistedContentMatches && outboxEntry) {
|
|
141
|
+
await acknowledgeOutboxEntry(outboxEntry);
|
|
142
|
+
} else if (!persistedContentMatches) {
|
|
143
|
+
// A stale/no-op save result is a source conflict, not a lost
|
|
144
|
+
// connection. Refetch below rebases the editor; never promise
|
|
145
|
+
// that simply reconnecting will save this obsolete snapshot.
|
|
146
|
+
queryClient.invalidateQueries({
|
|
147
|
+
queryKey: ["action", "get-design"],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
shouldClearLatestUnloadSave(
|
|
152
|
+
latestFileSaveForUnloadRef.current[pending.id],
|
|
153
|
+
pending,
|
|
154
|
+
!persistedContentMatches,
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
157
|
+
delete latestFileSaveForUnloadRef.current[pending.id];
|
|
158
|
+
}
|
|
159
|
+
setPatchProof((prev) =>
|
|
160
|
+
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
161
|
+
? { ...prev, status: "applied" }
|
|
162
|
+
: prev,
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// Drop the (evidently wrong) acked hash so the failure is
|
|
166
|
+
// one-shot: the DB-reconcile effect pulls the fresh server
|
|
167
|
+
// content, and the next save proceeds unguarded from that
|
|
168
|
+
// rebased state instead of failing forever on a dead hash.
|
|
169
|
+
delete lastAckedFileContentHashRef.current[pending.id];
|
|
170
|
+
queryClient.invalidateQueries({
|
|
171
|
+
queryKey: ["action", "get-design"],
|
|
172
|
+
});
|
|
173
|
+
const failureKind = classifyDesignSaveFailure(error, navigator.onLine);
|
|
174
|
+
if (failureKind === "offline") {
|
|
175
|
+
warnChangesWillRetry();
|
|
176
|
+
} else if (
|
|
177
|
+
failureKind !== "intentional-abort" &&
|
|
178
|
+
failureKind !== "conflict"
|
|
179
|
+
) {
|
|
180
|
+
// Conflicts already rebased above (acked-hash reset + get-design
|
|
181
|
+
// invalidation); a red toast for a routine rebase is just noise.
|
|
182
|
+
toast.error(
|
|
183
|
+
designSaveErrorMessage(error) ?? t("common.genericError"),
|
|
184
|
+
{ id: `design-save-error:${pending.id}` },
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
setPatchProof((prev) =>
|
|
188
|
+
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
189
|
+
? {
|
|
190
|
+
...prev,
|
|
191
|
+
status: "failed",
|
|
192
|
+
error:
|
|
193
|
+
error instanceof Error
|
|
194
|
+
? error.message
|
|
195
|
+
: t("common.genericError"),
|
|
196
|
+
}
|
|
197
|
+
: prev,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
fileSaveChainsRef.current[pending.id] = current;
|
|
202
|
+
void current.finally(() => {
|
|
203
|
+
if (fileSaveChainsRef.current[pending.id] === current) {
|
|
204
|
+
delete fileSaveChainsRef.current[pending.id];
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { CodeLayerProjection } from "@shared/code-layer";
|
|
2
|
+
import {
|
|
3
|
+
applyVisualEdit,
|
|
4
|
+
ensureCodeLayerNodeIdsInHtml,
|
|
5
|
+
} from "@shared/code-layer";
|
|
6
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ElementInfo,
|
|
11
|
+
ElementSelectionIntent,
|
|
12
|
+
} from "@/components/design/types";
|
|
13
|
+
import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
|
|
14
|
+
import {
|
|
15
|
+
canonicalizeElementInfoFromProjection,
|
|
16
|
+
resolveCodeLayerNodeFromElementInfo,
|
|
17
|
+
} from "@/pages/design-editor/code-layer-state";
|
|
18
|
+
import {
|
|
19
|
+
dedupeStringIds,
|
|
20
|
+
isScreenRootElementInfo,
|
|
21
|
+
shouldIgnoreOverviewLayerCreationEcho,
|
|
22
|
+
} from "@/pages/design-editor/selection-state";
|
|
23
|
+
import type { DesignTool, EditorMode } from "@/pages/design-editor/types";
|
|
24
|
+
|
|
25
|
+
export interface ScreenElementSelectArgs {
|
|
26
|
+
activeBreakpointWidthStateRef: RefObject<number | undefined>;
|
|
27
|
+
applyFileContentUpdate: (
|
|
28
|
+
fileId: string,
|
|
29
|
+
nextContent: string,
|
|
30
|
+
options?: {
|
|
31
|
+
refreshPreview?: boolean;
|
|
32
|
+
skipPreview?: boolean;
|
|
33
|
+
forcePreviewFullDocument?: boolean;
|
|
34
|
+
persist?: boolean;
|
|
35
|
+
recordHistory?: boolean;
|
|
36
|
+
updatedAt?: string;
|
|
37
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
38
|
+
},
|
|
39
|
+
) => void;
|
|
40
|
+
clearPendingOverviewLayerSelectionTimer: () => void;
|
|
41
|
+
focusDesignInspectorForSelection: () => void;
|
|
42
|
+
getCodeLayerProjectionForScreen: (
|
|
43
|
+
screenId: string,
|
|
44
|
+
) => CodeLayerProjection | null;
|
|
45
|
+
getScreenContent: (screenId: string) => string;
|
|
46
|
+
handleBreakpointBarSelect: (widthPx: number | undefined) => void;
|
|
47
|
+
id: string | undefined;
|
|
48
|
+
pendingOverviewLayerSelectionRef: RefObject<string | null>;
|
|
49
|
+
pendingOverviewScreenSelectionRef: RefObject<string | null>;
|
|
50
|
+
selectedLayerIdsState: string[];
|
|
51
|
+
setActiveFileId: Dispatch<SetStateAction<string | null>>;
|
|
52
|
+
setActiveTool: Dispatch<SetStateAction<DesignTool>>;
|
|
53
|
+
setCreatedOverviewLayerSelection: Dispatch<
|
|
54
|
+
SetStateAction<{ screenId: string; layerId: string } | null>
|
|
55
|
+
>;
|
|
56
|
+
setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
57
|
+
setHoveredElementScreenId: Dispatch<SetStateAction<string | null>>;
|
|
58
|
+
setMode: Dispatch<SetStateAction<EditorMode>>;
|
|
59
|
+
setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
|
|
60
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
61
|
+
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
62
|
+
shouldPreserveBlockedOverviewLayerSelectionRef: RefObject<
|
|
63
|
+
(screenId: string) => boolean
|
|
64
|
+
>;
|
|
65
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
66
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function runScreenElementSelect(
|
|
70
|
+
{
|
|
71
|
+
activeBreakpointWidthStateRef,
|
|
72
|
+
applyFileContentUpdate,
|
|
73
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
74
|
+
focusDesignInspectorForSelection,
|
|
75
|
+
getCodeLayerProjectionForScreen,
|
|
76
|
+
getScreenContent,
|
|
77
|
+
handleBreakpointBarSelect,
|
|
78
|
+
id,
|
|
79
|
+
pendingOverviewLayerSelectionRef,
|
|
80
|
+
pendingOverviewScreenSelectionRef,
|
|
81
|
+
selectedLayerIdsState,
|
|
82
|
+
setActiveFileId,
|
|
83
|
+
setActiveTool,
|
|
84
|
+
setCreatedOverviewLayerSelection,
|
|
85
|
+
setHoveredElement,
|
|
86
|
+
setHoveredElementScreenId,
|
|
87
|
+
setMode,
|
|
88
|
+
setOverviewSelectedScreenIds,
|
|
89
|
+
setSelectedElement,
|
|
90
|
+
setSelectedLayerIdsState,
|
|
91
|
+
shouldPreserveBlockedOverviewLayerSelectionRef,
|
|
92
|
+
t,
|
|
93
|
+
viewModeRef,
|
|
94
|
+
}: ScreenElementSelectArgs,
|
|
95
|
+
screenId: string,
|
|
96
|
+
info: ElementInfo,
|
|
97
|
+
intent?: ElementSelectionIntent,
|
|
98
|
+
options: {
|
|
99
|
+
persistPendingNodeId?: boolean;
|
|
100
|
+
breakpointWidthPx?: number;
|
|
101
|
+
} = {},
|
|
102
|
+
) {
|
|
103
|
+
const pendingLayerId = pendingOverviewLayerSelectionRef.current;
|
|
104
|
+
const pendingScreenId = pendingOverviewScreenSelectionRef.current;
|
|
105
|
+
const projection = getCodeLayerProjectionForScreen(screenId);
|
|
106
|
+
const canonical = projection
|
|
107
|
+
? canonicalizeElementInfoFromProjection(projection, info)
|
|
108
|
+
: info;
|
|
109
|
+
const node = projection
|
|
110
|
+
? resolveCodeLayerNodeFromElementInfo(projection, canonical)
|
|
111
|
+
: null;
|
|
112
|
+
if (
|
|
113
|
+
shouldIgnoreOverviewLayerCreationEcho({
|
|
114
|
+
pendingLayerId,
|
|
115
|
+
pendingScreenId,
|
|
116
|
+
screenId,
|
|
117
|
+
info: canonical,
|
|
118
|
+
resolvedLayerId: node?.id,
|
|
119
|
+
event: "select",
|
|
120
|
+
})
|
|
121
|
+
) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
125
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
126
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
127
|
+
setCreatedOverviewLayerSelection(null);
|
|
128
|
+
if (
|
|
129
|
+
shouldPreserveBlockedOverviewLayerSelectionRef.current(screenId) &&
|
|
130
|
+
(isScreenRootElementInfo(canonical) ||
|
|
131
|
+
!node ||
|
|
132
|
+
selectedLayerIdsState.includes(node.id))
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Node-id integrity (id-on-demand): AI-generated/duplicated screens
|
|
137
|
+
// frequently ship elements with a missing or empty-string
|
|
138
|
+
// `data-agent-native-node-id` — every id-keyed operation on that
|
|
139
|
+
// element (move/reorder, style commits that resolve a targetNode,
|
|
140
|
+
// motion tracks, scrub) then silently no-ops or throws "Node with
|
|
141
|
+
// data-agent-native-node-id=\"\" not found in sourceHtml". The bridge
|
|
142
|
+
// (editor-chrome.bridge.ts's getElementInfo) mints a durable
|
|
143
|
+
// `pendingNodeId` on the SELECTION payload whenever it can't resolve a
|
|
144
|
+
// stable id for the element (`!sourceId`) and exposes it as
|
|
145
|
+
// `canonical.pendingNodeId`; persist it as the element's real
|
|
146
|
+
// `data-agent-native-node-id` right now via the same deterministic,
|
|
147
|
+
// guarded write path every other edit uses (applyVisualEdit's new
|
|
148
|
+
// "attribute" intent + applyFileContentUpdate), so every subsequent
|
|
149
|
+
// id-keyed op against this element resolves normally afterward. This is
|
|
150
|
+
// more reliable than resolving through the host's own static-HTML
|
|
151
|
+
// projection (`node`, below) — the bridge already knows the live DOM
|
|
152
|
+
// element and its working selector candidates even when the host's
|
|
153
|
+
// positional-selector projection match drifts.
|
|
154
|
+
const pendingNodeId = (canonical as { pendingNodeId?: string }).pendingNodeId;
|
|
155
|
+
if (
|
|
156
|
+
options.persistPendingNodeId !== false &&
|
|
157
|
+
!isScreenRootElementInfo(canonical) &&
|
|
158
|
+
pendingNodeId &&
|
|
159
|
+
!canonical.sourceId &&
|
|
160
|
+
canonical.selector
|
|
161
|
+
) {
|
|
162
|
+
const rawContent = getScreenContent(screenId);
|
|
163
|
+
if (rawContent) {
|
|
164
|
+
const result = applyVisualEdit(
|
|
165
|
+
rawContent,
|
|
166
|
+
{
|
|
167
|
+
kind: "attribute",
|
|
168
|
+
target: { selector: canonical.selector },
|
|
169
|
+
name: "data-agent-native-node-id",
|
|
170
|
+
value: pendingNodeId,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
source: { kind: "design-file", designId: id, fileId: screenId },
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
if (result.result.status === "applied" && result.content !== rawContent) {
|
|
177
|
+
applyFileContentUpdate(screenId, result.content, {
|
|
178
|
+
recordHistory: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (
|
|
183
|
+
options.persistPendingNodeId !== false &&
|
|
184
|
+
// Fallback sweep: an element the bridge didn't mint a pendingNodeId
|
|
185
|
+
// for (older bridge instance, or a node resolved only through the
|
|
186
|
+
// host's own projection) but that still lacks a stable id per the
|
|
187
|
+
// host's own projection match. Runs the whole-document stamp helper
|
|
188
|
+
// so any other id-less siblings pick up ids in the same pass too.
|
|
189
|
+
!isScreenRootElementInfo(canonical) &&
|
|
190
|
+
node &&
|
|
191
|
+
!node.dataAttributes["data-agent-native-node-id"]?.trim()
|
|
192
|
+
) {
|
|
193
|
+
const rawContent = getScreenContent(screenId);
|
|
194
|
+
if (rawContent) {
|
|
195
|
+
const stamped = ensureCodeLayerNodeIdsInHtml(rawContent, {
|
|
196
|
+
source: { kind: "design-file", designId: id, fileId: screenId },
|
|
197
|
+
});
|
|
198
|
+
if (stamped.changed && stamped.content !== rawContent) {
|
|
199
|
+
applyFileContentUpdate(screenId, stamped.content, {
|
|
200
|
+
recordHistory: false,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Known limitation: elements rendered from a `<template x-for>`
|
|
206
|
+
// repeater (common in AI-generated Alpine.js list/task UIs) have no
|
|
207
|
+
// per-instance static DOM node in the SOURCE HTML at all — neither
|
|
208
|
+
// resolveCodeLayerNodeFromElementInfo nor a selector-based
|
|
209
|
+
// applyVisualEdit resolution can find a unique per-instance node to
|
|
210
|
+
// stamp. Fixing that requires the code-layer projection itself to
|
|
211
|
+
// model `<template>` repeater children as selectable/attributable
|
|
212
|
+
// nodes, which is out of scope for this selection-time fix.
|
|
213
|
+
const additiveSelection = Boolean(
|
|
214
|
+
node &&
|
|
215
|
+
(intent?.additive ||
|
|
216
|
+
intent?.range ||
|
|
217
|
+
intent?.shiftKey ||
|
|
218
|
+
intent?.metaKey ||
|
|
219
|
+
intent?.ctrlKey),
|
|
220
|
+
);
|
|
221
|
+
setActiveFileId(screenId);
|
|
222
|
+
setSelectedElement(canonical);
|
|
223
|
+
setHoveredElement(null);
|
|
224
|
+
setHoveredElementScreenId(null);
|
|
225
|
+
if (node && additiveSelection) {
|
|
226
|
+
setSelectedLayerIdsState((current) => {
|
|
227
|
+
const removeExisting =
|
|
228
|
+
Boolean(intent?.metaKey || intent?.ctrlKey) &&
|
|
229
|
+
!intent?.shiftKey &&
|
|
230
|
+
current.includes(node.id);
|
|
231
|
+
if (removeExisting) {
|
|
232
|
+
const next = current.filter((layerId) => layerId !== node.id);
|
|
233
|
+
return next.length > 0 ? next : [node.id];
|
|
234
|
+
}
|
|
235
|
+
return dedupeStringIds([...current, node.id]);
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
setSelectedLayerIdsState(node ? [node.id] : []);
|
|
239
|
+
}
|
|
240
|
+
if (viewModeRef.current === "overview") {
|
|
241
|
+
setOverviewSelectedScreenIds([]);
|
|
242
|
+
// A responsive sub-frame now owns a full editor bridge, so selection
|
|
243
|
+
// carries its exact width into the edit scope. Primary-frame clicks
|
|
244
|
+
// still return to Base. This prevents two identical selectors in the
|
|
245
|
+
// base and responsive runtimes from racing for one global scope.
|
|
246
|
+
if (options.breakpointWidthPx !== undefined) {
|
|
247
|
+
handleBreakpointBarSelect(options.breakpointWidthPx);
|
|
248
|
+
const guidanceKey = `design-responsive-edit-guidance:${id}:${screenId}`;
|
|
249
|
+
if (window.localStorage.getItem(guidanceKey) !== "shown") {
|
|
250
|
+
window.localStorage.setItem(guidanceKey, "shown");
|
|
251
|
+
toast.info(t("designEditor.breakpointBar.scope.firstEditGuidance"), {
|
|
252
|
+
duration: 6000,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
} else if (activeBreakpointWidthStateRef.current !== undefined) {
|
|
256
|
+
handleBreakpointBarSelect(undefined);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
setActiveTool("move");
|
|
260
|
+
setMode("edit");
|
|
261
|
+
focusDesignInspectorForSelection();
|
|
262
|
+
}
|