@agent-native/core 0.84.1 → 0.84.3

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 (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +15 -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/production-agent.ts +9 -2
  7. package/corpus/core/src/agent/run-loop-with-resume.ts +38 -8
  8. package/corpus/core/src/agent/run-manager.ts +8 -7
  9. package/corpus/core/src/agent/thread-data-builder.ts +6 -0
  10. package/corpus/core/src/cli/design-connect.ts +175 -0
  11. package/corpus/core/src/cli/skills.ts +5 -7
  12. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  13. package/corpus/core/src/client/MultiTabAssistantChat.tsx +4 -1
  14. package/corpus/core/src/client/agent-chat-adapter.ts +253 -24
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +70 -21
  16. package/corpus/core/src/client/sse-event-processor.ts +46 -4
  17. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  18. package/corpus/templates/analytics/AGENTS.md +1 -1
  19. package/corpus/templates/design/actions/apply-motion-edit.ts +50 -18
  20. package/corpus/templates/design/actions/get-motion-timeline.ts +4 -14
  21. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +156 -12
  22. package/corpus/templates/design/app/components/design/MotionDock.tsx +2 -3
  23. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +217 -75
  24. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +39 -39
  25. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +558 -37
  26. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +75 -3
  27. package/corpus/templates/design/app/components/design/types.ts +13 -0
  28. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  29. package/corpus/templates/design/app/i18n/zh-TW.ts +17 -0
  30. package/corpus/templates/design/app/i18n-data.ts +245 -0
  31. package/corpus/templates/design/app/pages/DesignEditor.tsx +890 -252
  32. package/corpus/templates/design/changelog/2026-06-30-agent-chat-now-reports-saved-design-generations-clearly-when.md +6 -0
  33. package/corpus/templates/design/changelog/2026-06-30-apply-styles-now-appears-only-for-localhost-visual-edit-scre.md +6 -0
  34. package/corpus/templates/design/changelog/2026-06-30-copying-or-dragging-screen-elements-onto-the-infinite-canvas.md +6 -0
  35. package/corpus/templates/design/changelog/2026-06-30-design-questions-now-use-tighter-editor-typography-and-contr.md +6 -0
  36. package/corpus/templates/design/changelog/2026-06-30-element-drags-can-be-cancelled-with-escape-before-they-commi.md +6 -0
  37. package/corpus/templates/design/changelog/2026-06-30-pending-visual-style-edits-now-warn-before-you-leave-the-edi.md +6 -0
  38. package/corpus/templates/design/changelog/2026-06-30-share-options-now-make-export-and-agent-handoff-easier-to-no.md +6 -0
  39. package/corpus/templates/design/changelog/2026-06-30-visual-style-drags-stay-live-while-pending-edits-can-be-appl.md +6 -0
  40. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-a-redundant-context-tab-above-the-composer.md +6 -0
  41. package/corpus/templates/design/changelog/2026-07-01-motion-track-creation-no-longer-fails-in-local-editors.md +6 -0
  42. package/corpus/templates/design/changelog/2026-07-01-share-general-access-menu-stays-open-when-choosing-organization.md +6 -0
  43. package/corpus/templates/design/server/plugins/db.ts +10 -0
  44. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  45. package/dist/agent/engine/builder-engine.js +61 -10
  46. package/dist/agent/engine/builder-engine.js.map +1 -1
  47. package/dist/agent/production-agent.d.ts.map +1 -1
  48. package/dist/agent/production-agent.js +9 -2
  49. package/dist/agent/production-agent.js.map +1 -1
  50. package/dist/agent/run-loop-with-resume.d.ts +2 -2
  51. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  52. package/dist/agent/run-loop-with-resume.js +31 -8
  53. package/dist/agent/run-loop-with-resume.js.map +1 -1
  54. package/dist/agent/run-manager.d.ts +8 -7
  55. package/dist/agent/run-manager.d.ts.map +1 -1
  56. package/dist/agent/run-manager.js +8 -7
  57. package/dist/agent/run-manager.js.map +1 -1
  58. package/dist/agent/thread-data-builder.d.ts +2 -0
  59. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  60. package/dist/agent/thread-data-builder.js +5 -0
  61. package/dist/agent/thread-data-builder.js.map +1 -1
  62. package/dist/cli/design-connect.d.ts +2 -0
  63. package/dist/cli/design-connect.d.ts.map +1 -1
  64. package/dist/cli/design-connect.js +140 -0
  65. package/dist/cli/design-connect.js.map +1 -1
  66. package/dist/cli/skills.d.ts.map +1 -1
  67. package/dist/cli/skills.js +5 -7
  68. package/dist/cli/skills.js.map +1 -1
  69. package/dist/client/AgentPanel.d.ts +5 -1
  70. package/dist/client/AgentPanel.d.ts.map +1 -1
  71. package/dist/client/AgentPanel.js +4 -4
  72. package/dist/client/AgentPanel.js.map +1 -1
  73. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  74. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  75. package/dist/client/MultiTabAssistantChat.js +2 -2
  76. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  77. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  78. package/dist/client/agent-chat-adapter.js +216 -22
  79. package/dist/client/agent-chat-adapter.js.map +1 -1
  80. package/dist/client/sharing/ShareButton.d.ts +6 -0
  81. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  82. package/dist/client/sharing/ShareButton.js +25 -11
  83. package/dist/client/sharing/ShareButton.js.map +1 -1
  84. package/dist/client/sse-event-processor.d.ts +18 -1
  85. package/dist/client/sse-event-processor.d.ts.map +1 -1
  86. package/dist/client/sse-event-processor.js +34 -5
  87. package/dist/client/sse-event-processor.js.map +1 -1
  88. package/dist/collab/awareness.d.ts +2 -2
  89. package/dist/collab/awareness.d.ts.map +1 -1
  90. package/dist/collab/routes.d.ts +2 -2
  91. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  92. package/dist/notifications/routes.d.ts +3 -3
  93. package/dist/observability/routes.d.ts +7 -7
  94. package/dist/resources/handlers.d.ts +2 -2
  95. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  96. package/dist/server/transcribe-voice.d.ts +1 -1
  97. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  98. package/docs/design/durable-agent-runs.md +34 -35
  99. package/package.json +1 -1
  100. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
@@ -117,6 +117,7 @@ import {
117
117
  IconExternalLink,
118
118
  IconCircleCheck,
119
119
  IconTerminal2,
120
+ IconLink,
120
121
  } from "@tabler/icons-react";
121
122
  import { useQueryClient } from "@tanstack/react-query";
122
123
  import {
@@ -131,7 +132,14 @@ import {
131
132
  type SetStateAction,
132
133
  } from "react";
133
134
  import { flushSync } from "react-dom";
134
- import { useParams, useNavigate, Link, useLocation } from "react-router";
135
+ import {
136
+ useParams,
137
+ useNavigate,
138
+ Link,
139
+ useLocation,
140
+ useBlocker,
141
+ useBeforeUnload,
142
+ } from "react-router";
135
143
  import { toast } from "sonner";
136
144
  import * as Y from "yjs";
137
145
 
@@ -186,6 +194,8 @@ import type {
186
194
  ElementInfo,
187
195
  ElementSelectionIntent,
188
196
  DeviceFrameType,
197
+ PortableStyleSnapshot,
198
+ PortableStyleSnapshotNode,
189
199
  } from "@/components/design/types";
190
200
  import {
191
201
  DEVICE_FRAME_VIEWPORTS,
@@ -193,9 +203,18 @@ import {
193
203
  } from "@/components/design/types";
194
204
  import PromptPopover from "@/components/editor/PromptDialog";
195
205
  import type { UploadedFile } from "@/components/editor/PromptDialog";
206
+ import {
207
+ AlertDialog,
208
+ AlertDialogAction,
209
+ AlertDialogCancel,
210
+ AlertDialogContent,
211
+ AlertDialogDescription,
212
+ AlertDialogFooter,
213
+ AlertDialogHeader,
214
+ AlertDialogTitle,
215
+ } from "@/components/ui/alert-dialog";
196
216
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
197
217
  import { Button } from "@/components/ui/button";
198
- import { Checkbox } from "@/components/ui/checkbox";
199
218
  import {
200
219
  Dialog,
201
220
  DialogContent,
@@ -226,7 +245,6 @@ import {
226
245
  } from "@/components/ui/popover";
227
246
  import { Spinner } from "@/components/ui/spinner";
228
247
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
229
- import { Textarea } from "@/components/ui/textarea";
230
248
  import {
231
249
  Tooltip,
232
250
  TooltipContent,
@@ -332,11 +350,29 @@ function dedupeStringIds(ids: string[]): string[] {
332
350
  return Array.from(new Set(ids.filter(Boolean)));
333
351
  }
334
352
 
353
+ function sameStringIds(a: string[], b: string[]) {
354
+ return a.length === b.length && a.every((value, index) => value === b[index]);
355
+ }
356
+
335
357
  export function isScreenRootElementInfo(info: ElementInfo | null | undefined) {
336
358
  const tagName = info?.tagName?.toUpperCase();
337
359
  return tagName === "BODY" || tagName === "HTML";
338
360
  }
339
361
 
362
+ export function shouldIgnoreOverviewLayerCreationEcho(args: {
363
+ pendingLayerId: string | null | undefined;
364
+ pendingScreenId: string | null | undefined;
365
+ screenId: string;
366
+ info?: ElementInfo | null;
367
+ event: "select" | "clear";
368
+ }) {
369
+ if (!args.pendingLayerId) return false;
370
+ if (args.pendingScreenId && args.pendingScreenId !== args.screenId) {
371
+ return false;
372
+ }
373
+ return args.event === "clear" || isScreenRootElementInfo(args.info);
374
+ }
375
+
340
376
  export function getSelectedScreenIdsForEditorState(args: {
341
377
  activeFileId: string | null | undefined;
342
378
  overviewSelectedScreenIds: string[];
@@ -866,6 +902,115 @@ interface LiveScreenSnapshot {
866
902
  contentType?: string;
867
903
  }
868
904
 
905
+ export interface PendingVisualStyleEdit {
906
+ screenId: string;
907
+ filename: string;
908
+ screenName: string;
909
+ selector: string;
910
+ sourceId?: string | null;
911
+ tagName?: string | null;
912
+ classes: string[];
913
+ styles: Record<string, string>;
914
+ updatedAt: number;
915
+ }
916
+
917
+ function pendingVisualStyleEditKey(edit: PendingVisualStyleEdit): string {
918
+ return [
919
+ edit.screenId,
920
+ edit.sourceId?.trim() || edit.selector.trim() || "unknown",
921
+ ].join("::");
922
+ }
923
+
924
+ export function mergePendingVisualStyleEdit(
925
+ edits: readonly PendingVisualStyleEdit[],
926
+ nextEdit: PendingVisualStyleEdit,
927
+ ): PendingVisualStyleEdit[] {
928
+ const nextKey = pendingVisualStyleEditKey(nextEdit);
929
+ let merged = false;
930
+ const next = edits.map((edit) => {
931
+ if (pendingVisualStyleEditKey(edit) !== nextKey) return edit;
932
+ merged = true;
933
+ return {
934
+ ...edit,
935
+ ...nextEdit,
936
+ classes: nextEdit.classes.length > 0 ? nextEdit.classes : edit.classes,
937
+ styles: { ...edit.styles, ...nextEdit.styles },
938
+ };
939
+ });
940
+ return merged ? next : [...edits, nextEdit];
941
+ }
942
+
943
+ export function getPendingVisualStylePropertyCount(
944
+ edits: readonly PendingVisualStyleEdit[],
945
+ ): number {
946
+ return edits.reduce(
947
+ (count, edit) => count + Object.keys(edit.styles).length,
948
+ 0,
949
+ );
950
+ }
951
+
952
+ export function shouldBlockPendingVisualStyleNavigation(args: {
953
+ hasPendingVisualStyleEdits: boolean;
954
+ currentPathname: string;
955
+ nextPathname: string;
956
+ }): boolean {
957
+ return (
958
+ args.hasPendingVisualStyleEdits &&
959
+ args.currentPathname !== args.nextPathname
960
+ );
961
+ }
962
+
963
+ export function formatPendingVisualStylePrompt(args: {
964
+ designId?: string | null;
965
+ designTitle?: string | null;
966
+ activeFileId?: string | null;
967
+ activeFilename?: string | null;
968
+ edits: readonly PendingVisualStyleEdit[];
969
+ }): string {
970
+ const title = args.designTitle?.trim();
971
+ const editPayload = args.edits.map((edit) => ({
972
+ screenId: edit.screenId,
973
+ filename: edit.filename,
974
+ screenName: edit.screenName,
975
+ selector: edit.selector,
976
+ sourceId: edit.sourceId ?? null,
977
+ tagName: edit.tagName ?? null,
978
+ classes: edit.classes,
979
+ styles: edit.styles,
980
+ }));
981
+
982
+ return [
983
+ `Apply these pending visual style edits${title ? ` to "${title}"` : ""}.`,
984
+ args.designId ? `Design id: "${args.designId}".` : "",
985
+ args.activeFileId
986
+ ? `Active screen: "${args.activeFilename ?? args.activeFileId}" (${args.activeFileId}).`
987
+ : "",
988
+ "",
989
+ "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.",
990
+ "",
991
+ "Pending style edits:",
992
+ JSON.stringify(editPayload, null, 2),
993
+ ]
994
+ .filter((line) => line !== "")
995
+ .join("\n");
996
+ }
997
+
998
+ export function shouldShowPendingVisualStyleApply(args: {
999
+ edits: readonly PendingVisualStyleEdit[];
1000
+ screenSourceTypes: ReadonlyMap<string, unknown>;
1001
+ fallbackSourceType?: unknown;
1002
+ }): boolean {
1003
+ return (
1004
+ args.edits.length > 0 &&
1005
+ args.edits.every(
1006
+ (edit) =>
1007
+ normalizeDesignSourceType(
1008
+ args.screenSourceTypes.get(edit.screenId) ?? args.fallbackSourceType,
1009
+ ) === "localhost",
1010
+ )
1011
+ );
1012
+ }
1013
+
869
1014
  interface DesignData {
870
1015
  id: string;
871
1016
  title: string;
@@ -893,6 +1038,7 @@ interface CanvasLayerClipboardEntry {
893
1038
  html: string;
894
1039
  rootNodeId?: string;
895
1040
  sourceFileId: string;
1041
+ portableStyleSnapshot?: PortableStyleSnapshot;
896
1042
  }
897
1043
 
898
1044
  interface SelectedCanvasLayerSnapshot extends CanvasLayerClipboardEntry {
@@ -1451,6 +1597,80 @@ function setAbsolutePositioningForNodeInHtml(
1451
1597
  }
1452
1598
  }
1453
1599
 
1600
+ function getAbsolutePositioningForNodeInHtml(
1601
+ content: string,
1602
+ nodeAttrId: string,
1603
+ ): { x: number; y: number } | null {
1604
+ if (typeof window === "undefined") return null;
1605
+ try {
1606
+ const doc = new DOMParser().parseFromString(content, "text/html");
1607
+ const element = doc.querySelector(
1608
+ `[data-agent-native-node-id="${CSS.escape(nodeAttrId)}"]`,
1609
+ ) as HTMLElement | null;
1610
+ if (!element) return null;
1611
+ return {
1612
+ x: Number.parseFloat(element.style.left || "0") || 0,
1613
+ y: Number.parseFloat(element.style.top || "0") || 0,
1614
+ };
1615
+ } catch {
1616
+ return null;
1617
+ }
1618
+ }
1619
+
1620
+ function elementAtPortableStylePath(
1621
+ root: Element,
1622
+ node: PortableStyleSnapshotNode,
1623
+ ): Element | null {
1624
+ let current: Element | null = root;
1625
+ for (const index of node.path) {
1626
+ if (!current || !Number.isInteger(index) || index < 0) return null;
1627
+ current = current.children.item(index);
1628
+ }
1629
+ return current;
1630
+ }
1631
+
1632
+ function applyPortableStyles(
1633
+ element: Element | null,
1634
+ styles: Record<string, string>,
1635
+ ) {
1636
+ if (!element) return;
1637
+ const host = styleHost(element);
1638
+ if (!host) return;
1639
+ Object.entries(styles).forEach(([property, value]) => {
1640
+ if (!value) return;
1641
+ if (property.startsWith("--") || property.includes("-")) {
1642
+ host.style.setProperty(property, value);
1643
+ return;
1644
+ }
1645
+ (host.style as any)[property] = value;
1646
+ });
1647
+ }
1648
+
1649
+ function applyPortableStyleSnapshotToHtml(
1650
+ content: string,
1651
+ nodeAttrId: string,
1652
+ snapshot?: PortableStyleSnapshot,
1653
+ ): string {
1654
+ if (typeof window === "undefined" || !snapshot?.nodes?.length) {
1655
+ return content;
1656
+ }
1657
+ try {
1658
+ const doc = new DOMParser().parseFromString(content, "text/html");
1659
+ const root = doc.querySelector(
1660
+ `[data-agent-native-node-id="${CSS.escape(nodeAttrId)}"]`,
1661
+ );
1662
+ if (!root) return content;
1663
+ root.setAttribute("data-agent-native-preserve-styles", "true");
1664
+ snapshot.nodes.forEach((node) => {
1665
+ const target = elementAtPortableStylePath(root, node);
1666
+ if (target) applyPortableStyles(target, node.styles);
1667
+ });
1668
+ return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
1669
+ } catch {
1670
+ return content;
1671
+ }
1672
+ }
1673
+
1454
1674
  function isAbsoluteCodeLayerNode(node: CodeLayerNode | null | undefined) {
1455
1675
  const position = String(node?.style.position ?? "").toLowerCase();
1456
1676
  return position === "absolute" || position === "fixed";
@@ -1951,6 +2171,7 @@ function setRootLayerPosition(
1951
2171
  function prepareClonedHtmlLayer(
1952
2172
  doc: Document,
1953
2173
  layerHtml: string,
2174
+ styleSnapshot?: PortableStyleSnapshot,
1954
2175
  ): { element: Element; rootNodeId: string } | null {
1955
2176
  const layerDoc = new DOMParser().parseFromString(
1956
2177
  `<template>${layerHtml}</template>`,
@@ -1961,6 +2182,13 @@ function prepareClonedHtmlLayer(
1961
2182
  layerDoc.body.firstElementChild;
1962
2183
  if (!source) return null;
1963
2184
  const clone = doc.importNode(source, true) as Element;
2185
+ if (styleSnapshot) {
2186
+ clone.setAttribute("data-agent-native-preserve-styles", "true");
2187
+ styleSnapshot.nodes.forEach((node) => {
2188
+ const target = elementAtPortableStylePath(clone, node);
2189
+ if (target) applyPortableStyles(target, node.styles);
2190
+ });
2191
+ }
1964
2192
  const rootNodeId = uniqueLayerId("copy");
1965
2193
  clone.setAttribute("data-agent-native-node-id", rootNodeId);
1966
2194
  Array.from(clone.querySelectorAll("[data-agent-native-node-id]")).forEach(
@@ -1983,6 +2211,7 @@ function insertClonedHtmlLayers(
1983
2211
  placement?: "before" | "after" | "inside";
1984
2212
  stripRootPosition?: boolean;
1985
2213
  positions?: Array<{ x: number; y: number } | null | undefined>;
2214
+ styleSnapshots?: Array<PortableStyleSnapshot | null | undefined>;
1986
2215
  } = {},
1987
2216
  ): { content: string; rootNodeIds: string[] } | null {
1988
2217
  if (typeof window === "undefined" || layerHtmls.length === 0) return null;
@@ -1992,7 +2221,11 @@ function insertClonedHtmlLayers(
1992
2221
  const fragment = doc.createDocumentFragment();
1993
2222
  const rootNodeIds: string[] = [];
1994
2223
  layerHtmls.forEach((layerHtml, index) => {
1995
- const prepared = prepareClonedHtmlLayer(doc, layerHtml);
2224
+ const prepared = prepareClonedHtmlLayer(
2225
+ doc,
2226
+ layerHtml,
2227
+ options.styleSnapshots?.[index] ?? undefined,
2228
+ );
1996
2229
  if (!prepared) return;
1997
2230
  const position = options.positions?.[index];
1998
2231
  if (position) {
@@ -3078,7 +3311,11 @@ function DesignWorkspaceRail({
3078
3311
  "group-hover:bg-transparent group-hover:text-muted-foreground",
3079
3312
  )}
3080
3313
  >
3081
- <IconChevronUp className="size-[15px]" />
3314
+ {motionOpen ? (
3315
+ <IconChevronDown className="size-[15px]" />
3316
+ ) : (
3317
+ <IconChevronUp className="size-[15px]" />
3318
+ )}
3082
3319
  </span>
3083
3320
  <span className="max-w-full truncate leading-none">
3084
3321
  {"Motion" /* i18n-ignore */}
@@ -3970,6 +4207,9 @@ export default function DesignEditor() {
3970
4207
  const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(
3971
4208
  null,
3972
4209
  );
4210
+ const [pendingVisualStyleEdits, setPendingVisualStyleEdits] = useState<
4211
+ PendingVisualStyleEdit[]
4212
+ >([]);
3973
4213
  const [textEditingState, setTextEditingState] = useState<{
3974
4214
  active: boolean;
3975
4215
  selector?: string;
@@ -4930,8 +5170,8 @@ export default function DesignEditor() {
4930
5170
  null,
4931
5171
  );
4932
5172
  const [codingHandoffLoading, setCodingHandoffLoading] = useState(false);
4933
- const [downloadZipInstead, setDownloadZipInstead] = useState(false);
4934
- const [codingHandoffDetail, setCodingHandoffDetail] = useState("");
5173
+ const [shareLinkCopied, setShareLinkCopied] = useState(false);
5174
+ const shareLinkCopiedResetRef = useRef<number | null>(null);
4935
5175
  const [, setPatchProof] = useState<PatchProofState | null>(null);
4936
5176
  const pendingFileSavesRef = useRef<Record<string, FileContentSaveRequest>>(
4937
5177
  {},
@@ -5126,6 +5366,13 @@ export default function DesignEditor() {
5126
5366
  if (!id || typeof window === "undefined") return undefined;
5127
5367
  return getDesignEditorShareUrl(id, window.location.origin, appBasePath());
5128
5368
  }, [id]);
5369
+ useEffect(() => {
5370
+ return () => {
5371
+ if (shareLinkCopiedResetRef.current !== null) {
5372
+ window.clearTimeout(shareLinkCopiedResetRef.current);
5373
+ }
5374
+ };
5375
+ }, []);
5129
5376
  const {
5130
5377
  designSystems,
5131
5378
  defaultSystem,
@@ -6738,6 +6985,7 @@ export default function DesignEditor() {
6738
6985
  // Canvas container ref for cursor overlay coordinate mapping.
6739
6986
  const canvasContextMenuRef = useRef<CanvasContextMenuHandle | null>(null);
6740
6987
  const canvasContainerRef = useRef<HTMLDivElement>(null);
6988
+ const activeEditorDragRef = useRef(false);
6741
6989
 
6742
6990
  // Live handle to the active DesignCanvas preview iframe. DesignCanvas owns the
6743
6991
  // <iframe> internally (tagged data-design-preview-iframe) and does not forward
@@ -6755,6 +7003,25 @@ export default function DesignEditor() {
6755
7003
  [],
6756
7004
  );
6757
7005
 
7006
+ const handleEditorDragStateChange = useCallback((active: boolean) => {
7007
+ activeEditorDragRef.current = active;
7008
+ }, []);
7009
+
7010
+ const cancelActiveEditorDrag = useCallback(() => {
7011
+ if (!activeEditorDragRef.current) return false;
7012
+ activeEditorDragRef.current = false;
7013
+ if (typeof document === "undefined") return true;
7014
+ document
7015
+ .querySelectorAll<HTMLIFrameElement>("iframe[data-design-preview-iframe]")
7016
+ .forEach((iframe) => {
7017
+ iframe.contentWindow?.postMessage(
7018
+ { type: "agent-native:cancel-active-drag" },
7019
+ "*",
7020
+ );
7021
+ });
7022
+ return true;
7023
+ }, []);
7024
+
6758
7025
  const handleRunDesignAudit = useCallback(async () => {
6759
7026
  if (!id || !activeFile?.id) return;
6760
7027
  const auditFileId = activeFile.id;
@@ -7002,6 +7269,98 @@ export default function DesignEditor() {
7002
7269
  },
7003
7270
  [liveScreenSnapshotsById],
7004
7271
  );
7272
+ const recordPendingVisualStyleEdit = useCallback(
7273
+ (
7274
+ screenId: string,
7275
+ selector: string,
7276
+ styles: Record<string, string>,
7277
+ elementInfo?: ElementInfo,
7278
+ ) => {
7279
+ if (!canEditDesign) return;
7280
+ const entries = Object.entries(styles).filter(
7281
+ ([, value]) => value !== undefined,
7282
+ );
7283
+ if (entries.length === 0) return;
7284
+ const stylePatch = Object.fromEntries(entries);
7285
+ const screen = files.find((file) => file.id === screenId);
7286
+ const fallbackName = screen?.filename ?? screenId;
7287
+ const sourceId =
7288
+ elementInfo?.sourceId ??
7289
+ (screenId === activeFile?.id ? selectedElement?.sourceId : null);
7290
+ const proofId =
7291
+ typeof crypto !== "undefined" && "randomUUID" in crypto
7292
+ ? crypto.randomUUID()
7293
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
7294
+ const [firstProperty, firstValue] = entries[0];
7295
+
7296
+ setPendingVisualStyleEdits((current) =>
7297
+ mergePendingVisualStyleEdit(current, {
7298
+ screenId,
7299
+ filename: fallbackName,
7300
+ screenName: prettyScreenName(fallbackName),
7301
+ selector,
7302
+ sourceId,
7303
+ tagName: elementInfo?.tagName ?? null,
7304
+ classes: elementInfo?.classes ?? [],
7305
+ styles: stylePatch,
7306
+ updatedAt: Date.now(),
7307
+ }),
7308
+ );
7309
+ setPatchProof({
7310
+ id: proofId,
7311
+ fileId: screenId,
7312
+ filename: fallbackName,
7313
+ selector,
7314
+ sourceId: sourceId ?? undefined,
7315
+ property:
7316
+ entries.length === 1
7317
+ ? firstProperty
7318
+ : entries.map(([property]) => property).join(", "),
7319
+ previousValue:
7320
+ elementInfo?.computedStyles?.[firstProperty] ??
7321
+ (screenId === activeFile?.id
7322
+ ? selectedElement?.computedStyles?.[firstProperty]
7323
+ : undefined),
7324
+ nextValue:
7325
+ entries.length === 1
7326
+ ? firstValue
7327
+ : entries
7328
+ .map(([property, value]) => `${property}: ${value}`)
7329
+ .join("; "),
7330
+ previousContent: getProjectionContentForScreen(screenId),
7331
+ capability: "deterministic-style-edit",
7332
+ confidence: 0.92,
7333
+ status: "runtime",
7334
+ createdAt: Date.now(),
7335
+ });
7336
+
7337
+ if (screenId !== activeFile?.id) return;
7338
+ setSelectedElement((prev) => {
7339
+ const base = elementInfo ?? prev;
7340
+ if (!base) return prev;
7341
+ return {
7342
+ ...base,
7343
+ sourceId: sourceId ?? base.sourceId,
7344
+ selector: selector || base.selector,
7345
+ computedStyles: {
7346
+ ...base.computedStyles,
7347
+ ...stylePatch,
7348
+ },
7349
+ };
7350
+ });
7351
+ if (sourceId) {
7352
+ setSelectedLayerIdsState([sourceId]);
7353
+ }
7354
+ },
7355
+ [
7356
+ activeFile?.id,
7357
+ canEditDesign,
7358
+ files,
7359
+ getProjectionContentForScreen,
7360
+ selectedElement?.computedStyles,
7361
+ selectedElement?.sourceId,
7362
+ ],
7363
+ );
7005
7364
  const activeProjectionContent =
7006
7365
  activeFile?.id !== undefined
7007
7366
  ? getProjectionContentForScreen(activeFile.id)
@@ -7688,6 +8047,7 @@ export default function DesignEditor() {
7688
8047
  fileId?: unknown;
7689
8048
  timelineId?: unknown;
7690
8049
  updatedAt?: unknown;
8050
+ contentPatched?: unknown;
7691
8051
  };
7692
8052
  if (typeof response.timelineId === "string") {
7693
8053
  setMotionTimelineId(response.timelineId);
@@ -7699,7 +8059,8 @@ export default function DesignEditor() {
7699
8059
  }
7700
8060
  if (
7701
8061
  typeof response.fileId === "string" &&
7702
- response.fileId === activeFile.id
8062
+ response.fileId === activeFile.id &&
8063
+ response.contentPatched !== false
7703
8064
  ) {
7704
8065
  applyFileContentUpdate(response.fileId, localPatchedContent, {
7705
8066
  refreshPreview: true,
@@ -7925,7 +8286,8 @@ export default function DesignEditor() {
7925
8286
  // new primitive selected, matching Figma behaviour. We activate the
7926
8287
  // target screen (so the layers panel shows its content) and select the
7927
8288
  // new node, but do NOT switch to single/full view.
7928
- pendingOverviewScreenSelectionRef.current = screenId;
8289
+ pendingOverviewScreenSelectionRef.current =
8290
+ options?.selectFrame === false ? null : screenId;
7929
8291
  pendingOverviewLayerSelectionRef.current = nodeId;
7930
8292
  clearPendingOverviewLayerSelectionTimer();
7931
8293
  flushSync(() => {
@@ -7980,13 +8342,14 @@ export default function DesignEditor() {
7980
8342
  const handleOverviewScreenSelectionChange = useCallback(
7981
8343
  (ids: string[]) => {
7982
8344
  const pendingId = pendingOverviewScreenSelectionRef.current;
7983
- const fileIds = new Set(files.map((file) => file.id));
8345
+ const fileIds = new Set(
8346
+ files.filter((file) => file.id !== boardFileId).map((file) => file.id),
8347
+ );
8348
+ const nextIds = ids.filter((layerId) => fileIds.has(layerId));
7984
8349
  if (pendingId && ids.length === 0) return;
7985
8350
  if (pendingId && ids.includes(pendingId)) {
7986
- setOverviewSelectedScreenIds(
7987
- ids.filter(
7988
- (layerId) => fileIds.has(layerId) || layerId === pendingId,
7989
- ),
8351
+ setOverviewSelectedScreenIds((current) =>
8352
+ sameStringIds(current, nextIds) ? current : nextIds,
7990
8353
  );
7991
8354
  if (fileIds.has(pendingId)) {
7992
8355
  pendingOverviewScreenSelectionRef.current = null;
@@ -7999,11 +8362,11 @@ export default function DesignEditor() {
7999
8362
  clearPendingOverviewLayerSelectionTimer();
8000
8363
  setCreatedOverviewLayerSelection(null);
8001
8364
  }
8002
- setOverviewSelectedScreenIds(
8003
- ids.filter((layerId) => fileIds.has(layerId)),
8365
+ setOverviewSelectedScreenIds((current) =>
8366
+ sameStringIds(current, nextIds) ? current : nextIds,
8004
8367
  );
8005
8368
  },
8006
- [clearPendingOverviewLayerSelectionTimer, files],
8369
+ [boardFileId, clearPendingOverviewLayerSelectionTimer, files],
8007
8370
  );
8008
8371
 
8009
8372
  const shouldPreserveBlockedOverviewLayerSelectionRef = useRef<
@@ -8469,14 +8832,14 @@ export default function DesignEditor() {
8469
8832
  (screenId: string, info: ElementInfo, intent?: ElementSelectionIntent) => {
8470
8833
  const pendingLayerId = pendingOverviewLayerSelectionRef.current;
8471
8834
  const pendingScreenId = pendingOverviewScreenSelectionRef.current;
8472
- const createdSelection = createdOverviewLayerSelection;
8473
- if (createdSelection?.screenId === screenId) {
8474
- return;
8475
- }
8476
8835
  if (
8477
- (pendingLayerId || createdSelection?.layerId) &&
8478
- (!pendingScreenId || pendingScreenId === screenId) &&
8479
- isScreenRootElementInfo(info)
8836
+ shouldIgnoreOverviewLayerCreationEcho({
8837
+ pendingLayerId,
8838
+ pendingScreenId,
8839
+ screenId,
8840
+ info,
8841
+ event: "select",
8842
+ })
8480
8843
  ) {
8481
8844
  return;
8482
8845
  }
@@ -8535,7 +8898,6 @@ export default function DesignEditor() {
8535
8898
  },
8536
8899
  [
8537
8900
  clearPendingOverviewLayerSelectionTimer,
8538
- createdOverviewLayerSelection,
8539
8901
  focusDesignInspectorForSelection,
8540
8902
  getCodeLayerProjectionForScreen,
8541
8903
  selectedLayerIdsState,
@@ -8546,13 +8908,13 @@ export default function DesignEditor() {
8546
8908
  (screenId: string) => {
8547
8909
  const pendingLayerId = pendingOverviewLayerSelectionRef.current;
8548
8910
  const pendingScreenId = pendingOverviewScreenSelectionRef.current;
8549
- const createdSelection = createdOverviewLayerSelection;
8550
- if (createdSelection?.screenId === screenId) {
8551
- return;
8552
- }
8553
8911
  if (
8554
- (pendingLayerId || createdSelection?.layerId) &&
8555
- (!pendingScreenId || pendingScreenId === screenId)
8912
+ shouldIgnoreOverviewLayerCreationEcho({
8913
+ pendingLayerId,
8914
+ pendingScreenId,
8915
+ screenId,
8916
+ event: "clear",
8917
+ })
8556
8918
  ) {
8557
8919
  return;
8558
8920
  }
@@ -8574,7 +8936,7 @@ export default function DesignEditor() {
8574
8936
  setActiveTool("move");
8575
8937
  setMode("edit");
8576
8938
  },
8577
- [clearPendingOverviewLayerSelectionTimer, createdOverviewLayerSelection],
8939
+ [clearPendingOverviewLayerSelectionTimer],
8578
8940
  );
8579
8941
 
8580
8942
  const handleElementSelect = useCallback(
@@ -9252,12 +9614,15 @@ export default function DesignEditor() {
9252
9614
  styles: Record<string, string>,
9253
9615
  elementInfo?: ElementInfo,
9254
9616
  ) => {
9255
- commitVisualStyles(selector, styles, {
9256
- runtimeApplied: true,
9617
+ if (!activeFile?.id) return;
9618
+ recordPendingVisualStyleEdit(
9619
+ activeFile.id,
9620
+ selector,
9621
+ styles,
9257
9622
  elementInfo,
9258
- });
9623
+ );
9259
9624
  },
9260
- [commitVisualStyles],
9625
+ [activeFile?.id, recordPendingVisualStyleEdit],
9261
9626
  );
9262
9627
 
9263
9628
  const handleVisualStructureChange = useCallback(
@@ -9270,6 +9635,9 @@ export default function DesignEditor() {
9270
9635
  sourceId?: string;
9271
9636
  anchorSourceId?: string;
9272
9637
  requestId?: string;
9638
+ dropMode?: "flow-insert" | "absolute-container";
9639
+ sourceRect?: { x: number; y: number; width: number; height: number };
9640
+ anchorRect?: { x: number; y: number; width: number; height: number };
9273
9641
  },
9274
9642
  ) => {
9275
9643
  if (!canEditDesign) return false;
@@ -9319,12 +9687,29 @@ export default function DesignEditor() {
9319
9687
  (node) => node.id === patch.result.after?.nodeId,
9320
9688
  )?.dataAttributes["data-agent-native-node-id"]
9321
9689
  : undefined);
9690
+ const absoluteContainerOffset =
9691
+ details?.dropMode === "absolute-container" &&
9692
+ details.sourceRect &&
9693
+ details.anchorRect
9694
+ ? {
9695
+ x: details.sourceRect.x - details.anchorRect.x,
9696
+ y: details.sourceRect.y - details.anchorRect.y,
9697
+ }
9698
+ : null;
9322
9699
  const nextContent =
9323
9700
  isAbsoluteCodeLayerNode(targetNode) && movedNodeAttrId
9324
- ? removeAbsolutePositioningFromNodeInHtml(
9325
- patch.content,
9326
- movedNodeAttrId,
9327
- )
9701
+ ? details?.dropMode === "absolute-container"
9702
+ ? absoluteContainerOffset
9703
+ ? setAbsolutePositioningForNodeInHtml(
9704
+ patch.content,
9705
+ movedNodeAttrId,
9706
+ absoluteContainerOffset,
9707
+ )
9708
+ : patch.content
9709
+ : removeAbsolutePositioningFromNodeInHtml(
9710
+ patch.content,
9711
+ movedNodeAttrId,
9712
+ )
9328
9713
  : patch.content;
9329
9714
  const nextProjection = buildCodeLayerProjection(nextContent);
9330
9715
  const movedNode =
@@ -9554,76 +9939,9 @@ export default function DesignEditor() {
9554
9939
  styles: Record<string, string>,
9555
9940
  elementInfo?: ElementInfo,
9556
9941
  ) => {
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 });
9942
+ recordPendingVisualStyleEdit(screenId, selector, styles, elementInfo);
9616
9943
  },
9617
- [
9618
- activeFile?.id,
9619
- applyFileContentUpdate,
9620
- canEditDesign,
9621
- getScreenContent,
9622
- handleVisualStyleChange,
9623
- liveScreenSnapshotsById,
9624
- t,
9625
- updateLiveScreenSnapshotContent,
9626
- ],
9944
+ [recordPendingVisualStyleEdit],
9627
9945
  );
9628
9946
 
9629
9947
  const handleScreenVisualStructureChange = useCallback(
@@ -9637,6 +9955,9 @@ export default function DesignEditor() {
9637
9955
  sourceId?: string;
9638
9956
  anchorSourceId?: string;
9639
9957
  requestId?: string;
9958
+ dropMode?: "flow-insert" | "absolute-container";
9959
+ sourceRect?: { x: number; y: number; width: number; height: number };
9960
+ anchorRect?: { x: number; y: number; width: number; height: number };
9640
9961
  },
9641
9962
  ) => {
9642
9963
  if (screenId === activeFile?.id) {
@@ -9687,15 +10008,51 @@ export default function DesignEditor() {
9687
10008
  );
9688
10009
  return false;
9689
10010
  }
9690
- applyFileContentUpdate(screenId, patch.content, { skipPreview: true });
9691
- const movedNode =
10011
+ const movedNodeAttrId =
10012
+ targetNode?.dataAttributes["data-agent-native-node-id"] ??
10013
+ details?.sourceId ??
10014
+ elementInfo?.sourceId ??
9692
10015
  (patch.result.after?.nodeId
9693
10016
  ? patch.projection.nodes.find(
9694
10017
  (node) => node.id === patch.result.after?.nodeId,
10018
+ )?.dataAttributes["data-agent-native-node-id"]
10019
+ : undefined);
10020
+ const absoluteContainerOffset =
10021
+ details?.dropMode === "absolute-container" &&
10022
+ details.sourceRect &&
10023
+ details.anchorRect
10024
+ ? {
10025
+ x: details.sourceRect.x - details.anchorRect.x,
10026
+ y: details.sourceRect.y - details.anchorRect.y,
10027
+ }
10028
+ : null;
10029
+ const nextContent =
10030
+ isAbsoluteCodeLayerNode(targetNode) && movedNodeAttrId
10031
+ ? details?.dropMode === "absolute-container"
10032
+ ? absoluteContainerOffset
10033
+ ? setAbsolutePositioningForNodeInHtml(
10034
+ patch.content,
10035
+ movedNodeAttrId,
10036
+ absoluteContainerOffset,
10037
+ )
10038
+ : patch.content
10039
+ : removeAbsolutePositioningFromNodeInHtml(
10040
+ patch.content,
10041
+ movedNodeAttrId,
10042
+ )
10043
+ : patch.content;
10044
+ const nextProjection = buildCodeLayerProjection(nextContent);
10045
+ applyFileContentUpdate(screenId, nextContent, { skipPreview: true });
10046
+ const movedNode =
10047
+ (movedNodeAttrId
10048
+ ? nextProjection.nodes.find(
10049
+ (node) =>
10050
+ node.dataAttributes["data-agent-native-node-id"] ===
10051
+ movedNodeAttrId,
9695
10052
  )
9696
10053
  : null) ??
9697
10054
  resolveCodeLayerNodeFromBridge(
9698
- patch.projection,
10055
+ nextProjection,
9699
10056
  selector,
9700
10057
  details?.sourceId ??
9701
10058
  elementInfo?.sourceId ??
@@ -9925,11 +10282,18 @@ export default function DesignEditor() {
9925
10282
  candidate.dataAttributes["data-agent-native-node-id"] === layerId,
9926
10283
  );
9927
10284
  if (!node?.source) continue;
10285
+ const portableStyleSnapshot =
10286
+ selectedElementLayerId &&
10287
+ node.id === selectedElementLayerId &&
10288
+ selectedElement?.portableStyleSnapshot
10289
+ ? selectedElement.portableStyleSnapshot
10290
+ : undefined;
9928
10291
  snapshots.push({
9929
10292
  html: content.slice(node.source.start, node.source.end),
9930
10293
  rootNodeId:
9931
10294
  node.dataAttributes["data-agent-native-node-id"] ?? node.id,
9932
10295
  sourceFileId: file.id,
10296
+ portableStyleSnapshot,
9933
10297
  node,
9934
10298
  sourceIndex: node.source.start,
9935
10299
  tree,
@@ -9956,6 +10320,7 @@ export default function DesignEditor() {
9956
10320
  selectedElement.sourceId ??
9957
10321
  selectedElement.id,
9958
10322
  sourceFileId: activeFile.id,
10323
+ portableStyleSnapshot: selectedElement.portableStyleSnapshot,
9959
10324
  node,
9960
10325
  sourceIndex: node.source?.start ?? Number.MAX_SAFE_INTEGER,
9961
10326
  tree,
@@ -10020,19 +10385,31 @@ export default function DesignEditor() {
10020
10385
  )
10021
10386
  .filter((node): node is CodeLayerNode => Boolean(node));
10022
10387
  if (insertedNodes.length === 0) return;
10388
+ const lastNode = insertedNodes[insertedNodes.length - 1];
10389
+ if (lastNode) {
10390
+ pendingOverviewScreenSelectionRef.current =
10391
+ screenId === boardFileId ? null : screenId;
10392
+ pendingOverviewLayerSelectionRef.current = lastNode.id;
10393
+ clearPendingOverviewLayerSelectionTimer();
10394
+ setCreatedOverviewLayerSelection({
10395
+ screenId,
10396
+ layerId: lastNode.id,
10397
+ });
10398
+ }
10023
10399
  setActiveFileId(screenId);
10024
10400
  setSelectedLayerIdsState(insertedNodes.map((node) => node.id));
10025
- const lastNode = insertedNodes[insertedNodes.length - 1];
10026
10401
  setSelectedElement(
10027
10402
  lastNode ? elementInfoFromCodeLayerNode(lastNode) : null,
10028
10403
  );
10029
10404
  setActiveTool("move");
10030
10405
  setMode("edit");
10031
10406
  if (viewModeRef.current === "overview") {
10032
- setOverviewSelectedScreenIds([screenId]);
10407
+ setOverviewSelectedScreenIds(
10408
+ screenId === boardFileId ? [] : [screenId],
10409
+ );
10033
10410
  }
10034
10411
  },
10035
- [],
10412
+ [boardFileId, clearPendingOverviewLayerSelectionTimer],
10036
10413
  );
10037
10414
 
10038
10415
  const handleCopySelection = useCallback(async () => {
@@ -10040,6 +10417,7 @@ export default function DesignEditor() {
10040
10417
  html: snapshot.html,
10041
10418
  rootNodeId: snapshot.rootNodeId,
10042
10419
  sourceFileId: snapshot.sourceFileId,
10420
+ portableStyleSnapshot: snapshot.portableStyleSnapshot,
10043
10421
  }));
10044
10422
  if (entries.length === 0) return;
10045
10423
  const clipboardText = entries.map((entry) => entry.html).join("\n");
@@ -10057,9 +10435,32 @@ export default function DesignEditor() {
10057
10435
  const handlePasteSelection = useCallback(
10058
10436
  (position?: { x: number; y: number }) => {
10059
10437
  const entries = getCanvasClipboardEntries();
10060
- if (!activeFile || !canEditDesign || entries.length === 0) return;
10061
- const baseContent = getFreshActiveContent();
10438
+ const targetFileId =
10439
+ viewModeRef.current === "overview" && position && boardFileId
10440
+ ? boardFileId
10441
+ : activeFile?.id;
10442
+ if (!targetFileId || !canEditDesign || entries.length === 0) return;
10443
+ const targetIsBoard = targetFileId === boardFileId;
10444
+ const baseContent =
10445
+ targetFileId === activeFile?.id
10446
+ ? getFreshActiveContent()
10447
+ : (getScreenContent(targetFileId) ?? "");
10448
+ if (!baseContent && targetFileId !== boardFileId) return;
10062
10449
  const layerHtmls = entries.map((entry) => entry.html);
10450
+ const styleSnapshots = targetIsBoard
10451
+ ? entries.map((entry) => entry.portableStyleSnapshot)
10452
+ : undefined;
10453
+ const applyPasteContentUpdate = (nextContent: string) => {
10454
+ if (targetFileId === activeFile?.id) {
10455
+ applyLocalContentUpdate(nextContent, {
10456
+ forcePreviewFullDocument: true,
10457
+ });
10458
+ return;
10459
+ }
10460
+ applyFileContentUpdate(targetFileId, nextContent, {
10461
+ forcePreviewFullDocument: true,
10462
+ });
10463
+ };
10063
10464
 
10064
10465
  // B7 fix: when an element is selected and no explicit canvas position was
10065
10466
  // given, insert the clone as an in-flow sibling right AFTER the selected
@@ -10067,20 +10468,19 @@ export default function DesignEditor() {
10067
10468
  // in normal document flow instead of being an absolutely-positioned body
10068
10469
  // child. Fall back to the old position-based clone when nothing is
10069
10470
  // selected or a "Paste here" position is provided.
10070
- if (!position && selectedElement?.selector) {
10471
+ if (!position && !targetIsBoard && selectedElement?.selector) {
10071
10472
  const selector = selectedCanvasSelector ?? selectedElement.selector;
10072
10473
  const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
10073
10474
  targetSelectors: [selector],
10074
10475
  placement: "after",
10075
10476
  stripRootPosition: true,
10477
+ styleSnapshots,
10076
10478
  });
10077
10479
  if (result) {
10078
10480
  pasteCascadeRef.current += 1;
10079
- applyLocalContentUpdate(result.content, {
10080
- forcePreviewFullDocument: true,
10081
- });
10481
+ applyPasteContentUpdate(result.content);
10082
10482
  selectInsertedLayers(
10083
- activeFile.id,
10483
+ targetFileId,
10084
10484
  result.content,
10085
10485
  result.rootNodeIds,
10086
10486
  );
@@ -10127,20 +10527,22 @@ export default function DesignEditor() {
10127
10527
  });
10128
10528
  const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
10129
10529
  positions,
10530
+ styleSnapshots,
10130
10531
  });
10131
10532
  if (!result) return;
10132
10533
  if (!position) pasteCascadeRef.current += 1;
10133
- applyLocalContentUpdate(result.content, {
10134
- forcePreviewFullDocument: true,
10135
- });
10136
- selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
10534
+ applyPasteContentUpdate(result.content);
10535
+ selectInsertedLayers(targetFileId, result.content, result.rootNodeIds);
10137
10536
  },
10138
10537
  [
10139
10538
  activeFile,
10539
+ applyFileContentUpdate,
10140
10540
  applyLocalContentUpdate,
10541
+ boardFileId,
10141
10542
  canEditDesign,
10142
10543
  getCanvasClipboardEntries,
10143
10544
  getFreshActiveContent,
10545
+ getScreenContent,
10144
10546
  selectInsertedLayers,
10145
10547
  selectedCanvasSelector,
10146
10548
  selectedElement,
@@ -10153,6 +10555,7 @@ export default function DesignEditor() {
10153
10555
  const baseContent = getFreshActiveContent();
10154
10556
  if (selectedElement?.boundingRect) {
10155
10557
  const { x, y } = selectedElement.boundingRect;
10558
+ const targetIsBoard = activeFile.id === boardFileId;
10156
10559
  const result = insertClonedHtmlLayers(
10157
10560
  baseContent,
10158
10561
  entries.map((entry) => entry.html),
@@ -10161,6 +10564,9 @@ export default function DesignEditor() {
10161
10564
  x: x + index * 16,
10162
10565
  y: y + index * 16,
10163
10566
  })),
10567
+ styleSnapshots: targetIsBoard
10568
+ ? entries.map((entry) => entry.portableStyleSnapshot)
10569
+ : undefined,
10164
10570
  },
10165
10571
  );
10166
10572
  if (!result) return;
@@ -10174,6 +10580,7 @@ export default function DesignEditor() {
10174
10580
  }, [
10175
10581
  activeFile,
10176
10582
  applyLocalContentUpdate,
10583
+ boardFileId,
10177
10584
  getCanvasClipboardEntries,
10178
10585
  getFreshActiveContent,
10179
10586
  handlePasteSelection,
@@ -10502,9 +10909,9 @@ export default function DesignEditor() {
10502
10909
  * Handle a primitive being drag-dropped onto another primitive in the
10503
10910
  * MultiScreenCanvas overview (CONTRACT: onPrimitiveReparent prop).
10504
10911
  *
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.
10912
+ * Same-screen: applies a moveNode intent then rebases the moved node's
10913
+ * absolute coordinates relative to the target rectangle. Cross-screen uses
10914
+ * moveNodeBetweenDocuments and persists both files.
10508
10915
  */
10509
10916
  const handleOverviewPrimitiveReparent = useCallback(
10510
10917
  ({
@@ -10544,27 +10951,38 @@ export default function DesignEditor() {
10544
10951
  return;
10545
10952
  }
10546
10953
 
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
10954
  const movedNodeAttrId =
10552
10955
  movePatch.projection.nodes.find(
10553
10956
  (n) =>
10554
10957
  n.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
10555
10958
  n.id === sourceNodeId,
10556
10959
  )?.dataAttributes["data-agent-native-node-id"] ?? sourceNodeId;
10557
- const strippedContent = removeAbsolutePositioningFromNodeInHtml(
10558
- movePatch.content,
10559
- movedNodeAttrId,
10960
+ const sourcePosition = getAbsolutePositioningForNodeInHtml(
10961
+ baseContent,
10962
+ sourceNodeId,
10560
10963
  );
10964
+ const targetPosition = getAbsolutePositioningForNodeInHtml(
10965
+ baseContent,
10966
+ targetNodeId,
10967
+ );
10968
+ const nextContent =
10969
+ sourcePosition && targetPosition
10970
+ ? setAbsolutePositioningForNodeInHtml(
10971
+ movePatch.content,
10972
+ movedNodeAttrId,
10973
+ {
10974
+ x: sourcePosition.x - targetPosition.x,
10975
+ y: sourcePosition.y - targetPosition.y,
10976
+ },
10977
+ )
10978
+ : movePatch.content;
10561
10979
 
10562
- applyFileContentUpdate(sourceScreenId, strippedContent, {
10980
+ applyFileContentUpdate(sourceScreenId, nextContent, {
10563
10981
  skipPreview: true,
10564
10982
  });
10565
10983
 
10566
10984
  // Re-select the moved node.
10567
- const nextProjection = buildCodeLayerProjection(strippedContent);
10985
+ const nextProjection = buildCodeLayerProjection(nextContent);
10568
10986
  const movedNodeAfter = nextProjection.nodes.find(
10569
10987
  (n) =>
10570
10988
  n.dataAttributes["data-agent-native-node-id"] === sourceNodeId ||
@@ -10616,18 +11034,26 @@ export default function DesignEditor() {
10616
11034
  return;
10617
11035
  }
10618
11036
 
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
11037
  const destNodeAttrId = result.movedNodeId ?? nodeAttrId;
10627
- const strippedDest = removeAbsolutePositioningFromNodeInHtml(
10628
- result.destHtml,
10629
- destNodeAttrId,
11038
+ const sourcePosition = getAbsolutePositioningForNodeInHtml(
11039
+ sourceContent,
11040
+ nodeAttrId,
10630
11041
  );
11042
+ const targetPosition = getAbsolutePositioningForNodeInHtml(
11043
+ destContent,
11044
+ anchorAttrId,
11045
+ );
11046
+ const nextDestContent =
11047
+ sourcePosition && targetPosition
11048
+ ? setAbsolutePositioningForNodeInHtml(
11049
+ result.destHtml,
11050
+ destNodeAttrId,
11051
+ {
11052
+ x: sourcePosition.x - targetPosition.x,
11053
+ y: sourcePosition.y - targetPosition.y,
11054
+ },
11055
+ )
11056
+ : result.destHtml;
10631
11057
 
10632
11058
  recordContentHistoryEntry({
10633
11059
  changes: [
@@ -10639,7 +11065,7 @@ export default function DesignEditor() {
10639
11065
  {
10640
11066
  fileId: targetScreenId,
10641
11067
  before: destContent,
10642
- after: strippedDest,
11068
+ after: nextDestContent,
10643
11069
  },
10644
11070
  ],
10645
11071
  });
@@ -10648,13 +11074,13 @@ export default function DesignEditor() {
10648
11074
  recordHistory: false,
10649
11075
  refreshPreview: true,
10650
11076
  });
10651
- applyFileContentUpdate(targetScreenId, strippedDest, {
11077
+ applyFileContentUpdate(targetScreenId, nextDestContent, {
10652
11078
  recordHistory: false,
10653
11079
  refreshPreview: true,
10654
11080
  });
10655
11081
 
10656
11082
  // Re-select the moved node in the destination.
10657
- const finalProjection = buildCodeLayerProjection(strippedDest);
11083
+ const finalProjection = buildCodeLayerProjection(nextDestContent);
10658
11084
  const movedNodeFinal = finalProjection.nodes.find(
10659
11085
  (n) => n.dataAttributes["data-agent-native-node-id"] === destNodeAttrId,
10660
11086
  );
@@ -10695,8 +11121,11 @@ export default function DesignEditor() {
10695
11121
  targetScreenId,
10696
11122
  targetAnchorNodeId,
10697
11123
  targetAnchorPlacement,
11124
+ targetDropMode,
11125
+ targetAnchorRect,
10698
11126
  targetLocalPoint,
10699
11127
  sourcePointerOffset,
11128
+ styleSnapshot,
10700
11129
  }: {
10701
11130
  sourceSelector: string;
10702
11131
  sourceNodeId?: string;
@@ -10704,9 +11133,17 @@ export default function DesignEditor() {
10704
11133
  targetScreenId: string;
10705
11134
  targetAnchorNodeId?: string;
10706
11135
  targetAnchorPlacement?: "before" | "after" | "inside";
11136
+ targetDropMode?: "flow-insert" | "absolute-container";
11137
+ targetAnchorRect?: {
11138
+ left: number;
11139
+ top: number;
11140
+ width: number;
11141
+ height: number;
11142
+ };
10707
11143
  targetCanvasPoint?: { x: number; y: number };
10708
11144
  targetLocalPoint?: { x: number; y: number };
10709
11145
  sourcePointerOffset?: { x: number; y: number };
11146
+ styleSnapshot?: PortableStyleSnapshot;
10710
11147
  }) => {
10711
11148
  if (!canEditDesign) return;
10712
11149
  if (sourceScreenId === targetScreenId) return;
@@ -10772,19 +11209,39 @@ export default function DesignEditor() {
10772
11209
  // there is no anchor, preserve absolute mode and rebase left/top to the
10773
11210
  // release point so screen↔board moves behave like Figma absolute layers.
10774
11211
  const destNodeAttrId = result.movedNodeId ?? nodeAttrId;
11212
+ const stylePreservedDest =
11213
+ targetScreenId === boardFileId
11214
+ ? applyPortableStyleSnapshotToHtml(
11215
+ result.destHtml,
11216
+ destNodeAttrId,
11217
+ styleSnapshot,
11218
+ )
11219
+ : result.destHtml;
10775
11220
  const nextDestContent = targetAnchorAttrId
10776
- ? removeAbsolutePositioningFromNodeInHtml(
10777
- result.destHtml,
10778
- destNodeAttrId,
10779
- )
11221
+ ? targetDropMode === "absolute-container"
11222
+ ? targetLocalPoint && targetAnchorRect
11223
+ ? setAbsolutePositioningForNodeInHtml(
11224
+ stylePreservedDest,
11225
+ destNodeAttrId,
11226
+ {
11227
+ x: targetLocalPoint.x - targetAnchorRect.left,
11228
+ y: targetLocalPoint.y - targetAnchorRect.top,
11229
+ },
11230
+ sourcePointerOffset,
11231
+ )
11232
+ : stylePreservedDest
11233
+ : removeAbsolutePositioningFromNodeInHtml(
11234
+ stylePreservedDest,
11235
+ destNodeAttrId,
11236
+ )
10780
11237
  : targetLocalPoint
10781
11238
  ? setAbsolutePositioningForNodeInHtml(
10782
- result.destHtml,
11239
+ stylePreservedDest,
10783
11240
  destNodeAttrId,
10784
11241
  targetLocalPoint,
10785
11242
  sourcePointerOffset,
10786
11243
  )
10787
- : result.destHtml;
11244
+ : stylePreservedDest;
10788
11245
 
10789
11246
  recordContentHistoryEntry({
10790
11247
  changes: [
@@ -10812,19 +11269,34 @@ export default function DesignEditor() {
10812
11269
 
10813
11270
  // Switch active screen to the target and select the moved node; viewMode
10814
11271
  // stays "overview" (no setViewMode call).
11272
+ pendingOverviewScreenSelectionRef.current =
11273
+ targetScreenId === boardFileId ? null : targetScreenId;
11274
+ pendingOverviewLayerSelectionRef.current = destNodeAttrId;
11275
+ clearPendingOverviewLayerSelectionTimer();
10815
11276
  setActiveFileId(targetScreenId);
10816
11277
  const finalProjection = buildCodeLayerProjection(nextDestContent);
10817
11278
  const movedNodeFinal = finalProjection.nodes.find(
10818
11279
  (n) => n.dataAttributes["data-agent-native-node-id"] === destNodeAttrId,
10819
11280
  );
10820
11281
  if (movedNodeFinal) {
11282
+ setCreatedOverviewLayerSelection({
11283
+ screenId: targetScreenId,
11284
+ layerId: movedNodeFinal.id,
11285
+ });
10821
11286
  setSelectedLayerIdsState([movedNodeFinal.id]);
10822
11287
  setSelectedElement(elementInfoFromCodeLayerNode(movedNodeFinal));
11288
+ if (viewModeRef.current === "overview") {
11289
+ setOverviewSelectedScreenIds(
11290
+ targetScreenId === boardFileId ? [] : [targetScreenId],
11291
+ );
11292
+ }
10823
11293
  }
10824
11294
  },
10825
11295
  [
10826
11296
  applyFileContentUpdate,
11297
+ boardFileId,
10827
11298
  canEditDesign,
11299
+ clearPendingOverviewLayerSelectionTimer,
10828
11300
  getScreenContent,
10829
11301
  recordContentHistoryEntry,
10830
11302
  t,
@@ -11707,6 +12179,7 @@ export default function DesignEditor() {
11707
12179
  ]);
11708
12180
 
11709
12181
  const handleEscapeHotkey = useCallback(() => {
12182
+ if (cancelActiveEditorDrag()) return;
11710
12183
  if (
11711
12184
  shouldEscapeToOverview({
11712
12185
  activeTool,
@@ -11730,6 +12203,7 @@ export default function DesignEditor() {
11730
12203
  setMode("edit");
11731
12204
  }, [
11732
12205
  activeTool,
12206
+ cancelActiveEditorDrag,
11733
12207
  drawMode,
11734
12208
  enterOverviewFromZoom,
11735
12209
  mode,
@@ -12078,17 +12552,13 @@ export default function DesignEditor() {
12078
12552
 
12079
12553
  const getCodingHandoffClipboardText = useCallback(
12080
12554
  (result: CodingHandoffResult | null) => {
12081
- const base =
12082
- typeof result?.clipboardText === "string"
12083
- ? result.clipboardText
12084
- : typeof result?.prompt === "string"
12085
- ? result.prompt
12086
- : "";
12087
- const detail = codingHandoffDetail.trim();
12088
- if (!base || !detail) return base;
12089
- return `${base}\n\nAdditional implementation detail:\n${detail}`;
12555
+ return typeof result?.clipboardText === "string"
12556
+ ? result.clipboardText
12557
+ : typeof result?.prompt === "string"
12558
+ ? result.prompt
12559
+ : "";
12090
12560
  },
12091
- [codingHandoffDetail],
12561
+ [],
12092
12562
  );
12093
12563
 
12094
12564
  const handleCopyCodingHandoff = useCallback(async () => {
@@ -12106,6 +12576,125 @@ export default function DesignEditor() {
12106
12576
  }
12107
12577
  }, [ensureCodingHandoff, getCodingHandoffClipboardText, t]);
12108
12578
 
12579
+ const handleCopyShareLink = useCallback(async () => {
12580
+ if (!editorShareUrl) return;
12581
+ try {
12582
+ await navigator.clipboard.writeText(editorShareUrl);
12583
+ setShareLinkCopied(true);
12584
+ if (shareLinkCopiedResetRef.current !== null) {
12585
+ window.clearTimeout(shareLinkCopiedResetRef.current);
12586
+ }
12587
+ shareLinkCopiedResetRef.current = window.setTimeout(() => {
12588
+ setShareLinkCopied(false);
12589
+ shareLinkCopiedResetRef.current = null;
12590
+ }, 1400);
12591
+ toast.success("Share link copied" /* i18n-ignore share copy toast */);
12592
+ } catch {
12593
+ toast.error(t("designEditor.toasts.clipboardBlocked"));
12594
+ }
12595
+ }, [editorShareUrl, t]);
12596
+
12597
+ const hasPendingVisualStyleEdits = pendingVisualStyleEdits.length > 0;
12598
+ useBeforeUnload(
12599
+ useCallback(
12600
+ (event: BeforeUnloadEvent) => {
12601
+ if (!hasPendingVisualStyleEdits) return;
12602
+ event.preventDefault();
12603
+ event.returnValue = "";
12604
+ },
12605
+ [hasPendingVisualStyleEdits],
12606
+ ),
12607
+ { capture: true },
12608
+ );
12609
+ const pendingVisualStyleNavigationBlocker = useBlocker(
12610
+ useCallback(
12611
+ ({ currentLocation, nextLocation }) =>
12612
+ shouldBlockPendingVisualStyleNavigation({
12613
+ hasPendingVisualStyleEdits,
12614
+ currentPathname: currentLocation.pathname,
12615
+ nextPathname: nextLocation.pathname,
12616
+ }),
12617
+ [hasPendingVisualStyleEdits],
12618
+ ),
12619
+ );
12620
+ const pendingVisualStyleWarningOpen =
12621
+ pendingVisualStyleNavigationBlocker.state === "blocked";
12622
+ const handleStayOnPendingVisualStyleNavigation = useCallback(() => {
12623
+ if (pendingVisualStyleNavigationBlocker.state !== "blocked") return;
12624
+ pendingVisualStyleNavigationBlocker.reset();
12625
+ }, [pendingVisualStyleNavigationBlocker]);
12626
+ const handleDiscardPendingVisualStylesAndNavigate = useCallback(() => {
12627
+ if (pendingVisualStyleNavigationBlocker.state !== "blocked") return;
12628
+ setPendingVisualStyleEdits([]);
12629
+ pendingVisualStyleNavigationBlocker.proceed();
12630
+ }, [pendingVisualStyleNavigationBlocker]);
12631
+
12632
+ const pendingVisualStylePropertyCount = useMemo(
12633
+ () => getPendingVisualStylePropertyCount(pendingVisualStyleEdits),
12634
+ [pendingVisualStyleEdits],
12635
+ );
12636
+ const pendingVisualStyleScreenSourceTypes = useMemo(
12637
+ () =>
12638
+ new Map<string, unknown>(
12639
+ overviewScreens.map((screen) => [
12640
+ screen.id,
12641
+ screen.sourceType ?? designSourceType,
12642
+ ]),
12643
+ ),
12644
+ [designSourceType, overviewScreens],
12645
+ );
12646
+ const showPendingVisualStyleApply = useMemo(
12647
+ () =>
12648
+ shouldShowPendingVisualStyleApply({
12649
+ edits: pendingVisualStyleEdits,
12650
+ screenSourceTypes: pendingVisualStyleScreenSourceTypes,
12651
+ fallbackSourceType: designSourceType,
12652
+ }),
12653
+ [
12654
+ designSourceType,
12655
+ pendingVisualStyleEdits,
12656
+ pendingVisualStyleScreenSourceTypes,
12657
+ ],
12658
+ );
12659
+ const pendingVisualStylePrompt = useMemo(
12660
+ () =>
12661
+ formatPendingVisualStylePrompt({
12662
+ designId: id,
12663
+ designTitle: design?.title,
12664
+ activeFileId: activeFile?.id,
12665
+ activeFilename: activeFile?.filename,
12666
+ edits: pendingVisualStyleEdits,
12667
+ }),
12668
+ [
12669
+ activeFile?.filename,
12670
+ activeFile?.id,
12671
+ design?.title,
12672
+ id,
12673
+ pendingVisualStyleEdits,
12674
+ ],
12675
+ );
12676
+ const handleApplyPendingVisualStylesWithAgent = useCallback(() => {
12677
+ if (pendingVisualStyleEdits.length === 0) return;
12678
+ sendToDesignAgentChat({
12679
+ message: t("designEditor.pendingVisualStyles.agentMessage"),
12680
+ context: pendingVisualStylePrompt,
12681
+ submit: true,
12682
+ openSidebar: true,
12683
+ });
12684
+ setPendingVisualStyleEdits([]);
12685
+ setActiveLeftPanel("agent");
12686
+ toast.success(t("designEditor.pendingVisualStyles.sentToast"));
12687
+ }, [pendingVisualStyleEdits.length, pendingVisualStylePrompt, t]);
12688
+ const handleCopyPendingVisualStylePrompt = useCallback(async () => {
12689
+ if (pendingVisualStyleEdits.length === 0) return;
12690
+ try {
12691
+ await navigator.clipboard.writeText(pendingVisualStylePrompt);
12692
+ toast.success(t("designEditor.pendingVisualStyles.copiedToast"));
12693
+ } catch {
12694
+ toast.error(t("designEditor.toasts.clipboardBlocked"));
12695
+ }
12696
+ }, [pendingVisualStyleEdits.length, pendingVisualStylePrompt, t]);
12697
+
12109
12698
  const triggerBlobDownload = useCallback((blob: Blob, filename: string) => {
12110
12699
  const url = URL.createObjectURL(blob);
12111
12700
  const a = document.createElement("a");
@@ -12173,22 +12762,6 @@ export default function DesignEditor() {
12173
12762
  });
12174
12763
  }, [exportZipMutation, fallbackExportName, id, t, triggerBlobDownload]);
12175
12764
 
12176
- const handleDownloadHandoffZip = useCallback(async () => {
12177
- const result = await ensureCodingHandoff();
12178
- if (!result?.zipUrl) {
12179
- toast.error(t("designEditor.toasts.zipCreateError"));
12180
- return;
12181
- }
12182
- const a = document.createElement("a");
12183
- a.href = result.zipUrl;
12184
- a.download = fallbackExportName("zip", "agent-handoff");
12185
- a.rel = "noopener";
12186
- document.body.appendChild(a);
12187
- a.click();
12188
- a.remove();
12189
- toast.success(t("designEditor.toasts.zipDownloaded"));
12190
- }, [ensureCodingHandoff, fallbackExportName, t]);
12191
-
12192
12765
  const handleDownloadPng = useCallback(
12193
12766
  async (settings?: Partial<ExportSettingsValue>) => {
12194
12767
  if (pngExportingRef.current) return;
@@ -12395,14 +12968,6 @@ ${serializedHtml}
12395
12968
  [handleDownloadPng, handleDownloadSvg],
12396
12969
  );
12397
12970
 
12398
- const handleSendToPrimaryAction = useCallback(() => {
12399
- if (downloadZipInstead) {
12400
- void handleDownloadHandoffZip();
12401
- return;
12402
- }
12403
- void handleCopyCodingHandoff();
12404
- }, [downloadZipInstead, handleCopyCodingHandoff, handleDownloadHandoffZip]);
12405
-
12406
12971
  const shareExportOptions: Array<{
12407
12972
  value: ShareExportFormat;
12408
12973
  title: string;
@@ -12566,84 +13131,72 @@ ${serializedHtml}
12566
13131
  <div className="flex flex-wrap items-center gap-2">
12567
13132
  <Button
12568
13133
  type="button"
12569
- onClick={handleSendToPrimaryAction}
12570
- disabled={
12571
- downloadZipInstead
12572
- ? !activeFile || codingHandoffLoading
12573
- : codingHandoffLoading
12574
- }
13134
+ onClick={() => void handleCopyCodingHandoff()}
13135
+ disabled={codingHandoffLoading}
12575
13136
  className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
12576
13137
  >
12577
- {downloadZipInstead ? (
12578
- <IconArchive className="size-3.5" />
12579
- ) : (
12580
- <IconClipboard className="size-3.5" />
12581
- )}
12582
- {
12583
- downloadZipInstead
12584
- ? t("designEditor.downloadZip")
12585
- : "Copy agent prompt" /* i18n-ignore share send action */
12586
- }
13138
+ <IconClipboard className="size-3.5" />
13139
+ {"Copy agent prompt" /* i18n-ignore share send action */}
12587
13140
  </Button>
12588
13141
  </div>
12589
-
12590
- <div className="space-y-3">
12591
- <div className="flex items-start gap-2.5">
12592
- <Checkbox
12593
- checked={downloadZipInstead}
12594
- onCheckedChange={(checked) =>
12595
- setDownloadZipInstead(checked === true)
12596
- }
12597
- className="mt-0.5"
12598
- />
12599
- <div className="min-w-0">
12600
- <div className="text-[12px] font-medium text-foreground">
12601
- {"Download zip instead" /* i18n-ignore share send option */}
12602
- </div>
12603
- <div className="mt-0.5 !text-[11px] leading-4 text-muted-foreground">
12604
- {
12605
- "For agents without the Design connector, drop the bundle into your agent's chat manually." /* i18n-ignore share send option description */
12606
- }
12607
- </div>
12608
- </div>
12609
- </div>
12610
-
12611
- <div className="space-y-1.5">
12612
- <label className="text-[12px] font-medium text-foreground">
12613
- {
12614
- "Give the agent more detail on what to implement" /* i18n-ignore share send detail label */
12615
- }{" "}
12616
- <span className="font-normal text-muted-foreground">
12617
- {"(optional)" /* i18n-ignore optional label */}
12618
- </span>
12619
- </label>
12620
- <Textarea
12621
- value={codingHandoffDetail}
12622
- onChange={(event) => setCodingHandoffDetail(event.target.value)}
12623
- placeholder={activeFile?.filename ?? "Add implementation notes..."}
12624
- className="min-h-20 resize-none rounded-md bg-[var(--design-editor-control-bg)] text-[12px]"
12625
- />
12626
- </div>
12627
- </div>
12628
13142
  </div>
12629
13143
  );
13144
+ const shareLinkFooter = (
13145
+ <div className="mt-3 flex flex-wrap items-center gap-2 border-t border-[var(--design-editor-panel-divider-color)] pt-3">
13146
+ <Button
13147
+ type="button"
13148
+ onClick={() => void handleCopyShareLink()}
13149
+ disabled={!editorShareUrl}
13150
+ className="h-8 min-w-[8.75rem] gap-1.5 rounded-md px-3 text-[12px]"
13151
+ >
13152
+ {shareLinkCopied ? (
13153
+ <IconCheck className="size-3.5" />
13154
+ ) : (
13155
+ <IconClipboard className="size-3.5" />
13156
+ )}
13157
+ {
13158
+ shareLinkCopied
13159
+ ? "Copied" /* i18n-ignore share copy action copied */
13160
+ : "Copy share link" /* i18n-ignore share copy action */
13161
+ }
13162
+ </Button>
13163
+ </div>
13164
+ );
13165
+ const designShareTabLabelClassName =
13166
+ "inline-flex items-center justify-center gap-1.5";
12630
13167
  const designSharePopoverClassName =
12631
13168
  "z-[100010] !w-[min(620px,calc(100vw-32px))] !p-3 " +
12632
- "[&_[role=tablist]]:!inline-flex [&_[role=tablist]]:!w-fit [&_[role=tablist]]:!self-start [&_[role=tablist]]:justify-start [&_[role=tablist]]:gap-0.5 [&_[role=tablist]]:rounded-none [&_[role=tablist]]:bg-transparent [&_[role=tablist]]:p-0 " +
12633
- "[&_[role=tab]]:!h-6 [&_[role=tab]]:!flex-none [&_[role=tab]]:rounded-md [&_[role=tab]]:px-2 [&_[role=tab]]:!text-[11px] [&_[role=tab]]:font-semibold [&_[role=tab]]:shadow-none [&_[role=tab]]:ring-0 " +
12634
- "[&_[role=tab]:hover]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab]:hover]:text-foreground [&_[role=tab][aria-selected=true]]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab][aria-selected=true]]:text-foreground [&_[role=tab][aria-selected=true]]:ring-0";
13169
+ "[&_[role=tablist]]:!inline-flex [&_[role=tablist]]:!w-fit [&_[role=tablist]]:!self-start [&_[role=tablist]]:justify-start [&_[role=tablist]]:gap-1 [&_[role=tablist]]:rounded-lg [&_[role=tablist]]:border [&_[role=tablist]]:border-[var(--design-editor-panel-divider-color)] [&_[role=tablist]]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tablist]]:p-1 " +
13170
+ "[&_[role=tab]]:!h-8 [&_[role=tab]]:!flex-none [&_[role=tab]]:rounded-md [&_[role=tab]]:px-3 [&_[role=tab]]:!text-[12px] [&_[role=tab]]:font-semibold [&_[role=tab]]:shadow-none [&_[role=tab]]:ring-0 " +
13171
+ "[&_[role=tab]:hover]:bg-white/70 dark:[&_[role=tab]:hover]:bg-[var(--design-editor-control-bg)] [&_[role=tab]:hover]:text-foreground " +
13172
+ "[&_[role=tab][aria-selected=true]]:bg-white dark:[&_[role=tab][aria-selected=true]]:bg-[var(--design-editor-control-bg)] [&_[role=tab][aria-selected=true]]:text-foreground [&_[role=tab][aria-selected=true]]:shadow-sm [&_[role=tab][aria-selected=true]]:ring-1 [&_[role=tab][aria-selected=true]]:ring-[var(--design-editor-control-border)]";
12635
13173
  const designShareTabs = {
12636
- shareLabel: "Share link" /* i18n-ignore share tab label */,
13174
+ shareLabel: (
13175
+ <span className={designShareTabLabelClassName}>
13176
+ <IconLink className="size-3.5" />
13177
+ {"Share link" /* i18n-ignore share tab label */}
13178
+ </span>
13179
+ ),
12637
13180
  defaultValue: "share",
12638
13181
  tabs: [
12639
13182
  {
12640
13183
  value: "export",
12641
- label: t("designEditor.export"),
13184
+ label: (
13185
+ <span className={designShareTabLabelClassName}>
13186
+ <IconFileExport className="size-3.5" />
13187
+ {t("designEditor.export")}
13188
+ </span>
13189
+ ),
12642
13190
  content: shareExportTab,
12643
13191
  },
12644
13192
  {
12645
13193
  value: "send",
12646
- label: "Send to agent" /* i18n-ignore share tab label */,
13194
+ label: (
13195
+ <span className={designShareTabLabelClassName}>
13196
+ <IconTerminal2 className="size-3.5" />
13197
+ {"Send to agent" /* i18n-ignore share tab label */}
13198
+ </span>
13199
+ ),
12647
13200
  content: shareSendToTab,
12648
13201
  },
12649
13202
  ],
@@ -14950,6 +15503,9 @@ ${serializedHtml}
14950
15503
  shareUrl={editorShareUrl}
14951
15504
  shareUrlLabel={t("designEditor.shareEditorLink")}
14952
15505
  shareUrlDescription={t("designEditor.shareEditorLinkDescription")}
15506
+ showShareLinks={false}
15507
+ showDoneButton={false}
15508
+ shareFooterContent={shareLinkFooter}
14953
15509
  shareTabs={designShareTabs}
14954
15510
  popoverClassName={designSharePopoverClassName}
14955
15511
  triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
@@ -15349,6 +15905,9 @@ ${serializedHtml}
15349
15905
  shareUrlDescription={t(
15350
15906
  "designEditor.shareEditorLinkDescription",
15351
15907
  )}
15908
+ showShareLinks={false}
15909
+ showDoneButton={false}
15910
+ shareFooterContent={shareLinkFooter}
15352
15911
  shareTabs={designShareTabs}
15353
15912
  popoverClassName={designSharePopoverClassName}
15354
15913
  triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
@@ -15445,6 +16004,7 @@ ${serializedHtml}
15445
16004
  t("chat.suggestionMobile"),
15446
16005
  ]}
15447
16006
  scope={designChatScope}
16007
+ showScopeBadge={false}
15448
16008
  browserTabId={browserTabId}
15449
16009
  />
15450
16010
  </div>
@@ -15665,6 +16225,49 @@ ${serializedHtml}
15665
16225
  }}
15666
16226
  />
15667
16227
  )}
16228
+ {showPendingVisualStyleApply ? (
16229
+ <div className="pointer-events-none absolute bottom-5 right-5 z-[70] flex items-end">
16230
+ <DropdownMenu>
16231
+ <DropdownMenuTrigger asChild>
16232
+ <Button
16233
+ 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"
16234
+ aria-label={t(
16235
+ "designEditor.pendingVisualStyles.applyAria",
16236
+ )}
16237
+ >
16238
+ <IconBrush className="h-4 w-4" />
16239
+ {t("designEditor.pendingVisualStyles.applyButton")}
16240
+ <span className="rounded bg-white/20 px-1.5 py-0.5 text-xs font-semibold text-white">
16241
+ {pendingVisualStylePropertyCount}
16242
+ </span>
16243
+ <IconChevronDown className="h-4 w-4" />
16244
+ </Button>
16245
+ </DropdownMenuTrigger>
16246
+ <DropdownMenuContent
16247
+ align="end"
16248
+ className="design-editor-app-menu-content w-64"
16249
+ >
16250
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
16251
+ {t("designEditor.pendingVisualStyles.previewLabel")}
16252
+ </DropdownMenuLabel>
16253
+ <DropdownMenuItem
16254
+ onClick={handleApplyPendingVisualStylesWithAgent}
16255
+ >
16256
+ <IconMessage className="mr-2 h-4 w-4" />
16257
+ {t(
16258
+ "designEditor.pendingVisualStyles.applyWithAgent",
16259
+ )}
16260
+ </DropdownMenuItem>
16261
+ <DropdownMenuItem
16262
+ onClick={handleCopyPendingVisualStylePrompt}
16263
+ >
16264
+ <IconClipboard className="mr-2 h-4 w-4" />
16265
+ {t("designEditor.pendingVisualStyles.copyPrompt")}
16266
+ </DropdownMenuItem>
16267
+ </DropdownMenuContent>
16268
+ </DropdownMenu>
16269
+ </div>
16270
+ ) : null}
15668
16271
  {viewMode === "overview" ? (
15669
16272
  <MultiScreenCanvas
15670
16273
  screens={overviewScreens}
@@ -16019,6 +16622,9 @@ ${serializedHtml}
16019
16622
  onElementHover={(info) =>
16020
16623
  handleScreenElementHover(screen.id, info)
16021
16624
  }
16625
+ onEditorDragStateChange={
16626
+ handleEditorDragStateChange
16627
+ }
16022
16628
  onClearSelection={() =>
16023
16629
  handleScreenElementClear(screen.id)
16024
16630
  }
@@ -16139,6 +16745,7 @@ ${serializedHtml}
16139
16745
  onElementSelect={handleElementSelect}
16140
16746
  onElementMarqueeSelect={handleElementMarqueeSelect}
16141
16747
  onElementHover={handleElementHover}
16748
+ onEditorDragStateChange={handleEditorDragStateChange}
16142
16749
  onClearSelection={() => {
16143
16750
  setSelectedElement(null);
16144
16751
  setHoveredElement(null);
@@ -16399,6 +17006,37 @@ ${serializedHtml}
16399
17006
  ) : null}
16400
17007
  </div>
16401
17008
 
17009
+ <AlertDialog open={pendingVisualStyleWarningOpen}>
17010
+ <AlertDialogContent>
17011
+ <AlertDialogHeader>
17012
+ <AlertDialogTitle>
17013
+ {t("designEditor.pendingVisualStyles.leaveTitle")}
17014
+ </AlertDialogTitle>
17015
+ <AlertDialogDescription>
17016
+ {t(
17017
+ pendingVisualStylePropertyCount === 1
17018
+ ? "designEditor.pendingVisualStyles.leaveDescriptionOne"
17019
+ : "designEditor.pendingVisualStyles.leaveDescriptionOther",
17020
+ { count: pendingVisualStylePropertyCount },
17021
+ )}
17022
+ </AlertDialogDescription>
17023
+ </AlertDialogHeader>
17024
+ <AlertDialogFooter>
17025
+ <AlertDialogCancel
17026
+ onClick={handleStayOnPendingVisualStyleNavigation}
17027
+ >
17028
+ {t("designEditor.pendingVisualStyles.stay")}
17029
+ </AlertDialogCancel>
17030
+ <AlertDialogAction
17031
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
17032
+ onClick={handleDiscardPendingVisualStylesAndNavigate}
17033
+ >
17034
+ {t("designEditor.pendingVisualStyles.leave")}
17035
+ </AlertDialogAction>
17036
+ </AlertDialogFooter>
17037
+ </AlertDialogContent>
17038
+ </AlertDialog>
17039
+
16402
17040
  {/* Motion dock (§6.3) — bottom timeline mounted while opening, open, or
16403
17041
  closing. Canvas remains visible above.
16404
17042
  Preview-only scrubbing fires a motion-preview postMessage to the