@agent-native/core 0.81.1 → 0.81.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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/frame.ts +55 -0
  5. package/corpus/core/src/mcp/build-server.ts +30 -4
  6. package/corpus/core/src/mcp/embed-app.ts +33 -18
  7. package/corpus/core/src/server/core-routes-plugin.ts +4 -5
  8. package/corpus/core/src/server/create-server.ts +3 -5
  9. package/corpus/core/src/server/google-oauth.ts +4 -0
  10. package/corpus/templates/calendar/actions/create-event.ts +9 -2
  11. package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
  12. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
  13. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
  14. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
  15. package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
  17. package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
  18. package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
  19. package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
  20. package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
  21. package/corpus/templates/design/actions/run-design-audit.ts +9 -2
  22. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
  23. package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
  24. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
  25. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
  26. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
  27. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
  28. package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
  29. package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
  30. package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
  31. package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
  32. package/corpus/templates/design/e2e/global-setup.ts +3 -1
  33. package/corpus/templates/design/e2e/helpers.ts +4 -1
  34. package/corpus/templates/design/playwright.config.ts +4 -1
  35. package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
  36. package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
  37. package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
  38. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  39. package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
  40. package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
  41. package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
  42. package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
  43. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  44. package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
  45. package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
  46. package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
  47. package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
  48. package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
  49. package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
  50. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
  51. package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
  52. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  53. package/corpus/templates/plan/actions/visual-answer.ts +1 -1
  54. package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
  55. package/corpus/templates/plan/app/entry.client.tsx +10 -0
  56. package/corpus/templates/plan/app/global.css +28 -0
  57. package/corpus/templates/plan/shared/plan-content.ts +10 -6
  58. package/dist/client/frame.d.ts.map +1 -1
  59. package/dist/client/frame.js +59 -0
  60. package/dist/client/frame.js.map +1 -1
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/awareness.d.ts.map +1 -1
  63. package/dist/collab/routes.d.ts +1 -1
  64. package/dist/mcp/build-server.d.ts.map +1 -1
  65. package/dist/mcp/build-server.js +31 -4
  66. package/dist/mcp/build-server.js.map +1 -1
  67. package/dist/mcp/embed-app.d.ts.map +1 -1
  68. package/dist/mcp/embed-app.js +33 -18
  69. package/dist/mcp/embed-app.js.map +1 -1
  70. package/dist/notifications/routes.d.ts +3 -3
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +3 -3
  74. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  75. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  76. package/dist/server/core-routes-plugin.js +1 -2
  77. package/dist/server/core-routes-plugin.js.map +1 -1
  78. package/dist/server/create-server.d.ts.map +1 -1
  79. package/dist/server/create-server.js +1 -2
  80. package/dist/server/create-server.js.map +1 -1
  81. package/dist/server/google-oauth.d.ts +2 -0
  82. package/dist/server/google-oauth.d.ts.map +1 -1
  83. package/dist/server/google-oauth.js +3 -0
  84. package/dist/server/google-oauth.js.map +1 -1
  85. package/dist/server/transcribe-voice.d.ts +1 -1
  86. package/package.json +1 -1
@@ -778,6 +778,18 @@ interface CodingHandoffResult {
778
778
  fileCount?: number;
779
779
  }
780
780
 
