@agent-native/core 0.85.5 → 0.85.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -1
- package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
- package/corpus/core/src/collab/presence.ts +63 -3
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
- package/corpus/templates/design/AGENTS.md +40 -4
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
- package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
- package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
- package/corpus/templates/design/actions/connect-localhost.ts +25 -18
- package/corpus/templates/design/actions/edit-design.ts +5 -3
- package/corpus/templates/design/actions/export-pdf.ts +8 -1
- package/corpus/templates/design/actions/export-zip.ts +10 -1
- package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
- package/corpus/templates/design/actions/insert-asset.ts +190 -7
- package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
- package/corpus/templates/design/actions/present-design-variants.ts +12 -8
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
- package/corpus/templates/design/actions/run-design-audit.ts +7 -4
- package/corpus/templates/design/actions/write-local-file.ts +47 -22
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
- package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
- package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1520 -410
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
- package/corpus/templates/design/app/components/design/types.ts +16 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +18 -0
- package/corpus/templates/design/app/lib/design-import.ts +95 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4015 -840
- package/corpus/templates/design/changelog/2026-07-02-creating-two-frames-in-a-row-no-longer-freezes-and-reloads-t.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-design-retries-stalled-screen-edits-with-a-smaller-safer-fol.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
- package/corpus/templates/design/server/lib/design-export.ts +43 -1
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/design/shared/board-file.ts +25 -5
- package/corpus/templates/design/shared/canvas-math.ts +754 -9
- package/corpus/templates/design/shared/code-layer.ts +304 -26
- package/corpus/templates/design/shared/color-utils.ts +84 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
- package/corpus/templates/design/shared/motion-compiler.ts +75 -31
- package/corpus/templates/design/shared/motion-timeline.ts +335 -0
- package/corpus/templates/design/shared/pen-path.ts +200 -23
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/use-pinch-zoom.d.ts.map +1 -1
- package/dist/client/use-pinch-zoom.js +76 -11
- package/dist/client/use-pinch-zoom.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +54 -3
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +1 -1
|
@@ -25,24 +25,28 @@ export default defineAction({
|
|
|
25
25
|
|
|
26
26
|
const db = getDb();
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
(
|
|
28
|
+
// The delete result's rows-affected shape varies by driver (and some
|
|
29
|
+
// drivers do not report it at all), so SELECT the scoped grant first and
|
|
30
|
+
// derive `revoked` from its existence — portable across dialects without
|
|
31
|
+
// assuming RETURNING support.
|
|
32
|
+
const scope = and(
|
|
33
|
+
eq(schema.designLocalhostWriteGrants.designId, designId),
|
|
34
|
+
eq(schema.designLocalhostWriteGrants.connectionId, connectionId),
|
|
35
|
+
eq(schema.designLocalhostWriteGrants.ownerEmail, ownerEmail),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const [grant] = await db
|
|
39
|
+
.select({ id: schema.designLocalhostWriteGrants.id })
|
|
40
|
+
.from(schema.designLocalhostWriteGrants)
|
|
41
|
+
.where(scope)
|
|
42
|
+
.limit(1);
|
|
43
|
+
|
|
44
|
+
await db.delete(schema.designLocalhostWriteGrants).where(scope);
|
|
41
45
|
|
|
42
46
|
return {
|
|
43
47
|
designId,
|
|
44
48
|
connectionId,
|
|
45
|
-
revoked:
|
|
49
|
+
revoked: Boolean(grant),
|
|
46
50
|
};
|
|
47
51
|
},
|
|
48
52
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* run-design-audit — read-only a11y audit over a design's rendered HTML/DOM.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Flags low-opacity text color classes as a contrast hint (real contrast
|
|
5
|
+
* ratios require a DOM/CSS cascade resolver that isn't available server-side,
|
|
6
|
+
* so this is not a computed ratio check), plus tap-target sizes, missing
|
|
7
|
+
* alt/labels, focus visibility, and reduced-motion concerns, all by static
|
|
8
|
+
* analysis of the stored HTML. Does NOT perform writes. Results are returned
|
|
9
|
+
* as `A11yFinding[]` and may be persisted by the caller via
|
|
10
|
+
* `create-design-review-snapshot`.
|
|
8
11
|
*
|
|
9
12
|
* See DESIGN-STUDIO-PLAN.md §6.5 + §7 (Review surface).
|
|
10
13
|
*/
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
* must exist. The agent CANNOT bypass this check.
|
|
9
9
|
* 4. Path confinement: assertPathInside ensures the target stays inside
|
|
10
10
|
* rootPath (pre-bridge check; bridge also validates with realpath).
|
|
11
|
-
* 5. Bridge token: the X-Bridge-Token header is set to the
|
|
12
|
-
*
|
|
11
|
+
* 5. Bridge token: the X-Bridge-Token header is set to the connection's
|
|
12
|
+
* CURRENT bridge token (falling back to the token snapshotted on the
|
|
13
|
+
* grant). The CLI mints a fresh token on every bridge start, so a bridge
|
|
14
|
+
* restart + reconnect rotates the connection token while the user's
|
|
15
|
+
* time-boxed consent grant stays valid; preferring the connection token
|
|
16
|
+
* keeps writes working across restarts. A bridge 401/403 is surfaced as a
|
|
17
|
+
* specific stale-token error telling the user to re-run design connect
|
|
18
|
+
* and re-grant write consent.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
21
|
import path from "node:path";
|
|
@@ -53,6 +59,30 @@ function isLoopbackHostname(hostname: string): boolean {
|
|
|
53
59
|
);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Build the error for a failed bridge call. 401/403 means the bridge rejected
|
|
64
|
+
* the token — after a bridge restart the CLI mints a fresh token, so a token
|
|
65
|
+
* snapshotted at consent time goes stale even though the grant itself is
|
|
66
|
+
* still valid. Surface that as a specific, actionable message instead of a
|
|
67
|
+
* generic failure.
|
|
68
|
+
*/
|
|
69
|
+
function bridgeRequestError(
|
|
70
|
+
operation: string,
|
|
71
|
+
status: number,
|
|
72
|
+
errText: string,
|
|
73
|
+
): Error {
|
|
74
|
+
if (status === 401 || status === 403) {
|
|
75
|
+
return new Error(
|
|
76
|
+
`Bridge ${operation} rejected authentication (${status}). ` +
|
|
77
|
+
"The stored bridge token is stale — the design bridge was likely restarted " +
|
|
78
|
+
"since write consent was granted (each bridge start mints a fresh token). " +
|
|
79
|
+
"Re-run `npx @agent-native/core@latest design connect` and re-grant write " +
|
|
80
|
+
"consent, then retry.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return new Error(`Bridge ${operation} failed (${status}): ${errText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
function normalizeBridgeUrl(value: string): string {
|
|
57
87
|
const parsed = new URL(value.trim());
|
|
58
88
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
@@ -140,10 +170,13 @@ export default defineAction({
|
|
|
140
170
|
);
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
// --- Resolve bridge URL ---
|
|
173
|
+
// --- Resolve bridge URL + current token ---
|
|
144
174
|
const db = getDb();
|
|
145
175
|
const [connection] = await db
|
|
146
|
-
.select({
|
|
176
|
+
.select({
|
|
177
|
+
bridgeUrl: schema.designLocalhostConnections.bridgeUrl,
|
|
178
|
+
bridgeToken: schema.designLocalhostConnections.bridgeToken,
|
|
179
|
+
})
|
|
147
180
|
.from(schema.designLocalhostConnections)
|
|
148
181
|
.where(
|
|
149
182
|
and(
|
|
@@ -160,10 +193,15 @@ export default defineAction({
|
|
|
160
193
|
);
|
|
161
194
|
}
|
|
162
195
|
|
|
196
|
+
// Prefer the connection's CURRENT bridge token over the one snapshotted on
|
|
197
|
+
// the grant: the CLI mints a fresh token on every bridge start, and a
|
|
198
|
+
// later connect-localhost by the same authenticated user refreshes the
|
|
199
|
+
// connection row. The user's time-boxed consent grant is unchanged — only
|
|
200
|
+
// the transport token rotated — so writes keep working across restarts.
|
|
163
201
|
const bridgeUrl = normalizeBridgeUrl(connection.bridgeUrl);
|
|
164
202
|
const headers: Record<string, string> = {
|
|
165
203
|
"Content-Type": "application/json",
|
|
166
|
-
"X-Bridge-Token": grant.bridgeToken,
|
|
204
|
+
"X-Bridge-Token": connection.bridgeToken || grant.bridgeToken,
|
|
167
205
|
};
|
|
168
206
|
|
|
169
207
|
if (content !== undefined) {
|
|
@@ -175,23 +213,12 @@ export default defineAction({
|
|
|
175
213
|
});
|
|
176
214
|
if (!res.ok) {
|
|
177
215
|
const errText = await res.text().catch(() => res.statusText);
|
|
178
|
-
throw
|
|
216
|
+
throw bridgeRequestError("write-file", res.status, errText);
|
|
179
217
|
}
|
|
180
218
|
return { designId, relPath, operation: "write" as const, written: true };
|
|
181
219
|
} else {
|
|
182
|
-
// Search-and-replace patch
|
|
183
|
-
|
|
184
|
-
method: "POST",
|
|
185
|
-
headers,
|
|
186
|
-
body: JSON.stringify({ relPath }),
|
|
187
|
-
});
|
|
188
|
-
if (!readRes.ok) {
|
|
189
|
-
const errText = await readRes.text().catch(() => readRes.statusText);
|
|
190
|
-
throw new Error(
|
|
191
|
-
`Bridge read-file failed (${readRes.status}): ${errText}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
220
|
+
// Search-and-replace patch. The bridge's /apply-edit validates the file
|
|
221
|
+
// itself (404s on a missing file), so no pre-read round-trip is needed.
|
|
195
222
|
const applyRes = await fetch(`${bridgeUrl}/apply-edit`, {
|
|
196
223
|
method: "POST",
|
|
197
224
|
headers,
|
|
@@ -203,9 +230,7 @@ export default defineAction({
|
|
|
203
230
|
});
|
|
204
231
|
if (!applyRes.ok) {
|
|
205
232
|
const errText = await applyRes.text().catch(() => applyRes.statusText);
|
|
206
|
-
throw
|
|
207
|
-
`Bridge apply-edit failed (${applyRes.status}): ${errText}`,
|
|
208
|
-
);
|
|
233
|
+
throw bridgeRequestError("apply-edit", applyRes.status, errText);
|
|
209
234
|
}
|
|
210
235
|
return { designId, relPath, operation: "patch" as const, written: true };
|
|
211
236
|
}
|
|
@@ -447,6 +447,14 @@ interface DesignCanvasProps {
|
|
|
447
447
|
* needs the tracks so the bridge can interpolate values at each tick.
|
|
448
448
|
*/
|
|
449
449
|
motionTracks?: MotionTrackWire[];
|
|
450
|
+
/**
|
|
451
|
+
* Timeline-level default easing applied to any keyframe that omits its own
|
|
452
|
+
* `ease`. Threaded to the motion-preview bridge alongside the tracks so the
|
|
453
|
+
* scrub/playback preview matches the persisted CSS (the compiler uses the
|
|
454
|
+
* same `defaultEase` fallback when emitting `animation-timing-function`).
|
|
455
|
+
* When omitted the bridge falls back to "ease".
|
|
456
|
+
*/
|
|
457
|
+
motionDefaultEase?: string;
|
|
450
458
|
/**
|
|
451
459
|
* Explicit iframe width in pixels. When provided it overrides the width
|
|
452
460
|
* derived from `deviceFrame`, enabling per-breakpoint preview (e.g. Mobile
|
|
@@ -683,6 +691,7 @@ export function DesignCanvas({
|
|
|
683
691
|
commentContextLabel,
|
|
684
692
|
onPrototypeNavigate,
|
|
685
693
|
motionTracks,
|
|
694
|
+
motionDefaultEase,
|
|
686
695
|
previewWidthPx,
|
|
687
696
|
onComponentSourceJump,
|
|
688
697
|
shaderFillPreview,
|
|
@@ -695,6 +704,40 @@ export function DesignCanvas({
|
|
|
695
704
|
const runtimeReplacementContentRef = useRef(runtimeReplacementContent);
|
|
696
705
|
const runtimeReplacementKeyRef = useRef(runtimeReplacementKey);
|
|
697
706
|
const lastRuntimeReplacementKeyRef = useRef(runtimeReplacementKey);
|
|
707
|
+
// Bridge-ready handshake (see EDITOR_CHROME_BRIDGE_SCRIPT's
|
|
708
|
+
// agent-native:editor-chrome-ready post on install). One-shot commands —
|
|
709
|
+
// begin-text-edit, set-editor-chrome-scale, style-change, delete-element,
|
|
710
|
+
// replace-document-content — are fire-and-forget postMessages with no retry:
|
|
711
|
+
// if the iframe document is still loading (fresh srcdoc, screen switch, or a
|
|
712
|
+
// mid-flight reload) the bridge script hasn't attached its message listener
|
|
713
|
+
// yet and the command is silently dropped. replayIframeEditorState only
|
|
714
|
+
// re-sends steady-state selection/hover/tweak/motion values, never these
|
|
715
|
+
// one-shot commands, so a dropped one-shot never recovers on its own.
|
|
716
|
+
// Queue them here until ready fires (or the iframe finishes loading, as a
|
|
717
|
+
// fallback for older/interact-mode documents that never inject the chrome
|
|
718
|
+
// bridge and thus never post ready) and flush in order.
|
|
719
|
+
const bridgeReadyRef = useRef(false);
|
|
720
|
+
const pendingOneShotMessagesRef = useRef<unknown[]>([]);
|
|
721
|
+
const flushPendingOneShotMessages = useCallback(() => {
|
|
722
|
+
const iframe = iframeRef.current;
|
|
723
|
+
const win = iframe?.contentWindow;
|
|
724
|
+
if (!win) return;
|
|
725
|
+
const queued = pendingOneShotMessagesRef.current;
|
|
726
|
+
if (queued.length === 0) return;
|
|
727
|
+
pendingOneShotMessagesRef.current = [];
|
|
728
|
+
queued.forEach((message) => win.postMessage(message, "*"));
|
|
729
|
+
}, []);
|
|
730
|
+
const postOneShotBridgeMessage = useCallback((message: unknown) => {
|
|
731
|
+
const iframe = iframeRef.current;
|
|
732
|
+
const win = iframe?.contentWindow;
|
|
733
|
+
if (!win) return false;
|
|
734
|
+
if (!bridgeReadyRef.current) {
|
|
735
|
+
pendingOneShotMessagesRef.current.push(message);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
win.postMessage(message, "*");
|
|
739
|
+
return true;
|
|
740
|
+
}, []);
|
|
698
741
|
const [renderedContent, setRenderedContent] = useState(content);
|
|
699
742
|
const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
|
|
700
743
|
const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
|
|
@@ -865,6 +908,15 @@ export function DesignCanvas({
|
|
|
865
908
|
if (previousContentKeyRef.current !== contentKey) {
|
|
866
909
|
previousContentKeyRef.current = contentKey;
|
|
867
910
|
lastRuntimeReplacementKeyRef.current = runtimeReplacementKey;
|
|
911
|
+
// A content-key change rebuilds srcdoc, which reloads the iframe with a
|
|
912
|
+
// brand-new document. The previous document's ready handshake (and any
|
|
913
|
+
// one-shot commands still queued against it) no longer apply — reset
|
|
914
|
+
// synchronously here rather than on the iframe's `load` event, since
|
|
915
|
+
// `load` fires AFTER the freshly (re)injected editor-chrome bridge script
|
|
916
|
+
// has already run and posted its own new ready message; resetting on
|
|
917
|
+
// `load` would incorrectly clobber that just-arrived ready signal.
|
|
918
|
+
bridgeReadyRef.current = false;
|
|
919
|
+
pendingOneShotMessagesRef.current = [];
|
|
868
920
|
setRenderedContent(content);
|
|
869
921
|
}
|
|
870
922
|
// Same-screen visual edits are already applied optimistically inside the
|
|
@@ -888,11 +940,14 @@ export function DesignCanvas({
|
|
|
888
940
|
// outside Edit mode. The editor chrome bridge is omitted for Interact mode
|
|
889
941
|
// so preview/app users can interact with the app normally.
|
|
890
942
|
//
|
|
891
|
-
// readOnly
|
|
892
|
-
// !interactMode and the initial __READ_ONLY__
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
943
|
+
// readOnly and editMode are intentionally NOT deps here: the bridge is
|
|
944
|
+
// injected whenever !interactMode and the initial __READ_ONLY__ /
|
|
945
|
+
// __TEXT_EDITING_ENABLED__ placeholders are baked from the *first* render
|
|
946
|
+
// only (so first paint never flashes the wrong mode). After that, live
|
|
947
|
+
// readOnly / editMode changes flow through the set-read-only and
|
|
948
|
+
// set-text-editing-enabled postMessages (see the useEffects below) so
|
|
949
|
+
// switching the active surface (board ↔ screen) or toggling Edit ⇄ Preview
|
|
950
|
+
// never rebuilds srcdoc / reloads every screen iframe.
|
|
896
951
|
const srcdoc = useMemo(() => {
|
|
897
952
|
if (externalPreviewUrl) return undefined;
|
|
898
953
|
const editorChromeBridge = interactMode
|
|
@@ -917,12 +972,20 @@ export function DesignCanvas({
|
|
|
917
972
|
"__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
|
|
918
973
|
isEmbeddedFrame ? "true" : "false",
|
|
919
974
|
);
|
|
975
|
+
// ALWAYS injected (like the other always-on bridges above) so
|
|
976
|
+
// MultiScreenCanvas's cross-screen drag hit-testing
|
|
977
|
+
// (agent-native:hit-test / agent-native:hit-test-result) resolves an
|
|
978
|
+
// anchor+placement against this screen's live DOM too, not only the
|
|
979
|
+
// static-fallback board thumbnails that call appendHitTestResponder.
|
|
980
|
+
// Without this, a drop onto a live/active screen has no responder and
|
|
981
|
+
// always falls through to the 50ms request timeout.
|
|
920
982
|
const bridgeToInject =
|
|
921
983
|
MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
922
984
|
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
923
985
|
TWEAK_BRIDGE_SCRIPT +
|
|
924
986
|
ZOOM_BRIDGE_SCRIPT +
|
|
925
987
|
NAV_BRIDGE_SCRIPT +
|
|
988
|
+
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
|
|
926
989
|
embeddedWheelBridge +
|
|
927
990
|
editorChromeBridge;
|
|
928
991
|
const frameContent = getEmbeddedFrameDocumentContent({
|
|
@@ -954,10 +1017,10 @@ export function DesignCanvas({
|
|
|
954
1017
|
// baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
|
|
955
1018
|
// postMessage above. Including them here rebuilds srcdoc on every zoom commit,
|
|
956
1019
|
// which reloads the iframe and flashes the screen content white.
|
|
957
|
-
// readOnly
|
|
1020
|
+
// readOnly and editMode are intentionally NOT deps: live changes flow
|
|
1021
|
+
// through set-read-only / set-text-editing-enabled.
|
|
958
1022
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
959
1023
|
}, [
|
|
960
|
-
editMode,
|
|
961
1024
|
boardSurface,
|
|
962
1025
|
externalPreviewUrl,
|
|
963
1026
|
interactMode,
|
|
@@ -996,6 +1059,11 @@ export function DesignCanvas({
|
|
|
996
1059
|
return;
|
|
997
1060
|
}
|
|
998
1061
|
if (!e.data || !e.data.type) return;
|
|
1062
|
+
if (e.data.type === "agent-native:editor-chrome-ready") {
|
|
1063
|
+
bridgeReadyRef.current = true;
|
|
1064
|
+
flushPendingOneShotMessages();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
999
1067
|
if (e.data.type === "clear-selection") {
|
|
1000
1068
|
onClearSelection?.();
|
|
1001
1069
|
return;
|
|
@@ -1256,17 +1324,30 @@ export function DesignCanvas({
|
|
|
1256
1324
|
const iframe = iframeRef.current;
|
|
1257
1325
|
const scroll = scrollContainerRef.current;
|
|
1258
1326
|
if (!iframe || !scroll) return;
|
|
1327
|
+
// Guard against non-finite values (NaN/undefined/string) before they
|
|
1328
|
+
// reach Math.max/Math.min/Math.exp — an unguarded NaN here would
|
|
1329
|
+
// propagate through nextZoom and silently break zoom (mirrors the
|
|
1330
|
+
// Number.isFinite-style validation the embedded-canvas-wheel sibling
|
|
1331
|
+
// handler above applies to its own wheel deltas/coordinates).
|
|
1332
|
+
const rawDeltaY = Number(e.data.deltaY);
|
|
1333
|
+
if (!Number.isFinite(rawDeltaY)) return;
|
|
1259
1334
|
// Mirror usePinchZoom's algorithm here. We can't reliably re-dispatch
|
|
1260
1335
|
// a synthetic WheelEvent to trigger the hook's listener — untrusted
|
|
1261
1336
|
// events are inconsistent across browsers — so just compute the
|
|
1262
1337
|
// next zoom directly using the same exponential factor + cursor-anchor
|
|
1263
1338
|
// math. Clamp range matches the usePinchZoom call above (10–500).
|
|
1264
1339
|
const currentZoom = zoomRef.current;
|
|
1265
|
-
const clampedDelta = Math.max(-50, Math.min(50,
|
|
1340
|
+
const clampedDelta = Math.max(-50, Math.min(50, rawDeltaY));
|
|
1266
1341
|
const factor = Math.exp(-clampedDelta * 0.01);
|
|
1267
1342
|
const nextZoom = Math.max(10, Math.min(500, currentZoom * factor));
|
|
1268
|
-
if (nextZoom === currentZoom) return;
|
|
1343
|
+
if (!Number.isFinite(nextZoom) || nextZoom === currentZoom) return;
|
|
1269
1344
|
if (deviceFrame === "none") {
|
|
1345
|
+
const rawClientX = Number(e.data.clientX);
|
|
1346
|
+
const rawClientY = Number(e.data.clientY);
|
|
1347
|
+
if (!Number.isFinite(rawClientX) || !Number.isFinite(rawClientY)) {
|
|
1348
|
+
onZoomChange(nextZoom);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1270
1351
|
// The iframe lives inside a `transform: scale(zoom/100)` wrapper, so
|
|
1271
1352
|
// its visual scale relative to viewport is currentZoom / 100. Convert
|
|
1272
1353
|
// the iframe-document point under the cursor → viewport point →
|
|
@@ -1274,8 +1355,8 @@ export function DesignCanvas({
|
|
|
1274
1355
|
const iframeRect = iframe.getBoundingClientRect();
|
|
1275
1356
|
const scrollRect = scroll.getBoundingClientRect();
|
|
1276
1357
|
const scale = currentZoom / 100;
|
|
1277
|
-
const viewportX = iframeRect.left +
|
|
1278
|
-
const viewportY = iframeRect.top +
|
|
1358
|
+
const viewportX = iframeRect.left + rawClientX * scale;
|
|
1359
|
+
const viewportY = iframeRect.top + rawClientY * scale;
|
|
1279
1360
|
const cx = viewportX - scrollRect.left + scroll.scrollLeft;
|
|
1280
1361
|
const cy = viewportY - scrollRect.top + scroll.scrollTop;
|
|
1281
1362
|
const ratio = nextZoom / currentZoom;
|
|
@@ -1315,6 +1396,7 @@ export function DesignCanvas({
|
|
|
1315
1396
|
isEmbeddedFrame,
|
|
1316
1397
|
sourceType,
|
|
1317
1398
|
fusionUrl,
|
|
1399
|
+
flushPendingOneShotMessages,
|
|
1318
1400
|
]);
|
|
1319
1401
|
|
|
1320
1402
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -1362,7 +1444,11 @@ export function DesignCanvas({
|
|
|
1362
1444
|
// Re-send motion tracks so the preview bridge is ready after a reload.
|
|
1363
1445
|
if (motionTracks && motionTracks.length > 0) {
|
|
1364
1446
|
iframe.contentWindow?.postMessage(
|
|
1365
|
-
{
|
|
1447
|
+
{
|
|
1448
|
+
type: "motion-load-tracks",
|
|
1449
|
+
tracks: motionTracks,
|
|
1450
|
+
defaultEase: motionDefaultEase,
|
|
1451
|
+
},
|
|
1366
1452
|
"*",
|
|
1367
1453
|
);
|
|
1368
1454
|
} else {
|
|
@@ -1392,6 +1478,7 @@ export function DesignCanvas({
|
|
|
1392
1478
|
hiddenSelectors,
|
|
1393
1479
|
lockedSelectors,
|
|
1394
1480
|
motionTracks,
|
|
1481
|
+
motionDefaultEase,
|
|
1395
1482
|
scaleMode,
|
|
1396
1483
|
selectedSelector,
|
|
1397
1484
|
selectedSelectorCandidates,
|
|
@@ -1411,6 +1498,27 @@ export function DesignCanvas({
|
|
|
1411
1498
|
return () => iframe.removeEventListener("load", replayIframeEditorState);
|
|
1412
1499
|
}, [replayIframeEditorState]);
|
|
1413
1500
|
|
|
1501
|
+
// Fallback for documents that never inject the editor-chrome bridge (e.g.
|
|
1502
|
+
// interactMode, or an external-preview iframe where srcdoc is undefined):
|
|
1503
|
+
// those never post agent-native:editor-chrome-ready, so nothing would ever
|
|
1504
|
+
// flush a queued one-shot command. Treat the iframe's `load` event as an
|
|
1505
|
+
// implicit ready in that case — the queued commands (begin-text-edit,
|
|
1506
|
+
// style-change, delete-element, replace-document-content) target the
|
|
1507
|
+
// editor-chrome bridge's own message handlers, so with no chrome bridge
|
|
1508
|
+
// present there is nothing that would have handled them regardless; this
|
|
1509
|
+
// just prevents the queue from growing unbounded across repeated reloads.
|
|
1510
|
+
useEffect(() => {
|
|
1511
|
+
const iframe = iframeRef.current;
|
|
1512
|
+
if (!iframe) return;
|
|
1513
|
+
function handleLoadReadyFallback() {
|
|
1514
|
+
if (bridgeReadyRef.current) return;
|
|
1515
|
+
bridgeReadyRef.current = true;
|
|
1516
|
+
flushPendingOneShotMessages();
|
|
1517
|
+
}
|
|
1518
|
+
iframe.addEventListener("load", handleLoadReadyFallback);
|
|
1519
|
+
return () => iframe.removeEventListener("load", handleLoadReadyFallback);
|
|
1520
|
+
}, [flushPendingOneShotMessages]);
|
|
1521
|
+
|
|
1414
1522
|
useEffect(() => {
|
|
1415
1523
|
if (clearSelectionRequest === undefined) return;
|
|
1416
1524
|
iframeRef.current?.contentWindow?.postMessage(
|
|
@@ -1429,11 +1537,15 @@ export function DesignCanvas({
|
|
|
1429
1537
|
win.postMessage({ type: "motion-preview-clear" }, "*");
|
|
1430
1538
|
} else {
|
|
1431
1539
|
win.postMessage(
|
|
1432
|
-
{
|
|
1540
|
+
{
|
|
1541
|
+
type: "motion-load-tracks",
|
|
1542
|
+
tracks: motionTracks,
|
|
1543
|
+
defaultEase: motionDefaultEase,
|
|
1544
|
+
},
|
|
1433
1545
|
"*",
|
|
1434
1546
|
);
|
|
1435
1547
|
}
|
|
1436
|
-
}, [motionTracks]);
|
|
1548
|
+
}, [motionTracks, motionDefaultEase]);
|
|
1437
1549
|
|
|
1438
1550
|
// Sync shader-fill preview to the iframe whenever the prop changes.
|
|
1439
1551
|
// When cleared (null / undefined) send a clear message so the bridge
|
|
@@ -1461,16 +1573,16 @@ export function DesignCanvas({
|
|
|
1461
1573
|
// overview zoom settles. This is intentionally separate from the srcdoc build so
|
|
1462
1574
|
// a scale change never rebuilds srcdoc / reloads the iframe (which flashes the
|
|
1463
1575
|
// content white). The baked __EDITOR_CHROME_SCALE__ values cover first paint.
|
|
1576
|
+
// Routed through the one-shot queue too: a zoom settle that lands while the
|
|
1577
|
+
// iframe is mid-reload would otherwise be silently dropped, leaving the
|
|
1578
|
+
// chrome at a stale scale until the next zoom change.
|
|
1464
1579
|
useEffect(() => {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
"*",
|
|
1472
|
-
);
|
|
1473
|
-
}, [editorChromeScaleX, editorChromeScaleY]);
|
|
1580
|
+
postOneShotBridgeMessage({
|
|
1581
|
+
type: "set-editor-chrome-scale",
|
|
1582
|
+
scaleX: editorChromeScaleX,
|
|
1583
|
+
scaleY: editorChromeScaleY,
|
|
1584
|
+
});
|
|
1585
|
+
}, [editorChromeScaleX, editorChromeScaleY, postOneShotBridgeMessage]);
|
|
1474
1586
|
|
|
1475
1587
|
// Sync readOnly to the bridge IN-PLACE via postMessage so switching the active
|
|
1476
1588
|
// surface (board ↔ screen) does not rebuild srcdoc / reload the iframe.
|
|
@@ -1495,6 +1607,34 @@ export function DesignCanvas({
|
|
|
1495
1607
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1496
1608
|
}, [readOnly]);
|
|
1497
1609
|
|
|
1610
|
+
// Sync editMode to the bridge IN-PLACE via postMessage so toggling Edit ⇄
|
|
1611
|
+
// Preview does not rebuild srcdoc / reload every screen iframe (which was
|
|
1612
|
+
// PF21: __TEXT_EDITING_ENABLED__ used to be baked into srcdoc, so flipping
|
|
1613
|
+
// edit/preview mode reloaded every iframe — white flash + lost in-iframe
|
|
1614
|
+
// Alpine state). The initial baked __TEXT_EDITING_ENABLED__ placeholder
|
|
1615
|
+
// covers first paint; subsequent changes arrive here. Also re-send on
|
|
1616
|
+
// iframe load so the bridge is in sync after any content-key-driven reload.
|
|
1617
|
+
const editModeRef = useRef(editMode);
|
|
1618
|
+
editModeRef.current = editMode;
|
|
1619
|
+
useEffect(() => {
|
|
1620
|
+
const iframe = iframeRef.current;
|
|
1621
|
+
if (!iframe) return;
|
|
1622
|
+
function sendTextEditingEnabled() {
|
|
1623
|
+
iframe!.contentWindow?.postMessage(
|
|
1624
|
+
{
|
|
1625
|
+
type: "set-text-editing-enabled",
|
|
1626
|
+
enabled: editModeRef.current,
|
|
1627
|
+
},
|
|
1628
|
+
"*",
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
sendTextEditingEnabled();
|
|
1632
|
+
iframe.addEventListener("load", sendTextEditingEnabled);
|
|
1633
|
+
return () => iframe.removeEventListener("load", sendTextEditingEnabled);
|
|
1634
|
+
// Only re-run when editMode changes; iframe identity is stable.
|
|
1635
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1636
|
+
}, [editMode]);
|
|
1637
|
+
|
|
1498
1638
|
/**
|
|
1499
1639
|
* Trigger immediate text-editing mode for a specific node inside the iframe,
|
|
1500
1640
|
* identified by its data-agent-native-node-id value. Call this after
|
|
@@ -1504,14 +1644,18 @@ export function DesignCanvas({
|
|
|
1504
1644
|
* Posts { type: 'begin-text-edit', nodeId } to the iframe bridge.
|
|
1505
1645
|
* The bridge enters the same contenteditable text-editing path used on dblclick.
|
|
1506
1646
|
*/
|
|
1507
|
-
const beginTextEdit = useCallback(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
{
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1647
|
+
const beginTextEdit = useCallback(
|
|
1648
|
+
(nodeId: string) => {
|
|
1649
|
+
const iframe = iframeRef.current;
|
|
1650
|
+
if (!iframe?.contentWindow || !nodeId) return;
|
|
1651
|
+
postOneShotBridgeMessage({
|
|
1652
|
+
type: "begin-text-edit",
|
|
1653
|
+
nodeId,
|
|
1654
|
+
force: true,
|
|
1655
|
+
});
|
|
1656
|
+
},
|
|
1657
|
+
[postOneShotBridgeMessage],
|
|
1658
|
+
);
|
|
1515
1659
|
|
|
1516
1660
|
const sendStyleChange = useCallback(
|
|
1517
1661
|
(
|
|
@@ -1522,19 +1666,16 @@ export function DesignCanvas({
|
|
|
1522
1666
|
) => {
|
|
1523
1667
|
const iframe = iframeRef.current;
|
|
1524
1668
|
if (!iframe?.contentWindow) return;
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
},
|
|
1534
|
-
"*",
|
|
1535
|
-
);
|
|
1669
|
+
postOneShotBridgeMessage({
|
|
1670
|
+
type: "style-change",
|
|
1671
|
+
selector,
|
|
1672
|
+
property,
|
|
1673
|
+
value,
|
|
1674
|
+
selectorCandidates: options?.selectorCandidates ?? [],
|
|
1675
|
+
nodeId: options?.nodeId ?? "",
|
|
1676
|
+
});
|
|
1536
1677
|
},
|
|
1537
|
-
[],
|
|
1678
|
+
[postOneShotBridgeMessage],
|
|
1538
1679
|
);
|
|
1539
1680
|
|
|
1540
1681
|
/**
|
|
@@ -1601,19 +1742,15 @@ export function DesignCanvas({
|
|
|
1601
1742
|
) => {
|
|
1602
1743
|
const iframe = iframeRef.current;
|
|
1603
1744
|
if (!iframe?.contentWindow) return false;
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
},
|
|
1612
|
-
"*",
|
|
1613
|
-
);
|
|
1614
|
-
return true;
|
|
1745
|
+
return postOneShotBridgeMessage({
|
|
1746
|
+
type: "replace-document-content",
|
|
1747
|
+
content: nextContent,
|
|
1748
|
+
selectedSelector: selector ?? "",
|
|
1749
|
+
selectorCandidates: candidates ?? [],
|
|
1750
|
+
forceFullDocument: options?.forceFullDocument === true,
|
|
1751
|
+
});
|
|
1615
1752
|
},
|
|
1616
|
-
[],
|
|
1753
|
+
[postOneShotBridgeMessage],
|
|
1617
1754
|
);
|
|
1618
1755
|
|
|
1619
1756
|
const replaceRuntimeContentInPlace = useCallback(
|
|
@@ -1680,17 +1817,13 @@ export function DesignCanvas({
|
|
|
1680
1817
|
(selector?: string | null, candidates?: string[]) => {
|
|
1681
1818
|
const iframe = iframeRef.current;
|
|
1682
1819
|
if (!iframe?.contentWindow) return false;
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
},
|
|
1689
|
-
"*",
|
|
1690
|
-
);
|
|
1691
|
-
return true;
|
|
1820
|
+
return postOneShotBridgeMessage({
|
|
1821
|
+
type: "delete-element",
|
|
1822
|
+
selector: selector ?? "",
|
|
1823
|
+
selectorCandidates: candidates ?? [],
|
|
1824
|
+
});
|
|
1692
1825
|
},
|
|
1693
|
-
[],
|
|
1826
|
+
[postOneShotBridgeMessage],
|
|
1694
1827
|
);
|
|
1695
1828
|
|
|
1696
1829
|
// Expose iframe runtime mutations for the editor orchestrator.
|
|
@@ -1826,7 +1959,29 @@ export function DesignCanvas({
|
|
|
1826
1959
|
sandbox={
|
|
1827
1960
|
externalPreviewUrl
|
|
1828
1961
|
? "allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
1829
|
-
:
|
|
1962
|
+
: // NOTE(security): allow-same-origin is still present on this
|
|
1963
|
+
// branch (the inline srcdoc iframe, where agent/user-generated
|
|
1964
|
+
// design HTML executes) even though allow-scripts +
|
|
1965
|
+
// allow-same-origin together defeat the sandbox in principle.
|
|
1966
|
+
// This is a deliberate, currently-blocked tradeoff, not an
|
|
1967
|
+
// oversight — see the long comment above DesignCanvasProps /
|
|
1968
|
+
// sourceType for the security model, and the PNG/SVG export
|
|
1969
|
+
// handlers in DesignEditor.tsx (handleDownloadPng /
|
|
1970
|
+
// handleDownloadSvg). Those handlers read this same iframe's
|
|
1971
|
+
// `contentDocument` directly and hand the LIVE document element
|
|
1972
|
+
// to html2canvas, and read live CSSOM (getComputedStyle,
|
|
1973
|
+
// doc.styleSheets) for SVG serialization. That cannot be
|
|
1974
|
+
// expressed over the postMessage bridge without moving
|
|
1975
|
+
// html2canvas execution and CSSOM-dependent serialization logic
|
|
1976
|
+
// INTO the sandboxed iframe itself — a separate, larger rewrite,
|
|
1977
|
+
// not a bridge-handler addition. All other former
|
|
1978
|
+
// `contentDocument`/`contentWindow.document` reads on this
|
|
1979
|
+
// iframe (e.g. text-edit-status) now go through bridge
|
|
1980
|
+
// request/response postMessage handlers in
|
|
1981
|
+
// editor-chrome.bridge.ts and no longer need allow-same-origin.
|
|
1982
|
+
// Do not remove this flag until PNG/SVG export is rebuilt to run
|
|
1983
|
+
// inside the iframe.
|
|
1984
|
+
"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
1830
1985
|
}
|
|
1831
1986
|
data-design-preview-iframe
|
|
1832
1987
|
data-screen-iframe-id={
|