@agent-native/core 0.90.4 → 0.90.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.
Files changed (189) hide show
  1. package/README.md +34 -7
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +21 -0
  4. package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
  5. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  6. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  8. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  9. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  10. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  12. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  13. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  14. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  15. package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
  16. package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
  17. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
  18. package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
  19. package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
  20. package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
  21. package/corpus/core/docs/content/toolkit-history.mdx +1 -1
  22. package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
  23. package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
  24. package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
  25. package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
  26. package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
  27. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
  28. package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
  29. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
  30. package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
  31. package/corpus/core/package.json +1 -1
  32. package/corpus/core/src/agent/durable-background.ts +18 -18
  33. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
  34. package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
  35. package/corpus/core/src/agent/production-agent.ts +22 -11
  36. package/corpus/core/src/agent/run-manager.ts +17 -6
  37. package/corpus/core/src/agent/run-store.ts +16 -11
  38. package/corpus/core/src/cli/design-connect.ts +16 -2
  39. package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
  40. package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
  41. package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
  42. package/corpus/core/src/client/use-db-sync.ts +99 -1
  43. package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
  44. package/corpus/core/src/collab/client.ts +49 -4
  45. package/corpus/core/src/deploy/build.ts +103 -0
  46. package/corpus/core/src/server/core-routes-plugin.ts +137 -8
  47. package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
  48. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
  49. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
  50. package/corpus/templates/content/AGENTS.md +1 -0
  51. package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
  52. package/corpus/templates/content/actions/_database-utils.ts +2 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
  54. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
  55. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
  56. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
  57. package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
  58. package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
  59. package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
  60. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  61. package/corpus/templates/content/app/i18n-data.ts +31 -0
  62. package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
  63. package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
  64. package/corpus/templates/content/parity/matrix.md +24 -24
  65. package/corpus/templates/content/parity/matrix.ts +2 -0
  66. package/corpus/templates/content/shared/api.ts +59 -4
  67. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
  68. package/corpus/templates/design/actions/connect-localhost.ts +26 -3
  69. package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
  70. package/corpus/templates/design/actions/read-source-file.ts +28 -0
  71. package/corpus/templates/design/actions/update-file.ts +76 -7
  72. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
  73. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
  74. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
  75. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
  76. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
  77. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
  78. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
  79. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
  80. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
  81. package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
  82. package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
  83. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  84. package/corpus/templates/design/app/i18n-data.ts +19 -0
  85. package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
  86. package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
  87. package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
  88. package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
  89. package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
  90. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
  91. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
  92. package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
  93. package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
  94. package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
  95. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
  96. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
  97. package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
  98. package/corpus/templates/design/server/source-workspace.ts +27 -3
  99. package/corpus/templates/design/shared/board-file.ts +359 -0
  100. package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
  101. package/corpus/templates/design/shared/code-layer.ts +145 -10
  102. package/dist/agent/durable-background.d.ts +12 -12
  103. package/dist/agent/durable-background.js +18 -18
  104. package/dist/agent/durable-background.js.map +1 -1
  105. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  106. package/dist/agent/engine/ai-sdk-engine.js +8 -2
  107. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  108. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  109. package/dist/agent/engine/anthropic-engine.js +11 -1
  110. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  111. package/dist/agent/production-agent.d.ts +2 -2
  112. package/dist/agent/production-agent.d.ts.map +1 -1
  113. package/dist/agent/production-agent.js +18 -11
  114. package/dist/agent/production-agent.js.map +1 -1
  115. package/dist/agent/run-manager.d.ts +7 -1
  116. package/dist/agent/run-manager.d.ts.map +1 -1
  117. package/dist/agent/run-manager.js +6 -1
  118. package/dist/agent/run-manager.js.map +1 -1
  119. package/dist/agent/run-store.d.ts +1 -1
  120. package/dist/agent/run-store.d.ts.map +1 -1
  121. package/dist/agent/run-store.js +15 -10
  122. package/dist/agent/run-store.js.map +1 -1
  123. package/dist/cli/design-connect.d.ts.map +1 -1
  124. package/dist/cli/design-connect.js +12 -2
  125. package/dist/cli/design-connect.js.map +1 -1
  126. package/dist/client/KeepTabOpenNotice.d.ts +9 -10
  127. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
  128. package/dist/client/KeepTabOpenNotice.js +8 -4
  129. package/dist/client/KeepTabOpenNotice.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  131. package/dist/client/RunStuckBanner.js +14 -13
  132. package/dist/client/RunStuckBanner.js.map +1 -1
  133. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  134. package/dist/client/agent-chat-adapter.js +25 -17
  135. package/dist/client/agent-chat-adapter.js.map +1 -1
  136. package/dist/client/use-db-sync.d.ts +18 -0
  137. package/dist/client/use-db-sync.d.ts.map +1 -1
  138. package/dist/client/use-db-sync.js +88 -1
  139. package/dist/client/use-db-sync.js.map +1 -1
  140. package/dist/client/use-run-stuck-detection.d.ts +1 -1
  141. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  142. package/dist/client/use-run-stuck-detection.js +6 -4
  143. package/dist/client/use-run-stuck-detection.js.map +1 -1
  144. package/dist/collab/client.d.ts.map +1 -1
  145. package/dist/collab/client.js +43 -4
  146. package/dist/collab/client.js.map +1 -1
  147. package/dist/collab/routes.d.ts +1 -1
  148. package/dist/collab/struct-routes.d.ts +1 -1
  149. package/dist/deploy/build.d.ts +1 -0
  150. package/dist/deploy/build.d.ts.map +1 -1
  151. package/dist/deploy/build.js +57 -0
  152. package/dist/deploy/build.js.map +1 -1
  153. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  154. package/dist/notifications/routes.d.ts +2 -2
  155. package/dist/resources/handlers.d.ts +3 -3
  156. package/dist/secrets/routes.d.ts +9 -9
  157. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  158. package/dist/server/core-routes-plugin.d.ts +10 -0
  159. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  160. package/dist/server/core-routes-plugin.js +103 -9
  161. package/dist/server/core-routes-plugin.js.map +1 -1
  162. package/docs/content/agent-native-toolkit.mdx +1 -1
  163. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  164. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  165. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  166. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  167. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  168. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  169. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  170. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  171. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  172. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  173. package/docs/content/toolkit-agent-ux.mdx +1 -1
  174. package/docs/content/toolkit-app-adapters.mdx +1 -1
  175. package/docs/content/toolkit-collaboration-ui.mdx +1 -1
  176. package/docs/content/toolkit-collaboration.mdx +1 -1
  177. package/docs/content/toolkit-command-navigation.mdx +2 -2
  178. package/docs/content/toolkit-comments-review.mdx +1 -1
  179. package/docs/content/toolkit-history.mdx +1 -1
  180. package/docs/content/toolkit-observability.mdx +1 -1
  181. package/docs/content/toolkit-org-team.mdx +1 -1
  182. package/docs/content/toolkit-resources.mdx +1 -1
  183. package/docs/content/toolkit-settings.mdx +1 -1
  184. package/docs/content/toolkit-setup-connections.mdx +1 -1
  185. package/docs/content/toolkit-sharing-ui.mdx +1 -1
  186. package/docs/content/toolkit-sharing.mdx +1 -1
  187. package/docs/content/toolkit-shell-hooks.mdx +1 -1
  188. package/docs/content/toolkit-ui.mdx +1 -1
  189. package/package.json +1 -1
@@ -242,6 +242,43 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
242
242
  return !!(el && !isDocumentRootElement(el) && getSourceId(el));
243
243
  }
244
244
 
245
+ // Detects an Alpine `<template x-for>` runtime clone: Alpine keeps the
246
+ // `<template>` element itself in the live DOM (as a hidden, zero-size
247
+ // marker) and inserts every rendered instance as a DIRECT SIBLING of that
248
+ // template, all still children of the same parent — so `ul > template,
249
+ // li, li, li` is the live shape for `<ul><template x-for>...</template>
250
+ // rendering 3 items</ul>`. The static SOURCE HTML the host resolves moves
251
+ // against only ever contains the single template child, never the N
252
+ // runtime clones, so structural moves (reorder/reparent) targeting a
253
+ // clone — or targeting another clone as the anchor — can never resolve on
254
+ // the host and always come back `applied:false`. Detected once per drag
255
+ // via an ancestor walk (not just the immediate parent) so nested x-for
256
+ // clones (e.g. a subtask `<li>` inside a per-task `<ul>` that is itself
257
+ // x-for'd) are also caught, stopping at the first stable-id ancestor
258
+ // (anything inside a stamped subtree has a real anchor and is fine).
259
+ function isTemplateCloneElement(el: Element | null): boolean {
260
+ var node: Element | null = el;
261
+ while (node && !isDocumentRootElement(node)) {
262
+ if (hasStableOwnSource(node)) return false;
263
+ var parent = node.parentElement;
264
+ if (!parent) return false;
265
+ var siblings = parent.children;
266
+ for (var i = 0; i < siblings.length; i += 1) {
267
+ var sib = siblings[i];
268
+ if (
269
+ sib !== node &&
270
+ sib.tagName &&
271
+ sib.tagName.toLowerCase() === "template" &&
272
+ sib.hasAttribute("x-for")
273
+ ) {
274
+ return true;
275
+ }
276
+ }
277
+ node = parent;
278
+ }
279
+ return false;
280
+ }
281
+
245
282
  function selectionTargetForHit(hit: Element | null): Element | null {
246
283
  if (!hit || isDocumentRootElement(hit)) return hit;
247
284
  if (selectedEl && hit !== selectedEl && selectedEl.contains(hit))
@@ -520,6 +557,30 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
520
557
  return path;
521
558
  }
522
559
 
560
+ // Editor-internal CSS custom-property prefixes — selection chrome colors,
561
+ // editor-chrome scale compensation, framework clipboard/surface tokens.
562
+ // These have no meaning outside this editor session and must never leak
563
+ // into persisted user HTML/exports. DesignEditor.tsx's
564
+ // applyPortableStyleSnapshotToHtml (isEditorInternalCssVar /
565
+ // EDITOR_INTERNAL_CSS_VAR_PREFIXES) already filters them back out on the
566
+ // apply side; filtering here too at COLLECTION time is pure bloat
567
+ // reduction (skips carrying them across the postMessage boundary at all)
568
+ // and changes no observable behavior on the apply side.
569
+ //
570
+ // keep in sync with DesignEditor.tsx's EDITOR_INTERNAL_CSS_VAR_PREFIXES
571
+ var EDITOR_INTERNAL_CSS_VAR_PREFIXES = [
572
+ "--design-editor-",
573
+ "--agent-native-editor-chrome-",
574
+ "--agent-native-",
575
+ ];
576
+
577
+ function isEditorInternalCssVarName(name: string): boolean {
578
+ for (var i = 0; i < EDITOR_INTERNAL_CSS_VAR_PREFIXES.length; i += 1) {
579
+ if (name.indexOf(EDITOR_INTERNAL_CSS_VAR_PREFIXES[i]) === 0) return true;
580
+ }
581
+ return false;
582
+ }
583
+
523
584
  function collectPortableComputedStyles(
524
585
  el: Element | null,
525
586
  ): Record<string, string> {
@@ -534,7 +595,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
534
595
  });
