@agent-native/core 0.81.2 → 0.82.0
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/file-upload/builder.ts +199 -37
- package/corpus/core/src/file-upload/index.ts +2 -0
- package/corpus/core/src/file-upload/types.ts +38 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- package/corpus/templates/clips/actions/create-recording.ts +44 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +198 -88
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +12 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +83 -29
- package/corpus/templates/clips/app/routes/record.tsx +12 -1
- package/corpus/templates/clips/server/lib/resumable-session.ts +39 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +2 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +186 -29
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +4 -0
- package/corpus/templates/clips/shared/recording-core.ts +5 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +63 -16
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +28 -33
- package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +47 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +37 -0
- package/corpus/templates/content/changelog/2026-06-30-sidebar-favorites-now-keep-long-titles-tidy-and-stay-in-sync.md +6 -0
- 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/pages/DesignEditor.tsx +557 -211
- 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/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/routes.d.ts +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +137 -25
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.d.ts.map +1 -1
- package/dist/file-upload/index.js.map +1 -1
- package/dist/file-upload/types.d.ts +26 -0
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +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/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;
|
|
@@ -1351,7 +1363,12 @@ function normalizedDesignFileType(
|
|
|
1351
1363
|
|
|
1352
1364
|
function nextBlankScreenFilename(files: DesignFile[]): string {
|
|
1353
1365
|
const existing = new Set(files.map((file) => file.filename));
|
|
1354
|
-
|
|
1366
|
+
const screenCount = files.filter(
|
|
1367
|
+
(file) =>
|
|
1368
|
+
normalizedDesignFileType(file.fileType) === "html" &&
|
|
1369
|
+
!isBoardFile(file.filename),
|
|
1370
|
+
).length;
|
|
1371
|
+
let index = screenCount + 1;
|
|
1355
1372
|
let candidate = `screen-${index}.html`;
|
|
1356
1373
|
while (existing.has(candidate)) {
|
|
1357
1374
|
index += 1;
|
|
@@ -1461,6 +1478,9 @@ function appendCanvasPrimitiveToHtml(
|
|
|
1461
1478
|
const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1462
1479
|
const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1463
1480
|
const markerId = `${nodeId}-arrow`;
|
|
1481
|
+
const explicitPathData = primitive.pathData?.trim()
|
|
1482
|
+
? primitive.pathData
|
|
1483
|
+
: null;
|
|
1464
1484
|
const points = primitive.points?.length
|
|
1465
1485
|
? primitive.points
|
|
1466
1486
|
: [
|
|
@@ -1471,7 +1491,7 @@ function appendCanvasPrimitiveToHtml(
|
|
|
1471
1491
|
const originY = Math.min(...points.map((point) => point.y));
|
|
1472
1492
|
path.setAttribute(
|
|
1473
1493
|
"d",
|
|
1474
|
-
|
|
1494
|
+
explicitPathData ??
|
|
1475
1495
|
points
|
|
1476
1496
|
.map((point, index) => {
|
|
1477
1497
|
const command = index === 0 ? "M" : "L";
|
|
@@ -1518,7 +1538,13 @@ function appendCanvasPrimitiveToHtml(
|
|
|
1518
1538
|
}
|
|
1519
1539
|
svg.setAttribute("data-agent-native-node-id", nodeId);
|
|
1520
1540
|
svg.setAttribute("data-agent-native-layer-name", layerName);
|
|
1521
|
-
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
|
+
);
|
|
1522
1548
|
svg.setAttribute(
|
|
1523
1549
|
"style",
|
|
1524
1550
|
[
|
|
@@ -1690,44 +1716,126 @@ function cloneHtmlLayerAtPosition(
|
|
|
1690
1716
|
layerHtml: string,
|
|
1691
1717
|
position: { x: number; y: number },
|
|
1692
1718
|
): string | null {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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) => {
|
|
1710
1776
|
node.setAttribute(
|
|
1711
1777
|
"data-agent-native-node-id",
|
|
1712
|
-
uniqueLayerId(
|
|
1778
|
+
uniqueLayerId("copy-child"),
|
|
1713
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);
|
|
1714
1813
|
});
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
+
}
|
|
1728
1834
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1835
|
+
return {
|
|
1836
|
+
content: `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`,
|
|
1837
|
+
rootNodeIds,
|
|
1838
|
+
};
|
|
1731
1839
|
} catch {
|
|
1732
1840
|
return null;
|
|
1733
1841
|
}
|
|
@@ -1771,52 +1879,13 @@ function insertClonedHtmlLayer(
|
|
|
1771
1879
|
placement?: "before" | "after" | "inside";
|
|
1772
1880
|
},
|
|
1773
1881
|
): string | null {
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const source =
|
|
1782
|
-
layerDoc.querySelector("template")?.content.firstElementChild ??
|
|
1783
|
-
layerDoc.body.firstElementChild;
|
|
1784
|
-
if (!source || !doc.body) return null;
|
|
1785
|
-
const clone = doc.importNode(source, true) as HTMLElement | SVGElement;
|
|
1786
|
-
const clonedNodes = [
|
|
1787
|
-
clone,
|
|
1788
|
-
...Array.from(clone.querySelectorAll("[data-agent-native-node-id]")),
|
|
1789
|
-
] as Array<HTMLElement | SVGElement>;
|
|
1790
|
-
clonedNodes.forEach((node, index) => {
|
|
1791
|
-
node.setAttribute(
|
|
1792
|
-
"data-agent-native-node-id",
|
|
1793
|
-
uniqueLayerId(index === 0 ? "copy" : "copy-child"),
|
|
1794
|
-
);
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
|
-
const target = queryFirstSelector(doc, options.targetSelectors);
|
|
1798
|
-
const anchor =
|
|
1799
|
-
queryFirstSelector(doc, options.anchorSelectors ?? []) ?? target;
|
|
1800
|
-
const placement = options.placement ?? "after";
|
|
1801
|
-
if (!anchor) {
|
|
1802
|
-
doc.body.appendChild(clone);
|
|
1803
|
-
} else if (placement === "inside") {
|
|
1804
|
-
anchor.appendChild(clone);
|
|
1805
|
-
} else if (placement === "before") {
|
|
1806
|
-
if (anchor.parentElement)
|
|
1807
|
-
anchor.parentElement.insertBefore(clone, anchor);
|
|
1808
|
-
else doc.body.appendChild(clone);
|
|
1809
|
-
} else {
|
|
1810
|
-
if (anchor.parentElement) {
|
|
1811
|
-
anchor.parentElement.insertBefore(clone, anchor.nextSibling);
|
|
1812
|
-
} else {
|
|
1813
|
-
doc.body.appendChild(clone);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
1817
|
-
} catch {
|
|
1818
|
-
return null;
|
|
1819
|
-
}
|
|
1882
|
+
return (
|
|
1883
|
+
insertClonedHtmlLayers(content, [cloneHtml], {
|
|
1884
|
+
targetSelectors: options.targetSelectors,
|
|
1885
|
+
anchorSelectors: options.anchorSelectors,
|
|
1886
|
+
placement: options.placement,
|
|
1887
|
+
})?.content ?? null
|
|
1888
|
+
);
|
|
1820
1889
|
}
|
|
1821
1890
|
|
|
1822
1891
|
function getElementOuterHtml(content: string, selector: string): string | null {
|
|
@@ -1858,6 +1927,55 @@ function extractLayerPosition(
|
|
|
1858
1927
|
}
|
|
1859
1928
|
}
|
|
1860
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
|
+
|
|
1861
1979
|
function removeElementFromHtml(
|
|
1862
1980
|
content: string,
|
|
1863
1981
|
selector: string,
|
|
@@ -3640,6 +3758,7 @@ export default function DesignEditor() {
|
|
|
3640
3758
|
useState(0);
|
|
3641
3759
|
const [hasCanvasClipboard, setHasCanvasClipboard] = useState(false);
|
|
3642
3760
|
const [hasPropsClipboard, setHasPropsClipboard] = useState(false);
|
|
3761
|
+
const copiedLayerEntriesRef = useRef<CanvasLayerClipboardEntry[]>([]);
|
|
3643
3762
|
const copiedLayerHtmlRef = useRef<string | null>(null);
|
|
3644
3763
|
// Cascade offset for repeated keyboard pastes so successive clones don't stack
|
|
3645
3764
|
// pixel-perfectly on top of each other. Reset on each fresh copy/cut.
|
|
@@ -7205,9 +7324,7 @@ export default function DesignEditor() {
|
|
|
7205
7324
|
// enter text-edit mode — fixing the "click to add text should let me
|
|
7206
7325
|
// type immediately" bug. The ref is read once and cleared.
|
|
7207
7326
|
if (primitive.kind === "text") {
|
|
7208
|
-
|
|
7209
|
-
typeof result === "string" ? result : (primitive.nodeId ?? null);
|
|
7210
|
-
pendingTextEditNodeIdRef.current = textNodeId;
|
|
7327
|
+
pendingTextEditNodeIdRef.current = primitive.nodeId ?? null;
|
|
7211
7328
|
} else {
|
|
7212
7329
|
pendingTextEditNodeIdRef.current = null;
|
|
7213
7330
|
}
|
|
@@ -7252,17 +7369,15 @@ export default function DesignEditor() {
|
|
|
7252
7369
|
});
|
|
7253
7370
|
// viewMode stays at "overview" — no setViewMode("single") call here.
|
|
7254
7371
|
|
|
7255
|
-
// Immediately enter text-editing for newly created TEXT primitives
|
|
7256
|
-
//
|
|
7257
|
-
//
|
|
7258
|
-
//
|
|
7259
|
-
//
|
|
7260
|
-
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;
|
|
7261
7378
|
pendingTextEditNodeIdRef.current = null;
|
|
7262
7379
|
if (textNodeId) {
|
|
7263
|
-
|
|
7264
|
-
(window as any).__designCanvasBeginTextEdit?.(textNodeId);
|
|
7265
|
-
}, 50);
|
|
7380
|
+
scheduleBeginTextEditForScreen(screenId, textNodeId);
|
|
7266
7381
|
}
|
|
7267
7382
|
},
|
|
7268
7383
|
[clearPendingOverviewLayerSelectionTimer],
|
|
@@ -7281,28 +7396,14 @@ export default function DesignEditor() {
|
|
|
7281
7396
|
if (!result) return false;
|
|
7282
7397
|
|
|
7283
7398
|
// For TEXT primitives drawn on the board surface, immediately enter
|
|
7284
|
-
// text-editing mode via begin-text-edit.
|
|
7285
|
-
//
|
|
7286
|
-
// not set; instead we broadcast the message to all preview iframes — the
|
|
7287
|
-
// one that contains the new nodeId will handle it, others ignore it.
|
|
7288
|
-
// 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.
|
|
7289
7401
|
if (primitive.kind === "text") {
|
|
7290
7402
|
const textNodeId =
|
|
7291
7403
|
pendingTextEditNodeIdRef.current ?? primitive.nodeId ?? null;
|
|
7292
7404
|
pendingTextEditNodeIdRef.current = null;
|
|
7293
7405
|
if (textNodeId) {
|
|
7294
|
-
|
|
7295
|
-
document
|
|
7296
|
-
.querySelectorAll<HTMLIFrameElement>(
|
|
7297
|
-
"iframe[data-design-preview-iframe]",
|
|
7298
|
-
)
|
|
7299
|
-
.forEach((iframe) => {
|
|
7300
|
-
iframe.contentWindow?.postMessage(
|
|
7301
|
-
{ type: "begin-text-edit", nodeId: textNodeId },
|
|
7302
|
-
"*",
|
|
7303
|
-
);
|
|
7304
|
-
});
|
|
7305
|
-
}, 50);
|
|
7406
|
+
scheduleBeginTextEditForScreen(boardFileId, textNodeId);
|
|
7306
7407
|
}
|
|
7307
7408
|
}
|
|
7308
7409
|
|
|
@@ -8994,27 +9095,167 @@ export default function DesignEditor() {
|
|
|
8994
9095
|
],
|
|
8995
9096
|
);
|
|
8996
9097
|
|
|
8997
|
-
const
|
|
8998
|
-
|
|
8999
|
-
const
|
|
9000
|
-
|
|
9001
|
-
|
|
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),
|
|
9002
9103
|
);
|
|
9003
|
-
if (
|
|
9004
|
-
|
|
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;
|
|
9005
9244
|
pasteCascadeRef.current = 0;
|
|
9006
9245
|
setHasCanvasClipboard(true);
|
|
9007
9246
|
try {
|
|
9008
|
-
await navigator.clipboard.writeText(
|
|
9247
|
+
await navigator.clipboard.writeText(clipboardText);
|
|
9009
9248
|
} catch {
|
|
9010
9249
|
toast.error(t("designEditor.toasts.clipboardBlocked"));
|
|
9011
9250
|
}
|
|
9012
|
-
}, [
|
|
9251
|
+
}, [getSelectedLayerSnapshots, t]);
|
|
9013
9252
|
|
|
9014
9253
|
const handlePasteSelection = useCallback(
|
|
9015
9254
|
(position?: { x: number; y: number }) => {
|
|
9016
|
-
|
|
9255
|
+
const entries = getCanvasClipboardEntries();
|
|
9256
|
+
if (!activeFile || !canEditDesign || entries.length === 0) return;
|
|
9017
9257
|
const baseContent = getFreshActiveContent();
|
|
9258
|
+
const layerHtmls = entries.map((entry) => entry.html);
|
|
9018
9259
|
|
|
9019
9260
|
// B7 fix: when an element is selected and no explicit canvas position was
|
|
9020
9261
|
// given, insert the clone as an in-flow sibling right AFTER the selected
|
|
@@ -9024,38 +9265,19 @@ export default function DesignEditor() {
|
|
|
9024
9265
|
// selected or a "Paste here" position is provided.
|
|
9025
9266
|
if (!position && selectedElement?.selector) {
|
|
9026
9267
|
const selector = selectedCanvasSelector ?? selectedElement.selector;
|
|
9027
|
-
|
|
9028
|
-
// in-flow sibling (not absolute).
|
|
9029
|
-
const strippedHtml = (() => {
|
|
9030
|
-
try {
|
|
9031
|
-
const parser = new DOMParser();
|
|
9032
|
-
const tmp = parser.parseFromString(
|
|
9033
|
-
`<template>${copiedLayerHtmlRef.current!}</template>`,
|
|
9034
|
-
"text/html",
|
|
9035
|
-
);
|
|
9036
|
-
const root =
|
|
9037
|
-
tmp.querySelector("template")?.content.firstElementChild ??
|
|
9038
|
-
tmp.body.firstElementChild;
|
|
9039
|
-
if (root && root instanceof HTMLElement) {
|
|
9040
|
-
root.style.position = "";
|
|
9041
|
-
root.style.left = "";
|
|
9042
|
-
root.style.top = "";
|
|
9043
|
-
root.style.right = "";
|
|
9044
|
-
root.style.bottom = "";
|
|
9045
|
-
}
|
|
9046
|
-
return root?.outerHTML ?? copiedLayerHtmlRef.current!;
|
|
9047
|
-
} catch {
|
|
9048
|
-
return copiedLayerHtmlRef.current!;
|
|
9049
|
-
}
|
|
9050
|
-
})();
|
|
9051
|
-
|
|
9052
|
-
const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
|
|
9268
|
+
const result = insertClonedHtmlLayers(baseContent, layerHtmls, {
|
|
9053
9269
|
targetSelectors: [selector],
|
|
9054
9270
|
placement: "after",
|
|
9271
|
+
stripRootPosition: true,
|
|
9055
9272
|
});
|
|
9056
|
-
if (
|
|
9273
|
+
if (result) {
|
|
9057
9274
|
pasteCascadeRef.current += 1;
|
|
9058
|
-
applyLocalContentUpdate(
|
|
9275
|
+
applyLocalContentUpdate(result.content);
|
|
9276
|
+
selectInsertedLayers(
|
|
9277
|
+
activeFile.id,
|
|
9278
|
+
result.content,
|
|
9279
|
+
result.rootNodeIds,
|
|
9280
|
+
);
|
|
9059
9281
|
return;
|
|
9060
9282
|
}
|
|
9061
9283
|
// Fall through to position-based clone if insert failed.
|
|
@@ -9064,59 +9286,154 @@ export default function DesignEditor() {
|
|
|
9064
9286
|
// Explicit positions (e.g. "Paste here" at the cursor) are honored as-is.
|
|
9065
9287
|
// Keyboard pastes land near the source layer and cascade so repeats don't
|
|
9066
9288
|
// stack exactly.
|
|
9067
|
-
const
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
return src
|
|
9073
|
-
? { x: src.x + 10 + offset, y: src.y + 10 + offset }
|
|
9074
|
-
: { x: 120 + offset, y: 120 + offset };
|
|
9075
|
-
})();
|
|
9076
|
-
const nextContent = cloneHtmlLayerAtPosition(
|
|
9077
|
-
baseContent,
|
|
9078
|
-
copiedLayerHtmlRef.current,
|
|
9079
|
-
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),
|
|
9080
9294
|
);
|
|
9081
|
-
|
|
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;
|
|
9082
9326
|
if (!position) pasteCascadeRef.current += 1;
|
|
9083
|
-
applyLocalContentUpdate(
|
|
9327
|
+
applyLocalContentUpdate(result.content);
|
|
9328
|
+
selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
|
|
9084
9329
|
},
|
|
9085
9330
|
[
|
|
9086
9331
|
activeFile,
|
|
9087
9332
|
applyLocalContentUpdate,
|
|
9088
9333
|
canEditDesign,
|
|
9334
|
+
getCanvasClipboardEntries,
|
|
9089
9335
|
getFreshActiveContent,
|
|
9336
|
+
selectInsertedLayers,
|
|
9090
9337
|
selectedCanvasSelector,
|
|
9091
9338
|
selectedElement,
|
|
9092
9339
|
],
|
|
9093
9340
|
);
|
|
9094
9341
|
|
|
9095
9342
|
const handlePasteOverSelection = useCallback(() => {
|
|
9096
|
-
|
|
9343
|
+
const entries = getCanvasClipboardEntries();
|
|
9344
|
+
if (!activeFile || entries.length === 0) return;
|
|
9097
9345
|
const baseContent = getFreshActiveContent();
|
|
9098
9346
|
if (selectedElement?.boundingRect) {
|
|
9099
9347
|
const { x, y } = selectedElement.boundingRect;
|
|
9100
|
-
const
|
|
9348
|
+
const result = insertClonedHtmlLayers(
|
|
9101
9349
|
baseContent,
|
|
9102
|
-
|
|
9103
|
-
{
|
|
9350
|
+
entries.map((entry) => entry.html),
|
|
9351
|
+
{
|
|
9352
|
+
positions: entries.map((_, index) => ({
|
|
9353
|
+
x: x + index * 16,
|
|
9354
|
+
y: y + index * 16,
|
|
9355
|
+
})),
|
|
9356
|
+
},
|
|
9104
9357
|
);
|
|
9105
|
-
if (!
|
|
9106
|
-
applyLocalContentUpdate(
|
|
9358
|
+
if (!result) return;
|
|
9359
|
+
applyLocalContentUpdate(result.content);
|
|
9360
|
+
selectInsertedLayers(activeFile.id, result.content, result.rootNodeIds);
|
|
9107
9361
|
} else {
|
|
9108
9362
|
handlePasteSelection();
|
|
9109
9363
|
}
|
|
9110
9364
|
}, [
|
|
9111
9365
|
activeFile,
|
|
9112
9366
|
applyLocalContentUpdate,
|
|
9367
|
+
getCanvasClipboardEntries,
|
|
9113
9368
|
getFreshActiveContent,
|
|
9114
9369
|
handlePasteSelection,
|
|
9370
|
+
selectInsertedLayers,
|
|
9115
9371
|
selectedElement,
|
|
9116
9372
|
]);
|
|
9117
9373
|
|
|
9118
9374
|
const handleDuplicateSelection = useCallback(() => {
|
|
9119
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
|
+
|
|
9120
9437
|
if (selectedElement?.selector) {
|
|
9121
9438
|
const baseContent = getFreshActiveContent();
|
|
9122
9439
|
const html = getElementOuterHtml(baseContent, selectedElement.selector);
|
|
@@ -9164,9 +9481,13 @@ export default function DesignEditor() {
|
|
|
9164
9481
|
if (activeFile) handleDuplicateScreen(activeFile.id);
|
|
9165
9482
|
}, [
|
|
9166
9483
|
activeFile,
|
|
9484
|
+
applyFileContentUpdate,
|
|
9167
9485
|
applyLocalContentUpdate,
|
|
9168
9486
|
canEditDesign,
|
|
9487
|
+
files,
|
|
9169
9488
|
getFreshActiveContent,
|
|
9489
|
+
getScreenContent,
|
|
9490
|
+
getSelectedLayerSnapshots,
|
|
9170
9491
|
handleDuplicateScreen,
|
|
9171
9492
|
selectedCanvasSelector,
|
|
9172
9493
|
selectedElement,
|
|
@@ -9175,65 +9496,89 @@ export default function DesignEditor() {
|
|
|
9175
9496
|
|
|
9176
9497
|
const handleDeleteSelection = useCallback(() => {
|
|
9177
9498
|
if (!canEditDesign) return;
|
|
9178
|
-
const
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
if (candidateIds.length > 1) {
|
|
9190
|
-
let content = baseContent;
|
|
9191
|
-
const removedSelectors: string[] = [];
|
|
9192
|
-
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;
|
|
9193
9510
|
const projection = buildCodeLayerProjection(content);
|
|
9194
|
-
const
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
const
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
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 });
|
|
9547
|
+
}
|
|
9548
|
+
if (!didDelete) return;
|
|
9549
|
+
activeRuntimeSelectors.forEach((selector) =>
|
|
9550
|
+
deleteRuntimeElement(selector),
|
|
9551
|
+
);
|
|
9552
|
+
setSelectedElement(null);
|
|
9553
|
+
setSelectedLayerIdsState([]);
|
|
9554
|
+
if (viewModeRef.current === "overview") {
|
|
9555
|
+
setOverviewSelectedScreenIds([]);
|
|
9210
9556
|
}
|
|
9211
|
-
|
|
9557
|
+
return;
|
|
9212
9558
|
}
|
|
9213
9559
|
|
|
9214
9560
|
if (!selectedElement?.selector) return;
|
|
9215
|
-
const
|
|
9216
|
-
const
|
|
9217
|
-
|
|
9218
|
-
selectedElement,
|
|
9561
|
+
const baseContent = getFreshActiveContent();
|
|
9562
|
+
const nextContent = removeElementFromHtml(
|
|
9563
|
+
baseContent,
|
|
9564
|
+
selectedElement.selector,
|
|
9219
9565
|
);
|
|
9220
|
-
const nextContent =
|
|
9221
|
-
(targetNode
|
|
9222
|
-
? removeCodeLayerNodeFromHtml(baseContent, targetNode)
|
|
9223
|
-
: null) ?? removeElementFromHtml(baseContent, selectedElement.selector);
|
|
9224
9566
|
if (!nextContent) return;
|
|
9225
9567
|
deleteRuntimeElement(selectedElement.selector);
|
|
9226
9568
|
applyLocalContentUpdate(nextContent, { refreshPreview: false });
|
|
9227
9569
|
setSelectedElement(null);
|
|
9228
9570
|
setSelectedLayerIdsState([]);
|
|
9229
9571
|
}, [
|
|
9572
|
+
activeFile?.id,
|
|
9573
|
+
applyFileContentUpdate,
|
|
9230
9574
|
applyLocalContentUpdate,
|
|
9231
9575
|
canEditDesign,
|
|
9232
9576
|
deleteRuntimeElement,
|
|
9233
9577
|
files,
|
|
9234
9578
|
getFreshActiveContent,
|
|
9579
|
+
getScreenContent,
|
|
9580
|
+
getSelectedLayerSnapshots,
|
|
9235
9581
|
selectedElement,
|
|
9236
|
-
selectedLayerIdsState,
|
|
9237
9582
|
]);
|
|
9238
9583
|
|
|
9239
9584
|
// Wrap the current multi-layer selection into a new group container.
|
|
@@ -14299,6 +14644,7 @@ ${serializedHtml}
|
|
|
14299
14644
|
<DesignCanvas
|
|
14300
14645
|
content={screenContent}
|
|
14301
14646
|
contentKey={screenContentKey}
|
|
14647
|
+
screenId={screen.id}
|
|
14302
14648
|
zoom={100}
|
|
14303
14649
|
deviceFrame="none"
|
|
14304
14650
|
sourceType={designSourceType}
|