@agent-native/core 0.85.5 → 0.85.7
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 +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -1
- package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
- package/corpus/core/src/collab/presence.ts +63 -3
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
- package/corpus/templates/design/AGENTS.md +40 -4
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
- package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
- package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
- package/corpus/templates/design/actions/connect-localhost.ts +25 -18
- package/corpus/templates/design/actions/edit-design.ts +5 -3
- package/corpus/templates/design/actions/export-pdf.ts +8 -1
- package/corpus/templates/design/actions/export-zip.ts +10 -1
- package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
- package/corpus/templates/design/actions/insert-asset.ts +190 -7
- package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
- package/corpus/templates/design/actions/present-design-variants.ts +12 -8
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
- package/corpus/templates/design/actions/run-design-audit.ts +7 -4
- package/corpus/templates/design/actions/write-local-file.ts +47 -22
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
- package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
- package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1520 -410
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
- package/corpus/templates/design/app/components/design/types.ts +16 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +18 -0
- package/corpus/templates/design/app/lib/design-import.ts +95 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4015 -840
- package/corpus/templates/design/changelog/2026-07-02-creating-two-frames-in-a-row-no-longer-freezes-and-reloads-t.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-design-retries-stalled-screen-edits-with-a-smaller-safer-fol.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
- package/corpus/templates/design/server/lib/design-export.ts +43 -1
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/design/shared/board-file.ts +25 -5
- package/corpus/templates/design/shared/canvas-math.ts +754 -9
- package/corpus/templates/design/shared/code-layer.ts +304 -26
- package/corpus/templates/design/shared/color-utils.ts +84 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
- package/corpus/templates/design/shared/motion-compiler.ts +75 -31
- package/corpus/templates/design/shared/motion-timeline.ts +335 -0
- package/corpus/templates/design/shared/pen-path.ts +200 -23
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/use-pinch-zoom.d.ts.map +1 -1
- package/dist/client/use-pinch-zoom.js +76 -11
- package/dist/client/use-pinch-zoom.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +54 -3
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +1 -1
|
@@ -30,8 +30,25 @@ declare var __DESIGN_CANVAS_SCREEN_ID__: string;
|
|
|
30
30
|
declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
31
31
|
|
|
32
32
|
(function () {
|
|
33
|
+
// Idempotency guard: replace-document-content / srcdoc rebuilds can end up
|
|
34
|
+
// re-injecting this script into a document where a previous instance's
|
|
35
|
+
// listeners, overlays, and observers are still alive (e.g. a head-only
|
|
36
|
+
// content swap in replaceRuntimeDocument that preserves persistent overlay
|
|
37
|
+
// nodes but re-runs inline <script> tags). Without this, a second instance
|
|
38
|
+
// would double-post every message and double-attach every document-level
|
|
39
|
+
// listener. Bail out entirely if an instance is already installed.
|
|
40
|
+
if ((window as any).__anEditorChromeBridge) return;
|
|
41
|
+
(window as any).__anEditorChromeBridge = true;
|
|
42
|
+
|
|
33
43
|
var readOnly = __READ_ONLY__;
|
|
34
|
-
|
|
44
|
+
// Raw host-controlled flag, kept separate from the derived
|
|
45
|
+
// `textEditingEnabled` below. The host (DesignCanvas.tsx) live-updates this
|
|
46
|
+
// via the `set-text-editing-enabled` postMessage instead of rebuilding
|
|
47
|
+
// srcdoc, exactly like `set-read-only`. See that handler for why: baking
|
|
48
|
+
// edit/preview-mode toggles into srcdoc would reload every screen iframe on
|
|
49
|
+
// every mode switch (white flash + lost in-iframe/Alpine state).
|
|
50
|
+
var textEditingEnabledFlag = __TEXT_EDITING_ENABLED__;
|
|
51
|
+
var textEditingEnabled = !readOnly && textEditingEnabledFlag;
|
|
35
52
|
var designCanvasScreenId = __DESIGN_CANVAS_SCREEN_ID__ || "";
|
|
36
53
|
var designCanvasBoardSurface = !!__DESIGN_CANVAS_BOARD_SURFACE__;
|
|
37
54
|
var scaleToolEnabled = false;
|
|
@@ -548,6 +565,38 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
548
565
|
};
|
|
549
566
|
}
|
|
550
567
|
|
|
568
|
+
// Raw authored (not computed) inline style values for the properties the
|
|
569
|
+
// EditPanel constraints/position/auto-size readers need to distinguish
|
|
570
|
+
// "unset" from "resolved to a computed pixel value" (e.g. an absolutely
|
|
571
|
+
// positioned element with only `left` authored still computes both `left`
|
|
572
|
+
// and `right` — only the inline style tells you which side was actually
|
|
573
|
+
// set). Empty-string values are omitted so callers can treat key-absence as
|
|
574
|
+
// "not authored".
|
|
575
|
+
var INLINE_STYLE_PROPERTIES = [
|
|
576
|
+
"position",
|
|
577
|
+
"left",
|
|
578
|
+
"right",
|
|
579
|
+
"top",
|
|
580
|
+
"bottom",
|
|
581
|
+
"width",
|
|
582
|
+
"height",
|
|
583
|
+
"transform",
|
|
584
|
+
"whiteSpace",
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
function collectInlineStyles(el: Element): Record<string, string> {
|
|
588
|
+
var styles: Record<string, string> = {};
|
|
589
|
+
var inline = (el as HTMLElement).style;
|
|
590
|
+
if (!inline) return styles;
|
|
591
|
+
INLINE_STYLE_PROPERTIES.forEach(function (property) {
|
|
592
|
+
var value = inline[property as never] as unknown as string;
|
|
593
|
+
if (typeof value === "string" && value !== "") {
|
|
594
|
+
styles[property] = value;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return styles;
|
|
598
|
+
}
|
|
599
|
+
|
|
551
600
|
function chromeColorForElement(el: Element | null): string {
|
|
552
601
|
return elementLooksLikeComponent(el)
|
|
553
602
|
? "var(--design-editor-component-color)"
|
|
@@ -748,7 +797,20 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
748
797
|
filter: cs.filter,
|
|
749
798
|
mixBlendMode: cs.mixBlendMode,
|
|
750
799
|
zIndex: cs.zIndex,
|
|
800
|
+
transform: cs.transform,
|
|
801
|
+
scale: cs.scale,
|
|
802
|
+
visibility: cs.visibility,
|
|
803
|
+
backdropFilter: cs.backdropFilter,
|
|
804
|
+
webkitBackdropFilter: (
|
|
805
|
+
cs as unknown as { webkitBackdropFilter?: string }
|
|
806
|
+
).webkitBackdropFilter,
|
|
807
|
+
flexWrap: cs.flexWrap,
|
|
808
|
+
alignContent: cs.alignContent,
|
|
809
|
+
isolation: cs.isolation,
|
|
810
|
+
whiteSpace: cs.whiteSpace,
|
|
751
811
|
},
|
|
812
|
+
inlineStyles: collectInlineStyles(el),
|
|
813
|
+
primitiveKind: el.getAttribute("data-an-primitive") || undefined,
|
|
752
814
|
portableStyleSnapshot: collectPortableStyleSnapshot(el),
|
|
753
815
|
boundingRect: {
|
|
754
816
|
x: rect.x + (window.scrollX || window.pageXOffset || 0),
|
|
@@ -776,6 +838,49 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
776
838
|
};
|
|
777
839
|
}
|
|
778
840
|
|
|
841
|
+
// Light hover descriptor: every pointer hover posts one of these instead of
|
|
842
|
+
// the full getElementInfo() payload. getElementInfo() runs getComputedStyle
|
|
843
|
+
// over ~130 properties on the element PLUS (via collectPortableStyleSnapshot)
|
|
844
|
+
// up to 80 descendants — fine at select/drag-start time, too expensive to run
|
|
845
|
+
// on every mousemove. Hover-only consumers (outline positioning, code-layer
|
|
846
|
+
// resolution by selector/id/tagName/classes/text) never read computedStyles
|
|
847
|
+
// or portableStyleSnapshot, so this intentionally omits both. Full detail is
|
|
848
|
+
// still posted on element-select / drag-start / edit-time messages via
|
|
849
|
+
// getElementInfo().
|
|
850
|
+
function getLightElementInfo(el: Element): unknown {
|
|
851
|
+
var rect = el.getBoundingClientRect();
|
|
852
|
+
var componentName = componentNameForElement(el);
|
|
853
|
+
var sourceBacked =
|
|
854
|
+
hasStableOwnSource(el) || !!closestStableSourceElement(el);
|
|
855
|
+
var sourceId = sourceBacked ? getSourceId(el) || getSelector(el) : "";
|
|
856
|
+
var parentStyles = el.parentElement
|
|
857
|
+
? window.getComputedStyle(el.parentElement)
|
|
858
|
+
: null;
|
|
859
|
+
var parentDisplay = parentStyles ? parentStyles.display : undefined;
|
|
860
|
+
var cs = window.getComputedStyle(el);
|
|
861
|
+
return {
|
|
862
|
+
tagName: el.tagName.toLowerCase(),
|
|
863
|
+
componentName: componentName || undefined,
|
|
864
|
+
id: el.id || undefined,
|
|
865
|
+
sourceId: sourceId,
|
|
866
|
+
selector: getSelector(el),
|
|
867
|
+
classes: Array.from(el.classList),
|
|
868
|
+
computedStyles: {},
|
|
869
|
+
boundingRect: {
|
|
870
|
+
x: rect.x + (window.scrollX || window.pageXOffset || 0),
|
|
871
|
+
y: rect.y + (window.scrollY || window.pageYOffset || 0),
|
|
872
|
+
width: rect.width,
|
|
873
|
+
height: rect.height,
|
|
874
|
+
},
|
|
875
|
+
textContent: el.textContent ? el.textContent.slice(0, 200) : undefined,
|
|
876
|
+
childElementCount: el.children ? el.children.length : 0,
|
|
877
|
+
isFlexContainer: cs.display === "flex" || cs.display === "inline-flex",
|
|
878
|
+
isGridContainer: cs.display === "grid" || cs.display === "inline-grid",
|
|
879
|
+
isFlexChild: parentDisplay === "flex" || parentDisplay === "inline-flex",
|
|
880
|
+
parentDisplay: parentDisplay,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
779
884
|
function selectionIntentFromEvent(e): {
|
|
780
885
|
additive: boolean;
|
|
781
886
|
range: boolean;
|
|
@@ -1145,6 +1250,33 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
1145
1250
|
onUp: (ev: MouseEvent) => void;
|
|
1146
1251
|
} | null = null;
|
|
1147
1252
|
var activeTextEditEl: HTMLElement | null = null;
|
|
1253
|
+
// Session-captured original min-width/min-height for the active text edit
|
|
1254
|
+
// (T19): refreshOverlays() re-applies these on every reflow via
|
|
1255
|
+
// updateTextEditingChrome, so it needs the real originals rather than "" —
|
|
1256
|
+
// otherwise every overlay refresh during an edit clobbers the saved size
|
|
1257
|
+
// back to the "1px"/"1em" empty-text defaults.
|
|
1258
|
+
var activeTextEditOriginalMinWidth = "";
|
|
1259
|
+
var activeTextEditOriginalMinHeight = "";
|
|
1260
|
+
// Module-level ref to the in-flight text edit session's finish() closure
|
|
1261
|
+
// (T4). replaceRuntimeDocument (forceFullDocument path, e.g. HMR/localhost
|
|
1262
|
+
// reload) must commit or discard the active edit through the same path a
|
|
1263
|
+
// user Escape/blur would use — removing its listeners and clearing overlay
|
|
1264
|
+
// chrome — instead of only resetting the activeTextEditEl variable, which
|
|
1265
|
+
// left the session's keydown/blur/paste/input/selectionchange listeners
|
|
1266
|
+
// (including a document-level "selectionchange" listener) attached forever.
|
|
1267
|
+
var finishActiveTextEdit: ((commit: boolean) => void) | null = null;
|
|
1268
|
+
// Buffered runtime-content-update payload dropped while a text edit session
|
|
1269
|
+
// is active (T13). replaceRuntimeDocument silently no-ops non-force updates
|
|
1270
|
+
// during an edit (so the user's in-progress typing isn't yanked out from
|
|
1271
|
+
// under them), but the host's one-shot queue still marks the update as
|
|
1272
|
+
// applied. Without buffering, the canvas is left stale once the edit
|
|
1273
|
+
// session ends. We keep only the latest dropped payload — a newer update
|
|
1274
|
+
// supersedes an older one.
|
|
1275
|
+
var pendingRuntimeDocumentUpdate: {
|
|
1276
|
+
html: string;
|
|
1277
|
+
preferredSelector: string;
|
|
1278
|
+
selectorCandidates: string[];
|
|
1279
|
+
} | null = null;
|
|
1148
1280
|
var textEditPointerState: {
|
|
1149
1281
|
shield: string;
|
|
1150
1282
|
selection: string;
|
|
@@ -1414,15 +1546,34 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
1414
1546
|
): void {
|
|
1415
1547
|
if (typeof html !== "string") return;
|
|
1416
1548
|
if (activeTextEditEl && !forceFullDocument) {
|
|
1549
|
+
// Don't yank a runtime content update out from under an in-progress
|
|
1550
|
+
// text edit — but don't silently lose it either (T13). Buffer only the
|
|
1551
|
+
// latest payload; it is applied once the edit session ends via
|
|
1552
|
+
// finishActiveTextEdit's replay below.
|
|
1553
|
+
pendingRuntimeDocumentUpdate = {
|
|
1554
|
+
html: html,
|
|
1555
|
+
preferredSelector: preferredSelector,
|
|
1556
|
+
selectorCandidates: Array.isArray(selectorCandidates)
|
|
1557
|
+
? selectorCandidates
|
|
1558
|
+
: [],
|
|
1559
|
+
};
|
|
1417
1560
|
applyHiddenSelectors();
|
|
1418
1561
|
refreshOverlays();
|
|
1419
1562
|
return;
|
|
1420
1563
|
}
|
|
1421
1564
|
if (activeTextEditEl) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1565
|
+
// Commit (or discard, if empty) the active edit through the same path
|
|
1566
|
+
// Escape/blur would use — this removes the session's listeners
|
|
1567
|
+
// (including the document-level "selectionchange" listener) instead of
|
|
1568
|
+
// just resetting activeTextEditEl, which leaked them (T4).
|
|
1569
|
+
if (finishActiveTextEdit) {
|
|
1570
|
+
finishActiveTextEdit(true);
|
|
1571
|
+
} else {
|
|
1572
|
+
postTextEditingState(activeTextEditEl, false);
|
|
1573
|
+
activeTextEditEl = null;
|
|
1574
|
+
setTextEditingPointerPassthrough(false);
|
|
1575
|
+
setSelectionOverlayResizeChromeVisible(true);
|
|
1576
|
+
}
|
|
1426
1577
|
}
|
|
1427
1578
|
var parser = new DOMParser();
|
|
1428
1579
|
var nextDoc = parser.parseFromString(html, "text/html");
|
|
@@ -2193,7 +2344,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2193
2344
|
hideMeasurements();
|
|
2194
2345
|
if (changed) {
|
|
2195
2346
|
(window.parent as Window).postMessage(
|
|
2196
|
-
{ type: "element-hover", payload:
|
|
2347
|
+
{ type: "element-hover", payload: getLightElementInfo(selectedEl) },
|
|
2197
2348
|
"*",
|
|
2198
2349
|
);
|
|
2199
2350
|
}
|
|
@@ -2345,54 +2496,60 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2345
2496
|
});
|
|
2346
2497
|
}
|
|
2347
2498
|
|
|
2499
|
+
// Rotation-aware local-box placement shared by selectionOverlay, the hover
|
|
2500
|
+
// highlightOverlay, and the passive multi-selection overlays: prefer the CSS
|
|
2501
|
+
// box + rotation transform so the outline hugs the rotated element rather
|
|
2502
|
+
// than its inflated axis-aligned bounding box. Returns true when it placed
|
|
2503
|
+
// the overlay (caller should skip the AABB fallback), false when the element
|
|
2504
|
+
// has no usable local box (falls back to getBoundingClientRect).
|
|
2505
|
+
function positionOverlayForRotatedLocalBox(
|
|
2506
|
+
overlay: HTMLElement,
|
|
2507
|
+
el: Element,
|
|
2508
|
+
): boolean {
|
|
2509
|
+
var elCs = window.getComputedStyle(el);
|
|
2510
|
+
var elLeft = readFinitePx(el.style.left || elCs.left);
|
|
2511
|
+
var elTop = readFinitePx(el.style.top || elCs.top);
|
|
2512
|
+
var elW = readFinitePx(el.style.width || elCs.width);
|
|
2513
|
+
var elH = readFinitePx(el.style.height || elCs.height);
|
|
2514
|
+
var elRot = currentRotation(el);
|
|
2515
|
+
var canUseLocalBox =
|
|
2516
|
+
Math.abs(elRot) > 0.01 &&
|
|
2517
|
+
elLeft !== null &&
|
|
2518
|
+
elTop !== null &&
|
|
2519
|
+
elW !== null &&
|
|
2520
|
+
elH !== null;
|
|
2521
|
+
if (!canUseLocalBox) return false;
|
|
2522
|
+
// Convert element-local left/top to viewport coords by walking to the
|
|
2523
|
+
// nearest positioned ancestor (same reference frame as getBoundingClientRect).
|
|
2524
|
+
var parentRect = (
|
|
2525
|
+
(el as HTMLElement).offsetParent || document.documentElement
|
|
2526
|
+
).getBoundingClientRect();
|
|
2527
|
+
overlay.style.display = "block";
|
|
2528
|
+
overlay.style.left = parentRect.left + elLeft + "px";
|
|
2529
|
+
overlay.style.top = parentRect.top + elTop + "px";
|
|
2530
|
+
overlay.style.width = elW + "px";
|
|
2531
|
+
overlay.style.height = elH + "px";
|
|
2532
|
+
overlay.style.transform = "rotate(" + elRot + "deg)";
|
|
2533
|
+
overlay.style.transformOrigin = "0 0";
|
|
2534
|
+
return true;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2348
2537
|
function positionOverlay(overlay: HTMLElement, el: Element): void {
|
|
2349
2538
|
if (!el || !document.documentElement.contains(el)) {
|
|
2350
2539
|
overlay.style.display = "none";
|
|
2351
2540
|
if (overlay === selectionOverlay) hideSelectionOverlay();
|
|
2352
2541
|
return;
|
|
2353
2542
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
var canUseLocalBox =
|
|
2364
|
-
Math.abs(elRot) > 0.01 &&
|
|
2365
|
-
elLeft !== null &&
|
|
2366
|
-
elTop !== null &&
|
|
2367
|
-
elW !== null &&
|
|
2368
|
-
elH !== null;
|
|
2369
|
-
// Convert element-local left/top to viewport coords by walking to the
|
|
2370
|
-
// nearest positioned ancestor (same reference frame as getBoundingClientRect).
|
|
2371
|
-
if (canUseLocalBox) {
|
|
2372
|
-
var parentRect = (
|
|
2373
|
-
(el as HTMLElement).offsetParent || document.documentElement
|
|
2374
|
-
).getBoundingClientRect();
|
|
2375
|
-
overlay.style.display = "block";
|
|
2376
|
-
overlay.style.left = parentRect.left + elLeft + "px";
|
|
2377
|
-
overlay.style.top = parentRect.top + elTop + "px";
|
|
2378
|
-
overlay.style.width = elW + "px";
|
|
2379
|
-
overlay.style.height = elH + "px";
|
|
2380
|
-
overlay.style.transform = "rotate(" + elRot + "deg)";
|
|
2381
|
-
overlay.style.transformOrigin = "0 0";
|
|
2382
|
-
applySelectionChrome(el);
|
|
2383
|
-
updateSpacingOverlay(el);
|
|
2384
|
-
updateComponentTag(el);
|
|
2385
|
-
updateParentAutoLayoutOverlay(el);
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2543
|
+
var placedRotatedLocalBox = positionOverlayForRotatedLocalBox(overlay, el);
|
|
2544
|
+
if (!placedRotatedLocalBox) {
|
|
2545
|
+
var rect = el.getBoundingClientRect();
|
|
2546
|
+
overlay.style.display = "block";
|
|
2547
|
+
overlay.style.top = rect.top + "px";
|
|
2548
|
+
overlay.style.left = rect.left + "px";
|
|
2549
|
+
overlay.style.width = rect.width + "px";
|
|
2550
|
+
overlay.style.height = rect.height + "px";
|
|
2551
|
+
overlay.style.transform = "";
|
|
2388
2552
|
}
|
|
2389
|
-
var rect = el.getBoundingClientRect();
|
|
2390
|
-
overlay.style.display = "block";
|
|
2391
|
-
overlay.style.top = rect.top + "px";
|
|
2392
|
-
overlay.style.left = rect.left + "px";
|
|
2393
|
-
overlay.style.width = rect.width + "px";
|
|
2394
|
-
overlay.style.height = rect.height + "px";
|
|
2395
|
-
overlay.style.transform = "";
|
|
2396
2553
|
if (overlay === selectionOverlay) {
|
|
2397
2554
|
applySelectionChrome(el);
|
|
2398
2555
|
updateSpacingOverlay(el);
|
|
@@ -2416,7 +2573,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2416
2573
|
}
|
|
2417
2574
|
if (textEditingEl) {
|
|
2418
2575
|
if (activeTextEditEl === textEditingEl) {
|
|
2419
|
-
updateTextEditingChrome(
|
|
2576
|
+
updateTextEditingChrome(
|
|
2577
|
+
textEditingEl,
|
|
2578
|
+
activeTextEditOriginalMinWidth,
|
|
2579
|
+
activeTextEditOriginalMinHeight,
|
|
2580
|
+
);
|
|
2420
2581
|
}
|
|
2421
2582
|
if (!hasTextCharacters(textEditingEl)) {
|
|
2422
2583
|
hideSelectionOverlay();
|
|
@@ -2430,6 +2591,103 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2430
2591
|
var overlay = passiveSelectionOverlays[index];
|
|
2431
2592
|
if (overlay) positionOverlay(overlay, el);
|
|
2432
2593
|
});
|
|
2594
|
+
syncOverlayObservers();
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// Coalesced overlay refresh: ResizeObserver/MutationObserver callbacks can
|
|
2598
|
+
// fire in bursts (e.g. a font/image load reflowing many ancestors, or an
|
|
2599
|
+
// Alpine x-show toggling several siblings in one microtask). Collapse any
|
|
2600
|
+
// number of triggers within a frame into a single refreshOverlays() call.
|
|
2601
|
+
var refreshOverlaysScheduled = false;
|
|
2602
|
+
function scheduleRefreshOverlays(): void {
|
|
2603
|
+
if (refreshOverlaysScheduled) return;
|
|
2604
|
+
refreshOverlaysScheduled = true;
|
|
2605
|
+
window.requestAnimationFrame(function () {
|
|
2606
|
+
refreshOverlaysScheduled = false;
|
|
2607
|
+
refreshOverlays();
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// ResizeObserver on the selected + hovered elements catches size changes
|
|
2612
|
+
// that scroll/resize listeners miss entirely: webfont swap reflow, image
|
|
2613
|
+
// decode, CSS transitions/animations, and Alpine/Vue reactivity toggling
|
|
2614
|
+
// classes on the element itself. MutationObserver (attributes + childList,
|
|
2615
|
+
// scoped to the selected element and its parent) catches structural/attr
|
|
2616
|
+
// changes that resize an element without necessarily firing a ResizeObserver
|
|
2617
|
+
// entry on that exact node (e.g. a sibling insertion shifting layout).
|
|
2618
|
+
var overlayResizeObserver: ResizeObserver | null = null;
|
|
2619
|
+
var overlayMutationObserver: MutationObserver | null = null;
|
|
2620
|
+
var observedResizeEls: Element[] = [];
|
|
2621
|
+
var observedMutationRoot: Element | null = null;
|
|
2622
|
+
|
|
2623
|
+
function ensureOverlayObservers(): void {
|
|
2624
|
+
if (!overlayResizeObserver && typeof ResizeObserver !== "undefined") {
|
|
2625
|
+
overlayResizeObserver = new ResizeObserver(function () {
|
|
2626
|
+
scheduleRefreshOverlays();
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
if (!overlayMutationObserver && typeof MutationObserver !== "undefined") {
|
|
2630
|
+
overlayMutationObserver = new MutationObserver(function () {
|
|
2631
|
+
scheduleRefreshOverlays();
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function syncOverlayObservers(): void {
|
|
2637
|
+
ensureOverlayObservers();
|
|
2638
|
+
if (overlayResizeObserver) {
|
|
2639
|
+
var nextTargets: Element[] = [];
|
|
2640
|
+
if (selectedEl && document.documentElement.contains(selectedEl)) {
|
|
2641
|
+
nextTargets.push(selectedEl);
|
|
2642
|
+
}
|
|
2643
|
+
if (
|
|
2644
|
+
hoveredEl &&
|
|
2645
|
+
hoveredEl !== selectedEl &&
|
|
2646
|
+
document.documentElement.contains(hoveredEl)
|
|
2647
|
+
) {
|
|
2648
|
+
nextTargets.push(hoveredEl);
|
|
2649
|
+
}
|
|
2650
|
+
var targetsChanged =
|
|
2651
|
+
nextTargets.length !== observedResizeEls.length ||
|
|
2652
|
+
nextTargets.some(function (el, i) {
|
|
2653
|
+
return observedResizeEls[i] !== el;
|
|
2654
|
+
});
|
|
2655
|
+
if (targetsChanged) {
|
|
2656
|
+
observedResizeEls.forEach(function (el) {
|
|
2657
|
+
overlayResizeObserver!.unobserve(el);
|
|
2658
|
+
});
|
|
2659
|
+
nextTargets.forEach(function (el) {
|
|
2660
|
+
overlayResizeObserver!.observe(el);
|
|
2661
|
+
});
|
|
2662
|
+
observedResizeEls = nextTargets;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
if (overlayMutationObserver) {
|
|
2666
|
+
var nextRoot: Element | null =
|
|
2667
|
+
selectedEl && document.documentElement.contains(selectedEl)
|
|
2668
|
+
? selectedEl.parentElement || selectedEl
|
|
2669
|
+
: null;
|
|
2670
|
+
if (nextRoot !== observedMutationRoot) {
|
|
2671
|
+
overlayMutationObserver.disconnect();
|
|
2672
|
+
if (nextRoot) {
|
|
2673
|
+
overlayMutationObserver.observe(nextRoot, {
|
|
2674
|
+
attributes: true,
|
|
2675
|
+
childList: true,
|
|
2676
|
+
subtree: false,
|
|
2677
|
+
});
|
|
2678
|
+
// Also watch the selected element itself for attribute changes
|
|
2679
|
+
// (e.g. class/style toggles) when it isn't the observed root.
|
|
2680
|
+
if (nextRoot !== selectedEl && selectedEl) {
|
|
2681
|
+
overlayMutationObserver.observe(selectedEl, {
|
|
2682
|
+
attributes: true,
|
|
2683
|
+
childList: true,
|
|
2684
|
+
subtree: false,
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
observedMutationRoot = nextRoot;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2433
2691
|
}
|
|
2434
2692
|
|
|
2435
2693
|
function hideMeasurements(): void {
|
|
@@ -2728,6 +2986,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
2728
2986
|
}
|
|
2729
2987
|
|
|
2730
2988
|
function shouldForwardDesignHotkey(e) {
|
|
2989
|
+
// Read-only surfaces (e.g. background/inactive board screens) must never
|
|
2990
|
+
// forward edit hotkeys or preventDefault() native browser shortcuts —
|
|
2991
|
+
// Escape/Enter/Tab/Delete/arrow-key/undo-redo forwarding is an editing
|
|
2992
|
+
// affordance and has no business intercepting keys on a passive view.
|
|
2993
|
+
if (readOnly) return false;
|
|
2731
2994
|
if (activeTextEditEl || isEditorTypingTarget(e.target) || e.isComposing)
|
|
2732
2995
|
return false;
|
|
2733
2996
|
var key = e.key;
|
|
@@ -3564,6 +3827,64 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
3564
3827
|
selection.addRange(range);
|
|
3565
3828
|
}
|
|
3566
3829
|
|
|
3830
|
+
// T2: Figma-style text editing treats Enter as a line break while editing
|
|
3831
|
+
// (Escape or blur commits and exits). Uses the same insertText execCommand
|
|
3832
|
+
// path as insertPlainTextAtSelection so undo grouping/IME behavior matches,
|
|
3833
|
+
// falling back to a manual <br> insertion when insertText isn't supported.
|
|
3834
|
+
function insertLineBreak(): void {
|
|
3835
|
+
if (
|
|
3836
|
+
document.queryCommandSupported &&
|
|
3837
|
+
document.queryCommandSupported("insertText")
|
|
3838
|
+
) {
|
|
3839
|
+
document.execCommand("insertText", false, "\n");
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
var selection: Selection | null = window.getSelection
|
|
3843
|
+
? window.getSelection()
|
|
3844
|
+
: null;
|
|
3845
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
3846
|
+
var range = selection.getRangeAt(0);
|
|
3847
|
+
range.deleteContents();
|
|
3848
|
+
var br = document.createElement("br");
|
|
3849
|
+
range.insertNode(br);
|
|
3850
|
+
range.setStartAfter(br);
|
|
3851
|
+
range.setEndAfter(br);
|
|
3852
|
+
selection.removeAllRanges();
|
|
3853
|
+
selection.addRange(range);
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
// T12: applyTextRangeStyle wraps a fresh <span> per invocation, so repeated
|
|
3857
|
+
// scrub/commit cycles on the same range nest span chains
|
|
3858
|
+
// (<span><span><span>text</span></span></span>) that persist in saved HTML.
|
|
3859
|
+
// Collapse any run of nested spans that carry the exact same style
|
|
3860
|
+
// attribute and no other attributes down to a single span. Only merges
|
|
3861
|
+
// spans that are the sole child of their parent span (an exact 1:1 nesting,
|
|
3862
|
+
// not spans that merely overlap a wider range).
|
|
3863
|
+
function normalizeNestedIdenticalSpans(root: Element | null): void {
|
|
3864
|
+
if (!root) return;
|
|
3865
|
+
var spans = Array.prototype.slice.call(root.querySelectorAll("span"));
|
|
3866
|
+
for (var i = 0; i < spans.length; i += 1) {
|
|
3867
|
+
var span = spans[i];
|
|
3868
|
+
if (!span || !span.parentNode) continue;
|
|
3869
|
+
var parent = span.parentNode;
|
|
3870
|
+
while (
|
|
3871
|
+
parent &&
|
|
3872
|
+
parent.nodeType === 1 &&
|
|
3873
|
+
(parent as Element).tagName === "SPAN" &&
|
|
3874
|
+
(parent as Element).childNodes.length === 1 &&
|
|
3875
|
+
(parent as Element).getAttribute("style") ===
|
|
3876
|
+
span.getAttribute("style") &&
|
|
3877
|
+
(parent as Element).attributes.length === span.attributes.length
|
|
3878
|
+
) {
|
|
3879
|
+
var grandparent = parent.parentNode;
|
|
3880
|
+
if (!grandparent) break;
|
|
3881
|
+
grandparent.insertBefore(span, parent);
|
|
3882
|
+
grandparent.removeChild(parent);
|
|
3883
|
+
parent = grandparent;
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3567
3888
|
function selectionBelongsToElement(
|
|
3568
3889
|
selection: Selection | null,
|
|
3569
3890
|
el: Element | null,
|
|
@@ -3597,6 +3918,50 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
3597
3918
|
return true;
|
|
3598
3919
|
}
|
|
3599
3920
|
|
|
3921
|
+
// T12: if the current selection exactly covers an existing <span>'s content
|
|
3922
|
+
// (i.e. the user is re-scrubbing/re-applying a style to the same range
|
|
3923
|
+
// rather than a new sub-range), reuse that span instead of wrapping another
|
|
3924
|
+
// one around it. Without this, a scrub gesture that fires applyTextRangeStyle
|
|
3925
|
+
// many times per gesture nests a new <span> on every tick
|
|
3926
|
+
// (<span><span><span>text</span></span></span>), and those chains persist
|
|
3927
|
+
// in the saved HTML.
|
|
3928
|
+
function exactCoverSpanForRange(range: Range): HTMLSpanElement | null {
|
|
3929
|
+
var start = range.startContainer;
|
|
3930
|
+
var end = range.endContainer;
|
|
3931
|
+
if (start !== end) return null;
|
|
3932
|
+
|
|
3933
|
+
// Shape A: start/end container IS the span itself, with node-offsets
|
|
3934
|
+
// spanning all of its children (this is what
|
|
3935
|
+
// Range.selectNodeContents(span) produces — offsets into an Element
|
|
3936
|
+
// container count child nodes, not characters).
|
|
3937
|
+
if (start.nodeType === 1) {
|
|
3938
|
+
var containerEl = start as HTMLElement;
|
|
3939
|
+
if (containerEl.tagName !== "SPAN") return null;
|
|
3940
|
+
if (containerEl.childNodes.length !== 1) return null;
|
|
3941
|
+
if (range.startOffset !== 0 || range.endOffset !== 1) return null;
|
|
3942
|
+
return containerEl as HTMLSpanElement;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
// Shape B: start/end container is a text node, with character offsets
|
|
3946
|
+
// spanning its full length (this is what surroundContents +
|
|
3947
|
+
// selectNodeContents(span) round-trips to on a subsequent read, or what a
|
|
3948
|
+
// caller-constructed range over the text node directly looks like).
|
|
3949
|
+
if (start.nodeType !== 3) return null; // text node
|
|
3950
|
+
var parent = start.parentNode;
|
|
3951
|
+
if (!parent || parent.nodeType !== 1) return null;
|
|
3952
|
+
var el = parent as HTMLElement;
|
|
3953
|
+
if (el.tagName !== "SPAN") return null;
|
|
3954
|
+
// The span must contain exactly this one text node, and the range must
|
|
3955
|
+
// span the text node's full content (not a sub-range within it).
|
|
3956
|
+
if (el.childNodes.length !== 1 || el.childNodes[0] !== start) return null;
|
|
3957
|
+
if (
|
|
3958
|
+
range.startOffset !== 0 ||
|
|
3959
|
+
range.endOffset !== start.textContent!.length
|
|
3960
|
+
)
|
|
3961
|
+
return null;
|
|
3962
|
+
return el as HTMLSpanElement;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3600
3965
|
function applyTextRangeStyle(property: unknown, value: unknown): boolean {
|
|
3601
3966
|
if (!activeTextEditEl || !property) return false;
|
|
3602
3967
|
var selection: Selection | null = window.getSelection
|
|
@@ -3606,6 +3971,15 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
3606
3971
|
return false;
|
|
3607
3972
|
if (!selectionBelongsToElement(selection, activeTextEditEl)) return false;
|
|
3608
3973
|
var range = selection.getRangeAt(0);
|
|
3974
|
+
var reused = exactCoverSpanForRange(range);
|
|
3975
|
+
if (reused) {
|
|
3976
|
+
if (!applyInlineStyleProperty(reused, property, value)) return false;
|
|
3977
|
+
selection.removeAllRanges();
|
|
3978
|
+
var reusedRange = document.createRange();
|
|
3979
|
+
reusedRange.selectNodeContents(reused);
|
|
3980
|
+
selection.addRange(reusedRange);
|
|
3981
|
+
return true;
|
|
3982
|
+
}
|
|
3609
3983
|
var span = document.createElement("span");
|
|
3610
3984
|
applyInlineStyleProperty(span, property, value);
|
|
3611
3985
|
if (!span.getAttribute("style")) return false;
|
|
@@ -5096,6 +5470,18 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5096
5470
|
}
|
|
5097
5471
|
}
|
|
5098
5472
|
|
|
5473
|
+
// T5: elements that must never become contenteditable via the raw-target
|
|
5474
|
+
// fallback below, even when a caller opts into programmatic text editing.
|
|
5475
|
+
// Chrome overlays are never real content; img/svg/canvas cannot host a text
|
|
5476
|
+
// selection/caret the way findTextEditTarget expects and would leave the
|
|
5477
|
+
// editor in a broken state (see the warning comment in findTextEditTarget).
|
|
5478
|
+
function isRejectedRawTextEditTarget(el: Element | null): boolean {
|
|
5479
|
+
if (!el) return true;
|
|
5480
|
+
if (isOverlayElement(el)) return true;
|
|
5481
|
+
var tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
5482
|
+
return tag === "img" || tag === "svg" || tag === "canvas";
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5099
5485
|
function beginTextEditingFromEvent(e, forceTextEditing) {
|
|
5100
5486
|
if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target))
|
|
5101
5487
|
return;
|
|
@@ -5106,10 +5492,25 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5106
5492
|
stopNativeInteraction(e);
|
|
5107
5493
|
var eventTarget =
|
|
5108
5494
|
e && e.target && e.target.nodeType === 1 ? e.target : null;
|
|
5495
|
+
// The raw `eventTarget` fallback (no findTextEditTarget resolution at
|
|
5496
|
+
// all) bypasses findTextEditTarget's editable-ancestor check entirely, so
|
|
5497
|
+
// it is only safe when the caller has explicitly opted into programmatic
|
|
5498
|
+
// text editing (e.g. an agent action creating a new text primitive and
|
|
5499
|
+
// immediately entering edit mode on it) — and even then, never for a
|
|
5500
|
+
// chrome overlay or an img/svg/canvas element, which cannot be made
|
|
5501
|
+
// sensibly contenteditable.
|
|
5502
|
+
var programmaticFlag =
|
|
5503
|
+
!!e &&
|
|
5504
|
+
(e as unknown as { agentNativeProgrammaticTextEdit?: boolean })
|
|
5505
|
+
.agentNativeProgrammaticTextEdit === true;
|
|
5506
|
+
var rawTargetFallback =
|
|
5507
|
+
programmaticFlag && !isRejectedRawTextEditTarget(eventTarget)
|
|
5508
|
+
? eventTarget
|
|
5509
|
+
: null;
|
|
5109
5510
|
var target =
|
|
5110
5511
|
findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY)) ||
|
|
5111
5512
|
findTextEditTarget(eventTarget) ||
|
|
5112
|
-
|
|
5513
|
+
rawTargetFallback;
|
|
5113
5514
|
if (!target || target.nodeType !== 1) return;
|
|
5114
5515
|
// Anchor the selection identity to the nearest source-backed element. Text
|
|
5115
5516
|
// editing still operates on the actual target text node, but a later
|
|
@@ -5117,10 +5518,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5117
5518
|
// code-layer node rather than a runtime-only descendant (which would emit a
|
|
5118
5519
|
// brittle body > div:nth-of-type(...) selector that never resolves).
|
|
5119
5520
|
selectedEl = selectionTargetForHit(target) || target;
|
|
5120
|
-
var programmaticTextEdit =
|
|
5121
|
-
!!e &&
|
|
5122
|
-
(e as unknown as { agentNativeProgrammaticTextEdit?: boolean })
|
|
5123
|
-
.agentNativeProgrammaticTextEdit === true;
|
|
5521
|
+
var programmaticTextEdit = programmaticFlag;
|
|
5124
5522
|
var originalText = target.textContent || "";
|
|
5125
5523
|
var originalHtml = target.innerHTML || "";
|
|
5126
5524
|
var originalMinWidth = target.style.minWidth;
|
|
@@ -5130,6 +5528,27 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5130
5528
|
var originalOutlineOffset = target.style.outlineOffset;
|
|
5131
5529
|
var committed = false;
|
|
5132
5530
|
activeTextEditEl = target;
|
|
5531
|
+
// T19: publish this session's captured originals so refreshOverlays()
|
|
5532
|
+
// (which runs on ResizeObserver/MutationObserver ticks during the edit,
|
|
5533
|
+
// not just from inside this closure) can pass the real values instead of
|
|
5534
|
+
// "" to updateTextEditingChrome.
|
|
5535
|
+
activeTextEditOriginalMinWidth = originalMinWidth;
|
|
5536
|
+
activeTextEditOriginalMinHeight = originalMinHeight;
|
|
5537
|
+
// T20: per-keystroke chrome updates (onInput/onSelectionChange) and the
|
|
5538
|
+
// 3-4 postMessages they cause (text-editing-state + a caret-move
|
|
5539
|
+
// selectionchange on every arrow key / click) are coalesced into a single
|
|
5540
|
+
// rAF tick instead of firing synchronously on every event.
|
|
5541
|
+
var chromeUpdateScheduled = false;
|
|
5542
|
+
function scheduleTextEditingChromeUpdate() {
|
|
5543
|
+
if (chromeUpdateScheduled) return;
|
|
5544
|
+
chromeUpdateScheduled = true;
|
|
5545
|
+
window.requestAnimationFrame(function () {
|
|
5546
|
+
chromeUpdateScheduled = false;
|
|
5547
|
+
if (committed) return;
|
|
5548
|
+
updateTextEditingChrome(target, originalMinWidth, originalMinHeight);
|
|
5549
|
+
postTextEditingState(target, true);
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5133
5552
|
target.setAttribute("contenteditable", "true");
|
|
5134
5553
|
target.setAttribute("data-agent-native-text-editing", "true");
|
|
5135
5554
|
target.style.cursor = "text";
|
|
@@ -5174,19 +5593,43 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5174
5593
|
setTextEditingPointerPassthrough(false);
|
|
5175
5594
|
setSelectionOverlayResizeChromeVisible(true);
|
|
5176
5595
|
if (activeTextEditEl === target) activeTextEditEl = null;
|
|
5596
|
+
// T4: this session no longer owns the active-edit slot.
|
|
5597
|
+
if (finishActiveTextEdit === finish) finishActiveTextEdit = null;
|
|
5177
5598
|
postTextEditingState(target, false);
|
|
5178
5599
|
if (!commit) {
|
|
5179
5600
|
target.innerHTML = originalHtml;
|
|
5180
5601
|
refreshOverlays();
|
|
5181
5602
|
return;
|
|
5182
5603
|
}
|
|
5604
|
+
// T12: collapse any nested-identical-style <span> chains left behind by
|
|
5605
|
+
// repeated applyTextRangeStyle scrub/commit cycles before reading out
|
|
5606
|
+
// the committed HTML.
|
|
5607
|
+
normalizeNestedIdenticalSpans(target);
|
|
5183
5608
|
var next = target.textContent || "";
|
|
5184
5609
|
var nextHtml = target.innerHTML || "";
|
|
5185
5610
|
refreshOverlays();
|
|
5186
5611
|
if (next !== originalText || nextHtml !== originalHtml) {
|
|
5187
5612
|
postTextContentChange(target, next, nextHtml);
|
|
5188
5613
|
}
|
|
5614
|
+
// T13: replay the latest runtime-content update that arrived (and was
|
|
5615
|
+
// buffered) while this edit session was active, so the canvas isn't
|
|
5616
|
+
// left stale now that editing has ended.
|
|
5617
|
+
if (pendingRuntimeDocumentUpdate) {
|
|
5618
|
+
var pending = pendingRuntimeDocumentUpdate;
|
|
5619
|
+
pendingRuntimeDocumentUpdate = null;
|
|
5620
|
+
replaceRuntimeDocument(
|
|
5621
|
+
pending.html,
|
|
5622
|
+
pending.preferredSelector,
|
|
5623
|
+
pending.selectorCandidates,
|
|
5624
|
+
true,
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5189
5627
|
}
|
|
5628
|
+
// T4: publish this session's finish() so replaceRuntimeDocument (and any
|
|
5629
|
+
// other caller that must end an in-progress edit deterministically) can
|
|
5630
|
+
// commit/discard through the same listener-teardown path a user
|
|
5631
|
+
// Escape/blur would take, instead of only resetting activeTextEditEl.
|
|
5632
|
+
finishActiveTextEdit = finish;
|
|
5190
5633
|
|
|
5191
5634
|
function onBlur() {
|
|
5192
5635
|
if (programmaticTextEdit && !(target.textContent || "").trim()) {
|
|
@@ -5202,16 +5645,42 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5202
5645
|
}
|
|
5203
5646
|
|
|
5204
5647
|
function onKeyDown(ev) {
|
|
5648
|
+
// T3: bail out on IME composition input (e.g. CJK/Korean input method
|
|
5649
|
+
// candidate selection) so a composing Enter/Escape keystroke isn't
|
|
5650
|
+
// intercepted as a commit/newline before the IME has finished composing
|
|
5651
|
+
// the character. keyCode 229 is the legacy signal browsers send for
|
|
5652
|
+
// composition keydowns that don't set isComposing.
|
|
5653
|
+
if (ev.isComposing || ev.keyCode === 229) return;
|
|
5205
5654
|
if (ev.key === "Escape") {
|
|
5206
5655
|
ev.preventDefault();
|
|
5207
5656
|
finish(true);
|
|
5208
5657
|
target.blur();
|
|
5209
5658
|
return;
|
|
5210
5659
|
}
|
|
5660
|
+
// T2: Enter inserts a line break while editing (Figma convention);
|
|
5661
|
+
// Escape or blur (click-out) is what commits and exits the session.
|
|
5211
5662
|
if (ev.key === "Enter" && !ev.shiftKey) {
|
|
5212
5663
|
ev.preventDefault();
|
|
5213
|
-
|
|
5214
|
-
|
|
5664
|
+
insertLineBreak();
|
|
5665
|
+
scheduleTextEditingChromeUpdate();
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
// T21: forward Cmd/Ctrl+B and Cmd/Ctrl+I within the edit session to
|
|
5669
|
+
// execCommand bold/italic on the current selection. normalizeNestedIdenticalSpans
|
|
5670
|
+
// (T12) cleans up any span nesting execCommand leaves behind when the
|
|
5671
|
+
// session commits.
|
|
5672
|
+
var metaOrCtrl = ev.metaKey || ev.ctrlKey;
|
|
5673
|
+
if (metaOrCtrl && !ev.altKey && ev.key.toLowerCase() === "b") {
|
|
5674
|
+
ev.preventDefault();
|
|
5675
|
+
document.execCommand("bold");
|
|
5676
|
+
scheduleTextEditingChromeUpdate();
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5679
|
+
if (metaOrCtrl && !ev.altKey && ev.key.toLowerCase() === "i") {
|
|
5680
|
+
ev.preventDefault();
|
|
5681
|
+
document.execCommand("italic");
|
|
5682
|
+
scheduleTextEditingChromeUpdate();
|
|
5683
|
+
return;
|
|
5215
5684
|
}
|
|
5216
5685
|
}
|
|
5217
5686
|
|
|
@@ -5220,18 +5689,15 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5220
5689
|
insertPlainTextAtSelection(
|
|
5221
5690
|
(ev.clipboardData && ev.clipboardData.getData("text/plain")) || "",
|
|
5222
5691
|
);
|
|
5223
|
-
|
|
5224
|
-
postTextEditingState(target, true);
|
|
5692
|
+
scheduleTextEditingChromeUpdate();
|
|
5225
5693
|
}
|
|
5226
5694
|
|
|
5227
5695
|
function onInput() {
|
|
5228
|
-
|
|
5229
|
-
postTextEditingState(target, true);
|
|
5696
|
+
scheduleTextEditingChromeUpdate();
|
|
5230
5697
|
}
|
|
5231
5698
|
|
|
5232
5699
|
function onSelectionChange() {
|
|
5233
|
-
|
|
5234
|
-
postTextEditingState(target, true);
|
|
5700
|
+
scheduleTextEditingChromeUpdate();
|
|
5235
5701
|
}
|
|
5236
5702
|
|
|
5237
5703
|
target.addEventListener("blur", onBlur, true);
|
|
@@ -5300,7 +5766,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5300
5766
|
} else {
|
|
5301
5767
|
hideMeasurements();
|
|
5302
5768
|
}
|
|
5303
|
-
var info =
|
|
5769
|
+
var info = getLightElementInfo(hoveredEl);
|
|
5304
5770
|
(window.parent as Window).postMessage(
|
|
5305
5771
|
{ type: "element-hover", payload: info },
|
|
5306
5772
|
"*",
|
|
@@ -5363,11 +5829,17 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5363
5829
|
window.addEventListener("message", function (e) {
|
|
5364
5830
|
if (e.source !== window.parent) return;
|
|
5365
5831
|
if (!e.data) return;
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5832
|
+
// NOTE: no message type in this handler is sourced from a `payload`
|
|
5833
|
+
// sub-object — every host sender (DesignCanvas.tsx) puts its fields
|
|
5834
|
+
// directly on the top-level message. A previous blanket
|
|
5835
|
+
// `Object.keys(e.data.payload).forEach(...)` hoist here copied every key
|
|
5836
|
+
// of an arbitrary `payload` object onto `e.data` for ANY message type,
|
|
5837
|
+
// which could let an attacker-controlled `payload` (e.g. relayed through
|
|
5838
|
+
// a less-trusted surface) inject fields like `readOnly`, `force`, or
|
|
5839
|
+
// `content` into a message type that never intended to accept them. If a
|
|
5840
|
+
// future message type needs payload-sourced fields, extract them
|
|
5841
|
+
// explicitly inside that type's own branch below instead of reintroducing
|
|
5842
|
+
// a blanket hoist.
|
|
5371
5843
|
// set-read-only: toggle the bridge's readOnly state in-place without a reload.
|
|
5372
5844
|
// When readOnly becomes true the shield/selection/drag/edit entry points are
|
|
5373
5845
|
// gated so the surface is safe for background/inactive display use.
|
|
@@ -5375,7 +5847,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5375
5847
|
var nextReadOnly = !!e.data.readOnly;
|
|
5376
5848
|
if (readOnly === nextReadOnly) return;
|
|
5377
5849
|
readOnly = nextReadOnly;
|
|
5378
|
-
textEditingEnabled = !readOnly &&
|
|
5850
|
+
textEditingEnabled = !readOnly && textEditingEnabledFlag;
|
|
5379
5851
|
if (readOnly) {
|
|
5380
5852
|
// Leave the text editor gracefully before going read-only.
|
|
5381
5853
|
if (activeTextEditEl) {
|
|
@@ -5388,6 +5860,27 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5388
5860
|
}
|
|
5389
5861
|
return;
|
|
5390
5862
|
}
|
|
5863
|
+
// set-text-editing-enabled: toggle the bridge's edit/preview-mode flag
|
|
5864
|
+
// in-place without a reload, mirroring set-read-only above. The host
|
|
5865
|
+
// flips this whenever DesignCanvas's `editMode` prop changes (Edit ⇄
|
|
5866
|
+
// Preview). Without this postMessage path, `editMode` would need to stay
|
|
5867
|
+
// a srcdoc dependency, which rebuilds and reloads every screen iframe on
|
|
5868
|
+
// every edit/preview toggle.
|
|
5869
|
+
if (e.data.type === "set-text-editing-enabled") {
|
|
5870
|
+
var nextTextEditingEnabledFlag = !!e.data.enabled;
|
|
5871
|
+
if (textEditingEnabledFlag === nextTextEditingEnabledFlag) return;
|
|
5872
|
+
textEditingEnabledFlag = nextTextEditingEnabledFlag;
|
|
5873
|
+
var nextTextEditingEnabled = !readOnly && textEditingEnabledFlag;
|
|
5874
|
+
if (textEditingEnabled === nextTextEditingEnabled) return;
|
|
5875
|
+
textEditingEnabled = nextTextEditingEnabled;
|
|
5876
|
+
// Leaving text-editing-enabled mode: gracefully exit any in-progress
|
|
5877
|
+
// text edit rather than leaving a live contenteditable behind, mirroring
|
|
5878
|
+
// the readOnly transition above.
|
|
5879
|
+
if (!textEditingEnabled && activeTextEditEl) {
|
|
5880
|
+
activeTextEditEl.blur();
|
|
5881
|
+
}
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5391
5884
|
// begin-text-edit: enter text-editing mode for the element identified by
|
|
5392
5885
|
// nodeId immediately (no double-click needed). Used after programmatic
|
|
5393
5886
|
// text-element creation so the user can type right away and the autosize
|
|
@@ -5473,6 +5966,53 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5473
5966
|
);
|
|
5474
5967
|
return;
|
|
5475
5968
|
}
|
|
5969
|
+
// agent-native:text-edit-status: host-side query used instead of a direct
|
|
5970
|
+
// (now sandbox-blocked) iframe.contentDocument read. Mirrors the exact
|
|
5971
|
+
// resolution `postBeginTextEditToPreviewIframes` (DesignEditor.tsx) used to
|
|
5972
|
+
// do itself: find the node by data-agent-native-node-id, and report whether
|
|
5973
|
+
// a text-edit session is currently "active" on it (focused element carries
|
|
5974
|
+
// data-agent-native-text-editing), "done" (non-empty committed text and not
|
|
5975
|
+
// actively focused), or neither.
|
|
5976
|
+
if (e.data.type === "agent-native:text-edit-status") {
|
|
5977
|
+
var textEditStatusCorrelationId: string =
|
|
5978
|
+
typeof e.data.correlationId === "string" ? e.data.correlationId : "";
|
|
5979
|
+
var textEditStatusNodeId: string =
|
|
5980
|
+
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
5981
|
+
var textEditStatus: "active" | "done" | false = false;
|
|
5982
|
+
if (textEditStatusNodeId) {
|
|
5983
|
+
var escapedTextEditStatusNodeId = textEditStatusNodeId
|
|
5984
|
+
.replace(/\\/g, "\\\\")
|
|
5985
|
+
.replace(/"/g, '\\"');
|
|
5986
|
+
var textEditStatusNode: Element | null = document.querySelector(
|
|
5987
|
+
'[data-agent-native-node-id="' + escapedTextEditStatusNodeId + '"]',
|
|
5988
|
+
);
|
|
5989
|
+
var textEditStatusEditingEl: Element | null = document.querySelector(
|
|
5990
|
+
'[data-agent-native-node-id="' +
|
|
5991
|
+
escapedTextEditStatusNodeId +
|
|
5992
|
+
'"][data-agent-native-text-editing]',
|
|
5993
|
+
);
|
|
5994
|
+
if (
|
|
5995
|
+
textEditStatusEditingEl &&
|
|
5996
|
+
document.activeElement === textEditStatusEditingEl
|
|
5997
|
+
) {
|
|
5998
|
+
textEditStatus = "active";
|
|
5999
|
+
} else if (
|
|
6000
|
+
textEditStatusNode &&
|
|
6001
|
+
(textEditStatusNode.textContent ?? "").trim().length > 0
|
|
6002
|
+
) {
|
|
6003
|
+
textEditStatus = "done";
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
(window.parent as Window).postMessage(
|
|
6007
|
+
{
|
|
6008
|
+
type: "agent-native:text-edit-status-result",
|
|
6009
|
+
correlationId: textEditStatusCorrelationId,
|
|
6010
|
+
status: textEditStatus,
|
|
6011
|
+
},
|
|
6012
|
+
"*",
|
|
6013
|
+
);
|
|
6014
|
+
return;
|
|
6015
|
+
}
|
|
5476
6016
|
if (e.data.type === "select-elements") {
|
|
5477
6017
|
var passiveTargets: Element[] = [];
|
|
5478
6018
|
var selectorGroups: unknown[] = Array.isArray(e.data.selectorGroups)
|
|
@@ -5675,16 +6215,32 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5675
6215
|
);
|
|
5676
6216
|
}
|
|
5677
6217
|
var el = findRuntimeTarget(String(sel || ""), candidatesForStyle);
|
|
6218
|
+
// T11: a live text-edit session's selection lives inside activeTextEditEl,
|
|
6219
|
+
// but the incoming style-change's selector/candidates were captured from
|
|
6220
|
+
// selectedEl at edit-start time, which selectionTargetForHit may have
|
|
6221
|
+
// anchored to a source-backed ancestor rather than activeTextEditEl
|
|
6222
|
+
// itself (when the actual edit target is a runtime-only descendant).
|
|
6223
|
+
// Requiring `el === activeTextEditEl` then never matches, and the whole
|
|
6224
|
+
// ancestor gets restyled instead of just the visible range. Route on
|
|
6225
|
+
// activeTextEditEl's own relationship to the resolved target instead: a
|
|
6226
|
+
// style-change should still be treated as "editing the active session"
|
|
6227
|
+
// when the resolved element IS activeTextEditEl, or IS an ancestor that
|
|
6228
|
+
// activeTextEditEl lives inside (the panel is targeting "the thing the
|
|
6229
|
+
// user double-clicked into", which is this edit session, even though the
|
|
6230
|
+
// selector re-anchored to its stable container).
|
|
6231
|
+
var styleChangeTargetsActiveTextEdit =
|
|
6232
|
+
!!activeTextEditEl &&
|
|
6233
|
+
!!el &&
|
|
6234
|
+
(el === activeTextEditEl || el.contains(activeTextEditEl));
|
|
5678
6235
|
if (
|
|
5679
6236
|
prop &&
|
|
5680
|
-
|
|
5681
|
-
el === activeTextEditEl &&
|
|
6237
|
+
styleChangeTargetsActiveTextEdit &&
|
|
5682
6238
|
applyTextRangeStyle(prop, val)
|
|
5683
6239
|
) {
|
|
5684
6240
|
postTextContentChange(
|
|
5685
6241
|
activeTextEditEl,
|
|
5686
|
-
activeTextEditEl
|
|
5687
|
-
activeTextEditEl
|
|
6242
|
+
activeTextEditEl!.textContent || "",
|
|
6243
|
+
activeTextEditEl!.innerHTML || "",
|
|
5688
6244
|
);
|
|
5689
6245
|
refreshOverlays();
|
|
5690
6246
|
return;
|
|
@@ -5755,4 +6311,16 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
|
|
|
5755
6311
|
window.addEventListener("scroll", refreshOverlays, true);
|
|
5756
6312
|
window.addEventListener("resize", refreshOverlays);
|
|
5757
6313
|
applyEditorChromeScale();
|
|
6314
|
+
|
|
6315
|
+
// One-time ready signal: tells the host that every message listener above is
|
|
6316
|
+
// now attached, so one-shot commands (begin-text-edit, set-editor-chrome-scale,
|
|
6317
|
+
// style-change, delete-element, replace-document-content) sent immediately
|
|
6318
|
+
// after (re)creating this iframe are safe to deliver. Without this, a command
|
|
6319
|
+
// posted before the bridge script has executed — or while the iframe is
|
|
6320
|
+
// reloading — is simply lost; replayIframeEditorState only replays
|
|
6321
|
+
// steady-state selection/hover/tweak/motion state, not one-shot commands.
|
|
6322
|
+
(window.parent as Window).postMessage(
|
|
6323
|
+
{ type: "agent-native:editor-chrome-ready" },
|
|
6324
|
+
"*",
|
|
6325
|
+
);
|
|
5758
6326
|
})();
|