781
+ interface CanvasLayerClipboardEntry {
782
+ html: string;
783
+ rootNodeId?: string;
784
+ sourceFileId: string;
785
+ }
786
+
787
+ interface SelectedCanvasLayerSnapshot extends CanvasLayerClipboardEntry {
788
+ node: CodeLayerNode;
789
+ sourceIndex: number;
790
+ tree: CodeLayerTreeNode[];
791
+ }
792
+
781
793
  export interface MotionTimelineRow {
782
794
  id: string | null;
783
795
  designId: string;
@@ -870,12 +882,35 @@ interface GeometryHistoryEntry {
870
882
  after: CanvasFrameGeometryById;
871
883
  }
872
884
 
873
- interface ContentHistoryEntry {
885
+ export interface ContentHistoryChange {
874
886
  fileId: string;
875
887
  before: string;
876
888
  after: string;
877
889
  }
878
890
 
891
+ export interface ContentHistoryGroup {
892
+ changes: ContentHistoryChange[];
893
+ }
894
+
895
+ export type ContentHistoryEntry = ContentHistoryChange | ContentHistoryGroup;
896
+
897
+ export function getContentHistoryChanges(
898
+ entry: ContentHistoryEntry,
899
+ ): ContentHistoryChange[] {
900
+ return "changes" in entry ? entry.changes : [entry];
901
+ }
902
+
903
+ export function getAvailableContentHistoryChanges(
904
+ entry: ContentHistoryEntry,
905
+ availableFileIds: Iterable<string>,
906
+ activeFileId?: string | null,
907
+ ): ContentHistoryChange[] {
908
+ const fileIds = new Set(availableFileIds);
909
+ return getContentHistoryChanges(entry).filter(
910
+ (change) => change.fileId === activeFileId || fileIds.has(change.fileId),
911
+ );
912
+ }
913
+
879
914
  type PatchProofStatus =
880
915
  | "runtime"
881
916
  | "queued"
@@ -1328,7 +1363,12 @@ function normalizedDesignFileType(
1328
1363
 
1329
1364
  function nextBlankScreenFilename(files: DesignFile[]): string {
1330
1365
  const existing = new Set(files.map((file) => file.filename));
1331
- let index = files.length + 1;
1366
+ const screenCount = files.filter(
1367
+ (file) =>
1368
+ normalizedDesignFileType(file.fileType) === "html" &&
1369
+ !isBoardFile(file.filename),
1370
+ ).length;
1371
+ let index = screenCount + 1;
1332
1372
  let candidate = `screen-${index}.html`;
1333
1373
  while (existing.has(candidate)) {
1334
1374
  index += 1;
@@ -1438,6 +1478,9 @@ function appendCanvasPrimitiveToHtml(
1438
1478
  const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
1439
1479
  const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1440
1480
  const markerId = `${nodeId}-arrow`;
1481
+ const explicitPathData = primitive.pathData?.trim()
1482
+ ? primitive.pathData
1483
+ : null;
1441
1484
  const points = primitive.points?.length
1442
1485
  ? primitive.points
1443
1486
  : [
@@ -1448,7 +1491,7 @@ function appendCanvasPrimitiveToHtml(
1448
1491
  const originY = Math.min(...points.map((point) => point.y));
1449
1492
  path.setAttribute(
1450
1493
  "d",
1451
- primitive.pathData ??
1494
+ explicitPathData ??
1452
1495
  points
1453
1496
  .map((point, index) => {
1454
1497
  const command = index === 0 ? "M" : "L";
@@ -1495,7 +1538,13 @@ function appendCanvasPrimitiveToHtml(
1495
1538
  }
1496
1539
  svg.setAttribute("data-agent-native-node-id", nodeId);
1497
1540
  svg.setAttribute("data-agent-native-layer-name", layerName);
1498
- svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1541
+ svg.setAttribute("data-an-primitive", primitive.kind);
1542
+ svg.setAttribute(
1543
+ "viewBox",
1544
+ explicitPathData
1545
+ ? `${left} ${top} ${width} ${height}`
1546
+ : `0 0 ${width} ${height}`,
1547
+ );
1499
1548
  svg.setAttribute(
1500
1549
  "style",
1501
1550
  [
@@ -1667,44 +1716,126 @@ function cloneHtmlLayerAtPosition(
1667
1716
  layerHtml: string,
1668
1717
  position: { x: number; y: number },
1669
1718
  ): string | null {
1670
- if (typeof window === "undefined") return null;
1671
- try {
1672
- const doc = new DOMParser().parseFromString(content, "text/html");
1673
- const layerDoc = new DOMParser().parseFromString(
1674
- `<template>${layerHtml}</template>`,
1675
- "text/html",
1676
- );
1677
- const source =
1678
- layerDoc.querySelector("template")?.content.firstElementChild ??
1679
- layerDoc.body.firstElementChild;
1680
- if (!source || !doc.body) return null;
1681
- const clone = doc.importNode(source, true) as HTMLElement | SVGElement;
1682
- const clonedNodes = [
1683
- clone,
1684
- ...Array.from(clone.querySelectorAll("[data-agent-native-node-id]")),
1685
- ] as Array<HTMLElement | SVGElement>;
1686
- clonedNodes.forEach((node, index) => {
1719
+ return (
1720
+ insertClonedHtmlLayers(content, [layerHtml], {
1721
+ positions: [position],
1722
+ })?.content ?? null
1723
+ );
1724
+ }
1725
+
1726
+ function styleHost(element: Element): (HTMLElement | SVGElement) | null {
1727
+ return element instanceof HTMLElement || element instanceof SVGElement
1728
+ ? element
1729
+ : null;
1730
+ }
1731
+
1732
+ function clearRootLayerPosition(element: Element) {
1733
+ const host = styleHost(element);
1734
+ if (!host) return;
1735
+ host.style.position = "";
1736
+ host.style.left = "";
1737
+ host.style.top = "";
1738
+ host.style.right = "";
1739
+ host.style.bottom = "";
1740
+ }
1741
+
1742
+ function setRootLayerPosition(
1743
+ element: Element,
1744
+ position: { x: number; y: number },
1745
+ ) {
1746
+ const host = styleHost(element);
1747
+ if (!host) return;
1748
+ // Use explicit style property assignments rather than prepending a raw
1749
+ // string. Prepending creates duplicate CSS properties in the same style
1750
+ // attribute, and in CSS the LAST occurrence wins, so existing left/top
1751
+ // values from the cloned element would override the new position.
1752
+ host.style.position = "absolute";
1753
+ host.style.left = `${Math.max(0, Math.round(position.x))}px`;
1754
+ host.style.top = `${Math.max(0, Math.round(position.y))}px`;
1755
+ host.style.right = "";
1756
+ host.style.bottom = "";
1757
+ }
1758
+
1759
+ function prepareClonedHtmlLayer(
1760
+ doc: Document,
1761
+ layerHtml: string,
1762
+ ): { element: Element; rootNodeId: string } | null {
1763
+ const layerDoc = new DOMParser().parseFromString(
1764
+ `<template>${layerHtml}</template>`,
1765
+ "text/html",
1766
+ );
1767
+ const source =
1768
+ layerDoc.querySelector("template")?.content.firstElementChild ??
1769
+ layerDoc.body.firstElementChild;
1770
+ if (!source) return null;
1771
+ const clone = doc.importNode(source, true) as Element;
1772
+ const rootNodeId = uniqueLayerId("copy");
1773
+ clone.setAttribute("data-agent-native-node-id", rootNodeId);
1774
+ Array.from(clone.querySelectorAll("[data-agent-native-node-id]")).forEach(
1775
+ (node) => {
1687
1776
  node.setAttribute(
1688
1777
  "data-agent-native-node-id",
1689
- uniqueLayerId(index === 0 ? "copy" : "copy-child"),
1778
+ uniqueLayerId("copy-child"),
1690
1779
  );
1780
+ },
1781
+ );
1782
+ return { element: clone, rootNodeId };
1783
+ }
1784
+
1785
+ function insertClonedHtmlLayers(
1786
+ content: string,
1787
+ layerHtmls: string[],
1788
+ options: {
1789
+ targetSelectors?: string[];
1790
+ anchorSelectors?: string[];
1791
+ placement?: "before" | "after" | "inside";
1792
+ stripRootPosition?: boolean;
1793
+ positions?: Array<{ x: number; y: number } | null | undefined>;
1794
+ } = {},
1795
+ ): { content: string; rootNodeIds: string[] } | null {
1796
+ if (typeof window === "undefined" || layerHtmls.length === 0) return null;
1797
+ try {
1798
+ const doc = new DOMParser().parseFromString(content, "text/html");
1799
+ if (!doc.body) return null;
1800
+ const fragment = doc.createDocumentFragment();
1801
+ const rootNodeIds: string[] = [];
1802
+ layerHtmls.forEach((layerHtml, index) => {
1803
+ const prepared = prepareClonedHtmlLayer(doc, layerHtml);
1804
+ if (!prepared) return;
1805
+ const position = options.positions?.[index];
1806
+ if (position) {
1807
+ setRootLayerPosition(prepared.element, position);
1808
+ } else if (options.stripRootPosition) {
1809
+ clearRootLayerPosition(prepared.element);
1810
+ }
1811
+ rootNodeIds.push(prepared.rootNodeId);
1812
+ fragment.appendChild(prepared.element);
1691
1813
  });
1692
- if (clone instanceof HTMLElement || clone instanceof SVGElement) {
1693
- // Use explicit style property assignments rather than prepending a raw
1694
- // string. Prepending creates duplicate CSS properties in the same style
1695
- // attribute, and in CSS the LAST occurrence wins, so existing left/top
1696
- // values from the cloned element would override the new position.
1697
- const cloneStyle = (clone as HTMLElement | SVGElement).style;
1698
- cloneStyle.position = "absolute";
1699
- cloneStyle.left = `${Math.max(0, Math.round(position.x))}px`;
1700
- cloneStyle.top = `${Math.max(0, Math.round(position.y))}px`;
1701
- // Clear conflicting properties that could shift the element away from
1702
- // the intended position.
1703
- cloneStyle.right = "";
1704
- cloneStyle.bottom = "";
1814
+ if (rootNodeIds.length === 0) return null;
1815
+
1816
+ const target = queryFirstSelector(doc, options.targetSelectors ?? []);
1817
+ const anchor =
1818
+ queryFirstSelector(doc, options.anchorSelectors ?? []) ?? target;
1819
+ const placement = options.placement ?? "after";
1820
+ if (!anchor) {
1821
+ doc.body.appendChild(fragment);
1822
+ } else if (placement === "inside") {
1823
+ anchor.appendChild(fragment);
1824
+ } else if (placement === "before") {
1825
+ if (anchor.parentElement)
1826
+ anchor.parentElement.insertBefore(fragment, anchor);
1827
+ else doc.body.appendChild(fragment);
1828
+ } else {
1829
+ if (anchor.parentElement) {
1830
+ anchor.parentElement.insertBefore(fragment, anchor.nextSibling);
1831
+ } else {
1832
+ doc.body.appendChild(fragment);
1833
+ }
1705
1834
  }
1706
- doc.body.appendChild(clone);
1707
- return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
1835
+ return {
1836
+ content: `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`,
1837
+ rootNodeIds,
1838
+ };
1708
1839
  } catch {
1709
1840
  return null;
1710
1841
  }
@@ -1748,52 +1879,13 @@ function insertClonedHtmlLayer(
1748
1879
  placement?: "before" | "after" | "inside";
1749
1880
  },
1750
1881
  ): string | null {
1751
- if (typeof window === "undefined") return null;
1752
- try {
1753
- const doc = new DOMParser().parseFromString(content, "text/html");
1754
- const layerDoc = new DOMParser().parseFromString(
1755
- `<template>${cloneHtml}</template>`,
1756
- "text/html",
1757
- );
1758
- const source =
1759
- layerDoc.querySelector("template")?.content.firstElementChild ??
1760
- layerDoc.body.firstElementChild;
1761
- if (!source || !doc.body) return null;
1762
- const clone = doc.importNode(source, true) as HTMLElement | SVGElement;
1763
- const clonedNodes = [
1764
- clone,
1765
- ...Array.from(clone.querySelectorAll("[data-agent-native-node-id]")),
1766
- ] as Array<HTMLElement | SVGElement>;
1767
- clonedNodes.forEach((node, index) => {
1768
- node.setAttribute(
1769
- "data-agent-native-node-id",
1770
- uniqueLayerId(index === 0 ? "copy" : "copy-child"),
1771
- );
1772
- });
1773
-
1774
- const target = queryFirstSelector(doc, options.targetSelectors);
1775
- const anchor =
1776
- queryFirstSelector(doc, options.anchorSelectors ?? []) ?? target;
1777
- const placement = options.placement ?? "after";
1778
- if (!anchor) {
1779
- doc.body.appendChild(clone);
1780
- } else if (placement === "inside") {
1781
- anchor.appendChild(clone);
1782
- } else if (placement === "before") {
1783
- if (anchor.parentElement)
1784
- anchor.parentElement.insertBefore(clone, anchor);
1785
- else doc.body.appendChild(clone);
1786
- } else {
1787
- if (anchor.parentElement) {
1788
- anchor.parentElement.insertBefore(clone, anchor.nextSibling);
1789
- } else {
1790
- doc.body.appendChild(clone);
1791
- }
1792
- }
1793
- return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
1794
- } catch {
1795
- return null;
1796
- }
1882
+ return (
1883
+ insertClonedHtmlLayers(content, [cloneHtml], {
1884
+ targetSelectors: options.targetSelectors,
1885
+ anchorSelectors: options.anchorSelectors,
1886
+ placement: options.placement,
1887
+ })?.content ?? null
1888
+ );
1797
1889
  }
1798
1890
 
1799
1891
  function getElementOuterHtml(content: string, selector: string): string | null {
@@ -1835,6 +1927,55 @@ function extractLayerPosition(
1835
1927
  }
1836
1928
  }
1837
1929
 
1930
+ function postBeginTextEditToPreviewIframes(
1931
+ screenId: string | null,
1932
+ nodeId: string,
1933
+ ): boolean {
1934
+ if (typeof document === "undefined" || !nodeId) return false;
1935
+ const iframes = Array.from(
1936
+ document.querySelectorAll<HTMLIFrameElement>(
1937
+ "iframe[data-design-preview-iframe]",
1938
+ ),
1939
+ );
1940
+ const targetIframes = iframes.filter(
1941
+ (iframe) => screenId && iframe.dataset.screenIframeId === screenId,
1942
+ );
1943
+ const orderedIframes = targetIframes.length > 0 ? targetIframes : iframes;
1944
+ const selector = `[data-agent-native-node-id="${nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"][data-agent-native-text-editing]`;
1945
+ if (
1946
+ orderedIframes.some((iframe) => {
1947
+ try {
1948
+ return iframe.contentDocument?.querySelector(selector);
1949
+ } catch {
1950
+ return false;
1951
+ }
1952
+ })
1953
+ ) {
1954
+ return true;
1955
+ }
1956
+ orderedIframes.forEach((iframe) => {
1957
+ iframe.contentWindow?.postMessage(
1958
+ { type: "begin-text-edit", nodeId, force: true },
1959
+ "*",
1960
+ );
1961
+ });
1962
+ return false;
1963
+ }
1964
+
1965
+ function scheduleBeginTextEditForScreen(
1966
+ screenId: string | null,
1967
+ nodeId: string,
1968
+ ) {
1969
+ if (typeof window === "undefined") return;
1970
+ let sawEditing = false;
1971
+ [50, 150, 300, 600, 900, 1200, 1800, 2400, 3200, 4200].forEach((delay) => {
1972
+ window.setTimeout(() => {
1973
+ if (sawEditing) return;
1974
+ sawEditing = postBeginTextEditToPreviewIframes(screenId, nodeId);
1975
+ }, delay);
1976
+ });
1977
+ }
1978
+
1838
1979
  function removeElementFromHtml(
1839
1980
  content: string,
1840
1981
  selector: string,
@@ -3533,6 +3674,7 @@ export default function DesignEditor() {
3533
3674
  useState<{ screenId: string; layerId: string } | null>(null);
3534
3675
  const pendingOverviewScreenSelectionRef = useRef<string | null>(null);
3535
3676
  const pendingOverviewLayerSelectionRef = useRef<string | null>(null);
3677
+ const lastOverviewSelectedScreenIdsRef = useRef<string[]>([]);
3536
3678
  // Tracks the nodeId of the most recently created TEXT primitive across one
3537
3679
  // handleCreatePrimitive → handlePrimitiveCreated round-trip. Cleared after
3538
3680
  // use. Lets handlePrimitiveCreated trigger begin-text-edit without needing
@@ -3616,6 +3758,7 @@ export default function DesignEditor() {
3616
3758
  useState(0);
3617
3759
  const [hasCanvasClipboard, setHasCanvasClipboard] = useState(false);
3618
3760
  const [hasPropsClipboard, setHasPropsClipboard] = useState(false);
3761
+ const copiedLayerEntriesRef = useRef<CanvasLayerClipboardEntry[]>([]);
3619
3762
  const copiedLayerHtmlRef = useRef<string | null>(null);
3620
3763
  // Cascade offset for repeated keyboard pastes so successive clones don't stack
3621
3764
  // pixel-perfectly on top of each other. Reset on each fresh copy/cut.
@@ -3850,10 +3993,13 @@ export default function DesignEditor() {
3850
3993
  }, []);
3851
3994
  const recordContentHistoryEntry = useCallback(
3852
3995
  (entry: ContentHistoryEntry) => {
3853
- if (entry.before === entry.after) return;
3996
+ const changes = getContentHistoryChanges(entry).filter(
3997
+ (change) => change.before !== change.after,
3998
+ );
3999
+ if (changes.length === 0) return;
3854
4000
  contentUndoStackRef.current = [
3855
4001
  ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
3856
- entry,
4002
+ changes.length === 1 ? changes[0] : { changes },
3857
4003
  ];
3858
4004
  contentRedoStackRef.current = [];
3859
4005
  historyOrderRef.current = [
@@ -3910,6 +4056,13 @@ export default function DesignEditor() {
3910
4056
  useEffect(() => {
3911
4057
  viewModeRef.current = viewMode;
3912
4058
  }, [viewMode]);
4059
+
4060
+ useEffect(() => {
4061
+ if (viewMode !== "overview" || overviewSelectedScreenIds.length === 0) {
4062
+ return;
4063
+ }
4064
+ lastOverviewSelectedScreenIdsRef.current = [...overviewSelectedScreenIds];
4065
+ }, [overviewSelectedScreenIds, viewMode]);
3913
4066
  const [hasPendingGeneration, setHasPendingGeneration] = useState(() =>
3914
4067
  hasFreshPendingGeneration(id),
3915
4068
  );
@@ -6733,20 +6886,24 @@ export default function DesignEditor() {
6733
6886
  skipPreview?: boolean;
6734
6887
  immediateSave?: boolean;
6735
6888
  persist?: boolean;
6889
+ recordHistory?: boolean;
6736
6890
  updatedAt?: string;
6737
6891
  } = {},
6738
6892
  ) => {
6739
6893
  if (!activeFile || !canEditDesignRef.current) return;
6894
+ const shouldRecordHistory =
6895
+ options.recordHistory !== false && !options.updatedAt;
6740
6896
  const previousContent =
6741
6897
  collabContentFileIdRef.current === activeFile.id &&
6742
6898
  typeof collabContentRef.current === "string"
6743
6899
  ? collabContentRef.current
6744
6900
  : (activeFile.content ?? "");
6745
6901
  const yjsHistoryAvailable = Boolean(
6746
- ydoc && isSynced && undoManagerRef.current,
6902
+ shouldRecordHistory && ydoc && isSynced && undoManagerRef.current,
6747
6903
  );
6748
6904
  if (
6749
6905
  !suppressContentHistoryRef.current &&
6906
+ shouldRecordHistory &&
6750
6907
  !yjsHistoryAvailable &&
6751
6908
  previousContent !== nextContent
6752
6909
  ) {
@@ -6810,10 +6967,13 @@ export default function DesignEditor() {
6810
6967
  if (ydoc && isSynced) {
6811
6968
  const ytext = ydoc.getText("content");
6812
6969
  if (ytext.toString() !== nextContent) {
6813
- ydoc.transact(() => {
6814
- ytext.delete(0, ytext.length);
6815
- ytext.insert(0, nextContent);
6816
- }, LOCAL_EDIT_ORIGIN);
6970
+ ydoc.transact(
6971
+ () => {
6972
+ ytext.delete(0, ytext.length);
6973
+ ytext.insert(0, nextContent);
6974
+ },
6975
+ shouldRecordHistory ? LOCAL_EDIT_ORIGIN : TAB_ID,
6976
+ );
6817
6977
  }
6818
6978
  }
6819
6979
  if (options.persist === false) {
@@ -6849,6 +7009,7 @@ export default function DesignEditor() {
6849
7009
  refreshPreview?: boolean;
6850
7010
  skipPreview?: boolean;
6851
7011
  persist?: boolean;
7012
+ recordHistory?: boolean;
6852
7013
  updatedAt?: string;
6853
7014
  } = {},
6854
7015
  ) => {
@@ -6858,6 +7019,21 @@ export default function DesignEditor() {
6858
7019
  return;
6859
7020
  }
6860
7021
  const previousFile = files.find((file) => file.id === fileId);
7022
+ const previousContent =
7023
+ getScreenContent(fileId) ?? previousFile?.content ?? "";
7024
+ const shouldRecordHistory =
7025
+ options.recordHistory !== false && !options.updatedAt;
7026
+ if (
7027
+ !suppressContentHistoryRef.current &&
7028
+ shouldRecordHistory &&
7029
+ previousContent !== nextContent
7030
+ ) {
7031
+ recordContentHistoryEntry({
7032
+ fileId,
7033
+ before: previousContent,
7034
+ after: nextContent,
7035
+ });
7036
+ }
6861
7037
  if (options.updatedAt) {
6862
7038
  clearPendingLocalFileContent(fileId);
6863
7039
  } else {
@@ -6902,9 +7078,11 @@ export default function DesignEditor() {
6902
7078
  cancelQueuedFileContentSave,
6903
7079
  clearPendingLocalFileContent,
6904
7080
  files,
7081
+ getScreenContent,
6905
7082
  id,
6906
7083
  markPendingLocalFileContent,
6907
7084
  queryClient,
7085
+ recordContentHistoryEntry,
6908
7086
  saveFileContent,
6909
7087
  ],
6910
7088
  );
@@ -7146,9 +7324,7 @@ export default function DesignEditor() {
7146
7324
  // enter text-edit mode — fixing the "click to add text should let me
7147
7325
  // type immediately" bug. The ref is read once and cleared.
7148
7326
  if (primitive.kind === "text") {
7149
- const textNodeId =
7150
- typeof result === "string" ? result : (primitive.nodeId ?? null);
7151
- pendingTextEditNodeIdRef.current = textNodeId;
7327
+ pendingTextEditNodeIdRef.current = primitive.nodeId ?? null;
7152
7328
  } else {
7153
7329
  pendingTextEditNodeIdRef.current = null;
7154
7330
  }
@@ -7193,17 +7369,15 @@ export default function DesignEditor() {
7193
7369
  });
7194
7370
  // viewMode stays at "overview" — no setViewMode("single") call here.
7195
7371
 
7196
- // Immediately enter text-editing for newly created TEXT primitives so the
7197
- // user can start typing without a second click (fixes auto-size flow too).
7198
- // We read-and-clear the ref set by handleCreatePrimitive just above. The
7199
- // 50 ms delay gives React a tick to commit the new content into the active
7200
- // screen's iframe before the bridge tries to find the element by nodeId.
7201
- const textNodeId = pendingTextEditNodeIdRef.current;
7372
+ // Immediately enter text-editing for newly created TEXT primitives. In
7373
+ // overview mode the target iframe may become active and receive the
7374
+ // inserted HTML over separate renders, so post directly to the target
7375
+ // iframe with a few short retries instead of relying on a single global
7376
+ // bridge callback.
7377
+ const textNodeId = pendingTextEditNodeIdRef.current ?? nodeId;
7202
7378
  pendingTextEditNodeIdRef.current = null;
7203
7379
  if (textNodeId) {
7204
- setTimeout(() => {
7205
- (window as any).__designCanvasBeginTextEdit?.(textNodeId);
7206
- }, 50);
7380
+ scheduleBeginTextEditForScreen(screenId, textNodeId);
7207
7381
  }
7208
7382
  },
7209
7383
  [clearPendingOverviewLayerSelectionTimer],
@@ -7222,28 +7396,14 @@ export default function DesignEditor() {
7222
7396
  if (!result) return false;
7223
7397
 
7224
7398
  // For TEXT primitives drawn on the board surface, immediately enter
7225
- // text-editing mode via begin-text-edit. The board DesignCanvas uses
7226
- // registerRuntimeBridge=false so window.__designCanvasBeginTextEdit is
7227
- // not set; instead we broadcast the message to all preview iframes — the
7228
- // one that contains the new nodeId will handle it, others ignore it.
7229
- // The 50 ms delay lets the board iframe receive its content update first.
7399
+ // text-editing mode via begin-text-edit. The target is the board file,
7400
+ // so use the same iframe-targeted retry path as screen primitives.
7230
7401
  if (primitive.kind === "text") {
7231
7402
  const textNodeId =
7232
7403
  pendingTextEditNodeIdRef.current ?? primitive.nodeId ?? null;
7233
7404
  pendingTextEditNodeIdRef.current = null;
7234
7405
  if (textNodeId) {
7235
- setTimeout(() => {
7236
- document
7237
- .querySelectorAll<HTMLIFrameElement>(
7238
- "iframe[data-design-preview-iframe]",
7239
- )
7240
- .forEach((iframe) => {
7241
- iframe.contentWindow?.postMessage(
7242
- { type: "begin-text-edit", nodeId: textNodeId },
7243
- "*",
7244
- );
7245
- });
7246
- }, 50);
7406
+ scheduleBeginTextEditForScreen(boardFileId, textNodeId);
7247
7407
  }
7248
7408
  }
7249
7409
 
@@ -8935,27 +9095,167 @@ export default function DesignEditor() {
8935
9095
  ],
8936
9096
  );
8937
9097
 
8938
- const handleCopySelection = useCallback(async () => {
8939
- if (!selectedElement?.selector) return;
8940
- const html = getElementOuterHtml(
8941
- getFreshActiveContent(),
8942
- selectedElement.selector,
9098
+ const getSelectedLayerSnapshots = useCallback(() => {
9099
+ const fileIds = new Set(files.map((file) => file.id));
9100
+ const candidateIds = selectedLayerIdsState.filter(
9101
+ (layerId) =>
9102
+ layerId && !layerId.startsWith("__") && !fileIds.has(layerId),
8943
9103
  );
8944
- if (!html) return;
8945
- copiedLayerHtmlRef.current = html;
9104
+ if (
9105
+ selectedElementLayerId &&
9106
+ !candidateIds.includes(selectedElementLayerId)
9107
+ ) {
9108
+ candidateIds.push(selectedElementLayerId);
9109
+ }
9110
+
9111
+ const snapshots: SelectedCanvasLayerSnapshot[] = [];
9112
+ for (const file of files) {
9113
+ const content = getScreenContent(file.id);
9114
+ if (!content) continue;
9115
+ const projection = buildCodeLayerProjection(content);
9116
+ const tree = buildCodeLayerTree(projection);
9117
+ for (const layerId of candidateIds) {
9118
+ const node = projection.nodes.find(
9119
+ (candidate) =>
9120
+ candidate.id === layerId ||
9121
+ candidate.dataAttributes["data-agent-native-node-id"] === layerId,
9122
+ );
9123
+ if (!node?.source) continue;
9124
+ snapshots.push({
9125
+ html: content.slice(node.source.start, node.source.end),
9126
+ rootNodeId:
9127
+ node.dataAttributes["data-agent-native-node-id"] ?? node.id,
9128
+ sourceFileId: file.id,
9129
+ node,
9130
+ sourceIndex: node.source.start,
9131
+ tree,
9132
+ });
9133
+ }
9134
+ }
9135
+
9136
+ if (snapshots.length === 0 && activeFile && selectedElement?.selector) {
9137
+ const content = getFreshActiveContent();
9138
+ const projection = buildCodeLayerProjection(content);
9139
+ const tree = buildCodeLayerTree(projection);
9140
+ const node = resolveCodeLayerNodeFromElementInfo(
9141
+ projection,
9142
+ selectedElement,
9143
+ );
9144
+ const html = node?.source
9145
+ ? content.slice(node.source.start, node.source.end)
9146
+ : getElementOuterHtml(content, selectedElement.selector);
9147
+ if (html && node) {
9148
+ snapshots.push({
9149
+ html,
9150
+ rootNodeId:
9151
+ node.dataAttributes["data-agent-native-node-id"] ??
9152
+ selectedElement.sourceId ??
9153
+ selectedElement.id,
9154
+ sourceFileId: activeFile.id,
9155
+ node,
9156
+ sourceIndex: node.source?.start ?? Number.MAX_SAFE_INTEGER,
9157
+ tree,
9158
+ });
9159
+ }
9160
+ }
9161
+
9162
+ const selectedKeys = new Set(
9163
+ snapshots.map(
9164
+ (snapshot) => `${snapshot.sourceFileId}:${snapshot.node.id}`,
9165
+ ),
9166
+ );
9167
+ const topLevelSnapshots = snapshots.filter(
9168
+ (snapshot) =>
9169
+ !collectCodeLayerAncestors(snapshot.tree, snapshot.node.id).some(
9170
+ (ancestorId) =>
9171
+ selectedKeys.has(`${snapshot.sourceFileId}:${ancestorId}`),
9172
+ ),
9173
+ );
9174
+ const fileOrder = new Map(files.map((file, index) => [file.id, index]));
9175
+ return topLevelSnapshots.sort((a, b) => {
9176
+ const fileDelta =
9177
+ (fileOrder.get(a.sourceFileId) ?? 0) -
9178
+ (fileOrder.get(b.sourceFileId) ?? 0);
9179
+ if (fileDelta !== 0) return fileDelta;
9180
+ return a.sourceIndex - b.sourceIndex;
9181
+ });
9182
+ }, [
9183
+ activeFile,
9184
+ files,
9185
+ getFreshActiveContent,
9186
+ getScreenContent,
9187
+ selectedElement,
9188
+ selectedElementLayerId,
9189
+ selectedLayerIdsState,
9190
+ ]);
9191
+
9192
+ const getCanvasClipboardEntries = useCallback(() => {
9193
+ if (copiedLayerEntriesRef.current.length > 0) {
9194
+ return copiedLayerEntriesRef.current;
9195
+ }
9196
+ return copiedLayerHtmlRef.current
9197
+ ? [
9198
+ {
9199
+ html: copiedLayerHtmlRef.current,
9200
+ sourceFileId: activeFile?.id ?? "",
9201
+ },
9202
+ ]
9203
+ : [];
9204
+ }, [activeFile?.id]);
9205
+
9206
+ const selectInsertedLayers = useCallback(
9207
+ (screenId: string, content: string, rootNodeIds: string[]) => {
9208
+ const projection = buildCodeLayerProjection(content);
9209
+ const insertedNodes = rootNodeIds
9210
+ .map((rootNodeId) =>
9211
+ projection.nodes.find(
9212
+ (node) =>
9213
+ node.id === rootNodeId ||
9214
+ node.dataAttributes["data-agent-native-node-id"] === rootNodeId,
9215
+ ),
9216
+ )
9217
+ .filter((node): node is CodeLayerNode => Boolean(node));
9218
+ if (insertedNodes.length === 0) return;
9219
+ setActiveFileId(screenId);
9220
+ setSelectedLayerIdsState(insertedNodes.map((node) => node.id));
9221
+ const lastNode = insertedNodes[insertedNodes.length - 1];
9222
+ setSelectedElement(
9223
+ lastNode ? elementInfoFromCodeLayerNode(lastNode) : null,
9224
+ );
9225
+ setActiveTool("move");
9226
+ setMode("edit");
9227
+ if (viewModeRef.current === "overview") {
9228
+ setOverviewSelectedScreenIds([screenId]);
9229
+ }
9230
+ },
9231
+ [],
9232
+ );
9233
+
9234
+ const handleCopySelection = useCallback(async () => {
9235
+ const entries = getSelectedLayerSnapshots().map((snapshot) => ({
9236
+ html: snapshot.html,
9237
+ rootNodeId: snapshot.rootNodeId,
9238
+ sourceFileId: snapshot.sourceFileId,
9239
+ }));
9240
+ if (entries.length === 0) return;
9241
+ const clipboardText = entries.map((entry) => entry.html).join("\n");
9242
+ copiedLayerEntriesRef.current = entries;
9243
+ copiedLayerHtmlRef.current = clipboardText;
8946
9244
  pasteCascadeRef.current = 0;
8947
9245
  setHasCanvasClipboard(true);
8948
9246
  try {
8949
- await navigator.clipboard.writeText(html);
9247
+ await navigator.clipboard.writeText(clipboardText);
8950
9248
  } catch {
8951
9249
  toast.error(t("designEditor.toasts.clipboardBlocked"));
8952
9250
  }
8953
- }, [getFreshActiveContent, selectedElement, t]);
9251
+ }, [getSelectedLayerSnapshots, t]);
8954
9252
 
8955
9253
  const handlePasteSelection = useCallback(
8956
9254
  (position?: { x: number; y: number }) => {
8957
- if (!activeFile || !canEditDesign || !copiedLayerHtmlRef.current) return;
9255
+ const entries = getCanvasClipboardEntries();
9256
+ if (!activeFile || !canEditDesign || entries.length === 0) return;
8958
9257
  const baseContent = getFreshActiveContent();
9258
+ const layerHtmls = entries.map((entry) => entry.html);
8959
9259
 
8960
9260
  // B7 fix: when an element is selected and no explicit canvas position was
8961
9261
  // given, insert the clone as an in-flow sibling right AFTER the selected
@@ -8965,38 +9265,19 @@ export default function DesignEditor() {
8965
9265
  // selected or a "Paste here" position is provided.
8966
9266
  if (!position && selectedElement?.selector) {
8967
9267
  const selector = selectedCanvasSelector ?? selectedElement.selector;
8968
- // Strip position properties from the pasted clone so it becomes an
8969
- // in-flow sibling (not absolute).
8970
- const strippedHtml = (() => {
8971
- try {
8972
- const parser = new DOMParser();
8973
- const tmp = parser.parseFromString(
8974
- `<template>${copiedLayerHtmlRef.current!}</template>`,
8975
- "text/html",
8976
- );
8977
- const root =
8978
- tmp.querySelector("template")?.content.firstElementChild ??
8979
- tmp.body.firstElementChild;
8980
- if (root && root instanceof HTMLElement) {
8981
- root.style.position = "";
8982
- root.style.left = "";
8983
- root.style.top = "";
8984
- root.style.right = "";
8985
- root.style.bottom = "";
8986
- }
8987
- return root?.outerHTML ?? copiedLayerHtmlRef.current!;
8988
- } catch {
8989
- return copiedLayerHtmlRef.current!;
8990
- }
8991
- })();
8992
-
8993
- const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
9268
+ const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
8994
9269
  targetSelectors: [selector],
8995
9270
  placement: "after",
9271
+ stripRootPosition: true,
8996
9272
  });
8997
- if (nextContent) {
9273
+ if (result) {
8998
9274
  pasteCascadeRef.current += 1;
8999
- applyLocalContentUpdate(nextContent);
9275
+ applyLocalContentUpdate(result.content);
9276
+ selectInsertedLayers(
9277
+ activeFile.id,
9278
+ result.content,
9279
+ result.rootNodeIds,
9280
+ );
9000
9281
  return;
9001
9282
  }
9002
9283
  // Fall through to position-based clone if insert failed.
@@ -9005,59 +9286,154 @@ export default function DesignEditor() {
9005
9286
  // Explicit positions (e.g. "Paste here" at the cursor) are honored as-is.
9006
9287
  // Keyboard pastes land near the source layer and cascade so repeats don't
9007
9288
  // stack exactly.
9008
- const targetPosition =
9009
- position ??
9010
- (() => {
9011
- const src = extractLayerPosition(copiedLayerHtmlRef.current!);
9012
- const offset = pasteCascadeRef.current * 16;
9013
- return src
9014
- ? { x: src.x + 10 + offset, y: src.y + 10 + offset }
9015
- : { x: 120 + offset, y: 120 + offset };
9016
- })();
9017
- const nextContent = cloneHtmlLayerAtPosition(
9018
- baseContent,
9019
- copiedLayerHtmlRef.current,
9020
- targetPosition,
9289
+ const sourcePositions = entries.map((entry) =>
9290
+ extractLayerPosition(entry.html),
9291
+ );
9292
+ const positionedSources = sourcePositions.filter(
9293
+ (source): source is { x: number; y: number } => Boolean(source),
9021
9294
  );
9022
- if (!nextContent) return;
9295
+ const minSourceX = positionedSources.length
9296
+ ? Math.min(...positionedSources.map((source) => source.x))
9297
+ : 0;
9298
+ const minSourceY = positionedSources.length
9299
+ ? Math.min(...positionedSources.map((source) => source.y))
9300
+ : 0;
9301
+ const cascadeOffset = pasteCascadeRef.current * 16;
9302
+ const positions = entries.map((_, index) => {
9303
+ const source = sourcePositions[index];
9304
+ if (position) {
9305
+ return source && positionedSources.length
9306
+ ? {
9307
+ x: position.x + source.x - minSourceX,
9308
+ y: position.y + source.y - minSourceY,
9309
+ }
9310
+ : { x: position.x + index * 16, y: position.y + index * 16 };
9311
+ }
9312
+ return source
9313
+ ? {
9314
+ x: source.x + 10 + cascadeOffset,
9315
+ y: source.y + 10 + cascadeOffset,
9316
+ }
9317
+ : {
9318
+ x: 120 + cascadeOffset + index * 16,
9319
+ y: 120 + cascadeOffset + index * 16,
9320
+ };
9321
+ });
9322
+ const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
9323
+ positions,
9324
+ });
9325
+ if (!result) return;
9023
9326
  if (!position) pasteCascadeRef.current += 1;
9024
- applyLocalContentUpdate(nextContent);
9327
+ applyLocalContentUpdate(result.content);
9328
+ selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
9025
9329
  },
9026
9330
  [
9027
9331
  activeFile,
9028
9332
  applyLocalContentUpdate,
9029
9333
  canEditDesign,
9334
+ getCanvasClipboardEntries,
9030
9335
  getFreshActiveContent,
9336
+ selectInsertedLayers,
9031
9337
  selectedCanvasSelector,
9032
9338
  selectedElement,
9033
9339
  ],
9034
9340
  );
9035
9341
 
9036
9342
  const handlePasteOverSelection = useCallback(() => {
9037
- if (!activeFile || !copiedLayerHtmlRef.current) return;
9343
+ const entries = getCanvasClipboardEntries();
9344
+ if (!activeFile || entries.length === 0) return;
9038
9345
  const baseContent = getFreshActiveContent();
9039
9346
  if (selectedElement?.boundingRect) {
9040
9347
  const { x, y } = selectedElement.boundingRect;
9041
- const nextContent = cloneHtmlLayerAtPosition(
9348
+ const result = insertClonedHtmlLayers(
9042
9349
  baseContent,
9043
- copiedLayerHtmlRef.current,
9044
- { x, y },
9350
+ entries.map((entry) => entry.html),
9351
+ {
9352
+ positions: entries.map((_, index) => ({
9353
+ x: x + index * 16,
9354
+ y: y + index * 16,
9355
+ })),
9356
+ },
9045
9357
  );
9046
- if (!nextContent) return;
9047
- applyLocalContentUpdate(nextContent);
9358
+ if (!result) return;
9359
+ applyLocalContentUpdate(result.content);
9360
+ selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
9048
9361
  } else {
9049
9362
  handlePasteSelection();
9050
9363
  }
9051
9364
  }, [
9052
9365
  activeFile,
9053
9366
  applyLocalContentUpdate,
9367
+ getCanvasClipboardEntries,
9054
9368
  getFreshActiveContent,
9055
9369
  handlePasteSelection,
9370
+ selectInsertedLayers,
9056
9371
  selectedElement,
9057
9372
  ]);
9058
9373
 
9059
9374
  const handleDuplicateSelection = useCallback(() => {
9060
9375
  if (!canEditDesign) return;
9376
+ const snapshots = getSelectedLayerSnapshots();
9377
+ if (snapshots.length > 0) {
9378
+ const selectedIds: string[] = [];
9379
+ const selectedScreenIds: string[] = [];
9380
+ let lastActiveNode: CodeLayerNode | null = null;
9381
+
9382
+ for (const file of files) {
9383
+ const group = snapshots.filter(
9384
+ (snapshot) => snapshot.sourceFileId === file.id,
9385
+ );
9386
+ if (group.length === 0) continue;
9387
+ let content = getScreenContent(file.id);
9388
+ const insertedRootNodeIds: string[] = [];
9389
+ for (const snapshot of [...group].sort(
9390
+ (a, b) => b.sourceIndex - a.sourceIndex,
9391
+ )) {
9392
+ const projection = buildCodeLayerProjection(content);
9393
+ const anchorNode =
9394
+ projection.nodes.find(
9395
+ (node) =>
9396
+ node.id === snapshot.node.id ||
9397
+ node.dataAttributes["data-agent-native-node-id"] ===
9398
+ snapshot.rootNodeId,
9399
+ ) ?? snapshot.node;
9400
+ const result = insertClonedHtmlLayers(content, [snapshot.html], {
9401
+ targetSelectors: codeLayerSelectorAliases(anchorNode),
9402
+ placement: "after",
9403
+ stripRootPosition: true,
9404
+ });
9405
+ if (!result) continue;
9406
+ content = result.content;
9407
+ insertedRootNodeIds.unshift(...result.rootNodeIds);
9408
+ }
9409
+ if (insertedRootNodeIds.length === 0) continue;
9410
+ applyFileContentUpdate(file.id, content, { refreshPreview: false });
9411
+ selectedScreenIds.push(file.id);
9412
+ const finalProjection = buildCodeLayerProjection(content);
9413
+ insertedRootNodeIds.forEach((rootNodeId) => {
9414
+ const insertedNode = finalProjection.nodes.find(
9415
+ (node) =>
9416
+ node.id === rootNodeId ||
9417
+ node.dataAttributes["data-agent-native-node-id"] === rootNodeId,
9418
+ );
9419
+ if (!insertedNode) return;
9420
+ selectedIds.push(insertedNode.id);
9421
+ if (file.id === activeFile?.id) lastActiveNode = insertedNode;
9422
+ });
9423
+ }
9424
+
9425
+ if (selectedIds.length > 0) {
9426
+ setSelectedLayerIdsState(selectedIds);
9427
+ setSelectedElement(
9428
+ lastActiveNode ? elementInfoFromCodeLayerNode(lastActiveNode) : null,
9429
+ );
9430
+ if (viewModeRef.current === "overview") {
9431
+ setOverviewSelectedScreenIds(selectedScreenIds);
9432
+ }
9433
+ return;
9434
+ }
9435
+ }
9436
+
9061
9437
  if (selectedElement?.selector) {
9062
9438
  const baseContent = getFreshActiveContent();
9063
9439
  const html = getElementOuterHtml(baseContent, selectedElement.selector);
@@ -9105,9 +9481,13 @@ export default function DesignEditor() {
9105
9481
  if (activeFile) handleDuplicateScreen(activeFile.id);
9106
9482
  }, [
9107
9483
  activeFile,
9484
+ applyFileContentUpdate,
9108
9485
  applyLocalContentUpdate,
9109
9486
  canEditDesign,
9487
+ files,
9110
9488
  getFreshActiveContent,
9489
+ getScreenContent,
9490
+ getSelectedLayerSnapshots,
9111
9491
  handleDuplicateScreen,
9112
9492
  selectedCanvasSelector,
9113
9493
  selectedElement,
@@ -9116,65 +9496,89 @@ export default function DesignEditor() {
9116
9496
 
9117
9497
  const handleDeleteSelection = useCallback(() => {
9118
9498
  if (!canEditDesign) return;
9119
- const baseContent = getFreshActiveContent();
9120
- // Multi-select delete: when several DOM/code layers are selected in the
9121
- // panel, remove all of them — not just the single focused element. Compose
9122
- // the removals against the running content (re-projecting each pass) so
9123
- // nested selections resolve correctly and earlier removals aren't clobbered.
9124
- const candidateIds = selectedLayerIdsState.filter(
9125
- (layerId) =>
9126
- layerId &&
9127
- !layerId.startsWith("__") &&
9128
- !files.some((file) => file.id === layerId),
9129
- );
9130
- if (candidateIds.length > 1) {
9131
- let content = baseContent;
9132
- const removedSelectors: string[] = [];
9133
- for (const layerId of candidateIds) {
9499
+ const snapshots = getSelectedLayerSnapshots();
9500
+ if (snapshots.length > 0) {
9501
+ const activeRuntimeSelectors: string[] = [];
9502
+ let didDelete = false;
9503
+ for (const file of files) {
9504
+ const group = snapshots.filter(
9505
+ (snapshot) => snapshot.sourceFileId === file.id,
9506
+ );
9507
+ if (group.length === 0) continue;
9508
+ const originalContent = getScreenContent(file.id);
9509
+ let content = originalContent;
9134
9510
  const projection = buildCodeLayerProjection(content);
9135
- const node =
9136
- projection.nodes.find((candidate) => candidate.id === layerId) ??
9137
- resolveCodeLayerNodeFromBridge(projection, layerId, layerId);
9138
- if (!node) continue;
9139
- const next = removeCodeLayerNodeFromHtml(content, node);
9140
- if (!next) continue;
9141
- const selector = preferredCodeLayerSelector(node);
9142
- if (selector) removedSelectors.push(selector);
9143
- content = next;
9144
- }
9145
- if (content !== baseContent) {
9146
- removedSelectors.forEach((selector) => deleteRuntimeElement(selector));
9147
- applyLocalContentUpdate(content, { refreshPreview: false });
9148
- setSelectedElement(null);
9149
- setSelectedLayerIdsState([]);
9150
- return;
9511
+ const tree = buildCodeLayerTree(projection);
9512
+ const selectedNodeIds = new Set(
9513
+ group.map((snapshot) => snapshot.node.id),
9514
+ );
9515
+ const nodes = group
9516
+ .map((snapshot) =>
9517
+ projection.nodes.find(
9518
+ (node) =>
9519
+ node.id === snapshot.node.id ||
9520
+ node.dataAttributes["data-agent-native-node-id"] ===
9521
+ snapshot.rootNodeId,
9522
+ ),
9523
+ )
9524
+ .filter((node): node is CodeLayerNode => Boolean(node?.source))
9525
+ .filter(
9526
+ (node) =>
9527
+ !collectCodeLayerAncestors(tree, node.id).some((ancestorId) =>
9528
+ selectedNodeIds.has(ancestorId),
9529
+ ),
9530
+ )
9531
+ .sort((a, b) => (b.source?.start ?? 0) - (a.source?.start ?? 0));
9532
+ if (nodes.length === 0) continue;
9533
+ const removedSelectors: string[] = [];
9534
+ for (const node of nodes) {
9535
+ const next = removeCodeLayerNodeFromHtml(content, node);
9536
+ if (!next) continue;
9537
+ const selector = preferredCodeLayerSelector(node);
9538
+ if (selector) removedSelectors.push(selector);
9539
+ content = next;
9540
+ }
9541
+ if (content === originalContent) continue;
9542
+ if (file.id === activeFile?.id) {
9543
+ activeRuntimeSelectors.push(...removedSelectors);
9544
+ }
9545
+ didDelete = true;
9546
+ applyFileContentUpdate(file.id, content, { refreshPreview: false });
9151
9547
  }
9152
- // Nothing resolved (stale ids) — fall through to the single path.
9548
+ if (!didDelete) return;
9549
+ activeRuntimeSelectors.forEach((selector) =>
9550
+ deleteRuntimeElement(selector),
9551
+ );
9552
+ setSelectedElement(null);
9553
+ setSelectedLayerIdsState([]);
9554
+ if (viewModeRef.current === "overview") {
9555
+ setOverviewSelectedScreenIds([]);
9556
+ }
9557
+ return;
9153
9558
  }
9154
9559
 
9155
9560
  if (!selectedElement?.selector) return;
9156
- const projection = buildCodeLayerProjection(baseContent);
9157
- const targetNode = resolveCodeLayerNodeFromElementInfo(
9158
- projection,
9159
- selectedElement,
9561
+ const baseContent = getFreshActiveContent();
9562
+ const nextContent = removeElementFromHtml(
9563
+ baseContent,
9564
+ selectedElement.selector,
9160
9565
  );
9161
- const nextContent =
9162
- (targetNode
9163
- ? removeCodeLayerNodeFromHtml(baseContent, targetNode)
9164
- : null) ?? removeElementFromHtml(baseContent, selectedElement.selector);
9165
9566
  if (!nextContent) return;
9166
9567
  deleteRuntimeElement(selectedElement.selector);
9167
9568
  applyLocalContentUpdate(nextContent, { refreshPreview: false });
9168
9569
  setSelectedElement(null);
9169
9570
  setSelectedLayerIdsState([]);
9170
9571
  }, [
9572
+ activeFile?.id,
9573
+ applyFileContentUpdate,
9171
9574
  applyLocalContentUpdate,
9172
9575
  canEditDesign,
9173
9576
  deleteRuntimeElement,
9174
9577
  files,
9175
9578
  getFreshActiveContent,
9579
+ getScreenContent,
9580
+ getSelectedLayerSnapshots,
9176
9581
  selectedElement,
9177
- selectedLayerIdsState,
9178
9582
  ]);
9179
9583
 
9180
9584
  // Wrap the current multi-layer selection into a new group container.
@@ -9461,10 +9865,27 @@ export default function DesignEditor() {
9461
9865
  destNodeAttrId,
9462
9866
  );
9463
9867
 
9868
+ recordContentHistoryEntry({
9869
+ changes: [
9870
+ {
9871
+ fileId: sourceScreenId,
9872
+ before: sourceContent,
9873
+ after: result.sourceHtml,
9874
+ },
9875
+ {
9876
+ fileId: targetScreenId,
9877
+ before: destContent,
9878
+ after: strippedDest,
9879
+ },
9880
+ ],
9881
+ });
9882
+
9464
9883
  applyFileContentUpdate(sourceScreenId, result.sourceHtml, {
9884
+ recordHistory: false,
9465
9885
  refreshPreview: true,
9466
9886
  });
9467
9887
  applyFileContentUpdate(targetScreenId, strippedDest, {
9888
+ recordHistory: false,
9468
9889
  refreshPreview: true,
9469
9890
  });
9470
9891
 
@@ -9478,7 +9899,13 @@ export default function DesignEditor() {
9478
9899
  setSelectedElement(elementInfoFromCodeLayerNode(movedNodeFinal));
9479
9900
  }
9480
9901
  },
9481
- [applyFileContentUpdate, canEditDesign, getScreenContent, t],
9902
+ [
9903
+ applyFileContentUpdate,
9904
+ canEditDesign,
9905
+ getScreenContent,
9906
+ recordContentHistoryEntry,
9907
+ t,
9908
+ ],
9482
9909
  );
9483
9910
 
9484
9911
  /**
@@ -9573,10 +10000,27 @@ export default function DesignEditor() {
9573
10000
  destNodeAttrId,
9574
10001
  );
9575
10002
 
10003
+ recordContentHistoryEntry({
10004
+ changes: [
10005
+ {
10006
+ fileId: sourceScreenId,
10007
+ before: sourceContent,
10008
+ after: result.sourceHtml,
10009
+ },
10010
+ {
10011
+ fileId: targetScreenId,
10012
+ before: destContent,
10013
+ after: strippedDest,
10014
+ },
10015
+ ],
10016
+ });
10017
+
9576
10018
  applyFileContentUpdate(sourceScreenId, result.sourceHtml, {
10019
+ recordHistory: false,
9577
10020
  refreshPreview: true,
9578
10021
  });
9579
10022
  applyFileContentUpdate(targetScreenId, strippedDest, {
10023
+ recordHistory: false,
9580
10024
  refreshPreview: true,
9581
10025
  });
9582
10026
 
@@ -9592,7 +10036,13 @@ export default function DesignEditor() {
9592
10036
  setSelectedElement(elementInfoFromCodeLayerNode(movedNodeFinal));
9593
10037
  }
9594
10038
  },
9595
- [applyFileContentUpdate, canEditDesign, getScreenContent, t],
10039
+ [
10040
+ applyFileContentUpdate,
10041
+ canEditDesign,
10042
+ getScreenContent,
10043
+ recordContentHistoryEntry,
10044
+ t,
10045
+ ],
9596
10046
  );
9597
10047
 
9598
10048
  const handleCutSelection = useCallback(async () => {
@@ -9800,12 +10250,12 @@ export default function DesignEditor() {
9800
10250
  const entry =
9801
10251
  contentUndoStackRef.current[contentUndoStackRef.current.length - 1];
9802
10252
  if (!entry) return false;
9803
- if (
9804
- entry.fileId !== activeFile?.id &&
9805
- !files.some((file) => file.id === entry.fileId)
9806
- ) {
9807
- return false;
9808
- }
10253
+ const changes = getAvailableContentHistoryChanges(
10254
+ entry,
10255
+ files.map((file) => file.id),
10256
+ activeFile?.id,
10257
+ );
10258
+ if (changes.length === 0) return false;
9809
10259
  contentUndoStackRef.current.pop();
9810
10260
  contentRedoStackRef.current = [
9811
10261
  ...contentRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
@@ -9817,27 +10267,34 @@ export default function DesignEditor() {
9817
10267
  ];
9818
10268
  suppressContentHistoryRef.current = true;
9819
10269
  try {
9820
- if (entry.fileId === activeFile?.id) {
9821
- applyLocalContentUpdate(entry.before, {
9822
- refreshPreview: false,
9823
- immediateSave: true,
9824
- });
9825
- } else {
9826
- applyFileContentUpdate(entry.fileId, entry.before, {
9827
- refreshPreview: false,
9828
- });
10270
+ for (const change of changes) {
10271
+ if (change.fileId === activeFile?.id) {
10272
+ applyLocalContentUpdate(change.before, {
10273
+ refreshPreview: false,
10274
+ immediateSave: true,
10275
+ recordHistory: false,
10276
+ });
10277
+ } else {
10278
+ applyFileContentUpdate(change.fileId, change.before, {
10279
+ recordHistory: false,
10280
+ refreshPreview: false,
10281
+ });
10282
+ }
9829
10283
  }
9830
10284
  } finally {
9831
10285
  suppressContentHistoryRef.current = false;
9832
10286
  }
9833
- if (entry.fileId === activeFile?.id) {
10287
+ const activeChange = changes.find(
10288
+ (change) => change.fileId === activeFile?.id,
10289
+ );
10290
+ if (activeChange) {
9834
10291
  setSelectedElement((prev) => {
9835
10292
  if (!prev) return prev;
9836
- return refreshElementInfoFromContent(entry.before, prev);
10293
+ return refreshElementInfoFromContent(activeChange.before, prev);
9837
10294
  });
9838
10295
  setHoveredElement((prev) => {
9839
10296
  if (!prev) return prev;
9840
- return refreshElementInfoFromContent(entry.before, prev);
10297
+ return refreshElementInfoFromContent(activeChange.before, prev);
9841
10298
  });
9842
10299
  }
9843
10300
  return true;
@@ -9927,12 +10384,12 @@ export default function DesignEditor() {
9927
10384
  const entry =
9928
10385
  contentRedoStackRef.current[contentRedoStackRef.current.length - 1];
9929
10386
  if (!entry) return false;
9930
- if (
9931
- entry.fileId !== activeFile?.id &&
9932
- !files.some((file) => file.id === entry.fileId)
9933
- ) {
9934
- return false;
9935
- }
10387
+ const changes = getAvailableContentHistoryChanges(
10388
+ entry,
10389
+ files.map((file) => file.id),
10390
+ activeFile?.id,
10391
+ );
10392
+ if (changes.length === 0) return false;
9936
10393
  contentRedoStackRef.current.pop();
9937
10394
  contentUndoStackRef.current = [
9938
10395
  ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
@@ -9944,27 +10401,34 @@ export default function DesignEditor() {
9944
10401
  ];
9945
10402
  suppressContentHistoryRef.current = true;
9946
10403
  try {
9947
- if (entry.fileId === activeFile?.id) {
9948
- applyLocalContentUpdate(entry.after, {
9949
- refreshPreview: false,
9950
- immediateSave: true,
9951
- });
9952
- } else {
9953
- applyFileContentUpdate(entry.fileId, entry.after, {
9954
- refreshPreview: false,
9955
- });
10404
+ for (const change of changes) {
10405
+ if (change.fileId === activeFile?.id) {
10406
+ applyLocalContentUpdate(change.after, {
10407
+ refreshPreview: false,
10408
+ immediateSave: true,
10409
+ recordHistory: false,
10410
+ });
10411
+ } else {
10412
+ applyFileContentUpdate(change.fileId, change.after, {
10413
+ recordHistory: false,
10414
+ refreshPreview: false,
10415
+ });
10416
+ }
9956
10417
  }
9957
10418
  } finally {
9958
10419
  suppressContentHistoryRef.current = false;
9959
10420
  }
9960
- if (entry.fileId === activeFile?.id) {
10421
+ const activeChange = changes.find(
10422
+ (change) => change.fileId === activeFile?.id,
10423
+ );
10424
+ if (activeChange) {
9961
10425
  setSelectedElement((prev) => {
9962
10426
  if (!prev) return prev;
9963
- return refreshElementInfoFromContent(entry.after, prev);
10427
+ return refreshElementInfoFromContent(activeChange.after, prev);
9964
10428
  });
9965
10429
  setHoveredElement((prev) => {
9966
10430
  if (!prev) return prev;
9967
- return refreshElementInfoFromContent(entry.after, prev);
10431
+ return refreshElementInfoFromContent(activeChange.after, prev);
9968
10432
  });
9969
10433
  }
9970
10434
  return true;
@@ -10077,6 +10541,15 @@ export default function DesignEditor() {
10077
10541
  transition?.updateCallbackDone?.catch(() => {});
10078
10542
  }, []);
10079
10543
 
10544
+ const getRestoredOverviewSelection = useCallback(() => {
10545
+ const fileIds = new Set(files.map((file) => file.id));
10546
+ const restored = lastOverviewSelectedScreenIdsRef.current.filter((id) =>
10547
+ fileIds.has(id),
10548
+ );
10549
+ if (restored.length > 0) return restored;
10550
+ return activeFileId && fileIds.has(activeFileId) ? [activeFileId] : [];
10551
+ }, [activeFileId, files]);
10552
+
10080
10553
  const enterOverviewFromZoom = useCallback(() => {
10081
10554
  if (viewModeRef.current === "overview") return;
10082
10555
  viewModeRef.current = "overview";
@@ -10084,6 +10557,7 @@ export default function DesignEditor() {
10084
10557
  pendingOverviewLayerSelectionRef.current = null;
10085
10558
  clearPendingOverviewLayerSelectionTimer();
10086
10559
  setCreatedOverviewLayerSelection(null);
10560
+ const restoredOverviewSelection = getRestoredOverviewSelection();
10087
10561
  runEditorViewTransition(() => {
10088
10562
  setDrawMode(false);
10089
10563
  setPinMode(false);
@@ -10091,9 +10565,15 @@ export default function DesignEditor() {
10091
10565
  setSelectedElement(null);
10092
10566
  setHoveredElement(null);
10093
10567
  setActiveTool("move");
10568
+ setOverviewSelectedScreenIds(restoredOverviewSelection);
10569
+ setSelectedLayerIdsState(restoredOverviewSelection);
10094
10570
  setViewMode("overview");
10095
10571
  });
10096
- }, [clearPendingOverviewLayerSelectionTimer, runEditorViewTransition]);
10572
+ }, [
10573
+ clearPendingOverviewLayerSelectionTimer,
10574
+ getRestoredOverviewSelection,
10575
+ runEditorViewTransition,
10576
+ ]);
10097
10577
 
10098
10578
  const enterSingleScreen = useCallback(
10099
10579
  (fileId?: string | null) => {
@@ -10198,6 +10678,14 @@ export default function DesignEditor() {
10198
10678
 
10199
10679
  const handleSidebarScreenSelect = useCallback(
10200
10680
  (screenId: string) => {
10681
+ if (
10682
+ viewModeRef.current === "overview" &&
10683
+ overviewSelectedScreenIds.length > 0
10684
+ ) {
10685
+ lastOverviewSelectedScreenIdsRef.current = [
10686
+ ...overviewSelectedScreenIds,
10687
+ ];
10688
+ }
10201
10689
  pendingOverviewScreenSelectionRef.current = null;
10202
10690
  pendingOverviewLayerSelectionRef.current = null;
10203
10691
  clearPendingOverviewLayerSelectionTimer();
@@ -10206,16 +10694,21 @@ export default function DesignEditor() {
10206
10694
  setSelectedLayerIdsState([]);
10207
10695
  enterSingleScreen(screenId);
10208
10696
  },
10209
- [clearPendingOverviewLayerSelectionTimer, enterSingleScreen],
10697
+ [
10698
+ clearPendingOverviewLayerSelectionTimer,
10699
+ enterSingleScreen,
10700
+ overviewSelectedScreenIds,
10701
+ ],
10210
10702
  );
10211
10703
 
10212
10704
  const handleSidebarScreenOverview = useCallback(() => {
10705
+ const restoredOverviewSelection = getRestoredOverviewSelection();
10213
10706
  pendingOverviewScreenSelectionRef.current = null;
10214
10707
  pendingOverviewLayerSelectionRef.current = null;
10215
10708
  clearPendingOverviewLayerSelectionTimer();
10216
10709
  setCreatedOverviewLayerSelection(null);
10217
- setOverviewSelectedScreenIds([]);
10218
- setSelectedLayerIdsState([]);
10710
+ setOverviewSelectedScreenIds(restoredOverviewSelection);
10711
+ setSelectedLayerIdsState(restoredOverviewSelection);
10219
10712
  if (viewModeRef.current === "overview") {
10220
10713
  setDrawMode(false);
10221
10714
  setPinMode(false);
@@ -10226,7 +10719,11 @@ export default function DesignEditor() {
10226
10719
  return;
10227
10720
  }
10228
10721
  enterOverviewFromZoom();
10229
- }, [clearPendingOverviewLayerSelectionTimer, enterOverviewFromZoom]);
10722
+ }, [
10723
+ clearPendingOverviewLayerSelectionTimer,
10724
+ enterOverviewFromZoom,
10725
+ getRestoredOverviewSelection,
10726
+ ]);
10230
10727
 
10231
10728
  const handlePinToolToggle = useCallback(() => {
10232
10729
  if (!activeFile || !canEditDesign) return;
@@ -12183,6 +12680,7 @@ ${serializedHtml}
12183
12680
  // Group by source file so multiple nodes from the same source are
12184
12681
  // applied sequentially against the running source content.
12185
12682
  const sourceContentMap = new Map<string, string>();
12683
+ const sourceOriginalContentMap = new Map<string, string>();
12186
12684
  const movedNodeIdByDraggedId = new Map<string, string>();
12187
12685
  for (const { draggedId, sourceFileId } of getLayerMoveIterationOrder(
12188
12686
  crossFileDrags,
@@ -12197,6 +12695,9 @@ ${serializedHtml}
12197
12695
  sourceFileContent: srcFile.content,
12198
12696
  sourceContentMap,
12199
12697
  });
12698
+ if (!sourceOriginalContentMap.has(sourceFileId)) {
12699
+ sourceOriginalContentMap.set(sourceFileId, currentSourceContent);
12700
+ }
12200
12701
 
12201
12702
  // The dragged node's data-agent-native-node-id is the node id tracked
12202
12703
  // by code-layer. Look up the actual attribute value from the owner.
@@ -12274,9 +12775,37 @@ ${serializedHtml}
12274
12775
  });
12275
12776
  }
12276
12777
 
12778
+ const hasCrossFileMoves = sourceContentMap.size > 0;
12779
+ if (hasCrossFileMoves) {
12780
+ recordContentHistoryEntry({
12781
+ changes: [
12782
+ ...Array.from(sourceContentMap.entries()).map(
12783
+ ([sourceFileId, newSourceContent]) => ({
12784
+ fileId: sourceFileId,
12785
+ before:
12786
+ sourceOriginalContentMap.get(sourceFileId) ??
12787
+ files.find((file) => file.id === sourceFileId)?.content ??
12788
+ "",
12789
+ after: newSourceContent,
12790
+ }),
12791
+ ),
12792
+ ...(nextDestContent !== destContent
12793
+ ? [
12794
+ {
12795
+ fileId: targetOwner.fileId,
12796
+ before: destContent,
12797
+ after: nextDestContent,
12798
+ },
12799
+ ]
12800
+ : []),
12801
+ ],
12802
+ });
12803
+ }
12804
+
12277
12805
  // Persist source files that changed.
12278
12806
  for (const [sourceFileId, newSourceContent] of sourceContentMap) {
12279
12807
  applyFileContentUpdate(sourceFileId, newSourceContent, {
12808
+ recordHistory: !hasCrossFileMoves,
12280
12809
  refreshPreview: false,
12281
12810
  });
12282
12811
  }
@@ -12284,6 +12813,7 @@ ${serializedHtml}
12284
12813
  // Persist dest file (which may also be the active file).
12285
12814
  if (nextDestContent !== destContent) {
12286
12815
  applyFileContentUpdate(targetOwner.fileId, nextDestContent, {
12816
+ recordHistory: !hasCrossFileMoves,
12287
12817
  refreshPreview: false,
12288
12818
  });
12289
12819
  }
@@ -12297,6 +12827,7 @@ ${serializedHtml}
12297
12827
  effectiveCodeLayerState,
12298
12828
  files,
12299
12829
  getFreshActiveContent,
12830
+ recordContentHistoryEntry,
12300
12831
  t,
12301
12832
  ],
12302
12833
  );
@@ -14113,6 +14644,7 @@ ${serializedHtml}
14113
14644
  <DesignCanvas
14114
14645
  content={screenContent}
14115
14646
  contentKey={screenContentKey}
14647
+ screenId={screen.id}
14116
14648
  zoom={100}
14117
14649
  deviceFrame="none"
14118
14650
  sourceType={designSourceType}