@agent-native/core 0.84.1 → 0.84.2

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.
Files changed (75) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/docs/design/durable-agent-runs.md +34 -35
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/engine/builder-engine.ts +81 -9
  6. package/corpus/core/src/agent/run-manager.ts +8 -7
  7. package/corpus/core/src/cli/design-connect.ts +175 -0
  8. package/corpus/core/src/cli/skills.ts +5 -7
  9. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +4 -1
  11. package/corpus/core/src/client/agent-chat-adapter.ts +45 -15
  12. package/corpus/core/src/client/sse-event-processor.ts +31 -4
  13. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  14. package/corpus/templates/design/actions/apply-motion-edit.ts +50 -18
  15. package/corpus/templates/design/actions/get-motion-timeline.ts +4 -14
  16. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +156 -12
  17. package/corpus/templates/design/app/components/design/MotionDock.tsx +2 -3
  18. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +217 -75
  19. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +39 -39
  20. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +558 -37
  21. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +75 -3
  22. package/corpus/templates/design/app/components/design/types.ts +13 -0
  23. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  24. package/corpus/templates/design/app/i18n/zh-TW.ts +17 -0
  25. package/corpus/templates/design/app/i18n-data.ts +245 -0
  26. package/corpus/templates/design/app/pages/DesignEditor.tsx +762 -153
  27. package/corpus/templates/design/changelog/2026-06-30-copying-or-dragging-screen-elements-onto-the-infinite-canvas.md +6 -0
  28. package/corpus/templates/design/changelog/2026-06-30-design-questions-now-use-tighter-editor-typography-and-contr.md +6 -0
  29. package/corpus/templates/design/changelog/2026-06-30-element-drags-can-be-cancelled-with-escape-before-they-commi.md +6 -0
  30. package/corpus/templates/design/changelog/2026-06-30-pending-visual-style-edits-now-warn-before-you-leave-the-edi.md +6 -0
  31. package/corpus/templates/design/changelog/2026-06-30-visual-style-drags-stay-live-while-pending-edits-can-be-appl.md +6 -0
  32. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-a-redundant-context-tab-above-the-composer.md +6 -0
  33. package/corpus/templates/design/changelog/2026-07-01-motion-track-creation-no-longer-fails-in-local-editors.md +6 -0
  34. package/corpus/templates/design/server/plugins/db.ts +10 -0
  35. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  36. package/dist/agent/engine/builder-engine.js +61 -10
  37. package/dist/agent/engine/builder-engine.js.map +1 -1
  38. package/dist/agent/run-manager.d.ts +8 -7
  39. package/dist/agent/run-manager.d.ts.map +1 -1
  40. package/dist/agent/run-manager.js +8 -7
  41. package/dist/agent/run-manager.js.map +1 -1
  42. package/dist/cli/design-connect.d.ts +2 -0
  43. package/dist/cli/design-connect.d.ts.map +1 -1
  44. package/dist/cli/design-connect.js +140 -0
  45. package/dist/cli/design-connect.js.map +1 -1
  46. package/dist/cli/skills.d.ts.map +1 -1
  47. package/dist/cli/skills.js +5 -7
  48. package/dist/cli/skills.js.map +1 -1
  49. package/dist/client/AgentPanel.d.ts +5 -1
  50. package/dist/client/AgentPanel.d.ts.map +1 -1
  51. package/dist/client/AgentPanel.js +4 -4
  52. package/dist/client/AgentPanel.js.map +1 -1
  53. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  54. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  55. package/dist/client/MultiTabAssistantChat.js +2 -2
  56. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  57. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  58. package/dist/client/agent-chat-adapter.js +37 -14
  59. package/dist/client/agent-chat-adapter.js.map +1 -1
  60. package/dist/client/sse-event-processor.d.ts +14 -1
  61. package/dist/client/sse-event-processor.d.ts.map +1 -1
  62. package/dist/client/sse-event-processor.js +21 -5
  63. package/dist/client/sse-event-processor.js.map +1 -1
  64. package/dist/collab/awareness.d.ts +2 -2
  65. package/dist/collab/awareness.d.ts.map +1 -1
  66. package/dist/collab/routes.d.ts +2 -2
  67. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  68. package/dist/notifications/routes.d.ts +3 -3
  69. package/dist/observability/routes.d.ts +2 -2
  70. package/dist/resources/handlers.d.ts +2 -2
  71. package/dist/server/transcribe-voice.d.ts +1 -1
  72. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  73. package/docs/design/durable-agent-runs.md +34 -35
  74. package/package.json +1 -1
  75. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
@@ -131,7 +131,14 @@ import {
131
131
  type SetStateAction,
132
132
  } from "react";
133
133
  import { flushSync } from "react-dom";
134
- import { useParams, useNavigate, Link, useLocation } from "react-router";
134
+ import {
135
+ useParams,
136
+ useNavigate,
137
+ Link,
138
+ useLocation,
139
+ useBlocker,
140
+ useBeforeUnload,
141
+ } from "react-router";
135
142
  import { toast } from "sonner";
136
143
  import * as Y from "yjs";
137
144
 
@@ -186,6 +193,8 @@ import type {
186
193
  ElementInfo,
187
194
  ElementSelectionIntent,
188
195
  DeviceFrameType,
196
+ PortableStyleSnapshot,
197
+ PortableStyleSnapshotNode,
189
198
  } from "@/components/design/types";
