@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/frame.ts +55 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- package/corpus/core/src/server/core-routes-plugin.ts +4 -5
- package/corpus/core/src/server/create-server.ts +3 -5
- package/corpus/core/src/server/google-oauth.ts +4 -0
- package/corpus/templates/calendar/actions/create-event.ts +9 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
- package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
- package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
- package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
- package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
- package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
- package/corpus/templates/design/actions/run-design-audit.ts +9 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
- package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -1
- package/corpus/templates/design/e2e/helpers.ts +4 -1
- package/corpus/templates/design/playwright.config.ts +4 -1
- package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
- package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
- package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/visual-answer.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
- package/corpus/templates/plan/app/entry.client.tsx +10 -0
- package/corpus/templates/plan/app/global.css +28 -0
- package/corpus/templates/plan/shared/plan-content.ts +10 -6
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +59 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +31 -4
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +33 -18
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +1 -2
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +1 -2
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/google-oauth.d.ts +2 -0
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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(
|
|
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 (
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1707
|
-
|
|
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
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
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
|
-
|
|
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
|
|
7197
|
-
//
|
|
7198
|
-
//
|
|
7199
|
-
//
|
|
7200
|
-
//
|
|
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
|
-
|
|
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.
|
|
7226
|
-
//
|
|
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
|
-
|
|
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
|
|
8939
|
-
|
|
8940
|
-
const
|
|
8941
|
-
|
|
8942
|
-
|
|
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 (
|
|
8945
|
-
|
|
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(
|
|
9247
|
+
await navigator.clipboard.writeText(clipboardText);
|
|
8950
9248
|
} catch {
|
|
8951
9249
|
toast.error(t("designEditor.toasts.clipboardBlocked"));
|
|
8952
9250
|
}
|
|
8953
|
-
}, [
|
|
9251
|
+
}, [getSelectedLayerSnapshots, t]);
|
|
8954
9252
|
|
|
8955
9253
|
const handlePasteSelection = useCallback(
|
|
8956
9254
|
(position?: { x: number; y: number }) => {
|
|
8957
|
-
|
|
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
|
-
|
|
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 (
|
|
9273
|
+
if (result) {
|
|
8998
9274
|
pasteCascadeRef.current += 1;
|
|
8999
|
-
applyLocalContentUpdate(
|
|
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
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
9348
|
+
const result = insertClonedHtmlLayers(
|
|
9042
9349
|
baseContent,
|
|
9043
|
-
|
|
9044
|
-
{
|
|
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 (!
|
|
9047
|
-
applyLocalContentUpdate(
|
|
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
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
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
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
const
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
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
|
-
|
|
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
|
|
9157
|
-
const
|
|
9158
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
-
|
|
9804
|
-
entry
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
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
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
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
|
-
|
|
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(
|
|
10293
|
+
return refreshElementInfoFromContent(activeChange.before, prev);
|
|
9837
10294
|
});
|
|
9838
10295
|
setHoveredElement((prev) => {
|
|
9839
10296
|
if (!prev) return prev;
|
|
9840
|
-
return refreshElementInfoFromContent(
|
|
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
|
-
|
|
9931
|
-
entry
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
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
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
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
|
-
|
|
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(
|
|
10427
|
+
return refreshElementInfoFromContent(activeChange.after, prev);
|
|
9964
10428
|
});
|
|
9965
10429
|
setHoveredElement((prev) => {
|
|
9966
10430
|
if (!prev) return prev;
|
|
9967
|
-
return refreshElementInfoFromContent(
|
|
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
|
-
}, [
|
|
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
|
-
[
|
|
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
|
-
}, [
|
|
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}
|