535
596
  for (var index = 0; index < cs.length; index += 1) {
536
597
  var name = cs.item(index);
537
- if (name && name.indexOf("--") === 0) {
598
+ if (
599
+ name &&
600
+ name.indexOf("--") === 0 &&
601
+ !isEditorInternalCssVarName(name)
602
+ ) {
538
603
  var customValue = cs.getPropertyValue(name);
539
604
  if (customValue && customValue.trim()) {
540
605
  styles[name] = customValue.trim();
@@ -677,7 +742,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
677
742
  el !== document.body &&
678
743
  el !== document.documentElement &&
679
744
  el.getAttribute &&
680
- el.setAttribute
745
+ el.setAttribute &&
746
+ // Defensive guard (mirrors hit-test.bridge.ts's getOrMintPendingNodeId):
747
+ // a template clone has no counterpart in source HTML, so no host
748
+ // persist call could ever durably write data-agent-native-node-id for
749
+ // it, and Alpine re-renders the clone from scratch on the next data
750
+ // change anyway (the stamped attribute would vanish). Fail closed
751
+ // instead of minting a pending id that can never be persisted.
752
+ !isTemplateCloneElement(el)
681
753
  ) {
682
754
  pendingNodeId = el.getAttribute("data-an-pending-node-id") || "";
683
755
  if (!pendingNodeId) {
@@ -1449,13 +1521,108 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
1449
1521
  selection: string;
1450
1522
  highlight: string;
1451
1523
  } | null = null;
1524
+ // T22: deferred begin-text-edit command for a node that hasn't landed in
1525
+ // this document yet. The host posts begin-text-edit immediately after
1526
+ // creating a text primitive, but the node itself arrives via the
1527
+ // replace-document-content persist round-trip — when the command wins that
1528
+ // race the old behavior silently dropped it, leaving the user typing into
1529
+ // nothing (worst case: Delete/arrow keystrokes fell through to host layer
1530
+ // shortcuts). Instead, poll briefly (bounded ~2s, rAF cadence) for the
1531
+ // nodeId and activate the edit the moment it appears. Only the newest
1532
+ // command is kept; any user pointerdown or a user-initiated text edit
1533
+ // cancels it (the user has moved on — never yank focus later).
1534
+ var pendingBeginTextEdit: {
1535
+ nodeId: string;
1536
+ force: boolean;
1537
+ deadline: number;
1538
+ raf: number;
1539
+ // Keystrokes typed INTO THIS IFRAME while the command waits for its node
1540
+ // (see the pending-window branch of the document keydown handler) —
1541
+ // replayed into the editable the moment it activates.
1542
+ buffer: string;
1543
+ } | null = null;
1544
+ function cancelPendingBeginTextEdit(): void {
1545
+ if (!pendingBeginTextEdit) return;
1546
+ if (pendingBeginTextEdit.raf) {
1547
+ window.cancelAnimationFrame(pendingBeginTextEdit.raf);
1548
+ }
1549
+ pendingBeginTextEdit = null;
1550
+ }
1551
+ // T25: tell the HOST (DesignCanvas) that a begin-text-edit command is
1552
+ // waiting for its node, so the host arms its own keystroke buffer for keys
1553
+ // that land on the HOST document during the window (the host cannot see
1554
+ // the begin-text-edit post itself — DesignEditor sends it straight to this
1555
+ // iframe). pending:false stands the host down when the wait is abandoned;
1556
+ // successful activation instead flows through text-editing-state(active),
1557
+ // which both flushes the host buffer and clears its pending flag.
1558
+ function postTextEditPending(nodeId: string, pending: boolean): void {
1559
+ (window.parent as Window).postMessage(
1560
+ { type: "text-edit-pending", nodeId: nodeId, pending: pending },
1561
+ "*",
1562
+ );
1563
+ }
1564
+ // T23: is the active text-edit element still part of this document? A
1565
+ // document patch (replaceRuntimeDocument subtree/body swap), a
1566
+ // delete-element command, or in-page reactivity (Alpine x-if) can detach
1567
+ // the edited node while its session is live — its blur/keydown listeners
1568
+ // then never fire again, so nothing ever runs the Escape/blur cleanup
1569
+ // path. The leaked activeTextEditEl blocked ALL drags/marquees
1570
+ // (beginPotentialShieldDrag/beginMarqueeSelection bail on it), kept the
1571
+ // shield pointer-passthrough disabled, swallowed every design hotkey, and
1572
+ // buffered every future runtime content update until a full reload.
1573
+ function isTextEditElConnected(): boolean {
1574
+ return !!(
1575
+ activeTextEditEl &&
1576
+ activeTextEditEl.isConnected &&
1577
+ document.documentElement.contains(activeTextEditEl)
1578
+ );
1579
+ }
1580
+ // T23: exit a stale (detached-element) text edit session through the SAME
1581
+ // cleanup path a user Escape/blur takes. Returns true when a stale session
1582
+ // was cleaned up. Callers that previously hard-bailed on activeTextEditEl
1583
+ // should call this first so a leaked session self-heals on the next
1584
+ // interaction instead of wedging the surface until reload.
1585
+ function exitStaleTextEditSession(): boolean {
1586
+ if (!activeTextEditEl || isTextEditElConnected()) return false;
1587
+ var staleEl = activeTextEditEl;
1588
+ if (finishActiveTextEdit) {
1589
+ finishActiveTextEdit(true);
1590
+ } else {
1591
+ postTextEditingState(staleEl, false);
1592
+ activeTextEditEl = null;
1593
+ setTextEditingPointerPassthrough(false);
1594
+ setSelectionOverlayResizeChromeVisible(true);
1595
+ }
1596
+ // Defensive: finish() only clears activeTextEditEl when it still points
1597
+ // at that session's own target. If overlapping sessions ever left a
1598
+ // different detached element behind, force-clear so the surface can't
1599
+ // stay wedged.
1600
+ if (activeTextEditEl === staleEl) {
1601
+ activeTextEditEl = null;
1602
+ setTextEditingPointerPassthrough(false);
1603
+ setSelectionOverlayResizeChromeVisible(true);
1604
+ }
1605
+ return true;
1606
+ }
1452
1607
  var pendingStructureMoves: Record<
1453
1608
  string,
1454
1609
  {
1455
1610
  requestId: string;
1456
1611
  el: Element;
1457
1612
  target: { anchor: Element; placement: string; axis?: string };
1458
- origin: { prevParent: Element; prevNextSibling: Node | null } | null;
1613
+ origin: {
1614
+ prevParent: Element;
1615
+ prevNextSibling: Node | null;
1616
+ // Inline position/left/top/right/bottom VALUES captured right before
1617
+ // the optimistic reorder's stripAbsolutePositioningForFlowInsert ran
1618
+ // (absent/undefined when that strip did not apply — e.g. an
1619
+ // absolute-container drop, or a flow-reorder of an already-flow
1620
+ // element with nothing to strip). Restored by the visual-structure-ack
1621
+ // failure branch alongside the parent/sibling revert so a rejected
1622
+ // move-node round-trip cannot leave the element stripped of its
1623
+ // absolute positioning while stuck in the wrong parent.
1624
+ prevInlinePositionStyles?: Record<string, string> | null;
1625
+ } | null;
1459
1626
  }
1460
1627
  > = {};
1461
1628
  var pendingShieldDrag: {
@@ -1713,6 +1880,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
1713
1880
  forceFullDocument?: boolean,
1714
1881
  ): void {
1715
1882
  if (typeof html !== "string") return;
1883
+ // T23: a session whose element was already detached (earlier patch,
1884
+ // delete-element, in-page reactivity) can never end via blur/Escape —
1885
+ // buffering behind it would freeze this surface's content forever. Exit
1886
+ // it through the canonical cleanup first, then treat this update
1887
+ // normally. (The nested pendingRuntimeDocumentUpdate replay inside
1888
+ // finish() runs before we continue; this newer payload then supersedes
1889
+ // its result, preserving ordering.)
1890
+ exitStaleTextEditSession();
1716
1891
  if (activeTextEditEl && !forceFullDocument) {
1717
1892
  // Don't yank a runtime content update out from under an in-progress
1718
1893
  // text edit — but don't silently lose it either (T13). Buffer only the
@@ -2849,48 +3024,133 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
2849
3024
  return false;
2850
3025
  }
2851
3026
 
2852
- function applyEditorChromeScale() {
2853
- syncEditorChromeScaleVars();
3027
+ // ── Selection-handle hit-zone inward clamp ────────────────────────────
3028
+ // Keep in sync with multi-screen/handle-hit-zones.ts (the host-side
3029
+ // selection chrome applies the same clamp rule to its screen-frame/board
3030
+ // handles; nominal sizes differ — bridge edge bars are 10px thick centered
3031
+ // on the edge, corner squares 7px with a 4px outward offset — but the
3032
+ // inward-reach clamp and its 0.25 fraction must stay identical).
3033
+ //
3034
+ // The edge/corner handles multiply by the chrome scale so they keep a
3035
+ // constant on-screen size, which makes their HIT zones grow without bound
3036
+ // in iframe-local px as the host zooms out: at 19% zoom the nominal 10px
3037
+ // edge bar is ~52.6 local px thick, reaching ~26.3 local px into the
3038
+ // element from each edge. Any element smaller than twice that reach has
3039
+ // its ENTIRE body covered by the two opposing bars — every press resolves
3040
+ // to a resize, so the element can never be grabbed for a move drag (or
3041
+ // clicked in its interior) at low zoom. Clamp only the INWARD reach of
3042
+ // each handle hit zone to a fraction of the element's own dimension on
3043
+ // that axis; the outward reach (which can never occlude the body) and the
3044
+ // corner handles' VISUAL size stay untouched. With 0.25, two opposing
3045
+ // handles consume at most half the dimension, so the central 50% band of
3046
+ // each axis always stays body-grabbable.
3047
+ var HANDLE_MAX_INWARD_FRACTION = 0.25;
3048
+
3049
+ // Mirror of clampHandleInwardReach in multi-screen/handle-hit-zones.ts.
3050
+ // Non-finite or non-positive dimensions (no overlaid element, degenerate
3051
+ // zero-size elements mid-creation) return the nominal reach unchanged —
3052
+ // exactly the pre-clamp behavior, and a zero-size element has no body to
3053
+ // protect.
3054
+ function clampHandleInwardReach(nominalInward, elementDimension) {
3055
+ if (!Number.isFinite(elementDimension) || elementDimension <= 0) {
3056
+ return nominalInward;
3057
+ }
3058
+ return Math.min(
3059
+ nominalInward,
3060
+ elementDimension * HANDLE_MAX_INWARD_FRACTION,
3061
+ );
3062
+ }
3063
+
3064
+ // Sizes the selection overlay's edge/corner handles for the current chrome
3065
+ // scale, clamping each handle's inward reach against the overlaid
3066
+ // element's own rect. Called from applyEditorChromeScale (scale changes)
3067
+ // AND from positionOverlay (selection/element changes), because the
3068
+ // clamped geometry depends on the element's dimensions, not just the
3069
+ // scale. On large elements this reproduces the historical geometry
3070
+ // exactly: edge bars 10*scale thick centered on the edge, corner squares
3071
+ // 7*scale offset -4*scale.
3072
+ function applySelectionHandleHitGeometry(el) {
2854
3073
  var sx = chromeScaleX();
2855
3074
  var sy = chromeScaleY();
2856
3075
  var line = chromeLineScale();
2857
- // Figma parity: the hover outline is visibly thinner than the selection
2858
- // outline hover is a light "you could select this" hint, selection is
2859
- // the stronger confirmed-state chrome. Matches the overview canvas's
2860
- // hover (1 * chromeScale) vs selection (1.5 * chromeScale) ratio.
2861
- highlightOverlay.style.borderWidth = 1 * line + "px";
2862
- parentAutoLayoutOverlay.style.borderWidth = 1 * line + "px";
2863
- selectionOverlay.style.borderWidth = 1.5 * line + "px";
2864
- marqueeSelectionOverlay.style.borderWidth = 1 * line + "px";
2865
- passiveSelectionOverlays.forEach(scalePassiveSelectionOverlay);
2866
- if (selectedEl) updateSpacingOverlay(selectedEl);
3076
+ var elWidth = NaN;
3077
+ var elHeight = NaN;
3078
+ if (el && document.documentElement.contains(el)) {
3079
+ var elRect = el.getBoundingClientRect();
3080
+ elWidth = elRect.width;
3081
+ elHeight = elRect.height;
3082
+ }
2867
3083
 
3084
+ // Invisible edge-resize hit bars: nominal 5*scale outward + 5*scale
3085
+ // inward. Only the inward half is clamped; the bar's outward side stays
3086
+ // anchored at -5*scale from the edge.
2868
3087
  selectionOverlay
2869
3088
  .querySelectorAll("[data-agent-native-edge-handle]")
2870
3089
  .forEach(function (edge) {
2871
3090
  var pos = edge.getAttribute("data-agent-native-edge-handle");
2872
3091
  if (pos === "n" || pos === "s") {
2873
- edge.style.height = 10 * sy + "px";
2874
- edge.style[pos === "n" ? "top" : "bottom"] = -5 * sy + "px";
3092
+ var outwardY = 5 * sy;
3093
+ var inwardY = clampHandleInwardReach(5 * sy, elHeight);
3094
+ edge.style.height = outwardY + inwardY + "px";
3095
+ edge.style[pos === "n" ? "top" : "bottom"] = -outwardY + "px";
2875
3096
  }
2876
3097
  if (pos === "e" || pos === "w") {
2877
- edge.style.width = 10 * sx + "px";
2878
- edge.style[pos === "w" ? "left" : "right"] = -5 * sx + "px";
3098
+ var outwardX = 5 * sx;
3099
+ var inwardX = clampHandleInwardReach(5 * sx, elWidth);
3100
+ edge.style.width = outwardX + inwardX + "px";
3101
+ edge.style[pos === "w" ? "left" : "right"] = -outwardX + "px";
2879
3102
  }
2880
3103
  });
2881
3104
 
3105
+ // Visible corner squares: the square keeps its constant on-screen size
3106
+ // (7*scale); when its nominal 3*scale inward overlap would exceed the
3107
+ // per-axis clamp, the square shifts outward so only the clamped reach
3108
+ // overlaps the body. Corners clamp per-axis independently.
2882
3109
  selectionOverlay
2883
3110
  .querySelectorAll("[data-agent-native-edit-handle]")
2884
3111
  .forEach(function (handle) {
2885
3112
  var pos = handle.getAttribute("data-agent-native-edit-handle") || "";
2886
- handle.style.width = 7 * sx + "px";
2887
- handle.style.height = 7 * sy + "px";
3113
+ var sizeX = 7 * sx;
3114
+ var sizeY = 7 * sy;
3115
+ // sizeY - 4*sy is exact (Sterbenz), so the unclamped offset below
3116
+ // reproduces the historical -4*scale bit-for-bit.
3117
+ var inwardX = clampHandleInwardReach(sizeX - 4 * sx, elWidth);
3118
+ var inwardY = clampHandleInwardReach(sizeY - 4 * sy, elHeight);
3119
+ handle.style.width = sizeX + "px";
3120
+ handle.style.height = sizeY + "px";
2888
3121
  handle.style.borderWidth = 1 * line + "px";
2889
- if (pos.indexOf("n") !== -1) handle.style.top = -4 * sy + "px";
2890
- if (pos.indexOf("s") !== -1) handle.style.bottom = -4 * sy + "px";
2891
- if (pos.indexOf("w") !== -1) handle.style.left = -4 * sx + "px";
2892
- if (pos.indexOf("e") !== -1) handle.style.right = -4 * sx + "px";
3122
+ if (pos.indexOf("n") !== -1) {
3123
+ handle.style.top = inwardY - sizeY + "px";
3124
+ }
3125
+ if (pos.indexOf("s") !== -1) {
3126
+ handle.style.bottom = inwardY - sizeY + "px";
3127
+ }
3128
+ if (pos.indexOf("w") !== -1) {
3129
+ handle.style.left = inwardX - sizeX + "px";
3130
+ }
3131
+ if (pos.indexOf("e") !== -1) {
3132
+ handle.style.right = inwardX - sizeX + "px";
3133
+ }
2893
3134
  });
3135
+ }
3136
+
3137
+ function applyEditorChromeScale() {
3138
+ syncEditorChromeScaleVars();
3139
+ var sx = chromeScaleX();
3140
+ var sy = chromeScaleY();
3141
+ var line = chromeLineScale();
3142
+ // Figma parity: the hover outline is visibly thinner than the selection
3143
+ // outline — hover is a light "you could select this" hint, selection is
3144
+ // the stronger confirmed-state chrome. Matches the overview canvas's
3145
+ // hover (1 * chromeScale) vs selection (1.5 * chromeScale) ratio.
3146
+ highlightOverlay.style.borderWidth = 1 * line + "px";
3147
+ parentAutoLayoutOverlay.style.borderWidth = 1 * line + "px";
3148
+ selectionOverlay.style.borderWidth = 1.5 * line + "px";
3149
+ marqueeSelectionOverlay.style.borderWidth = 1 * line + "px";
3150
+ passiveSelectionOverlays.forEach(scalePassiveSelectionOverlay);
3151
+ if (selectedEl) updateSpacingOverlay(selectedEl);
3152
+
3153
+ applySelectionHandleHitGeometry(selectedEl);
2894
3154
 
2895
3155
  selectionOverlay
2896
3156
  .querySelectorAll("[data-agent-native-rotate-handle]")
@@ -2961,6 +3221,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
2961
3221
  }
2962
3222
  if (overlay === selectionOverlay) {
2963
3223
  applySelectionChrome(el);
3224
+ // Re-clamp handle hit zones for THIS element's dimensions — the
3225
+ // clamped geometry is element-dependent, not just scale-dependent
3226
+ // (see applySelectionHandleHitGeometry).
3227
+ applySelectionHandleHitGeometry(el);
2964
3228
  updateSpacingOverlay(el);
2965
3229
  // `rect` is undefined on the rotated-local-box path; updateComponentTag
2966
3230
  // falls back to its own read in that case.
@@ -3836,7 +4100,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
3836
4100
  }
3837
4101
 
3838
4102
  function beginMarqueeSelection(e): void {
3839
- if (e.button !== 0 || activeTextEditEl) return;
4103
+ if (e.button !== 0) return;
4104
+ // T23: a stale session self-heals and the marquee proceeds; only a LIVE
4105
+ // session (connected element) blocks marquee starts.
4106
+ if (activeTextEditEl && !exitStaleTextEditSession()) return;
3840
4107
  clearActiveMarqueeSelection();
3841
4108
  var events = dragEventNames(e);
3842
4109
  var additive = Boolean(e && (e.metaKey || e.ctrlKey || e.shiftKey));
@@ -3959,6 +4226,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
3959
4226
  )
3960
4227
  return false;
3961
4228
  if (target.parentElement) target.parentElement.removeChild(target);
4229
+ // T23: the removed subtree may contain the active text-edit element —
4230
+ // its blur/keydown listeners are gone with it, so exit the session
4231
+ // through the canonical cleanup instead of leaking it.
4232
+ exitStaleTextEditSession();
3962
4233
  if (
3963
4234
  selectedEl === target ||
3964
4235
  !document.documentElement.contains(selectedEl)
@@ -4927,6 +5198,26 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
4927
5198
 
4928
5199
  function hideTransformBadge(): void {
4929
5200
  transformBadge.style.display = "none";
5201
+ transformBadge.style.removeProperty("background");
5202
+ transformBadge.style.removeProperty("color");
5203
+ transformBadge.style.removeProperty("border-color");
5204
+ }
5205
+
5206
+ // Rejection feedback for a drag that can never resolve on the host (see
5207
+ // isTemplateCloneElement): reuses transformBadge with a red-tinted style
5208
+ // instead of adding another chrome element, and a "not-allowed" cursor on
5209
+ // shieldOverlay for the duration of the gesture — clear, immediate signal
5210
+ // instead of an optimistic reorder that silently reverts ~1 frame later.
5211
+ function showRejectedDragBadge(
5212
+ text: string,
5213
+ clientX: number,
5214
+ clientY: number,
5215
+ ): void {
5216
+ showTransformBadge(text, clientX, clientY);
5217
+ transformBadge.style.background =
5218
+ "color-mix(in srgb, #dc2626 92%, transparent)";
5219
+ transformBadge.style.color = "#fff";
5220
+ transformBadge.style.borderColor = "#dc2626";
4930
5221
  }
4931
5222
 
4932
5223
  function hideInsertionGuide(): void {
@@ -4944,12 +5235,25 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
4944
5235
  );
4945
5236
  }
4946
5237
 
5238
+ // Anchor-candidate gate (companion to the dragged-element
5239
+ // isTemplateCloneElement rejection below): a template clone can never be
5240
+ // used as an insertion ANCHOR either — it has no counterpart in the static
5241
+ // source HTML, so before/after placement against it can never resolve on
5242
+ // the host any more than dragging the clone itself could. Filtering clones
5243
+ // out of the candidate list here (rather than only checking the dragged
5244
+ // element) is what fixes drops into a container whose ONLY children are
5245
+ // x-for clones: without this, nearestChildInsertionTarget's "nearest
5246
+ // child" search and reorderTargetForPoint's sibling-scan fallback would
5247
+ // both happily pick a clone as the anchor, and the resulting moveNode
5248
+ // against source HTML would silently fail (layerMoveFailed toast) even
5249
+ // though the drop gesture itself was completely valid.
4947
5250
  function draggableElementChildren(parent: Element): Element[] {
4948
5251
  return Array.prototype.slice.call(parent.children).filter(function (child) {
4949
5252
  return (
4950
5253
  child.nodeType === 1 &&
4951
5254
  !isOverlayElement(child) &&
4952
- !isLayerInteractionBlocked(child)
5255
+ !isLayerInteractionBlocked(child) &&
5256
+ !isTemplateCloneElement(child)
4953
5257
  );
4954
5258
  });
4955
5259
  }
@@ -5189,12 +5493,20 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5189
5493
  // pre-marker content — exactly the default white AND the destination is
5190
5494
  // light), switch it to `color:inherit` so it picks up the container's
5191
5495
  // effective text color. A color the user explicitly set is NEVER touched:
5192
- // DesignEditor should stamp `data-an-auto-text-color` when IT auto-picks
5193
- // the color at creation and remove the marker on any explicit color edit;
5194
- // when the marker is present the color is definitely auto (always safe to
5195
- // adapt), and when absent the conservative default-white + light-target
5196
- // heuristic below only fires in the exact case where the text would be
5197
- // invisible anyway.
5496
+ // DesignEditor's appendCanvasPrimitiveToHtml stamps `data-an-auto-text-color`
5497
+ // when IT auto-picks the color at creation (BOARD_TEXT_AUTO_COLOR_MARKER
5498
+ // export in DesignEditor.tsx) and any explicit color edit removes the
5499
+ // marker; when the marker is present the color is definitely auto (always
5500
+ // safe to adapt), and when absent the conservative default-white +
5501
+ // light-target heuristic below only fires in the exact case where the text
5502
+ // would be invisible anyway.
5503
+ //
5504
+ // keep in sync with DesignEditor.tsx's
5505
+ // adaptAutoTextColorForCrossScreenNode / shouldAdaptAutoTextColorForCrossScreenMove
5506
+ // — the cross-screen mirror of this same decision, applied host-side (HTML
5507
+ // string, post-reparent) after handleCrossScreenElementDrop moves a text
5508
+ // node between documents, since this in-iframe bridge only ever sees
5509
+ // same-document re-parents.
5198
5510
  var BOARD_TEXT_AUTO_COLOR_MARKER = "data-an-auto-text-color";
5199
5511
 
5200
5512
  function parseCssRgb(
@@ -5397,6 +5709,38 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5397
5709
  "li",
5398
5710
  ];
5399
5711
 
5712
+ // keep in sync with hit-test.bridge.ts BRIDGE_INTERACTIVE_LEAF_TAGS
5713
+ var BRIDGE_INTERACTIVE_LEAF_TAGS = ["button", "summary"];
5714
+
5715
+ // Drop-on-leaf fix: a `<button>` (or similar interactive leaf control) is
5716
+ // frequently styled `display:flex` purely to align its own icon + label —
5717
+ // that is NOT the same thing as a Figma "frame" a user expects to drop
5718
+ // items into. Neither the tag denylist (BRIDGE_LEAF_TAGS/TEXT_TAGS) nor the
5719
+ // flex/grid computed-display check alone can tell these apart (button is
5720
+ // in neither tag list, and it genuinely has display:flex), so this walks
5721
+ // the element's own children: if every child is itself a leaf/text tag
5722
+ // with no further container/flex descendant of its own, the element is
5723
+ // "leaf content" (an icon+label control) and must not accept nested
5724
+ // drops — only a container that itself hosts a real sub-layout (a nested
5725
+ // container/flex child) qualifies.
5726
+ function hasOnlyLeafContent(el: Element): boolean {
5727
+ var children = el.children;
5728
+ if (!children.length) return true;
5729
+ for (var i = 0; i < children.length; i += 1) {
5730
+ var child = children[i] as Element;
5731
+ var childTag = (child.tagName || "").toLowerCase();
5732
+ if (
5733
+ BRIDGE_LEAF_TAGS.indexOf(childTag) === -1 &&
5734
+ BRIDGE_TEXT_TAGS.indexOf(childTag) === -1 &&
5735
+ BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(childTag) === -1
5736
+ ) {
5737
+ return false;
5738
+ }
5739
+ if (child.children.length && !hasOnlyLeafContent(child)) return false;
5740
+ }
5741
+ return true;
5742
+ }
5743
+
5400
5744
  function isContainerDropTarget(el: Element | null): boolean {
5401
5745
  if (!el || el === document.documentElement) return false;
5402
5746
  if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
@@ -5408,6 +5752,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5408
5752
  BRIDGE_TEXT_TAGS.indexOf(tag) !== -1
5409
5753
  )
5410
5754
  return false;
5755
+ // Reject interactive leaf controls (button, summary) whose children are
5756
+ // all leaf/text content — see hasOnlyLeafContent above.
5757
+ if (
5758
+ BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(tag) !== -1 &&
5759
+ hasOnlyLeafContent(el)
5760
+ ) {
5761
+ return false;
5762
+ }
5411
5763
  var cs = window.getComputedStyle(el);
5412
5764
  if (
5413
5765
  cs.display === "flex" ||
@@ -5466,6 +5818,11 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5466
5818
  // with the container-fill affordance instead of an insertion line). Both
5467
5819
  // in-screen drag paths now route container-background hits through this
5468
5820
  // helper so dropping between children works and shows the line.
5821
+ //
5822
+ // keep in sync with hit-test.bridge.ts's own nearestChildInsertionTarget
5823
+ // (finding 6's cross-screen/canvas-to-screen mirror of this same fix —
5824
+ // that copy omits the `excludeEls` param since hit-test.bridge.ts never
5825
+ // has a dragged element of its own).
5469
5826
  function nearestChildInsertionTarget(
5470
5827
  container: Element,
5471
5828
  clientX: number,
@@ -5538,11 +5895,21 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5538
5895
  return false;
5539
5896
  }
5540
5897
  var hit = elementFromEditorPoint(clientX, clientY);
5898
+ // Anchor-candidate gate: a hit that resolves directly onto a template
5899
+ // clone (e.g. hovering over one of the rendered `<li>` items inside a
5900
+ // container whose ONLY children are x-for clones) can never anchor a
5901
+ // structural move — see draggableElementChildren's comment above. Falling
5902
+ // through here (instead of using `hit` as the anchor) routes to the
5903
+ // sibling-scan fallback below, which already filters clones out via
5904
+ // draggableElementChildren, so it correctly resolves to either a
5905
+ // non-clone sibling or, when there are none, no anchor at all (caller
5906
+ // falls back to the container itself with "inside" placement).
5541
5907
  if (
5542
5908
  hit &&
5543
5909
  hit !== document.documentElement &&
5544
5910
  !isDraggedOrInsideDragged(hit) &&
5545
- !isOverlayElement(hit)
5911
+ !isOverlayElement(hit) &&
5912
+ !isTemplateCloneElement(hit)
5546
5913
  ) {
5547
5914
  if (isContainerDropTarget(hit)) {
5548
5915
  var containerRect = hit.getBoundingClientRect();
@@ -5607,7 +5974,25 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5607
5974
  var siblings = draggableElementChildren(parent).filter(function (child) {
5608
5975
  return !isDraggedOrInsideDragged(child);
5609
5976
  });
5610
- if (!siblings.length) return null;
5977
+ if (!siblings.length) {
5978
+ // No non-clone, non-dragged sibling to anchor against — e.g. `parent`'s
5979
+ // only other children are x-for clones (draggableElementChildren
5980
+ // already filtered those out above). Fall back to the parent container
5981
+ // itself with "inside" placement instead of returning null: null here
5982
+ // would make onReorderUp treat this as "no valid drop target" and
5983
+ // silently no-op the whole gesture, even though moving into `parent`
5984
+ // (landing after the rendered clones, which in source HTML is simply
5985
+ // inside the container since clones don't exist there) is a completely
5986
+ // valid and expected drop.
5987
+ return {
5988
+ anchor: parent,
5989
+ placement: "inside",
5990
+ axis: axis,
5991
+ dropMode: isAbsolutePrimitiveContainer(parent)
5992
+ ? "absolute-container"
5993
+ : "flow-insert",
5994
+ };
5995
+ }
5611
5996
  var beforeTarget = null;
5612
5997
  for (var i = 0; i < siblings.length; i += 1) {
5613
5998
  var rect = siblings[i].getBoundingClientRect();
@@ -5721,29 +6106,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5721
6106
  // node whose parent happens to be body — must fall through to a plain
5722
6107
  // absolute placement instead of silently wrapping body in auto-layout.
5723
6108
  var parent = cursor.parentElement;
5724
- if (parent && parent !== document.body && isContainerDropTarget(parent)) {
5725
- var parentAxis = parentFlowAxis(parent);
5726
- var childRect = cursor.getBoundingClientRect();
5727
- var childCenter =
5728
- parentAxis === "x"
5729
- ? childRect.left + childRect.width / 2
5730
- : childRect.top + childRect.height / 2;
5731
- var childPointer = parentAxis === "x" ? clientX : clientY;
6109
+ // Absolute-primitive-container target (a canvas rectangle marked
6110
+ // data-an-primitive="rectangle"/"rect"): this is a dedicated
6111
+ // free-placement container, not a Figma-style auto-layout frame — the
6112
+ // matching reorderTargetForPoint (flow-reorder) branch already
6113
+ // recognizes it via this same helper and assigns dropMode
6114
+ // "absolute-container" so onUp skips the auto-layout conversion and
6115
+ // keeps the moved element's position:absolute.
6116
+ if (cursor !== document.body && isAbsolutePrimitiveContainer(cursor)) {
5732
6117
  return {
5733
6118
  anchor: cursor,
5734
- placement: childPointer < childCenter ? "before" : "after",
5735
- axis: parentAxis,
5736
- dropMode: "flow-insert",
5737
- needsAutoLayoutConversion: !isAutoLayoutElement(parent),
5738
- conversionTarget: parent,
6119
+ placement: "inside",
6120
+ axis: "y",
6121
+ dropMode: "absolute-container",
5739
6122
  };
5740
6123
  }
5741
- if (cursor !== document.body && isContainerDropTarget(cursor)) {
5742
- // B5-4 (absolute path): pointer over the container's own background
5743
- // its padding or a gap between children. Prefer the nearest child
5744
- // slot (insertion line at the pointer index) over plain "inside"
5745
- // (which appends after the last child); fall back to "inside" only
5746
- // for containers with no visible children.
6124
+ // Nest-inside-what-you're-hovering takes priority over sibling-insert
6125
+ // UNLESS cursor is already a managed flex-item of an ESTABLISHED
6126
+ // auto-layout parent (isAutoLayoutElement genuinely display:flex/grid,
6127
+ // not just an isContainerDropTarget tag match). That parent-is-already-
6128
+ // auto-layout signal is what distinguishes "cursor is a list item being
6129
+ // reordered within its existing list" (sibling-insert is correct: a
6130
+ // plain-block <div> chip that is itself a flex-item of #frame/#col)
6131
+ // from "cursor is genuinely being hovered as a nesting target" (a
6132
+ // pristine/empty container, or a real container whose own parent isn't
6133
+ // already running auto-layout — e.g. a top-level or newly-adjacent
6134
+ // rectangle, matching reorderTargetForPoint's isContainerDropTarget(hit)
6135
+ // priority for the flow-reorder gesture).
6136
+ //
6137
+ // This check used to run AFTER the sibling-insert branch below with no
6138
+ // such carve-out, so hovering directly over a pristine/empty auto-layout
6139
+ // container nested under a NON-auto-layout ancestor (e.g. a plain
6140
+ // <main>) — whose own parent still satisfies the old unconditional
6141
+ // "isContainerDropTarget(parent)" check — matched the sibling-insert
6142
+ // branch first and resolved the drop one level too high (anchoring
6143
+ // before/after the hovered container inside ITS parent, instead of
6144
+ // nesting inside the hovered container). Fixed by promoting this
6145
+ // nest-inside check ahead of the sibling-insert fallback, gated on the
6146
+ // parent NOT already being an established auto-layout list (so genuine
6147
+ // flex-item reordering inside an existing list is unaffected).
6148
+ if (
6149
+ cursor !== document.body &&
6150
+ isContainerDropTarget(cursor) &&
6151
+ !(parent && parent !== document.body && isAutoLayoutElement(parent))
6152
+ ) {
6153
+ // B5-4 (absolute path): pointer over the container's own
6154
+ // background — its padding or a gap between children. Prefer the
6155
+ // nearest child slot (insertion line at the pointer index) over
6156
+ // plain "inside" (which appends after the last child); fall back
6157
+ // to "inside" only for containers with no visible children.
5747
6158
  var betweenContainerChildren = nearestChildInsertionTarget(
5748
6159
  cursor,
5749
6160
  clientX,
@@ -5769,6 +6180,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5769
6180
  conversionTarget: cursor,
5770
6181
  };
5771
6182
  }
6183
+ if (parent && parent !== document.body && isContainerDropTarget(parent)) {
6184
+ // Anchor-candidate gate: cursor is a plain sibling under `parent`
6185
+ // being used as a before/after anchor — but if it's a template
6186
+ // clone (no counterpart in source HTML), fall back to the nearest
6187
+ // non-clone sibling via nearestChildInsertionTarget, else the
6188
+ // container itself with "inside" placement, exactly like
6189
+ // reorderTargetForPoint's equivalent fallback above.
6190
+ if (isTemplateCloneElement(cursor)) {
6191
+ var cloneFallback = nearestChildInsertionTarget(
6192
+ parent,
6193
+ clientX,
6194
+ clientY,
6195
+ dragged,
6196
+ );
6197
+ if (cloneFallback) {
6198
+ return {
6199
+ anchor: cloneFallback.anchor,
6200
+ placement: cloneFallback.placement,
6201
+ axis: cloneFallback.axis,
6202
+ dropMode: "flow-insert",
6203
+ needsAutoLayoutConversion: !isAutoLayoutElement(parent),
6204
+ conversionTarget: parent,
6205
+ };
6206
+ }
6207
+ return {
6208
+ anchor: parent,
6209
+ placement: "inside",
6210
+ axis: parentFlowAxis(parent),
6211
+ dropMode: "flow-insert",
6212
+ needsAutoLayoutConversion: !isAutoLayoutElement(parent),
6213
+ conversionTarget: parent,
6214
+ };
6215
+ }
6216
+ var parentAxis = parentFlowAxis(parent);
6217
+ var childRect = cursor.getBoundingClientRect();
6218
+ var childCenter =
6219
+ parentAxis === "x"
6220
+ ? childRect.left + childRect.width / 2
6221
+ : childRect.top + childRect.height / 2;
6222
+ var childPointer = parentAxis === "x" ? clientX : clientY;
6223
+ return {
6224
+ anchor: cursor,
6225
+ placement: childPointer < childCenter ? "before" : "after",
6226
+ axis: parentAxis,
6227
+ dropMode: "flow-insert",
6228
+ needsAutoLayoutConversion: !isAutoLayoutElement(parent),
6229
+ conversionTarget: parent,
6230
+ };
6231
+ }
5772
6232
  cursor = parent;
5773
6233
  }
5774
6234
  return null;
@@ -5824,9 +6284,171 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5824
6284
  }
5825
6285
  }
5826
6286
 
6287
+ // Absolute-into-flow teleport fix: a flow-insert reparent (the ONLY
6288
+ // dropMode applyRuntimeReorder ever nests an absolute-positioned member
6289
+ // through — "absolute-container" placements keep position:absolute by
6290
+ // design) must strip the leftover position/left/top/right/bottom the
6291
+ // absolute-drag onMove loop wrote onto the element throughout the drag.
6292
+ // Without this the element reparents into the flow container correctly
6293
+ // but stays absolutely positioned at its last drag offset — rendering
6294
+ // hundreds of px away from the slot the insertion guide indicated, since
6295
+ // position:absolute measures from the nearest positioned ancestor, not
6296
+ // flow layout. DesignEditor.tsx does the equivalent strip on the
6297
+ // PERSISTED source string once the host round-trips (see
6298
+ // removeAbsolutePositioningFromNodeInHtml); this mirrors it on the LIVE
6299
+ // runtime DOM so the optimistic in-iframe result is correct immediately,
6300
+ // not just after the host ack.
6301
+ var ABS_POSITION_INLINE_PROPS = [
6302
+ "position",
6303
+ "left",
6304
+ "top",
6305
+ "right",
6306
+ "bottom",
6307
+ ];
6308
+ // Snapshot of the inline position/left/top/right/bottom VALUES (not just
6309
+ // whether they existed) taken right before stripAbsolutePositioningForFlowInsert
6310
+ // runs, so a failed move-node round-trip can restore exactly what was
6311
+ // there — including "" for a property that had no inline value at all,
6312
+ // which style.removeProperty already treats correctly as "unset". Reused
6313
+ // by the visual-structure-ack failure branch below to undo the optimistic
6314
+ // strip together with the parent/sibling DOM revert.
6315
+ function snapshotInlinePositionStyles(el: Element): Record<string, string> {
6316
+ var htmlEl = el as HTMLElement;
6317
+ var snapshot: Record<string, string> = {};
6318
+ for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
6319
+ var prop = ABS_POSITION_INLINE_PROPS[i];
6320
+ snapshot[prop] = htmlEl.style.getPropertyValue(prop);
6321
+ }
6322
+ return snapshot;
6323
+ }
6324
+ function restoreInlinePositionStyles(
6325
+ el: Element,
6326
+ snapshot: Record<string, string> | null | undefined,
6327
+ ): void {
6328
+ if (!snapshot) return;
6329
+ var htmlEl = el as HTMLElement;
6330
+ for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
6331
+ var prop = ABS_POSITION_INLINE_PROPS[i];
6332
+ var value = snapshot[prop];
6333
+ if (value) {
6334
+ htmlEl.style.setProperty(prop, value);
6335
+ } else {
6336
+ htmlEl.style.removeProperty(prop);
6337
+ }
6338
+ }
6339
+ }
6340
+ // Builds the same snapshot shape as snapshotInlinePositionStyles, but from
6341
+ // a startMove memberState's TRUE pre-drag inline values (captured once at
6342
+ // gesture start — see memberStates below) rather than the live element's
6343
+ // CURRENT inline styles. Used at the startMove/applyGroupStructureDrop
6344
+ // call sites: those paths continuously rewrite the dragged element's
6345
+ // left/top to follow the pointer throughout the free-drag phase, so a
6346
+ // snapshot taken right before the strip would only capture the LAST
6347
+ // dragged-to position, not the position the element should return to when
6348
+ // the whole move-node round-trip is rejected and it goes back to its
6349
+ // original parent.
6350
+ function dragOriginInlinePositionStyles(state: {
6351
+ originalPosition: string;
6352
+ originalLeft: string;
6353
+ originalTop: string;
6354
+ }): Record<string, string> {
6355
+ return {
6356
+ position: state.originalPosition,
6357
+ left: state.originalLeft,
6358
+ top: state.originalTop,
6359
+ right: "",
6360
+ bottom: "",
6361
+ };
6362
+ }
6363
+ function stripAbsolutePositioningForFlowInsert(el: Element, target): void {
6364
+ if (!target || target.dropMode !== "flow-insert") return;
6365
+ var htmlEl = el as HTMLElement;
6366
+ var cs = window.getComputedStyle(htmlEl);
6367
+ if (cs.position !== "absolute" && cs.position !== "fixed") return;
6368
+ for (var i = 0; i < ABS_POSITION_INLINE_PROPS.length; i += 1) {
6369
+ htmlEl.style.removeProperty(ABS_POSITION_INLINE_PROPS[i]);
6370
+ }
6371
+ }
6372
+
6373
+ // Absolute-container nest rebase: an "absolute-container" drop keeps the
6374
+ // member position:absolute BY DESIGN (no flow-insert strip), but the drag
6375
+ // loop wrote its left/top in the member's ORIGINAL containing-block space
6376
+ // (typically the screen root). After reparenting into the drop container —
6377
+ // itself a positioned element — those same numbers re-resolve against the
6378
+ // NEW containing block, displacing the child by exactly the container's
6379
+ // origin: it renders outside the container's (unclipped) box and visually
6380
+ // "vanishes", and that corrupt geometry then persists. Convert left/top
6381
+ // into the new parent's coordinate space BEFORE the DOM move so (a) the
6382
+ // optimistic in-iframe render is correct immediately and (b)
6383
+ // postVisualStructureChange's sourceRect — measured AFTER the move — now
6384
+ // reflects the true on-screen position, which the host's
6385
+ // absoluteContainerOffset persistence math (sourceRect − anchorRect)
6386
+ // depends on. Delta math (old CB origin − new CB origin) keeps the
6387
+ // member's on-screen position identical through the reparent and stays
6388
+ // exact under margins/rotation, unlike re-deriving from the member's own
6389
+ // (transform-inflated) bounding box.
6390
+ function rebaseAbsoluteMemberForContainerDrop(el, target): void {
6391
+ if (!el || !target || target.dropMode !== "absolute-container") return;
6392
+ var container = dropContainerForTarget(target);
6393
+ if (!container || container === document.body || container === el) return;
6394
+ if (el.contains && el.contains(container)) return;
6395
+ var htmlEl = el as HTMLElement;
6396
+ var cs = window.getComputedStyle(htmlEl);
6397
+ if (cs.position !== "absolute" && cs.position !== "fixed") return;
6398
+ // New containing block origin: the container's padding edge, in client
6399
+ // coordinates (border box + border widths − its own scroll offsets).
6400
+ var containerRect = container.getBoundingClientRect();
6401
+ var containerCS = window.getComputedStyle(container);
6402
+ var newOriginX =
6403
+ containerRect.left +
6404
+ readPx(containerCS.borderLeftWidth) -
6405
+ container.scrollLeft;
6406
+ var newOriginY =
6407
+ containerRect.top +
6408
+ readPx(containerCS.borderTopWidth) -
6409
+ container.scrollTop;
6410
+ // Current containing block origin: the member's offsetParent when it is
6411
+ // a real containing block, else the initial containing block (client
6412
+ // 0,0 minus page scroll). offsetParent falls back to <body> even when
6413
+ // body is NOT positioned/transformed — detect that and use the ICB.
6414
+ var oldOriginX = -(window.scrollX || 0);
6415
+ var oldOriginY = -(window.scrollY || 0);
6416
+ var offsetParent = htmlEl.offsetParent as HTMLElement | null;
6417
+ if (offsetParent && offsetParent !== document.documentElement) {
6418
+ var offsetParentIsRealContainingBlock = true;
6419
+ if (offsetParent === document.body) {
6420
+ var bodyCS = window.getComputedStyle(document.body);
6421
+ offsetParentIsRealContainingBlock =
6422
+ bodyCS.position !== "static" ||
6423
+ bodyCS.transform !== "none" ||
6424
+ (bodyCS.getPropertyValue("translate") || "none") !== "none";
6425
+ }
6426
+ if (offsetParentIsRealContainingBlock) {
6427
+ var opRect = offsetParent.getBoundingClientRect();
6428
+ var opCS = window.getComputedStyle(offsetParent);
6429
+ oldOriginX =
6430
+ opRect.left + readPx(opCS.borderLeftWidth) - offsetParent.scrollLeft;
6431
+ oldOriginY =
6432
+ opRect.top + readPx(opCS.borderTopWidth) - offsetParent.scrollTop;
6433
+ }
6434
+ }
6435
+ var currentLeft = readPx(htmlEl.style.left || cs.left);
6436
+ var currentTop = readPx(htmlEl.style.top || cs.top);
6437
+ htmlEl.style.left = currentLeft + (oldOriginX - newOriginX) + "px";
6438
+ htmlEl.style.top = currentTop + (oldOriginY - newOriginY) + "px";
6439
+ }
6440
+
5827
6441
  function applyRuntimeReorder(el, target) {
5828
6442
  if (!el || !target || !target.anchor || !target.anchor.parentElement)
5829
6443
  return;
6444
+ stripAbsolutePositioningForFlowInsert(el, target);
6445
+ // Must run BEFORE the DOM move below: the delta math reads the member's
6446
+ // CURRENT containing block via offsetParent. Called here (the single
6447
+ // choke point for drop reparenting) so the single-drag, group-drag,
6448
+ // flow-reorder, and alt-drag-duplicate paths all rebase consistently.
6449
+ // Idempotent for already-nested members (old CB === new CB → delta 0),
6450
+ // so the visual-structure-ack replay path is safe too.
6451
+ rebaseAbsoluteMemberForContainerDrop(el, target);
5830
6452
  if (target.placement === "inside") {
5831
6453
  target.anchor.appendChild(el);
5832
6454
  return;
@@ -5900,7 +6522,20 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5900
6522
  // selection to each moved node as it processes each message, so a final
5901
6523
  // marquee-selection message restores the full multi-selection afterwards
5902
6524
  // (requirement: selection stays intact after a group drop).
5903
- function applyGroupStructureDrop(members: Element[], target, ev): void {
6525
+ // `originInlineStylesFor` (optional): resolves a member's TRUE pre-drag
6526
+ // position/left/top snapshot (see dragOriginInlinePositionStyles) when the
6527
+ // caller has that gesture-start state available (the startMove auto-layout
6528
+ // branch, whose onMove continuously rewrites left/top to follow the
6529
+ // pointer). Falls back to a live snapshot taken here for the flow-reorder
6530
+ // branch's group call site, which never mutates left/top during its drag
6531
+ // (flow-reorder has no free left/top phase), so the live value IS the
6532
+ // pre-strip/pre-move value there.
6533
+ function applyGroupStructureDrop(
6534
+ members: Element[],
6535
+ target,
6536
+ ev,
6537
+ originInlineStylesFor?: (member: Element) => Record<string, string>,
6538
+ ): void {
5904
6539
  var container = dropContainerForTarget(target);
5905
6540
  var previous: Element | null = null;
5906
6541
  for (var i = 0; i < members.length; i += 1) {
@@ -5918,6 +6553,13 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5918
6553
  };
5919
6554
  var prevParent = member.parentElement;
5920
6555
  var prevNextSibling = member.nextSibling;
6556
+ // Captured BEFORE applyRuntimeReorder so a rejected move-node
6557
+ // round-trip can restore the exact pre-strip inline values (see
6558
+ // snapshotInlinePositionStyles / dragOriginInlinePositionStyles doc
6559
+ // comments).
6560
+ var prevInlinePositionStyles = originInlineStylesFor
6561
+ ? originInlineStylesFor(member)
6562
+ : snapshotInlinePositionStyles(member);
5921
6563
  // Board-text auto-color: adapt before the DOM move so the re-parent
5922
6564
  // check sees the ORIGINAL parent (see adaptAutoTextColorForNest).
5923
6565
  adaptAutoTextColorForNest(member, container);
@@ -5925,6 +6567,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
5925
6567
  postVisualStructureChange(member, memberTarget, {
5926
6568
  prevParent: prevParent,
5927
6569
  prevNextSibling: prevNextSibling,
6570
+ prevInlinePositionStyles: prevInlinePositionStyles,
5928
6571
  });
5929
6572
  previous = member;
5930
6573
  }
@@ -6151,6 +6794,72 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6151
6794
  // move one element, which would tear the group apart.
6152
6795
  postCrossScreenDrag("cancel");
6153
6796
  }
6797
+ // Template-clone reorder rejection (CRITICAL fix): a runtime clone of an
6798
+ // Alpine `<template x-for>` item has no counterpart in the static source
6799
+ // HTML the host resolves structural moves against (only the single
6800
+ // template child exists there), so a reorder/reparent targeting one can
6801
+ // never succeed — the old behavior optimistically reordered the live DOM
6802
+ // then silently reverted it ~1 frame later with zero feedback. Reject up
6803
+ // front instead: no DOM mutation, no doomed host round-trip, clear
6804
+ // "can't reorder" cursor + badge feedback for the whole gesture. Scoped
6805
+ // to single-element, non-duplicate drags of a flow-reorder candidate —
6806
+ // group drags and alt-duplicates build a real, source-backed clone/copy
6807
+ // first (resetRuntimeStableIds), so they are unaffected.
6808
+ if (
6809
+ !isGroupDrag &&
6810
+ !duplicatedForDrag &&
6811
+ isFlowReorderCandidate(gestureEl) &&
6812
+ isTemplateCloneElement(gestureEl)
6813
+ ) {
6814
+ postCrossScreenDrag("cancel");
6815
+ var rejectedEl = gestureEl;
6816
+ function onRejectedMove(ev) {
6817
+ showRejectedDragBadge(
6818
+ "Can't reorder repeated items",
6819
+ ev.clientX,
6820
+ ev.clientY,
6821
+ );
6822
+ }
6823
+ function cleanupRejectedDrag() {
6824
+ document.removeEventListener(events.move, onRejectedMove, true);
6825
+ document.removeEventListener(events.up, onRejectedUp, true);
6826
+ document.removeEventListener("keydown", onRejectedKeyDown, true);
6827
+ clearActiveDragCancel(onRejectedEscape);
6828
+ shieldOverlay.style.cursor = "default";
6829
+ }
6830
+ function onRejectedEscape() {
6831
+ cleanupRejectedDrag();
6832
+ hideTransformBadge();
6833
+ suppressNextShieldClickBriefly();
6834
+ return true;
6835
+ }
6836
+ function onRejectedKeyDown(ev) {
6837
+ if (ev.key === "Escape") {
6838
+ stopNativeInteraction(ev);
6839
+ onRejectedEscape();
6840
+ }
6841
+ }
6842
+ function onRejectedUp() {
6843
+ cleanupRejectedDrag();
6844
+ hideTransformBadge();
6845
+ selectTargetAfterRejectedDrag();
6846
+ }
6847
+ function selectTargetAfterRejectedDrag(): void {
6848
+ selectedEl = rejectedEl;
6849
+ positionOverlay(selectionOverlay, selectedEl);
6850
+ }
6851
+ shieldOverlay.style.cursor = "not-allowed";
6852
+ showRejectedDragBadge(
6853
+ "Can't reorder repeated items",
6854
+ e.clientX,
6855
+ e.clientY,
6856
+ );
6857
+ document.addEventListener(events.move, onRejectedMove, true);
6858
+ document.addEventListener(events.up, onRejectedUp, true);
6859
+ document.addEventListener("keydown", onRejectedKeyDown, true);
6860
+ setActiveDragCancel(onRejectedEscape);
6861
+ return;
6862
+ }
6154
6863
  if (isFlowReorderCandidate(gestureEl)) {
6155
6864
  // Snapshot the element being reordered so a concurrent select-element or
6156
6865
  // clear-selection postMessage cannot mutate the wrong element mid-drag.
@@ -6318,6 +7027,13 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6318
7027
  // reports applied===false on the structure-ack.
6319
7028
  var prevParent = reorderEl.parentElement;
6320
7029
  var prevNextSibling = reorderEl.nextSibling;
7030
+ // Usually a no-op here (flow-reorder drags a member that's
7031
+ // already in flow, so there's nothing to strip), but captured for
7032
+ // consistency so an absolute-positioned element reordered through
7033
+ // this gesture still rolls back its inline styles correctly on a
7034
+ // rejected move-node round-trip.
7035
+ var prevInlinePositionStyles =
7036
+ snapshotInlinePositionStyles(reorderEl);
6321
7037
  adaptAutoTextColorForNest(
6322
7038
  reorderEl,
6323
7039
  dropContainerForTarget(currentTarget),
@@ -6329,6 +7045,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6329
7045
  postVisualStructureChange(reorderEl, currentTarget, {
6330
7046
  prevParent: prevParent,
6331
7047
  prevNextSibling: prevNextSibling,
7048
+ prevInlinePositionStyles: prevInlinePositionStyles,
6332
7049
  });
6333
7050
  }
6334
7051
  }
@@ -6606,10 +7323,31 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6606
7323
  );
6607
7324
  }
6608
7325
  if (isGroupDrag) {
6609
- applyGroupStructureDrop(groupEls, currentAutoLayoutTarget, ev);
7326
+ applyGroupStructureDrop(
7327
+ groupEls,
7328
+ currentAutoLayoutTarget,
7329
+ ev,
7330
+ function (member) {
7331
+ var state = memberStates.filter(function (s) {
7332
+ return s.el === member;
7333
+ })[0];
7334
+ return state
7335
+ ? dragOriginInlinePositionStyles(state)
7336
+ : snapshotInlinePositionStyles(member);
7337
+ },
7338
+ );
6610
7339
  } else {
6611
7340
  var prevParent = dragEl.parentElement;
6612
7341
  var prevNextSibling = dragEl.nextSibling;
7342
+ // The element's TRUE pre-drag inline position/left/top (gestureState
7343
+ // is captured once at drag start, before onMove's continuous
7344
+ // pointer-follow rewrites left/top) — not a snapshot taken here,
7345
+ // which would only capture the LAST dragged-to position. On a
7346
+ // rejected move-node round-trip the element goes back to its
7347
+ // ORIGINAL parent, so it must also go back to the position it had
7348
+ // in that original parent, not a mid-drag coordinate.
7349
+ var prevInlinePositionStyles =
7350
+ dragOriginInlinePositionStyles(gestureState);
6613
7351
  adaptAutoTextColorForNest(
6614
7352
  dragEl,
6615
7353
  dropContainerForTarget(currentAutoLayoutTarget),
@@ -6618,6 +7356,7 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6618
7356
  postVisualStructureChange(dragEl, currentAutoLayoutTarget, {
6619
7357
  prevParent: prevParent,
6620
7358
  prevNextSibling: prevNextSibling,
7359
+ prevInlinePositionStyles: prevInlinePositionStyles,
6621
7360
  });
6622
7361
  }
6623
7362
  } else {
@@ -6984,7 +7723,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
6984
7723
 
6985
7724
  function beginPotentialShieldDrag(e) {
6986
7725
  stopNativeInteraction(e);
6987
- if (e.button !== 0 || activeTextEditEl) return;
7726
+ if (e.button !== 0) return;
7727
+ // T23: a stale session self-heals and the drag proceeds; only a LIVE
7728
+ // session (connected element) blocks shield drags.
7729
+ if (activeTextEditEl && !exitStaleTextEditSession()) return;
6988
7730
  var events = dragEventNames(e);
6989
7731
  var hit = elementFromEditorPoint(e.clientX, e.clientY);
6990
7732
  var hitTarget = selectionTargetForHit(hit);
@@ -7193,6 +7935,106 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7193
7935
  document.addEventListener(
7194
7936
  "keydown",
7195
7937
  function (e) {
7938
+ // T25: pending-window keydown routing — a begin-text-edit is still
7939
+ // waiting for its node. Keystrokes that land in THIS document during
7940
+ // the wait belong to the upcoming text session: buffer printable
7941
+ // characters (replayed on activation), let Backspace edit the buffer,
7942
+ // and swallow Delete/Enter/Tab/arrows so they can never be forwarded
7943
+ // into host layer-deletion/navigation. IME composition and Cmd/Ctrl
7944
+ // chords pass through untouched.
7945
+ if (!activeTextEditEl && pendingBeginTextEdit) {
7946
+ if (!(e.isComposing || e.keyCode === 229) && !e.metaKey && !e.ctrlKey) {
7947
+ var pendingKey = e.key || "";
7948
+ if (pendingKey === "Escape") {
7949
+ var abandonedPendingNodeId = pendingBeginTextEdit.nodeId;
7950
+ cancelPendingBeginTextEdit();
7951
+ postTextEditPending(abandonedPendingNodeId, false);
7952
+ stopNativeInteraction(e);
7953
+ return;
7954
+ }
7955
+ if (pendingKey === "Backspace") {
7956
+ pendingBeginTextEdit.buffer = pendingBeginTextEdit.buffer.slice(
7957
+ 0,
7958
+ -1,
7959
+ );
7960
+ stopNativeInteraction(e);
7961
+ return;
7962
+ }
7963
+ if (pendingKey.length === 1) {
7964
+ pendingBeginTextEdit.buffer += pendingKey;
7965
+ stopNativeInteraction(e);
7966
+ return;
7967
+ }
7968
+ if (
7969
+ pendingKey === "Delete" ||
7970
+ pendingKey === "Enter" ||
7971
+ pendingKey === "Tab" ||
7972
+ pendingKey.indexOf("Arrow") === 0
7973
+ ) {
7974
+ stopNativeInteraction(e);
7975
+ return;
7976
+ }
7977
+ }
7978
+ }
7979
+ // T23/T24: text-edit keydown routing runs BEFORE hotkey forwarding so
7980
+ // a nominally-active session can never lose keys to host shortcuts.
7981
+ if (activeTextEditEl) {
7982
+ if (exitStaleTextEditSession()) {
7983
+ // The session was stale (element detached by a patch). Swallow
7984
+ // this keystroke entirely — letting it fall through in the same
7985
+ // event would forward Delete/Backspace straight into host
7986
+ // layer-deletion while the user believes they are typing text.
7987
+ stopNativeInteraction(e);
7988
+ return;
7989
+ }
7990
+ // Respect IME composition exactly like the session's own onKeyDown.
7991
+ if (e.isComposing || e.keyCode === 229) return;
7992
+ var activeNow = document.activeElement;
7993
+ var focusInsideEdit = !!(
7994
+ activeNow &&
7995
+ (activeNow === activeTextEditEl ||
7996
+ activeTextEditEl.contains(activeNow))
7997
+ );
7998
+ // T24: Escape must ALWAYS exit the session deterministically, even
7999
+ // when focus fell outside the editable (where the session's own
8000
+ // target-scoped keydown listener can never fire).
8001
+ if (e.key === "Escape") {
8002
+ if (!focusInsideEdit) {
8003
+ stopNativeInteraction(e);
8004
+ if (finishActiveTextEdit) finishActiveTextEdit(true);
8005
+ return;
8006
+ }
8007
+ // Focus is inside: fall through — the session's own capture
8008
+ // onKeyDown on the target handles Escape (commit + blur) next.
8009
+ return;
8010
+ }
8011
+ if (!focusInsideEdit) {
8012
+ // Race window: the session is active but focus sits elsewhere
8013
+ // (creation focus race, transient focus steal). If the user is
8014
+ // legitimately typing in a real form control, leave it alone;
8015
+ // otherwise pull focus back into the editable so the keystroke
8016
+ // lands as text — and never reaches host shortcuts.
8017
+ if (!isEditorTypingTarget(activeNow)) {
8018
+ try {
8019
+ activeTextEditEl.focus();
8020
+ var refocusRange = document.createRange();
8021
+ refocusRange.selectNodeContents(activeTextEditEl);
8022
+ refocusRange.collapse(false);
8023
+ var refocusSelection = window.getSelection();
8024
+ if (refocusSelection) {
8025
+ refocusSelection.removeAllRanges();
8026
+ refocusSelection.addRange(refocusRange);
8027
+ }
8028
+ } catch (_err) {
8029
+ /* focus/selection APIs unavailable — key is still swallowed */
8030
+ }
8031
+ e.stopPropagation();
8032
+ }
8033
+ }
8034
+ // While a live session exists, never forward hotkeys to the host
8035
+ // (matches shouldForwardDesignHotkey's activeTextEditEl guard).
8036
+ return;
8037
+ }
7196
8038
  if (!shouldForwardDesignHotkey(e)) return;
7197
8039
  var key = e.key;
7198
8040
  var normalized = key && key.length === 1 ? key.toLowerCase() : key;
@@ -7248,6 +8090,53 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7248
8090
  true,
7249
8091
  );
7250
8092
 
8093
+ // T23/T24: pointerdown-level text-edit session hygiene. Runs on DOCUMENT
8094
+ // capture (not the shield) because an active session sets the shield to
8095
+ // pointer-events:none — and a LEAKED session leaves it that way, so shield
8096
+ // handlers can never observe the pointerdown that should recover from it.
8097
+ // 1. A pointerdown means the user moved on: drop any deferred
8098
+ // begin-text-edit command so it can't yank focus later.
8099
+ // 2. A stale (detached-element) session self-heals on the next click.
8100
+ // 3. Click-away must exit the session even when the editable is NOT
8101
+ // focused (the blur-based commit can never fire in that state).
8102
+ document.addEventListener(
8103
+ "pointerdown",
8104
+ function (e) {
8105
+ if (pendingBeginTextEdit) {
8106
+ var canceledPendingNodeId = pendingBeginTextEdit.nodeId;
8107
+ cancelPendingBeginTextEdit();
8108
+ postTextEditPending(canceledPendingNodeId, false);
8109
+ }
8110
+ if (!activeTextEditEl) return;
8111
+ if (exitStaleTextEditSession()) return;
8112
+ var pointerTarget =
8113
+ e.target && (e.target as Element).nodeType === 1
8114
+ ? (e.target as Element)
8115
+ : null;
8116
+ if (
8117
+ pointerTarget &&
8118
+ (pointerTarget === activeTextEditEl ||
8119
+ activeTextEditEl.contains(pointerTarget))
8120
+ ) {
8121
+ return;
8122
+ }
8123
+ // Editor chrome (overlays) never hosts text content — clicking it
8124
+ // shouldn't force-commit here; the session's own blur handling decides.
8125
+ if (pointerTarget && isOverlayElement(pointerTarget)) return;
8126
+ // A real user pointerdown outside the editable is a deterministic
8127
+ // click-away: commit and exit NOW. This covers both broken states the
8128
+ // blur path can't reach — (a) focus already fell outside the editable
8129
+ // (blur will never fire), and (b) the programmatic empty-text session,
8130
+ // whose blur handler deliberately re-focuses on transient focus steals
8131
+ // but must NOT fight a real click elsewhere. For a healthy focused
8132
+ // session this simply commits a few ms before blur would have.
8133
+ if (finishActiveTextEdit) {
8134
+ finishActiveTextEdit(true);
8135
+ }
8136
+ },
8137
+ true,
8138
+ );
8139
+
7251
8140
  function hasFigmaClipboardPayload(value) {
7252
8141
  return /<[^>]+\sdata-(metadata|buffer)=["'][^"']*\((figmeta|figma)\)[^"']*["']/i.test(
7253
8142
  String(value || ""),
@@ -7338,6 +8227,23 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7338
8227
  return;
7339
8228
  }
7340
8229
  stopNativeInteraction(e);
8230
+ // A new edit supersedes any deferred begin-text-edit command. This is a
8231
+ // USER-initiated path (dblclick) whenever pending is still set here —
8232
+ // the programmatic activation paths clear pending BEFORE calling in —
8233
+ // so also stand the host keystroke buffer down: it must never flush
8234
+ // into this unrelated session.
8235
+ if (pendingBeginTextEdit) {
8236
+ var supersededPendingNodeId = pendingBeginTextEdit.nodeId;
8237
+ cancelPendingBeginTextEdit();
8238
+ postTextEditPending(supersededPendingNodeId, false);
8239
+ }
8240
+ // T23: a live session on a DIFFERENT element must end through the
8241
+ // canonical cleanup BEFORE the new one starts. Previously the new
8242
+ // session simply overwrote activeTextEditEl/finishActiveTextEdit, and
8243
+ // the old session's eventual blur-driven finish() then restored the
8244
+ // shield pointer-passthrough state and posted text-editing-state(false)
8245
+ // UNDERNEATH the new session, corrupting both.
8246
+ if (activeTextEditEl && finishActiveTextEdit) finishActiveTextEdit(true);
7341
8247
  var eventTarget =
7342
8248
  e && e.target && e.target.nodeType === 1 ? e.target : null;
7343
8249
  // The raw `eventTarget` fallback (no findTextEditTarget resolution at
@@ -7501,7 +8407,14 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7501
8407
  var next = target.textContent || "";
7502
8408
  var nextHtml = target.innerHTML || "";
7503
8409
  refreshOverlays();
7504
- if (next !== originalText || nextHtml !== originalHtml) {
8410
+ // T23: never post a content change from a DETACHED node — a document
8411
+ // patch already replaced it, so the in-document copy is the source of
8412
+ // truth and a selector computed from the orphan would target the wrong
8413
+ // (or no) element host-side.
8414
+ if (
8415
+ target.isConnected &&
8416
+ (next !== originalText || nextHtml !== originalHtml)
8417
+ ) {
7505
8418
  postTextContentChange(
7506
8419
  target,
7507
8420
  next,
@@ -7632,6 +8545,88 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7632
8545
  }
7633
8546
  }
7634
8547
 
8548
+ // T22: shared programmatic activation used by the begin-text-edit message
8549
+ // handler — both for an immediately-resolvable node and for one that lands
8550
+ // later via the deferred-retry window below.
8551
+ function queryBeginTextEditNode(nodeId: string): HTMLElement | null {
8552
+ var node: Element | null = document.querySelector(
8553
+ '[data-agent-native-node-id="' +
8554
+ nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
8555
+ '"]',
8556
+ );
8557
+ return node && node.nodeType === 1 ? (node as HTMLElement) : null;
8558
+ }
8559
+ function activateProgrammaticTextEdit(
8560
+ textTarget: HTMLElement,
8561
+ force: boolean,
8562
+ ): void {
8563
+ // If we are already editing this element, do nothing.
8564
+ if (activeTextEditEl && activeTextEditEl === textTarget) return;
8565
+ // Synthesise coordinates at the end of the element content so the caret
8566
+ // lands at the insertion point (right after any placeholder text).
8567
+ var bteRect = textTarget.getBoundingClientRect();
8568
+ var bteCenterX = bteRect.right - 2;
8569
+ var bteCenterY = bteRect.top + bteRect.height / 2;
8570
+ // Delegate to the canonical path so all state, events, and postMessages
8571
+ // stay consistent with a normal double-click text edit.
8572
+ beginTextEditingFromEvent(
8573
+ {
8574
+ clientX: bteCenterX,
8575
+ clientY: bteCenterY,
8576
+ target: textTarget,
8577
+ agentNativeProgrammaticTextEdit: true,
8578
+ preventDefault: function () {},
8579
+ stopPropagation: function () {},
8580
+ stopImmediatePropagation: function () {},
8581
+ } as unknown as MouseEvent,
8582
+ force,
8583
+ );
8584
+ }
8585
+ function pumpPendingBeginTextEdit(): void {
8586
+ if (!pendingBeginTextEdit) return;
8587
+ var entry = pendingBeginTextEdit;
8588
+ var node = queryBeginTextEditNode(entry.nodeId);
8589
+ if (node) {
8590
+ pendingBeginTextEdit = null;
8591
+ // The user may have started their own edit meanwhile — never steal it.
8592
+ if (!activeTextEditEl) {
8593
+ activateProgrammaticTextEdit(node, entry.force);
8594
+ // Replay keystrokes typed into this iframe during the wait — the
8595
+ // session is focused with the caret at the content end, so this
8596
+ // lands exactly where the user expects their first characters.
8597
+ if (entry.buffer && activeTextEditEl === node) {
8598
+ insertPlainTextAtSelection(entry.buffer);
8599
+ }
8600
+ }
8601
+ return;
8602
+ }
8603
+ if (Date.now() > entry.deadline) {
8604
+ pendingBeginTextEdit = null;
8605
+ postTextEditPending(entry.nodeId, false);
8606
+ return;
8607
+ }
8608
+ entry.raf = window.requestAnimationFrame(pumpPendingBeginTextEdit);
8609
+ }
8610
+ function scheduleBeginTextEditRetry(nodeId: string, force: boolean): void {
8611
+ // DesignEditor's own T6 loop re-posts begin-text-edit for the SAME node
8612
+ // every few hundred ms until it activates — those re-posts must extend
8613
+ // the wait, not reset it (a reset would drop keystrokes already buffered
8614
+ // for this node).
8615
+ if (pendingBeginTextEdit && pendingBeginTextEdit.nodeId === nodeId) {
8616
+ pendingBeginTextEdit.force = pendingBeginTextEdit.force || force;
8617
+ pendingBeginTextEdit.deadline = Date.now() + 2000;
8618
+ return;
8619
+ }
8620
+ cancelPendingBeginTextEdit();
8621
+ pendingBeginTextEdit = {
8622
+ nodeId: nodeId,
8623
+ force: force,
8624
+ deadline: Date.now() + 2000,
8625
+ raf: window.requestAnimationFrame(pumpPendingBeginTextEdit),
8626
+ buffer: "",
8627
+ };
8628
+ }
8629
+
7635
8630
  shieldOverlay.addEventListener("dblclick", beginTextEditingFromEvent, true);
7636
8631
  selectionOverlay.addEventListener(
7637
8632
  "dblclick",
@@ -7826,40 +8821,55 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
7826
8821
  var nodeId: string =
7827
8822
  typeof e.data.nodeId === "string" ? e.data.nodeId : "";
7828
8823
  if (!nodeId) return;
7829
- var nodeTarget: Element | null = document.querySelector(
7830
- '[data-agent-native-node-id="' +
7831
- nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
7832
- '"]',
7833
- );
7834
- if (!nodeTarget || nodeTarget.nodeType !== 1) return;
7835
8824
  // Edit the EXACT node identified by nodeId. Do NOT run it through
7836
8825
  // findTextEditTarget here — that helper climbs UP to the highest
7837
8826
  // inline-editable ancestor, which for a text node inside a text-heavy
7838
8827
  // screen resolves all the way to <main>, putting the ENTIRE screen into
7839
8828
  // edit mode instead of this node (keystrokes land in the wrong element).
7840
- var textTarget: HTMLElement | null = nodeTarget as HTMLElement;
7841
- if (!textTarget || textTarget.nodeType !== 1) return;
7842
- // If we are already editing this element, do nothing.
7843
- if (activeTextEditEl && activeTextEditEl === textTarget) return;
7844
- // Synthesise coordinates at the end of the element content so the caret
7845
- // lands at the insertion point (right after any placeholder text).
7846
- var bteRect = textTarget.getBoundingClientRect();
7847
- var bteCenterX = bteRect.right - 2;
7848
- var bteCenterY = bteRect.top + bteRect.height / 2;
7849
- // Delegate to the canonical path so all state, events, and postMessages
7850
- // stay consistent with a normal double-click text edit.
7851
- beginTextEditingFromEvent(
7852
- {
7853
- clientX: bteCenterX,
7854
- clientY: bteCenterY,
7855
- target: textTarget,
7856
- agentNativeProgrammaticTextEdit: true,
7857
- preventDefault: function () {},
7858
- stopPropagation: function () {},
7859
- stopImmediatePropagation: function () {},
7860
- } as unknown as MouseEvent,
7861
- forceBeginTextEdit,
7862
- );
8829
+ var textTarget = queryBeginTextEditNode(nodeId);
8830
+ if (!textTarget) {
8831
+ // T22: the node hasn't landed in this document yet — the command won
8832
+ // the race against the replace-document-content round trip that
8833
+ // carries the freshly-created element. Defer instead of dropping,
8834
+ // so the caret is live the moment the node appears. Tell the host so
8835
+ // it buffers HOST-focused keystrokes for the same window (T25).
8836
+ scheduleBeginTextEditRetry(nodeId, forceBeginTextEdit);
8837
+ postTextEditPending(nodeId, true);
8838
+ return;
8839
+ }
8840
+ cancelPendingBeginTextEdit();
8841
+ activateProgrammaticTextEdit(textTarget, forceBeginTextEdit);
8842
+ return;
8843
+ }
8844
+ // T25: replay keystrokes the HOST buffered during the creation→activation
8845
+ // race window (DesignCanvas suppresses host shortcuts and stashes
8846
+ // printable keys while its begin-text-edit is pending, then flushes them
8847
+ // here once the session reports active). Inserted at the caret through
8848
+ // the same execCommand path paste uses, so the session's own input
8849
+ // listener updates chrome/state naturally.
8850
+ if (e.data.type === "text-edit-insert-text") {
8851
+ var bufferedText = typeof e.data.text === "string" ? e.data.text : "";
8852
+ if (!bufferedText || !activeTextEditEl || !isTextEditElConnected())
8853
+ return;
8854
+ var bufferedActive = document.activeElement;
8855
+ if (
8856
+ !bufferedActive ||
8857
+ (bufferedActive !== activeTextEditEl &&
8858
+ !activeTextEditEl.contains(bufferedActive))
8859
+ ) {
8860
+ try {
8861
+ activeTextEditEl.focus();
8862
+ var bufferedRange = document.createRange();
8863
+ bufferedRange.selectNodeContents(activeTextEditEl);
8864
+ bufferedRange.collapse(false);
8865
+ var bufferedSelection = window.getSelection();
8866
+ if (bufferedSelection) {
8867
+ bufferedSelection.removeAllRanges();
8868
+ bufferedSelection.addRange(bufferedRange);
8869
+ }
8870
+ } catch (_err) {}
8871
+ }
8872
+ insertPlainTextAtSelection(bufferedText);
7863
8873
  return;
7864
8874
  }
7865
8875
  if (e.data.type === "set-editor-chrome-scale") {
@@ -8192,7 +9202,15 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
8192
9202
  postElementSelect(selectedEl);
8193
9203
  }
8194
9204
  } else {
8195
- // Revert the optimistic reorder to its pre-drag position.
9205
+ // Revert the optimistic reorder to its pre-drag position AND
9206
+ // restore any inline position/left/top/right/bottom the optimistic
9207
+ // reorder stripped (stripAbsolutePositioningForFlowInsert runs
9208
+ // inside applyRuntimeReorder for flow-insert drops). Without this
9209
+ // second half a rejected move-node round-trip left the element
9210
+ // re-parented back to its original container but permanently
9211
+ // stripped of its absolute positioning — worse than doing nothing,
9212
+ // since it now renders at the flow position of a detached style
9213
+ // instead of either its original spot or the intended drop slot.
8196
9214
  if (
8197
9215
  move.el &&
8198
9216
  move.el.isConnected &&
@@ -8204,6 +9222,10 @@ declare var __DESIGN_CANVAS_BOARD_SURFACE__: boolean;
8204
9222
  move.el,
8205
9223
  move.origin.prevNextSibling,
8206
9224
  );
9225
+ restoreInlinePositionStyles(
9226
+ move.el,
9227
+ move.origin.prevInlinePositionStyles,
9228
+ );
8207
9229
  selectedEl = move.el;
8208
9230
  positionOverlay(selectionOverlay, selectedEl);
8209
9231
  postElementSelect(selectedEl);