190
199
  import {
191
200
  DEVICE_FRAME_VIEWPORTS,
@@ -193,6 +202,16 @@ import {
193
202
  } from "@/components/design/types";
194
203
  import PromptPopover from "@/components/editor/PromptDialog";
195
204
  import type { UploadedFile } from "@/components/editor/PromptDialog";
205
+ import {
206
+ AlertDialog,
207
+ AlertDialogAction,
208
+ AlertDialogCancel,
209
+ AlertDialogContent,
210
+ AlertDialogDescription,
211
+ AlertDialogFooter,
212
+ AlertDialogHeader,
213
+ AlertDialogTitle,
214
+ } from "@/components/ui/alert-dialog";
196
215
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
197
216
  import { Button } from "@/components/ui/button";
198
217
  import { Checkbox } from "@/components/ui/checkbox";
@@ -332,11 +351,29 @@ function dedupeStringIds(ids: string[]): string[] {
332
351
  return Array.from(new Set(ids.filter(Boolean)));
333
352
  }
334
353
 
354
+ function sameStringIds(a: string[], b: string[]) {
355
+ return a.length === b.length && a.every((value, index) => value === b[index]);
356
+ }
357
+
335
358
  export function isScreenRootElementInfo(info: ElementInfo | null | undefined) {
336
359
  const tagName = info?.tagName?.toUpperCase();
337
360
  return tagName === "BODY" || tagName === "HTML";
338
361
  }
339
362
 
363
+ export function shouldIgnoreOverviewLayerCreationEcho(args: {
364
+ pendingLayerId: string | null | undefined;
365
+ pendingScreenId: string | null | undefined;
366
+ screenId: string;
367
+ info?: ElementInfo | null;
368
+ event: "select" | "clear";
369
+ }) {
370
+ if (!args.pendingLayerId) return false;
371
+ if (args.pendingScreenId && args.pendingScreenId !== args.screenId) {
372
+ return false;
373
+ }
374
+ return args.event === "clear" || isScreenRootElementInfo(args.info);
375
+ }
376
+
340
377
  export function getSelectedScreenIdsForEditorState(args: {
341
378
  activeFileId: string | null | undefined;
342
379
  overviewSelectedScreenIds: string[];
@@ -866,6 +903,99 @@ interface LiveScreenSnapshot {
866
903
  contentType?: string;
867
904
  }
868
905
 
906
+ export interface PendingVisualStyleEdit {
907
+ screenId: string;
908
+ filename: string;
909
+ screenName: string;
910
+ selector: string;
911
+ sourceId?: string | null;
912
+ tagName?: string | null;
913
+ classes: string[];
914
+ styles: Record<string, string>;
915
+ updatedAt: number;
916
+ }
917
+
918
+ function pendingVisualStyleEditKey(edit: PendingVisualStyleEdit): string {
919
+ return [
920
+ edit.screenId,
921
+ edit.sourceId?.trim() || edit.selector.trim() || "unknown",
922
+ ].join("::");
923
+ }
924
+
925
+ export function mergePendingVisualStyleEdit(
926
+ edits: readonly PendingVisualStyleEdit[],
927
+ nextEdit: PendingVisualStyleEdit,
928
+ ): PendingVisualStyleEdit[] {
929
+ const nextKey = pendingVisualStyleEditKey(nextEdit);
930
+ let merged = false;
931
+ const next = edits.map((edit) => {
932
+ if (pendingVisualStyleEditKey(edit) !== nextKey) return edit;
933
+ merged = true;
934
+ return {
935
+ ...edit,
936
+ ...nextEdit,
937
+ classes: nextEdit.classes.length > 0 ? nextEdit.classes : edit.classes,
938
+ styles: { ...edit.styles, ...nextEdit.styles },
939
+ };
940
+ });
941
+ return merged ? next : [...edits, nextEdit];
942
+ }
943
+
944
+ export function getPendingVisualStylePropertyCount(
945
+ edits: readonly PendingVisualStyleEdit[],
946
+ ): number {
947
+ return edits.reduce(
948
+ (count, edit) => count + Object.keys(edit.styles).length,
949
+ 0,
950
+ );
951
+ }
952
+
953
+ export function shouldBlockPendingVisualStyleNavigation(args: {
954
+ hasPendingVisualStyleEdits: boolean;
955
+ currentPathname: string;
956
+ nextPathname: string;
957
+ }): boolean {
958
+ return (
959
+ args.hasPendingVisualStyleEdits &&
960
+ args.currentPathname !== args.nextPathname
961
+ );
962
+ }
963
+
964
+ export function formatPendingVisualStylePrompt(args: {
965
+ designId?: string | null;
966
+ designTitle?: string | null;
967
+ activeFileId?: string | null;
968
+ activeFilename?: string | null;
969
+ edits: readonly PendingVisualStyleEdit[];
970
+ }): string {
971
+ const title = args.designTitle?.trim();
972
+ const editPayload = args.edits.map((edit) => ({
973
+ screenId: edit.screenId,
974
+ filename: edit.filename,
975
+ screenName: edit.screenName,
976
+ selector: edit.selector,
977
+ sourceId: edit.sourceId ?? null,
978
+ tagName: edit.tagName ?? null,
979
+ classes: edit.classes,
980
+ styles: edit.styles,
981
+ }));
982
+
983
+ return [
984
+ `Apply these pending visual style edits${title ? ` to "${title}"` : ""}.`,
985
+ args.designId ? `Design id: "${args.designId}".` : "",
986
+ args.activeFileId
987
+ ? `Active screen: "${args.activeFilename ?? args.activeFileId}" (${args.activeFileId}).`
988
+ : "",
989
+ "",
990
+ "Use the Design source tools to make the source match the current live canvas preview. Read each target screen, resolve source ids/selectors through the code-layer projection, then apply the style changes with focused source edits. Preserve layout, behavior, and unrelated styling.",
991
+ "",
992
+ "Pending style edits:",
993
+ JSON.stringify(editPayload, null, 2),
994
+ ]
995
+ .filter((line) => line !== "")
996
+ .join("\n");
997
+ }
998
+
869
999
  interface DesignData {
870
1000
  id: string;
871
1001
  title: string;
@@ -893,6 +1023,7 @@ interface CanvasLayerClipboardEntry {
893
1023
  html: string;
894
1024
  rootNodeId?: string;
895
1025
  sourceFileId: string;
1026
+ portableStyleSnapshot?: PortableStyleSnapshot;
896
1027
  }
897
1028
 
898
1029
  interface SelectedCanvasLayerSnapshot extends CanvasLayerClipboardEntry {
@@ -1451,6 +1582,80 @@ function setAbsolutePositioningForNodeInHtml(
1451
1582
  }
1452
1583
  }
1453
1584
 
1585
+ function getAbsolutePositioningForNodeInHtml(
1586
+ content: string,
1587
+ nodeAttrId: string,
1588
+ ): { x: number; y: number } | null {
1589
+ if (typeof window === "undefined") return null;
1590
+ try {
1591
+ const doc = new DOMParser().parseFromString(content, "text/html");
1592
+ const element = doc.querySelector(
1593
+ `[data-agent-native-node-id="${CSS.escape(nodeAttrId)}"]`,
1594
+ ) as HTMLElement | null;
1595
+ if (!element) return null;
1596
+ return {
1597
+ x: Number.parseFloat(element.style.left || "0") || 0,
1598
+ y: Number.parseFloat(element.style.top || "0") || 0,
1599
+ };
1600
+ } catch {
1601
+ return null;
1602
+ }
1603
+ }
1604
+
1605
+ function elementAtPortableStylePath(
1606
+ root: Element,
1607
+ node: PortableStyleSnapshotNode,
1608
+ ): Element | null {
1609
+ let current: Element | null = root;
1610
+ for (const index of node.path) {
1611
+ if (!current || !Number.isInteger(index) || index < 0) return null;
1612
+ current = current.children.item(index);
1613
+ }
1614
+ return current;
1615
+ }
1616
+
1617
+ function applyPortableStyles(
1618
+ element: Element | null,
1619
+ styles: Record<string, string>,
1620
+ ) {
1621
+ if (!element) return;
1622
+ const host = styleHost(element);
1623
+ if (!host) return;
1624
+ Object.entries(styles).forEach(([property, value]) => {
1625
+ if (!value) return;
1626
+ if (property.startsWith("--") || property.includes("-")) {
1627
+ host.style.setProperty(property, value);
1628
+ return;
1629
+ }
1630
+ (host.style as any)[property] = value;
1631
+ });
1632
+ }
1633
+
1634
+ function applyPortableStyleSnapshotToHtml(
1635
+ content: string,
1636
+ nodeAttrId: string,
1637
+ snapshot?: PortableStyleSnapshot,
1638
+ ): string {
1639
+ if (typeof window === "undefined" || !snapshot?.nodes?.length) {
1640
+ return content;
1641
+ }
1642
+ try {
1643
+ const doc = new DOMParser().parseFromString(content, "text/html");
1644
+ const root = doc.querySelector(
1645
+ `[data-agent-native-node-id="${CSS.escape(nodeAttrId)}"]`,
1646
+ );
1647
+ if (!root) return content;
1648
+ root.setAttribute("data-agent-native-preserve-styles", "true");
1649
+ snapshot.nodes.forEach((node) => {
1650
+ const target = elementAtPortableStylePath(root, node);
1651
+ if (target) applyPortableStyles(target, node.styles);
1652
+ });
1653
+ return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
1654
+ } catch {
1655
+ return content;
1656
+ }
1657
+ }
1658
+
1454
1659
  function isAbsoluteCodeLayerNode(node: CodeLayerNode | null | undefined) {
1455
1660
  const position = String(node?.style.position ?? "").toLowerCase();
1456
1661
  return position === "absolute" || position === "fixed";
@@ -1951,6 +2156,7 @@ function setRootLayerPosition(
1951
2156
  function prepareClonedHtmlLayer(
1952
2157
  doc: Document,
1953
2158
  layerHtml: string,
2159
+ styleSnapshot?: PortableStyleSnapshot,
1954
2160
  ): { element: Element; rootNodeId: string } | null {
1955
2161
  const layerDoc = new DOMParser().parseFromString(
1956
2162
  `<template>${layerHtml}</template>`,
@@ -1961,6 +2167,13 @@ function prepareClonedHtmlLayer(
1961
2167
  layerDoc.body.firstElementChild;
1962
2168
  if (!source) return null;
1963
2169
  const clone = doc.importNode(source, true) as Element;
2170
+ if (styleSnapshot) {
2171
+ clone.setAttribute("data-agent-native-preserve-styles", "true");
2172
+ styleSnapshot.nodes.forEach((node) => {
2173
+ const target = elementAtPortableStylePath(clone, node);
2174
+ if (target) applyPortableStyles(target, node.styles);
2175
+ });
2176
+ }
1964
2177
  const rootNodeId = uniqueLayerId("copy");
1965
2178
  clone.setAttribute("data-agent-native-node-id", rootNodeId);
1966
2179
  Array.from(clone.querySelectorAll("[data-agent-native-node-id]")).forEach(
@@ -1983,6 +2196,7 @@ function insertClonedHtmlLayers(
1983
2196
  placement?: "before" | "after" | "inside";
1984
2197
  stripRootPosition?: boolean;
1985
2198
  positions?: Array<{ x: number; y: number } | null | undefined>;
2199
+ styleSnapshots?: Array<PortableStyleSnapshot | null | undefined>;
1986
2200
  } = {},
1987
2201
  ): { content: string; rootNodeIds: string[] } | null {
1988
2202
  if (typeof window === "undefined" || layerHtmls.length === 0) return null;
@@ -1992,7 +2206,11 @@ function insertClonedHtmlLayers(
1992
2206
  const fragment = doc.createDocumentFragment();
1993
2207
  const rootNodeIds: string[] = [];
1994
2208
  layerHtmls.forEach((layerHtml, index) => {
1995
- const prepared = prepareClonedHtmlLayer(doc, layerHtml);
2209
+ const prepared = prepareClonedHtmlLayer(
2210
+ doc,
2211
+ layerHtml,
2212
+ options.styleSnapshots?.[index] ?? undefined,
2213
+ );
1996
2214
  if (!prepared) return;
1997
2215
  const position = options.positions?.[index];
1998
2216
  if (position) {
@@ -3078,7 +3296,11 @@ function DesignWorkspaceRail({
3078
3296
  "group-hover:bg-transparent group-hover:text-muted-foreground",
3079
3297
  )}
3080
3298
  >
3081
- <IconChevronUp className="size-[15px]" />
3299
+ {motionOpen ? (
3300
+ <IconChevronDown className="size-[15px]" />
3301
+ ) : (
3302
+ <IconChevronUp className="size-[15px]" />
3303
+ )}
3082
3304
  </span>
3083
3305
  <span className="max-w-full truncate leading-none">
3084
3306
  {"Motion" /* i18n-ignore */}
@@ -3970,6 +4192,9 @@ export default function DesignEditor() {
3970
4192
  const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(
3971
4193
  null,
3972
4194
  );
4195
+ const [pendingVisualStyleEdits, setPendingVisualStyleEdits] = useState<
4196
+ PendingVisualStyleEdit[]
4197
+ >([]);
3973
4198
  const [textEditingState, setTextEditingState] = useState<{
3974
4199
  active: boolean;
3975
4200
  selector?: string;
@@ -6738,6 +6963,7 @@ export default function DesignEditor() {
6738
6963
  // Canvas container ref for cursor overlay coordinate mapping.
6739
6964
  const canvasContextMenuRef = useRef<CanvasContextMenuHandle | null>(null);
6740
6965
  const canvasContainerRef = useRef<HTMLDivElement>(null);
6966
+ const activeEditorDragRef = useRef(false);
6741
6967
 
6742
6968
  // Live handle to the active DesignCanvas preview iframe. DesignCanvas owns the
6743
6969
  // <iframe> internally (tagged data-design-preview-iframe) and does not forward
@@ -6755,6 +6981,25 @@ export default function DesignEditor() {
6755
6981
  [],
6756
6982
  );
6757
6983
 
6984
+ const handleEditorDragStateChange = useCallback((active: boolean) => {
6985
+ activeEditorDragRef.current = active;
6986
+ }, []);
6987
+
6988
+ const cancelActiveEditorDrag = useCallback(() => {
6989
+ if (!activeEditorDragRef.current) return false;
6990
+ activeEditorDragRef.current = false;
6991
+ if (typeof document === "undefined") return true;
6992
+ document
6993
+ .querySelectorAll<HTMLIFrameElement>("iframe[data-design-preview-iframe]")
6994
+ .forEach((iframe) => {
6995
+ iframe.contentWindow?.postMessage(
6996
+ { type: "agent-native:cancel-active-drag" },
6997
+ "*",
6998
+ );
6999
+ });
7000
+ return true;
7001
+ }, []);
7002
+
6758
7003
  const handleRunDesignAudit = useCallback(async () => {
6759
7004
  if (!id || !activeFile?.id) return;
6760
7005
  const auditFileId = activeFile.id;
@@ -7002,6 +7247,98 @@ export default function DesignEditor() {
7002
7247
  },
7003
7248
  [liveScreenSnapshotsById],
7004
7249
  );
7250
+ const recordPendingVisualStyleEdit = useCallback(
7251
+ (
7252
+ screenId: string,
7253
+ selector: string,
7254
+ styles: Record<string, string>,
7255
+ elementInfo?: ElementInfo,
7256
+ ) => {
7257
+ if (!canEditDesign) return;
7258
+ const entries = Object.entries(styles).filter(
7259
+ ([, value]) => value !== undefined,
7260
+ );
7261
+ if (entries.length === 0) return;
7262
+ const stylePatch = Object.fromEntries(entries);
7263
+ const screen = files.find((file) => file.id === screenId);
7264
+ const fallbackName = screen?.filename ?? screenId;
7265
+ const sourceId =
7266
+ elementInfo?.sourceId ??
7267
+ (screenId === activeFile?.id ? selectedElement?.sourceId : null);
7268
+ const proofId =
7269
+ typeof crypto !== "undefined" && "randomUUID" in crypto
7270
+ ? crypto.randomUUID()
7271
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
7272
+ const [firstProperty, firstValue] = entries[0];
7273
+
7274
+ setPendingVisualStyleEdits((current) =>
7275
+ mergePendingVisualStyleEdit(current, {
7276
+ screenId,
7277
+ filename: fallbackName,
7278
+ screenName: prettyScreenName(fallbackName),
7279
+ selector,
7280
+ sourceId,
7281
+ tagName: elementInfo?.tagName ?? null,
7282
+ classes: elementInfo?.classes ?? [],
7283
+ styles: stylePatch,
7284
+ updatedAt: Date.now(),
7285
+ }),
7286
+ );
7287
+ setPatchProof({
7288
+ id: proofId,
7289
+ fileId: screenId,
7290
+ filename: fallbackName,
7291
+ selector,
7292
+ sourceId: sourceId ?? undefined,
7293
+ property:
7294
+ entries.length === 1
7295
+ ? firstProperty
7296
+ : entries.map(([property]) => property).join(", "),
7297
+ previousValue:
7298
+ elementInfo?.computedStyles?.[firstProperty] ??
7299
+ (screenId === activeFile?.id
7300
+ ? selectedElement?.computedStyles?.[firstProperty]
7301
+ : undefined),
7302
+ nextValue:
7303
+ entries.length === 1
7304
+ ? firstValue
7305
+ : entries
7306
+ .map(([property, value]) => `${property}: ${value}`)
7307
+ .join("; "),
7308
+ previousContent: getProjectionContentForScreen(screenId),
7309
+ capability: "deterministic-style-edit",
7310
+ confidence: 0.92,
7311
+ status: "runtime",
7312
+ createdAt: Date.now(),
7313
+ });
7314
+
7315
+ if (screenId !== activeFile?.id) return;
7316
+ setSelectedElement((prev) => {
7317
+ const base = elementInfo ?? prev;
7318
+ if (!base) return prev;
7319
+ return {
7320
+ ...base,
7321
+ sourceId: sourceId ?? base.sourceId,
7322
+ selector: selector || base.selector,
7323
+ computedStyles: {
7324
+ ...base.computedStyles,
7325
+ ...stylePatch,
7326
+ },
7327
+ };
7328
+ });
7329
+ if (sourceId) {
7330
+ setSelectedLayerIdsState([sourceId]);
7331
+ }
7332
+ },
7333
+ [
7334
+ activeFile?.id,
7335
+ canEditDesign,
7336
+ files,
7337
+ getProjectionContentForScreen,
7338
+ selectedElement?.computedStyles,
7339
+ selectedElement?.sourceId,
7340
+ ],
7341
+ );
7005
7342
  const activeProjectionContent =
7006
7343
  activeFile?.id !== undefined
7007
7344
  ? getProjectionContentForScreen(activeFile.id)
@@ -7688,6 +8025,7 @@ export default function DesignEditor() {
7688
8025
  fileId?: unknown;
7689
8026
  timelineId?: unknown;
7690
8027
  updatedAt?: unknown;
8028
+ contentPatched?: unknown;
7691
8029
  };
7692
8030
  if (typeof response.timelineId === "string") {
7693
8031
  setMotionTimelineId(response.timelineId);
@@ -7699,7 +8037,8 @@ export default function DesignEditor() {
7699
8037
  }
7700
8038
  if (
7701
8039
  typeof response.fileId === "string" &&
7702
- response.fileId === activeFile.id
8040
+ response.fileId === activeFile.id &&
8041
+ response.contentPatched !== false
7703
8042
  ) {
7704
8043
  applyFileContentUpdate(response.fileId, localPatchedContent, {
7705
8044
  refreshPreview: true,
@@ -7925,7 +8264,8 @@ export default function DesignEditor() {
7925
8264
  // new primitive selected, matching Figma behaviour. We activate the
7926
8265
  // target screen (so the layers panel shows its content) and select the
7927
8266
  // new node, but do NOT switch to single/full view.
7928
- pendingOverviewScreenSelectionRef.current = screenId;
8267
+ pendingOverviewScreenSelectionRef.current =
8268
+ options?.selectFrame === false ? null : screenId;
7929
8269
  pendingOverviewLayerSelectionRef.current = nodeId;
7930
8270
  clearPendingOverviewLayerSelectionTimer();
7931
8271
  flushSync(() => {
@@ -7980,13 +8320,14 @@ export default function DesignEditor() {
7980
8320
  const handleOverviewScreenSelectionChange = useCallback(
7981
8321
  (ids: string[]) => {
7982
8322
  const pendingId = pendingOverviewScreenSelectionRef.current;
7983
- const fileIds = new Set(files.map((file) => file.id));
8323
+ const fileIds = new Set(
8324
+ files.filter((file) => file.id !== boardFileId).map((file) => file.id),
8325
+ );
8326
+ const nextIds = ids.filter((layerId) => fileIds.has(layerId));
7984
8327
  if (pendingId && ids.length === 0) return;
7985
8328
  if (pendingId && ids.includes(pendingId)) {
7986
- setOverviewSelectedScreenIds(
7987
- ids.filter(
7988
- (layerId) => fileIds.has(layerId) || layerId === pendingId,
7989
- ),
8329
+ setOverviewSelectedScreenIds((current) =>
8330
+ sameStringIds(current, nextIds) ? current : nextIds,
7990
8331
  );
7991
8332
  if (fileIds.has(pendingId)) {
7992
8333
  pendingOverviewScreenSelectionRef.current = null;
@@ -7999,11 +8340,11 @@ export default function DesignEditor() {
7999
8340
  clearPendingOverviewLayerSelectionTimer();
8000
8341
  setCreatedOverviewLayerSelection(null);
8001
8342
  }
8002
- setOverviewSelectedScreenIds(
8003
- ids.filter((layerId) => fileIds.has(layerId)),
8343
+ setOverviewSelectedScreenIds((current) =>
8344
+ sameStringIds(current, nextIds) ? current : nextIds,
8004
8345
  );
8005
8346
  },
8006
- [clearPendingOverviewLayerSelectionTimer, files],
8347
+ [boardFileId, clearPendingOverviewLayerSelectionTimer, files],
8007
8348
  );
8008
8349
 
8009
8350
  const shouldPreserveBlockedOverviewLayerSelectionRef = useRef<
@@ -8469,14 +8810,14 @@ export default function DesignEditor() {
8469
8810
  (screenId: string, info: ElementInfo, intent?: ElementSelectionIntent) => {
8470
8811
  const pendingLayerId = pendingOverviewLayerSelectionRef.current;
8471
8812
  const pendingScreenId = pendingOverviewScreenSelectionRef.current;
8472
- const createdSelection = createdOverviewLayerSelection;
8473
- if (createdSelection?.screenId === screenId) {
8474
- return;
8475
- }
8476
8813
  if (
8477
- (pendingLayerId || createdSelection?.layerId) &&
8478
- (!pendingScreenId || pendingScreenId === screenId) &&
8479
- isScreenRootElementInfo(info)
8814
+ shouldIgnoreOverviewLayerCreationEcho({
8815
+ pendingLayerId,
8816
+ pendingScreenId,
8817
+ screenId,
8818
+ info,
8819
+ event: "select",
8820
+ })
8480
8821
  ) {
8481
8822
  return;
8482
8823
  }
@@ -8535,7 +8876,6 @@ export default function DesignEditor() {
8535
8876
  },
8536
8877
  [
8537
8878
  clearPendingOverviewLayerSelectionTimer,
8538
- createdOverviewLayerSelection,
8539
8879
  focusDesignInspectorForSelection,
8540
8880
  getCodeLayerProjectionForScreen,
8541
8881
  selectedLayerIdsState,
@@ -8546,13 +8886,13 @@ export default function DesignEditor() {
8546
8886
  (screenId: string) => {
8547
8887
  const pendingLayerId = pendingOverviewLayerSelectionRef.current;
8548
8888
  const pendingScreenId = pendingOverviewScreenSelectionRef.current;
8549
- const createdSelection = createdOverviewLayerSelection;
8550
- if (createdSelection?.screenId === screenId) {
8551
- return;
8552
- }
8553
8889
  if (
8554
- (pendingLayerId || createdSelection?.layerId) &&
8555
- (!pendingScreenId || pendingScreenId === screenId)
8890
+ shouldIgnoreOverviewLayerCreationEcho({
8891
+ pendingLayerId,
8892
+ pendingScreenId,
8893
+ screenId,
8894
+ event: "clear",
8895
+ })
8556
8896
  ) {
8557
8897
  return;
8558
8898
  }
@@ -8574,7 +8914,7 @@ export default function DesignEditor() {
8574
8914
  setActiveTool("move");
8575
8915
  setMode("edit");
8576
8916
  },
8577
- [clearPendingOverviewLayerSelectionTimer, createdOverviewLayerSelection],
8917
+ [clearPendingOverviewLayerSelectionTimer],
8578
8918
  );
8579
8919
 
8580
8920
  const handleElementSelect = useCallback(
@@ -9252,12 +9592,15 @@ export default function DesignEditor() {
9252
9592
  styles: Record<string, string>,
9253
9593
  elementInfo?: ElementInfo,
9254
9594
  ) => {
9255
- commitVisualStyles(selector, styles, {
9256
- runtimeApplied: true,
9595
+ if (!activeFile?.id) return;
9596
+ recordPendingVisualStyleEdit(
9597
+ activeFile.id,
9598
+ selector,
9599
+ styles,
9257
9600
  elementInfo,
9258
- });
9601
+ );
9259
9602
  },
9260
- [commitVisualStyles],
9603
+ [activeFile?.id, recordPendingVisualStyleEdit],
9261
9604
  );
9262
9605
 
9263
9606
  const handleVisualStructureChange = useCallback(
@@ -9270,6 +9613,9 @@ export default function DesignEditor() {
9270
9613
  sourceId?: string;
9271
9614
  anchorSourceId?: string;
9272
9615
  requestId?: string;
9616
+ dropMode?: "flow-insert" | "absolute-container";
9617
+ sourceRect?: { x: number; y: number; width: number; height: number };
9618
+ anchorRect?: { x: number; y: number; width: number; height: number };
9273
9619
  },
9274
9620
  ) => {
9275
9621
  if (!canEditDesign) return false;
@@ -9319,12 +9665,29 @@ export default function DesignEditor() {
9319
9665
  (node) => node.id === patch.result.after?.nodeId,
9320
9666
  )?.dataAttributes["data-agent-native-node-id"]
9321
9667
  : undefined);
9668
+ const absoluteContainerOffset =
9669
+ details?.dropMode === "absolute-container" &&
9670
+ details.sourceRect &&
9671
+ details.anchorRect
9672
+ ? {
9673
+ x: details.sourceRect.x - details.anchorRect.x,
9674
+ y: details.sourceRect.y - details.anchorRect.y,
9675
+ }
9676
+ : null;
9322
9677
  const nextContent =
9323
9678
  isAbsoluteCodeLayerNode(targetNode) && movedNodeAttrId
9324
- ? removeAbsolutePositioningFromNodeInHtml(
9325
- patch.content,
9326
- movedNodeAttrId,
9327
- )
9679
+ ? details?.dropMode === "absolute-container"
9680
+ ? absoluteContainerOffset
9681
+ ? setAbsolutePositioningForNodeInHtml(
9682
+ patch.content,
9683
+ movedNodeAttrId,
9684
+ absoluteContainerOffset,
9685
+ )
9686
+ : patch.content
9687
+ : removeAbsolutePositioningFromNodeInHtml(
9688
+ patch.content,
9689
+ movedNodeAttrId,
9690
+ )
9328
9691
  : patch.content;
9329
9692
  const nextProjection = buildCodeLayerProjection(nextContent);
9330
9693
  const movedNode =
@@ -9554,76 +9917,9 @@ export default function DesignEditor() {
9554
9917
  styles: Record<string, string>,
9555
9918
  elementInfo?: ElementInfo,
9556
9919
  ) => {
9557
- if (screenId === activeFile?.id) {
9558
- handleVisualStyleChange(selector, styles, elementInfo);
9559
- return;
9560
- }
9561
- if (!canEditDesign) return;
9562
- const entries = Object.entries(styles).filter(
9563
- ([, value]) => value !== undefined,
9564
- );
9565
- if (entries.length === 0) return;
9566
- const liveSnapshot = liveScreenSnapshotsById[screenId];
9567
- const baseContent = liveSnapshot?.html ?? getScreenContent(screenId);
9568
- const projection = buildCodeLayerProjection(baseContent);
9569
- const targetInfo = elementInfo ? { ...elementInfo, selector } : null;
9570
- const targetNode = targetInfo
9571
- ? resolveCodeLayerNodeFromElementInfo(projection, targetInfo)
9572
- : resolveCodeLayerNodeFromBridge(projection, selector);
9573
- const fallbackContent = applyInlineStylesToHtml(baseContent, selector, {
9574
- ...Object.fromEntries(entries),
9575
- });
9576
- const stylePatch = entries.reduce<{
9577
- content: string;
9578
- failed: string | null;
9579
- }>(
9580
- (current, [property, value]) => {
9581
- if (current.failed) return current;
9582
- const patch = applyVisualEdit(current.content, {
9583
- kind: "style",
9584
- target: targetNode ? { nodeId: targetNode.id } : { selector },
9585
- property,
9586
- value,
9587
- });
9588
- if (patch.result.status !== "applied") {
9589
- return {
9590
- content: current.content,
9591
- failed: codeLayerPatchMessage(
9592
- patch.result.message,
9593
- t("designEditor.patchProof.selectorMissing"),
9594
- ),
9595
- };
9596
- }
9597
- return { content: patch.content, failed: null };
9598
- },
9599
- { content: baseContent, failed: null },
9600
- );
9601
- const nextContent = stylePatch.failed
9602
- ? fallbackContent
9603
- : stylePatch.content;
9604
- if (!nextContent) {
9605
- toast.error(
9606
- stylePatch.failed ?? t("designEditor.patchProof.selectorMissing"),
9607
- { duration: 4000 },
9608
- );
9609
- return;
9610
- }
9611
- if (liveSnapshot) {
9612
- updateLiveScreenSnapshotContent(screenId, nextContent);
9613
- return;
9614
- }
9615
- applyFileContentUpdate(screenId, nextContent, { skipPreview: true });
9920
+ recordPendingVisualStyleEdit(screenId, selector, styles, elementInfo);
9616
9921
  },
9617
- [
9618
- activeFile?.id,
9619
- applyFileContentUpdate,
9620
- canEditDesign,
9621
- getScreenContent,
9622
- handleVisualStyleChange,
9623
- liveScreenSnapshotsById,
9624
- t,
9625
- updateLiveScreenSnapshotContent,
9626
- ],
9922
+ [recordPendingVisualStyleEdit],
9627
9923
  );
9628
9924
 
9629
9925
  const handleScreenVisualStructureChange = useCallback(
@@ -9637,6 +9933,9 @@ export default function DesignEditor() {
9637
9933
  sourceId?: string;
9638
9934
  anchorSourceId?: string;
9639
9935
  requestId?: string;
9936
+ dropMode?: "flow-insert" | "absolute-container";
9937
+ sourceRect?: { x: number; y: number; width: number; height: number };
9938
+ anchorRect?: { x: number; y: number; width: number; height: number };
9640
9939
  },
9641
9940
  ) => {
9642
9941
  if (screenId === activeFile?.id) {
@@ -9687,15 +9986,51 @@ export default function DesignEditor() {
9687
9986
  );
9688
9987
  return false;
9689
9988
  }
9690
- applyFileContentUpdate(screenId, patch.content, { skipPreview: true });
9691
- const movedNode =
9989
+ const movedNodeAttrId =
9990
+ targetNode?.dataAttributes["data-agent-native-node-id"] ??
9991
+ details?.sourceId ??
9992
+ elementInfo?.sourceId ??
9692
9993
  (patch.result.after?.nodeId
9693
9994
  ? patch.projection.nodes.find(
9694
9995
  (node) => node.id === patch.result.after?.nodeId,
9996
+ )?.dataAttributes["data-agent-native-node-id"]
9997
+ : undefined);
9998
+ const absoluteContainerOffset =
9999
+ details?.dropMode === "absolute-container" &&
10000
+ details.sourceRect &&
10001
+ details.anchorRect
10002
+ ? {
10003
+ x: details.sourceRect.x - details.anchorRect.x,
10004
+ y: details.sourceRect.y - details.anchorRect.y,
10005
+ }
10006
+ : null;
10007
+ const nextContent =
10008
+ isAbsoluteCodeLayerNode(targetNode) && movedNodeAttrId
10009
+ ? details?.dropMode === "absolute-container"
10010
+ ? absoluteContainerOffset
10011
+ ? setAbsolutePositioningForNodeInHtml(
10012
+ patch.content,
10013
+ movedNodeAttrId,
10014
+ absoluteContainerOffset,
10015
+ )
10016
+ : patch.content
10017
+ : removeAbsolutePositioningFromNodeInHtml(
10018
+ patch.content,
10019
+ movedNodeAttrId,
10020
+ )
10021
+ : patch.content;
10022
+ const nextProjection = buildCodeLayerProjection(nextContent);
10023
+ applyFileContentUpdate(screenId, nextContent, { skipPreview: true });
10024
+ const movedNode =
10025
+ (movedNodeAttrId
10026
+ ? nextProjection.nodes.find(
10027
+ (node) =>
10028
+ node.dataAttributes["data-agent-native-node-id"] ===
10029
+ movedNodeAttrId,
9695
10030
  )
9696
10031
  : null) ??
9697
10032
  resolveCodeLayerNodeFromBridge(
9698
- patch.projection,
10033
+ nextProjection,
9699
10034
  selector,
9700
10035
  details?.sourceId ??
9701
10036
  elementInfo?.sourceId ??
@@ -9925,11 +10260,18 @@ export default function DesignEditor() {
9925
10260
  candidate.dataAttributes["data-agent-native-node-id"] === layerId,
9926
10261
  );
9927
10262
  if (!node?.source) continue;
10263
+ const portableStyleSnapshot =
10264
+ selectedElementLayerId &&
10265
+ node.id === selectedElementLayerId &&
10266
+ selectedElement?.portableStyleSnapshot
10267
+ ? selectedElement.portableStyleSnapshot
10268
+ : undefined;
9928
10269
  snapshots.push({
9929
10270
  html: content.slice(node.source.start, node.source.end),
9930
10271
  rootNodeId:
9931
10272
  node.dataAttributes["data-agent-native-node-id"] ?? node.id,
9932
10273
  sourceFileId: file.id,
10274
+ portableStyleSnapshot,
9933
10275
  node,
9934
10276
  sourceIndex: node.source.start,
9935
10277
  tree,
@@ -9956,6 +10298,7 @@ export default function DesignEditor() {
9956
10298
  selectedElement.sourceId ??
9957
10299
  selectedElement.id,
9958
10300
  sourceFileId: activeFile.id,
10301
+ portableStyleSnapshot: selectedElement.portableStyleSnapshot,
9959
10302
  node,
9960
10303
  sourceIndex: node.source?.start ?? Number.MAX_SAFE_INTEGER,
9961
10304
  tree,
@@ -10020,19 +10363,31 @@ export default function DesignEditor() {
10020
10363
  )
10021
10364
  .filter((node): node is CodeLayerNode => Boolean(node));
10022
10365
  if (insertedNodes.length === 0) return;
10366
+ const lastNode = insertedNodes[insertedNodes.length - 1];
10367
+ if (lastNode) {
10368
+ pendingOverviewScreenSelectionRef.current =
10369
+ screenId === boardFileId ? null : screenId;
10370
+ pendingOverviewLayerSelectionRef.current = lastNode.id;
10371
+ clearPendingOverviewLayerSelectionTimer();
10372
+ setCreatedOverviewLayerSelection({
10373
+ screenId,
10374
+ layerId: lastNode.id,
10375
+ });
10376
+ }
10023
10377
  setActiveFileId(screenId);
10024
10378
  setSelectedLayerIdsState(insertedNodes.map((node) => node.id));
10025
- const lastNode = insertedNodes[insertedNodes.length - 1];
10026
10379
  setSelectedElement(
10027
10380
  lastNode ? elementInfoFromCodeLayerNode(lastNode) : null,
10028
10381
  );
10029
10382
  setActiveTool("move");
10030
10383
  setMode("edit");
10031
10384
  if (viewModeRef.current === "overview") {
10032
- setOverviewSelectedScreenIds([screenId]);
10385
+ setOverviewSelectedScreenIds(
10386
+ screenId === boardFileId ? [] : [screenId],
10387
+ );
10033
10388
  }
10034
10389
  },
10035
- [],
10390
+ [boardFileId, clearPendingOverviewLayerSelectionTimer],
10036
10391
  );
10037
10392
 
10038
10393
  const handleCopySelection = useCallback(async () => {
@@ -10040,6 +10395,7 @@ export default function DesignEditor() {
10040
10395
  html: snapshot.html,
10041
10396
  rootNodeId: snapshot.rootNodeId,
10042
10397
  sourceFileId: snapshot.sourceFileId,
10398
+ portableStyleSnapshot: snapshot.portableStyleSnapshot,
10043
10399
  }));
10044
10400
  if (entries.length === 0) return;
10045
10401
  const clipboardText = entries.map((entry) => entry.html).join("\n");
@@ -10057,9 +10413,32 @@ export default function DesignEditor() {
10057
10413
  const handlePasteSelection = useCallback(
10058
10414
  (position?: { x: number; y: number }) => {
10059
10415
  const entries = getCanvasClipboardEntries();
10060
- if (!activeFile || !canEditDesign || entries.length === 0) return;
10061
- const baseContent = getFreshActiveContent();
10416
+ const targetFileId =
10417
+ viewModeRef.current === "overview" && position && boardFileId
10418
+ ? boardFileId
10419
+ : activeFile?.id;
10420
+ if (!targetFileId || !canEditDesign || entries.length === 0) return;
10421
+ const targetIsBoard = targetFileId === boardFileId;
10422
+ const baseContent =
10423
+ targetFileId === activeFile?.id
10424
+ ? getFreshActiveContent()
10425
+ : (getScreenContent(targetFileId) ?? "");
10426
+ if (!baseContent && targetFileId !== boardFileId) return;
10062
10427
  const layerHtmls = entries.map((entry) => entry.html);
10428
+ const styleSnapshots = targetIsBoard
10429
+ ? entries.map((entry) => entry.portableStyleSnapshot)
10430
+ : undefined;
10431
+ const applyPasteContentUpdate = (nextContent: string) => {
10432
+ if (targetFileId === activeFile?.id) {
10433
+ applyLocalContentUpdate(nextContent, {
10434
+ forcePreviewFullDocument: true,
10435
+ });
10436
+ return;
10437
+ }
10438
+ applyFileContentUpdate(targetFileId, nextContent, {
10439
+ forcePreviewFullDocument: true,
10440
+ });
10441
+ };
10063
10442
 
10064
10443
  // B7 fix: when an element is selected and no explicit canvas position was
10065
10444
  // given, insert the clone as an in-flow sibling right AFTER the selected
@@ -10067,20 +10446,19 @@ export default function DesignEditor() {
10067
10446
  // in normal document flow instead of being an absolutely-positioned body
10068
10447
  // child. Fall back to the old position-based clone when nothing is
10069
10448
  // selected or a "Paste here" position is provided.
10070
- if (!position && selectedElement?.selector) {
10449
+ if (!position && !targetIsBoard && selectedElement?.selector) {
10071
10450
  const selector = selectedCanvasSelector ?? selectedElement.selector;
10072
10451
  const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
10073
10452
  targetSelectors: [selector],
10074
10453
  placement: "after",
10075
10454
  stripRootPosition: true,
10455
+ styleSnapshots,
10076
10456
  });
10077
10457
  if (result) {
10078
10458
  pasteCascadeRef.current += 1;
10079
- applyLocalContentUpdate(result.content, {
10080
- forcePreviewFullDocument: true,
10081
- });
10459
+ applyPasteContentUpdate(result.content);
10082
10460
  selectInsertedLayers(
10083
- activeFile.id,
10461
+ targetFileId,
10084
10462
  result.content,
10085
10463
  result.rootNodeIds,
10086
10464
  );
@@ -10127,20 +10505,22 @@ export default function DesignEditor() {
10127
10505
  });
10128
10506
  const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
10129
10507
  positions,
10508
+ styleSnapshots,
10130
10509
  });
10131
10510
  if (!result) return;
10132
10511
  if (!position) pasteCascadeRef.current += 1;
10133
- applyLocalContentUpdate(result.content, {
10134
- forcePreviewFullDocument: true,
10135
- });
10136
- selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
10512
+ applyPasteContentUpdate(result.content);
10513
+ selectInsertedLayers(targetFileId, result.content, result.rootNodeIds);
10137
10514
  },
10138
10515
  [
10139
10516
  activeFile,
10517
+ applyFileContentUpdate,
10140
10518
  applyLocalContentUpdate,
10519
+ boardFileId,
10141
10520
  canEditDesign,
10142
10521
  getCanvasClipboardEntries,
10143
10522
  getFreshActiveContent,
10523
+ getScreenContent,
10144
10524
  selectInsertedLayers,
10145
10525
  selectedCanvasSelector,
10146
10526
  selectedElement,
@@ -10153,6 +10533,7 @@ export default function DesignEditor() {
10153
10533
  const baseContent = getFreshActiveContent();
10154
10534
  if (selectedElement?.boundingRect) {
10155
10535
  const { x, y } = selectedElement.boundingRect;
10536
+ const targetIsBoard = activeFile.id === boardFileId;
10156
10537
  const result = insertClonedHtmlLayers(
10157
10538
  baseContent,
10158
10539
  entries.map((entry) => entry.html),
@@ -10161,6 +10542,9 @@ export default function DesignEditor() {
10161
10542
  x: x + index * 16,
10162
10543
  y: y + index * 16,
10163
10544
  })),
10545
+ styleSnapshots: targetIsBoard
10546
+ ? entries.map((entry) => entry.portableStyleSnapshot)
10547
+ : undefined,
10164
10548
  },
10165
10549
  );
10166
10550
  if (!result) return;
@@ -10174,6 +10558,7 @@ export default function DesignEditor() {
10174
10558
  }, [
10175
10559
  activeFile,
10176
10560
  applyLocalContentUpdate,
10561
+ boardFileId,
10177
10562
  getCanvasClipboardEntries,
10178
10563
  getFreshActiveContent,
10179
10564
  handlePasteSelection,
@@ -10502,9 +10887,9 @@ export default function DesignEditor() {
10502
10887
  * Handle a primitive being drag-dropped onto another primitive in the
10503
10888
  * MultiScreenCanvas overview (CONTRACT: onPrimitiveReparent prop).
10504
10889
  *
10505
- * Same-screen: applies a moveNode intent then strips the moved node's
10506
- * absolute positioning so it becomes a flow child of the container.
10507
- * Cross-screen: uses moveNodeBetweenDocuments and persists both files.
10890
+ * Same-screen: applies a moveNode intent then rebases the moved node's
10891
+ * absolute coordinates relative to the target rectangle. Cross-screen uses
10892
+ * moveNodeBetweenDocuments and persists both files.
10508
10893
  */
10509
10894
  const handleOverviewPrimitiveReparent = useCallback(
10510
10895
  ({
@@ -10544,27 +10929,38 @@ export default function DesignEditor() {
10544
10929
  return;
10545
10930
  }
10546
10931
 
10547
- // 2. Strip absolute positioning from the moved node so it flows naturally.
10548
- // Use removeAbsolutePositioningFromNodeInHtml (DOM-based) because the
10549
- // applyVisualEdit substrate rejects empty-string values in isSafeStyleValue,
10550
- // making applyVisualEdit({kind:"style",value:""}) a silent no-op.
10551
10932
  const movedNodeAttrId =
10552
10933
  movePatch.projection.nodes.find(
10553
10934
  (n) =>
10554
10935
  n.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
10555
10936
  n.id === sourceNodeId,
10556
10937
  )?.dataAttributes["data-agent-native-node-id"] ?? sourceNodeId;
10557
- const strippedContent = removeAbsolutePositioningFromNodeInHtml(
10558
- movePatch.content,
10559
- movedNodeAttrId,
10938
+ const sourcePosition = getAbsolutePositioningForNodeInHtml(
10939
+ baseContent,
10940
+ sourceNodeId,
10941
+ );
10942
+ const targetPosition = getAbsolutePositioningForNodeInHtml(
10943
+ baseContent,
10944
+ targetNodeId,
10560
10945
  );
10946
+ const nextContent =
10947
+ sourcePosition && targetPosition
10948
+ ? setAbsolutePositioningForNodeInHtml(
10949
+ movePatch.content,
10950
+ movedNodeAttrId,
10951
+ {
10952
+ x: sourcePosition.x - targetPosition.x,
10953
+ y: sourcePosition.y - targetPosition.y,
10954
+ },
10955
+ )
10956
+ : movePatch.content;
10561
10957
 
10562
- applyFileContentUpdate(sourceScreenId, strippedContent, {
10958
+ applyFileContentUpdate(sourceScreenId, nextContent, {
10563
10959
  skipPreview: true,
10564
10960
  });
10565
10961
 
10566
10962
  // Re-select the moved node.
10567
- const nextProjection = buildCodeLayerProjection(strippedContent);
10963
+ const nextProjection = buildCodeLayerProjection(nextContent);
10568
10964
  const movedNodeAfter = nextProjection.nodes.find(
10569
10965
  (n) =>
10570
10966
  n.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
@@ -10616,18 +11012,26 @@ export default function DesignEditor() {
10616
11012
  return;
10617
11013
  }
10618
11014
 
10619
- // Strip absolute positioning from the moved node in the destination.
10620
- // Use removeAbsolutePositioningFromNodeInHtml (DOM-based) because
10621
- // applyVisualEdit({kind:"style",value:""}) is a silent no-op (the
10622
- // substrate rejects empty-string values in isSafeStyleValue). Use
10623
- // result.movedNodeId (the final id in destHtml, which may differ from
10624
- // nodeAttrId when a collision triggered an id re-stamp) so the strip and
10625
- // re-selection always find the correct element.
10626
11015
  const destNodeAttrId = result.movedNodeId ?? nodeAttrId;
10627
- const strippedDest = removeAbsolutePositioningFromNodeInHtml(
10628
- result.destHtml,
10629
- destNodeAttrId,
11016
+ const sourcePosition = getAbsolutePositioningForNodeInHtml(
11017
+ sourceContent,
11018
+ nodeAttrId,
10630
11019
  );
11020
+ const targetPosition = getAbsolutePositioningForNodeInHtml(
11021
+ destContent,
11022
+ anchorAttrId,
11023
+ );
11024
+ const nextDestContent =
11025
+ sourcePosition && targetPosition
11026
+ ? setAbsolutePositioningForNodeInHtml(
11027
+ result.destHtml,
11028
+ destNodeAttrId,
11029
+ {
11030
+ x: sourcePosition.x - targetPosition.x,
11031
+ y: sourcePosition.y - targetPosition.y,
11032
+ },
11033
+ )
11034
+ : result.destHtml;
10631
11035
 
10632
11036
  recordContentHistoryEntry({
10633
11037
  changes: [
@@ -10639,7 +11043,7 @@ export default function DesignEditor() {
10639
11043
  {
10640
11044
  fileId: targetScreenId,
10641
11045
  before: destContent,
10642
- after: strippedDest,
11046
+ after: nextDestContent,
10643
11047
  },
10644
11048
  ],
10645
11049
  });
@@ -10648,13 +11052,13 @@ export default function DesignEditor() {
10648
11052
  recordHistory: false,
10649
11053
  refreshPreview: true,
10650
11054
  });
10651
- applyFileContentUpdate(targetScreenId, strippedDest, {
11055
+ applyFileContentUpdate(targetScreenId, nextDestContent, {
10652
11056
  recordHistory: false,
10653
11057
  refreshPreview: true,
10654
11058
  });
10655
11059
 
10656
11060
  // Re-select the moved node in the destination.
10657
- const finalProjection = buildCodeLayerProjection(strippedDest);
11061
+ const finalProjection = buildCodeLayerProjection(nextDestContent);
10658
11062
  const movedNodeFinal = finalProjection.nodes.find(
10659
11063
  (n) => n.dataAttributes["data-agent-native-node-id"] === destNodeAttrId,
10660
11064
  );
@@ -10695,8 +11099,11 @@ export default function DesignEditor() {
10695
11099
  targetScreenId,
10696
11100
  targetAnchorNodeId,
10697
11101
  targetAnchorPlacement,
11102
+ targetDropMode,
11103
+ targetAnchorRect,
10698
11104
  targetLocalPoint,
10699
11105
  sourcePointerOffset,
11106
+ styleSnapshot,
10700
11107
  }: {
10701
11108
  sourceSelector: string;
10702
11109
  sourceNodeId?: string;
@@ -10704,9 +11111,17 @@ export default function DesignEditor() {
10704
11111
  targetScreenId: string;
10705
11112
  targetAnchorNodeId?: string;
10706
11113
  targetAnchorPlacement?: "before" | "after" | "inside";
11114
+ targetDropMode?: "flow-insert" | "absolute-container";
11115
+ targetAnchorRect?: {
11116
+ left: number;
11117
+ top: number;
11118
+ width: number;
11119
+ height: number;
11120
+ };
10707
11121
  targetCanvasPoint?: { x: number; y: number };
10708
11122
  targetLocalPoint?: { x: number; y: number };
10709
11123
  sourcePointerOffset?: { x: number; y: number };
11124
+ styleSnapshot?: PortableStyleSnapshot;
10710
11125
  }) => {
10711
11126
  if (!canEditDesign) return;
10712
11127
  if (sourceScreenId === targetScreenId) return;
@@ -10772,19 +11187,39 @@ export default function DesignEditor() {
10772
11187
  // there is no anchor, preserve absolute mode and rebase left/top to the
10773
11188
  // release point so screen↔board moves behave like Figma absolute layers.
10774
11189
  const destNodeAttrId = result.movedNodeId ?? nodeAttrId;
11190
+ const stylePreservedDest =
11191
+ targetScreenId === boardFileId
11192
+ ? applyPortableStyleSnapshotToHtml(
11193
+ result.destHtml,
11194
+ destNodeAttrId,
11195
+ styleSnapshot,
11196
+ )
11197
+ : result.destHtml;
10775
11198
  const nextDestContent = targetAnchorAttrId
10776
- ? removeAbsolutePositioningFromNodeInHtml(
10777
- result.destHtml,
10778
- destNodeAttrId,
10779
- )
11199
+ ? targetDropMode === "absolute-container"
11200
+ ? targetLocalPoint && targetAnchorRect
11201
+ ? setAbsolutePositioningForNodeInHtml(
11202
+ stylePreservedDest,
11203
+ destNodeAttrId,
11204
+ {
11205
+ x: targetLocalPoint.x - targetAnchorRect.left,
11206
+ y: targetLocalPoint.y - targetAnchorRect.top,
11207
+ },
11208
+ sourcePointerOffset,
11209
+ )
11210
+ : stylePreservedDest
11211
+ : removeAbsolutePositioningFromNodeInHtml(
11212
+ stylePreservedDest,
11213
+ destNodeAttrId,
11214
+ )
10780
11215
  : targetLocalPoint
10781
11216
  ? setAbsolutePositioningForNodeInHtml(
10782
- result.destHtml,
11217
+ stylePreservedDest,
10783
11218
  destNodeAttrId,
10784
11219
  targetLocalPoint,
10785
11220
  sourcePointerOffset,
10786
11221
  )
10787
- : result.destHtml;
11222
+ : stylePreservedDest;
10788
11223
 
10789
11224
  recordContentHistoryEntry({
10790
11225
  changes: [
@@ -10812,19 +11247,34 @@ export default function DesignEditor() {
10812
11247
 
10813
11248
  // Switch active screen to the target and select the moved node; viewMode
10814
11249
  // stays "overview" (no setViewMode call).
11250
+ pendingOverviewScreenSelectionRef.current =
11251
+ targetScreenId === boardFileId ? null : targetScreenId;
11252
+ pendingOverviewLayerSelectionRef.current = destNodeAttrId;
11253
+ clearPendingOverviewLayerSelectionTimer();
10815
11254
  setActiveFileId(targetScreenId);
10816
11255
  const finalProjection = buildCodeLayerProjection(nextDestContent);
10817
11256
  const movedNodeFinal = finalProjection.nodes.find(
10818
11257
  (n) => n.dataAttributes["data-agent-native-node-id"] === destNodeAttrId,
10819
11258
  );
10820
11259
  if (movedNodeFinal) {
11260
+ setCreatedOverviewLayerSelection({
11261
+ screenId: targetScreenId,
11262
+ layerId: movedNodeFinal.id,
11263
+ });
10821
11264
  setSelectedLayerIdsState([movedNodeFinal.id]);
10822
11265
  setSelectedElement(elementInfoFromCodeLayerNode(movedNodeFinal));
11266
+ if (viewModeRef.current === "overview") {
11267
+ setOverviewSelectedScreenIds(
11268
+ targetScreenId === boardFileId ? [] : [targetScreenId],
11269
+ );
11270
+ }
10823
11271
  }
10824
11272
  },
10825
11273
  [
10826
11274
  applyFileContentUpdate,
11275
+ boardFileId,
10827
11276
  canEditDesign,
11277
+ clearPendingOverviewLayerSelectionTimer,
10828
11278
  getScreenContent,
10829
11279
  recordContentHistoryEntry,
10830
11280
  t,
@@ -11707,6 +12157,7 @@ export default function DesignEditor() {
11707
12157
  ]);
11708
12158
 
11709
12159
  const handleEscapeHotkey = useCallback(() => {
12160
+ if (cancelActiveEditorDrag()) return;
11710
12161
  if (
11711
12162
  shouldEscapeToOverview({
11712
12163
  activeTool,
@@ -11730,6 +12181,7 @@ export default function DesignEditor() {
11730
12181
  setMode("edit");
11731
12182
  }, [
11732
12183
  activeTool,
12184
+ cancelActiveEditorDrag,
11733
12185
  drawMode,
11734
12186
  enterOverviewFromZoom,
11735
12187
  mode,
@@ -12106,6 +12558,84 @@ export default function DesignEditor() {
12106
12558
  }
12107
12559
  }, [ensureCodingHandoff, getCodingHandoffClipboardText, t]);
12108
12560
 
12561
+ const hasPendingVisualStyleEdits = pendingVisualStyleEdits.length > 0;
12562
+ useBeforeUnload(
12563
+ useCallback(
12564
+ (event: BeforeUnloadEvent) => {
12565
+ if (!hasPendingVisualStyleEdits) return;
12566
+ event.preventDefault();
12567
+ event.returnValue = "";
12568
+ },
12569
+ [hasPendingVisualStyleEdits],
12570
+ ),
12571
+ { capture: true },
12572
+ );
12573
+ const pendingVisualStyleNavigationBlocker = useBlocker(
12574
+ useCallback(
12575
+ ({ currentLocation, nextLocation }) =>
12576
+ shouldBlockPendingVisualStyleNavigation({
12577
+ hasPendingVisualStyleEdits,
12578
+ currentPathname: currentLocation.pathname,
12579
+ nextPathname: nextLocation.pathname,
12580
+ }),
12581
+ [hasPendingVisualStyleEdits],
12582
+ ),
12583
+ );
12584
+ const pendingVisualStyleWarningOpen =
12585
+ pendingVisualStyleNavigationBlocker.state === "blocked";
12586
+ const handleStayOnPendingVisualStyleNavigation = useCallback(() => {
12587
+ if (pendingVisualStyleNavigationBlocker.state !== "blocked") return;
12588
+ pendingVisualStyleNavigationBlocker.reset();
12589
+ }, [pendingVisualStyleNavigationBlocker]);
12590
+ const handleDiscardPendingVisualStylesAndNavigate = useCallback(() => {
12591
+ if (pendingVisualStyleNavigationBlocker.state !== "blocked") return;
12592
+ setPendingVisualStyleEdits([]);
12593
+ pendingVisualStyleNavigationBlocker.proceed();
12594
+ }, [pendingVisualStyleNavigationBlocker]);
12595
+
12596
+ const pendingVisualStylePropertyCount = useMemo(
12597
+ () => getPendingVisualStylePropertyCount(pendingVisualStyleEdits),
12598
+ [pendingVisualStyleEdits],
12599
+ );
12600
+ const pendingVisualStylePrompt = useMemo(
12601
+ () =>
12602
+ formatPendingVisualStylePrompt({
12603
+ designId: id,
12604
+ designTitle: design?.title,
12605
+ activeFileId: activeFile?.id,
12606
+ activeFilename: activeFile?.filename,
12607
+ edits: pendingVisualStyleEdits,
12608
+ }),
12609
+ [
12610
+ activeFile?.filename,
12611
+ activeFile?.id,
12612
+ design?.title,
12613
+ id,
12614
+ pendingVisualStyleEdits,
12615
+ ],
12616
+ );
12617
+ const handleApplyPendingVisualStylesWithAgent = useCallback(() => {
12618
+ if (pendingVisualStyleEdits.length === 0) return;
12619
+ sendToDesignAgentChat({
12620
+ message: t("designEditor.pendingVisualStyles.agentMessage"),
12621
+ context: pendingVisualStylePrompt,
12622
+ submit: true,
12623
+ openSidebar: true,
12624
+ });
12625
+ setPendingVisualStyleEdits([]);
12626
+ setActiveLeftPanel("agent");
12627
+ toast.success(t("designEditor.pendingVisualStyles.sentToast"));
12628
+ }, [pendingVisualStyleEdits.length, pendingVisualStylePrompt, t]);
12629
+ const handleCopyPendingVisualStylePrompt = useCallback(async () => {
12630
+ if (pendingVisualStyleEdits.length === 0) return;
12631
+ try {
12632
+ await navigator.clipboard.writeText(pendingVisualStylePrompt);
12633
+ toast.success(t("designEditor.pendingVisualStyles.copiedToast"));
12634
+ } catch {
12635
+ toast.error(t("designEditor.toasts.clipboardBlocked"));
12636
+ }
12637
+ }, [pendingVisualStyleEdits.length, pendingVisualStylePrompt, t]);
12638
+
12109
12639
  const triggerBlobDownload = useCallback((blob: Blob, filename: string) => {
12110
12640
  const url = URL.createObjectURL(blob);
12111
12641
  const a = document.createElement("a");
@@ -15445,6 +15975,7 @@ ${serializedHtml}
15445
15975
  t("chat.suggestionMobile"),
15446
15976
  ]}
15447
15977
  scope={designChatScope}
15978
+ showScopeBadge={false}
15448
15979
  browserTabId={browserTabId}
15449
15980
  />
15450
15981
  </div>
@@ -15665,6 +16196,49 @@ ${serializedHtml}
15665
16196
  }}
15666
16197
  />
15667
16198
  )}
16199
+ {pendingVisualStyleEdits.length > 0 ? (
16200
+ <div className="pointer-events-none absolute bottom-5 right-5 z-[70] flex items-end">
16201
+ <DropdownMenu>
16202
+ <DropdownMenuTrigger asChild>
16203
+ <Button
16204
+ className="pointer-events-auto h-11 cursor-pointer rounded-md bg-blue-500 px-4 text-sm font-semibold text-white shadow-[0_18px_40px_-20px_rgba(37,99,235,0.9)] hover:bg-blue-400 focus-visible:ring-blue-400"
16205
+ aria-label={t(
16206
+ "designEditor.pendingVisualStyles.applyAria",
16207
+ )}
16208
+ >
16209
+ <IconBrush className="h-4 w-4" />
16210
+ {t("designEditor.pendingVisualStyles.applyButton")}
16211
+ <span className="rounded bg-white/20 px-1.5 py-0.5 text-xs font-semibold text-white">
16212
+ {pendingVisualStylePropertyCount}
16213
+ </span>
16214
+ <IconChevronDown className="h-4 w-4" />
16215
+ </Button>
16216
+ </DropdownMenuTrigger>
16217
+ <DropdownMenuContent
16218
+ align="end"
16219
+ className="design-editor-app-menu-content w-64"
16220
+ >
16221
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
16222
+ {t("designEditor.pendingVisualStyles.previewLabel")}
16223
+ </DropdownMenuLabel>
16224
+ <DropdownMenuItem
16225
+ onClick={handleApplyPendingVisualStylesWithAgent}
16226
+ >
16227
+ <IconMessage className="mr-2 h-4 w-4" />
16228
+ {t(
16229
+ "designEditor.pendingVisualStyles.applyWithAgent",
16230
+ )}
16231
+ </DropdownMenuItem>
16232
+ <DropdownMenuItem
16233
+ onClick={handleCopyPendingVisualStylePrompt}
16234
+ >
16235
+ <IconClipboard className="mr-2 h-4 w-4" />
16236
+ {t("designEditor.pendingVisualStyles.copyPrompt")}
16237
+ </DropdownMenuItem>
16238
+ </DropdownMenuContent>
16239
+ </DropdownMenu>
16240
+ </div>
16241
+ ) : null}
15668
16242
  {viewMode === "overview" ? (
15669
16243
  <MultiScreenCanvas
15670
16244
  screens={overviewScreens}
@@ -16019,6 +16593,9 @@ ${serializedHtml}
16019
16593
  onElementHover={(info) =>
16020
16594
  handleScreenElementHover(screen.id, info)
16021
16595
  }
16596
+ onEditorDragStateChange={
16597
+ handleEditorDragStateChange
16598
+ }
16022
16599
  onClearSelection={() =>
16023
16600
  handleScreenElementClear(screen.id)
16024
16601
  }
@@ -16139,6 +16716,7 @@ ${serializedHtml}
16139
16716
  onElementSelect={handleElementSelect}
16140
16717
  onElementMarqueeSelect={handleElementMarqueeSelect}
16141
16718
  onElementHover={handleElementHover}
16719
+ onEditorDragStateChange={handleEditorDragStateChange}
16142
16720
  onClearSelection={() => {
16143
16721
  setSelectedElement(null);
16144
16722
  setHoveredElement(null);
@@ -16399,6 +16977,37 @@ ${serializedHtml}
16399
16977
  ) : null}
16400
16978
  </div>
16401
16979
 
16980
+ <AlertDialog open={pendingVisualStyleWarningOpen}>
16981
+ <AlertDialogContent>
16982
+ <AlertDialogHeader>
16983
+ <AlertDialogTitle>
16984
+ {t("designEditor.pendingVisualStyles.leaveTitle")}
16985
+ </AlertDialogTitle>
16986
+ <AlertDialogDescription>
16987
+ {t(
16988
+ pendingVisualStylePropertyCount === 1
16989
+ ? "designEditor.pendingVisualStyles.leaveDescriptionOne"
16990
+ : "designEditor.pendingVisualStyles.leaveDescriptionOther",
16991
+ { count: pendingVisualStylePropertyCount },
16992
+ )}
16993
+ </AlertDialogDescription>
16994
+ </AlertDialogHeader>
16995
+ <AlertDialogFooter>
16996
+ <AlertDialogCancel
16997
+ onClick={handleStayOnPendingVisualStyleNavigation}
16998
+ >
16999
+ {t("designEditor.pendingVisualStyles.stay")}
17000
+ </AlertDialogCancel>
17001
+ <AlertDialogAction
17002
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
17003
+ onClick={handleDiscardPendingVisualStylesAndNavigate}
17004
+ >
17005
+ {t("designEditor.pendingVisualStyles.leave")}
17006
+ </AlertDialogAction>
17007
+ </AlertDialogFooter>
17008
+ </AlertDialogContent>
17009
+ </AlertDialog>
17010
+
16402
17011
  {/* Motion dock (§6.3) — bottom timeline mounted while opening, open, or
16403
17012
  closing. Canvas remains visible above.
16404
17013
  Preview-only scrubbing fires a motion-preview postMessage to the