@agent-native/core 0.90.5 → 0.90.8

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 (113) hide show
  1. package/README.md +34 -7
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +20 -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/client/use-db-sync.ts +99 -1
  33. package/corpus/core/src/collab/client.ts +49 -4
  34. package/corpus/core/src/deploy/build.ts +156 -0
  35. package/corpus/core/src/server/core-routes-plugin.ts +137 -8
  36. package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
  37. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
  38. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
  39. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
  40. package/corpus/templates/design/actions/read-source-file.ts +28 -0
  41. package/corpus/templates/design/actions/update-file.ts +76 -7
  42. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +181 -1
  43. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
  44. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
  45. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
  46. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
  47. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
  48. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
  49. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
  50. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
  51. package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
  52. package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
  53. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  54. package/corpus/templates/design/app/i18n-data.ts +19 -0
  55. package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
  60. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
  61. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
  62. package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
  63. package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
  64. package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
  65. package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
  66. package/corpus/templates/design/server/source-workspace.ts +27 -3
  67. package/corpus/templates/design/shared/board-file.ts +359 -0
  68. package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
  69. package/corpus/templates/design/shared/code-layer.ts +145 -10
  70. package/dist/client/use-db-sync.d.ts +18 -0
  71. package/dist/client/use-db-sync.d.ts.map +1 -1
  72. package/dist/client/use-db-sync.js +88 -1
  73. package/dist/client/use-db-sync.js.map +1 -1
  74. package/dist/collab/client.d.ts.map +1 -1
  75. package/dist/collab/client.js +43 -4
  76. package/dist/collab/client.js.map +1 -1
  77. package/dist/deploy/build.d.ts +2 -0
  78. package/dist/deploy/build.d.ts.map +1 -1
  79. package/dist/deploy/build.js +96 -0
  80. package/dist/deploy/build.js.map +1 -1
  81. package/dist/observability/routes.d.ts +3 -3
  82. package/dist/server/core-routes-plugin.d.ts +10 -0
  83. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  84. package/dist/server/core-routes-plugin.js +103 -9
  85. package/dist/server/core-routes-plugin.js.map +1 -1
  86. package/docs/content/agent-native-toolkit.mdx +1 -1
  87. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  88. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  89. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  90. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  91. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  92. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  93. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  94. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  95. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  96. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  97. package/docs/content/toolkit-agent-ux.mdx +1 -1
  98. package/docs/content/toolkit-app-adapters.mdx +1 -1
  99. package/docs/content/toolkit-collaboration-ui.mdx +1 -1
  100. package/docs/content/toolkit-collaboration.mdx +1 -1
  101. package/docs/content/toolkit-command-navigation.mdx +2 -2
  102. package/docs/content/toolkit-comments-review.mdx +1 -1
  103. package/docs/content/toolkit-history.mdx +1 -1
  104. package/docs/content/toolkit-observability.mdx +1 -1
  105. package/docs/content/toolkit-org-team.mdx +1 -1
  106. package/docs/content/toolkit-resources.mdx +1 -1
  107. package/docs/content/toolkit-settings.mdx +1 -1
  108. package/docs/content/toolkit-setup-connections.mdx +1 -1
  109. package/docs/content/toolkit-sharing-ui.mdx +1 -1
  110. package/docs/content/toolkit-sharing.mdx +1 -1
  111. package/docs/content/toolkit-shell-hooks.mdx +1 -1
  112. package/docs/content/toolkit-ui.mdx +1 -1
  113. package/package.json +1 -1
@@ -11,14 +11,44 @@
11
11
  *
12
12
  * Reply (iframe → window.parent):
13
13
  * { type: 'agent-native:hit-test-result', correlationId: string,
14
- * anchorNodeId: string, placement: 'before'|'after'|'inside',
15
- * axis: 'x'|'y',
14
+ * anchorNodeId: string, pendingNodeId: string | undefined,
15
+ * anchorSelector: string | undefined,
16
+ * placement: 'before'|'after'|'inside', axis: 'x'|'y',
16
17
  * anchorRect: { left: number, top: number, width: number, height: number } }
17
18
  *
18
- * Reads DOM only no mutations, no event interception. The container-drop and
19
- * placement logic is intentionally kept in sync with the corresponding helpers
20
- * inside editor-chrome.bridge.ts (search for "// keep in sync with
21
- * hit-test.bridge.ts" comments there).
19
+ * `anchorSelector` accompanies `pendingNodeId`: a body-rooted structural
20
+ * `tag:nth-of-type(n) > …` path whose nth indexes are SOURCE-EQUIVALENT
21
+ * computed against the live DOM but skipping Alpine-generated siblings
22
+ * (x-for clones and x-if instantiations, identified via the sibling
23
+ * templates' own `_x_lookup` / `_x_currentIfEl` bookkeeping) and
24
+ * editor-injected overlay elements, so the path resolves to the SAME element
25
+ * in the persisted source HTML (where none of those runtime nodes exist).
26
+ * The host uses it to persist `pendingNodeId` as the anchor's real
27
+ * `data-agent-native-node-id` in the stored document (two-step handshake,
28
+ * mirroring editor-chrome's getElementInfo selection contract) before
29
+ * resolving the drop against it. Omitted when the anchor itself is an
30
+ * Alpine-generated instance (no per-instance source node exists — the host
31
+ * keeps its absolute-placement fallback for that case).
32
+ *
33
+ * Reads DOM only, no event interception — with one narrow, intentional
34
+ * exception mirroring editor-chrome.bridge.ts's getElementInfo: when the
35
+ * resolved anchor has no stable id anywhere in its own ancestry (common on
36
+ * AI-generated screens, which frequently ship with zero
37
+ * data-agent-native-node-id attributes), getNodeId mints and stamps a
38
+ * `data-an-pending-node-id` marker on it and returns that id as
39
+ * `pendingNodeId` alongside an empty `anchorNodeId` — the same
40
+ * mint-then-let-the-host-persist contract getElementInfo already uses for
41
+ * in-screen selection (see the "Id-on-demand" comment there). Without this,
42
+ * every cross-screen/canvas-to-screen flow-insert into an id-less screen
43
+ * silently degrades to absolute placement, because the host has no anchor id
44
+ * to resolve against even when the hit-test correctly found a valid
45
+ * before/after/inside slot. The stamp itself is inert (an extra data-*
46
+ * attribute, not read by getNodeId's own stable-id list) until a host caller
47
+ * persists it into the document's real data-agent-native-node-id — same
48
+ * two-step handshake as the in-screen path. The container-drop and placement
49
+ * logic is intentionally kept in sync with the corresponding helpers inside
50
+ * editor-chrome.bridge.ts (search for "// keep in sync with hit-test.bridge.ts"
51
+ * comments there).
22
52
  *
23
53
  * Rules:
24
54
  * • No import/require of any module (DOM globals only).
@@ -98,6 +128,8 @@
98
128
  "label",
99
129
  "li",
100
130
  ];
131
+ // keep in sync with editor-chrome.bridge.ts BRIDGE_INTERACTIVE_LEAF_TAGS
132
+ var BRIDGE_INTERACTIVE_LEAF_TAGS = ["button", "summary"];
101
133
 
102
134
  function isOverlayElement(el: Element | null): boolean {
103
135
  return Boolean(
@@ -117,6 +149,25 @@
117
149
  return false;
118
150
  }
119
151
 
152
+ // keep in sync with editor-chrome.bridge.ts hasOnlyLeafContent
153
+ function hasOnlyLeafContent(el: Element): boolean {
154
+ var children = el.children;
155
+ if (!children.length) return true;
156
+ for (var i = 0; i < children.length; i += 1) {
157
+ var child = children[i] as Element;
158
+ var childTag = (child.tagName || "").toLowerCase();
159
+ if (
160
+ BRIDGE_LEAF_TAGS.indexOf(childTag) === -1 &&
161
+ BRIDGE_TEXT_TAGS.indexOf(childTag) === -1 &&
162
+ BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(childTag) === -1
163
+ ) {
164
+ return false;
165
+ }
166
+ if (child.children.length && !hasOnlyLeafContent(child)) return false;
167
+ }
168
+ return true;
169
+ }
170
+
120
171
  // keep in sync with editor-chrome.bridge.ts isContainerDropTarget
121
172
  function isContainerDropTarget(el: Element | null): boolean {
122
173
  if (!el || el === document.documentElement) return false;
@@ -128,6 +179,12 @@
128
179
  BRIDGE_TEXT_TAGS.indexOf(tag) !== -1
129
180
  )
130
181
  return false;
182
+ if (
183
+ BRIDGE_INTERACTIVE_LEAF_TAGS.indexOf(tag) !== -1 &&
184
+ hasOnlyLeafContent(el)
185
+ ) {
186
+ return false;
187
+ }
131
188
  var cs = window.getComputedStyle(el);
132
189
  if (
133
190
  cs.display === "flex" ||
@@ -252,6 +309,69 @@
252
309
  return null;
253
310
  }
254
311
 
312
+ function getNodeId(el: Element | null): string {
313
+ if (!el) return "";
314
+ return (
315
+ el.getAttribute("data-agent-native-node-id") ||
316
+ el.getAttribute("data-code-layer-id") ||
317
+ el.getAttribute("data-layer-id") ||
318
+ el.getAttribute("data-builder-id") ||
319
+ el.id ||
320
+ ""
321
+ );
322
+ }
323
+
324
+ // Detects an Alpine `<template x-for>` runtime clone: Alpine keeps the
325
+ // `<template>` element itself in the live DOM (as a hidden, zero-size
326
+ // marker) and inserts every rendered instance as a DIRECT SIBLING of that
327
+ // template, all still children of the same parent — so `ul > template,
328
+ // li, li, li` is the live shape for `<ul><template x-for>...</template>
329
+ // rendering 3 items</ul>`. The static SOURCE HTML the host resolves moves
330
+ // against only ever contains the single template child, never the N
331
+ // runtime clones, so a hit-test anchor resolved onto a clone — or onto a
332
+ // container whose only children are clones, if the caller doesn't skip
333
+ // them — can never resolve on the host and always comes back
334
+ // `applied:false`. Detected once per hit-test via an ancestor walk (not
335
+ // just the immediate parent) so nested x-for clones (e.g. a subtask `<li>`
336
+ // inside a per-task `<ul>` that is itself x-for'd) are also caught,
337
+ // stopping at the first stable-id ancestor (anything inside a stamped
338
+ // subtree has a real anchor and is fine).
339
+ //
340
+ // keep in sync with editor-chrome.bridge.ts isTemplateCloneElement
341
+ function isTemplateCloneElement(el: Element | null): boolean {
342
+ var node: Element | null = el;
343
+ while (node && node !== document.documentElement) {
344
+ if (getNodeId(node)) return false;
345
+ var parent = node.parentElement;
346
+ if (!parent) return false;
347
+ var siblings = parent.children;
348
+ for (var i = 0; i < siblings.length; i += 1) {
349
+ var sib = siblings[i];
350
+ if (
351
+ sib !== node &&
352
+ sib.tagName &&
353
+ sib.tagName.toLowerCase() === "template" &&
354
+ sib.hasAttribute("x-for")
355
+ ) {
356
+ return true;
357
+ }
358
+ }
359
+ node = parent;
360
+ }
361
+ return false;
362
+ }
363
+
364
+ // Anchor-candidate gate (companion to isTemplateCloneElement above): a
365
+ // template clone can never be used as an insertion ANCHOR — it has no
366
+ // counterpart in the static source HTML, so before/after placement
367
+ // against it can never resolve on the host. Filtering clones out of the
368
+ // candidate list here is what fixes drops into a container whose ONLY
369
+ // children are x-for clones: without this, nearestChildInsertionTarget's
370
+ // "nearest child" search would happily pick a clone as the anchor, and
371
+ // the resulting move would silently fail on the host (layerMoveFailed
372
+ // toast) even though the drop gesture itself was completely valid.
373
+ //
374
+ // keep in sync with editor-chrome.bridge.ts draggableElementChildren
255
375
  function draggableElementChildren(parent: Element): Element[] {
256
376
  return Array.prototype.slice.call(parent.children).filter(function (
257
377
  child: Element,
@@ -259,23 +379,204 @@
259
379
  return (
260
380
  child.nodeType === 1 &&
261
381
  !isOverlayElement(child) &&
262
- !isLayerInteractionBlocked(child)
382
+ !isLayerInteractionBlocked(child) &&
383
+ !isTemplateCloneElement(child)
263
384
  );
264
385
  });
265
386
  }
266
387
 
267
- function getNodeId(el: Element | null): string {
268
- if (!el) return "";
269
- return (
270
- el.getAttribute("data-agent-native-node-id") ||
271
- el.getAttribute("data-code-layer-id") ||
272
- el.getAttribute("data-layer-id") ||
273
- el.getAttribute("data-builder-id") ||
274
- el.id ||
275
- ""
388
+ // keep in sync with editor-chrome.bridge.ts freshRuntimeNodeId
389
+ function freshRuntimeNodeId(prefix: string): string {
390
+ var random = "";
391
+ try {
392
+ if (window.crypto && window.crypto.getRandomValues) {
393
+ var bytes = new Uint32Array(2);
394
+ window.crypto.getRandomValues(bytes);
395
+ random = Array.prototype.map
396
+ .call(bytes, function (part: number) {
397
+ return part.toString(36);
398
+ })
399
+ .join("");
400
+ }
401
+ } catch (_err) {}
402
+ if (!random)
403
+ random = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
404
+ return "an-" + String(prefix || "pending") + "-" + random;
405
+ }
406
+
407
+ // Id-on-demand fallback (see the file header comment): when the resolved
408
+ // anchor has no stable id, mint one and stamp it as
409
+ // data-an-pending-node-id — same marker/contract as editor-chrome.bridge.ts's
410
+ // getElementInfo — and return it so the caller can expose it as
411
+ // `pendingNodeId` for a host caller to persist. Deliberately NOT read by
412
+ // getNodeId itself (a pending id is not a stable id until persisted).
413
+ function getOrMintPendingNodeId(el: Element | null): string {
414
+ if (!el || !el.getAttribute || !el.setAttribute) return "";
415
+ // Defensive guard: resolveHitTarget's anchor-candidate gates (see
416
+ // isTemplateCloneElement call sites there) already keep template clones
417
+ // out of `result.anchor`, so this should never fire in practice — but a
418
+ // pending id stamped on a clone would be dead weight: the clone itself
419
+ // has no counterpart in source HTML, so no host persist call could ever
420
+ // write data-agent-native-node-id anywhere durable for it, and Alpine
421
+ // re-renders the clone from scratch on next data change anyway (the
422
+ // stamped attribute would vanish). Fail closed instead of minting.
423
+ if (isTemplateCloneElement(el)) return "";
424
+ var existing = el.getAttribute("data-an-pending-node-id");
425
+ if (existing) return existing;
426
+ var minted = freshRuntimeNodeId("pending");
427
+ try {
428
+ el.setAttribute("data-an-pending-node-id", minted);
429
+ } catch (_err) {}
430
+ return minted;
431
+ }
432
+
433
+ // ── Source-equivalent structural selector (anchorSelector) ────────────────
434
+ //
435
+ // The persisted source HTML never contains Alpine-generated runtime nodes
436
+ // (x-for clones, x-if instantiations) or editor-injected overlays, so a
437
+ // naive live-DOM nth-of-type path would mis-resolve on the host whenever
438
+ // any of those precede the anchor among same-tag siblings. These helpers
439
+ // compute nth indexes that count only source-present siblings, so the
440
+ // emitted path resolves to the SAME element in the host's DOMParser/
441
+ // projection view of the stored document.
442
+
443
+ // Elements generated by an Alpine template sibling: x-for clones are the
444
+ // values of the template's `_x_lookup` map; the x-if instantiation is the
445
+ // template's `_x_currentIfEl`. Both live as DIRECT SIBLINGS of their
446
+ // template in the live DOM while the source only contains the template.
447
+ //
448
+ // COUPLING WARNING: `_x_lookup` / `_x_currentIfEl` are Alpine.js PRIVATE
449
+ // internals (verified against the Alpine 3.x line served by the prototype
450
+ // CDN pin). If a future Alpine major renames them, the try/catch below
451
+ // swallows the breakage silently and generated clones would be counted as
452
+ // source siblings — buildSourceEquivalentSelector could then resolve a
453
+ // pending node id onto the WRONG source element. When bumping Alpine,
454
+ // re-verify these fields and the clone-vs-source guard tests in
455
+ // bridge.guard.spec.ts.
456
+ function alpineGeneratedChildrenOf(parent: Element): Element[] {
457
+ var generated: Element[] = [];
458
+ var children = parent.children;
459
+ for (var i = 0; i < children.length; i += 1) {
460
+ var child = children[i] as Element & {
461
+ _x_lookup?: Record<string, Element>;
462
+ _x_currentIfEl?: Element;
463
+ };
464
+ if (!child.tagName || child.tagName.toLowerCase() !== "template") {
465
+ continue;
466
+ }
467
+ try {
468
+ if (child._x_currentIfEl) generated.push(child._x_currentIfEl);
469
+ var lookup = child._x_lookup;
470
+ if (lookup) {
471
+ for (var key in lookup) {
472
+ if (Object.prototype.hasOwnProperty.call(lookup, key)) {
473
+ var item = lookup[key];
474
+ if (item) generated.push(item);
475
+ }
476
+ }
477
+ }
478
+ } catch (_err) {}
479
+ }
480
+ return generated;
481
+ }
482
+
483
+ function isEditorInjectedElement(el: Element): boolean {
484
+ return !!(
485
+ el.getAttribute &&
486
+ (el.getAttribute("data-agent-native-edit-overlay") !== null ||
487
+ el.getAttribute("data-agent-native-hit-test-preview") !== null)
276
488
  );
277
489
  }
278
490
 
491
+ // Body-rooted `tag:nth-of-type(n) > …` path with source-equivalent nth
492
+ // indexes, or "" when the anchor (or any ancestor on the way up) is itself
493
+ // an Alpine-generated instance — such elements have no per-instance source
494
+ // node, so no selector can honestly identify them in the stored document.
495
+ function buildSourceEquivalentSelector(el: Element | null): string {
496
+ if (!el || el === document.documentElement || el === document.body) {
497
+ return "";
498
+ }
499
+ var parts: string[] = [];
500
+ var node: Element | null = el;
501
+ while (node && node !== document.body) {
502
+ var parent: Element | null = node.parentElement;
503
+ if (!parent) return "";
504
+ var generated = alpineGeneratedChildrenOf(parent);
505
+ if (generated.indexOf(node) !== -1) return "";
506
+ if (isEditorInjectedElement(node)) return "";
507
+ var tag = node.tagName ? node.tagName.toLowerCase() : "";
508
+ if (!tag || tag === "template") return "";
509
+ var nth = 0;
510
+ var siblings = parent.children;
511
+ for (var i = 0; i < siblings.length; i += 1) {
512
+ var sib = siblings[i];
513
+ if (!sib.tagName || sib.tagName.toLowerCase() !== tag) continue;
514
+ if (generated.indexOf(sib) !== -1) continue;
515
+ if (isEditorInjectedElement(sib)) continue;
516
+ nth += 1;
517
+ if (sib === node) break;
518
+ }
519
+ if (nth === 0) return "";
520
+ parts.unshift(tag + ":nth-of-type(" + nth + ")");
521
+ node = parent;
522
+ }
523
+ if (!node) return "";
524
+ parts.unshift("body");
525
+ return parts.join(" > ");
526
+ }
527
+
528
+ // Resolves a between-children insertion inside `container` from the
529
+ // pointer position: the nearest visible child (by flow-axis center)
530
+ // becomes the anchor with before/after placement, which renders as the
531
+ // Figma-style insertion LINE between children. Returns null when the
532
+ // container has no eligible children (caller falls back to "inside").
533
+ //
534
+ // This is the finding-6 fix, ported from editor-chrome.bridge.ts's own
535
+ // B5-4 fix (nearestChildInsertionTarget there): hovering the container's
536
+ // own background — its padding, or the gaps BETWEEN children, which is
537
+ // where the pointer naturally sits when dropping "between two cards" —
538
+ // used to resolve to placement "inside" (append at end) instead of
539
+ // inserting at the hovered slot. hit-test.bridge.ts never has a dragged
540
+ // element of its own (it only resolves anchors for a cross-screen/
541
+ // canvas-to-screen drag whose source lives in a different iframe), so
542
+ // this version omits the editor-chrome original's `excludeEls` parameter.
543
+ //
544
+ // keep in sync with editor-chrome.bridge.ts nearestChildInsertionTarget
545
+ function nearestChildInsertionTarget(
546
+ container: Element,
547
+ clientX: number,
548
+ clientY: number,
549
+ ) {
550
+ var children = draggableElementChildren(container);
551
+ if (!children.length) return null;
552
+ var axis = parentFlowAxis(container);
553
+ var best: Element | null = null;
554
+ var bestDistance = Infinity;
555
+ var placement = "after";
556
+ for (var j = 0; j < children.length; j += 1) {
557
+ var rect = children[j].getBoundingClientRect();
558
+ // Skip zero-size children (e.g. Alpine <template> nodes, hidden
559
+ // elements) — they are not visible slots.
560
+ if (rect.width <= 0 || rect.height <= 0) continue;
561
+ var center =
562
+ axis === "x" ? rect.left + rect.width / 2 : rect.top + rect.height / 2;
563
+ var pointer = axis === "x" ? clientX : clientY;
564
+ var distance = Math.abs(pointer - center);
565
+ if (distance < bestDistance) {
566
+ bestDistance = distance;
567
+ best = children[j];
568
+ placement = pointer < center ? "before" : "after";
569
+ }
570
+ }
571
+ if (!best) return null;
572
+ return {
573
+ anchor: best,
574
+ placement: placement,
575
+ axis: axis,
576
+ dropMode: "flow-insert",
577
+ };
578
+ }
579
+
279
580
  /**
280
581
  * Resolve the deepest container element under (x, y) and a placement hint,
281
582
  * mirroring reorderTargetForPoint from editor-chrome.bridge.ts but
@@ -300,6 +601,29 @@
300
601
  if (isLayerInteractionBlocked(cursor)) return null;
301
602
  var parent: Element | null = cursor.parentElement;
302
603
  if (parent && isAutoLayoutElement(parent)) {
604
+ // Anchor-candidate gate: cursor is a plain flex/grid item being used
605
+ // as a before/after anchor — but if it's a template clone (no
606
+ // counterpart in source HTML), fall back to the nearest non-clone
607
+ // sibling via nearestChildInsertionTarget, else the container itself
608
+ // with "inside" placement. Mirrors editor-chrome.bridge.ts's
609
+ // reorderTargetForPoint / autoLayoutInsertionTargetForPoint clone
610
+ // fallback — this is the primary path a cursor hits when hovering
611
+ // directly over a rendered x-for clone item inside a flex/grid
612
+ // container (e.g. a filter card whose only children are clones).
613
+ if (isTemplateCloneElement(cursor)) {
614
+ var cloneFallback = nearestChildInsertionTarget(
615
+ parent,
616
+ clientX,
617
+ clientY,
618
+ );
619
+ if (cloneFallback) return cloneFallback;
620
+ return {
621
+ anchor: parent,
622
+ placement: "inside",
623
+ axis: parentFlowAxis(parent),
624
+ dropMode: "flow-insert",
625
+ };
626
+ }
303
627
  var parentAxis = parentFlowAxis(parent);
304
628
  var childRect = cursor.getBoundingClientRect();
305
629
  var childCenter =
@@ -331,6 +655,17 @@
331
655
  dropMode: "flow-insert",
332
656
  };
333
657
  }
658
+ // finding 6: the pointer is over the container's inner area — its
659
+ // padding or the gap BETWEEN children (a direct child under the
660
+ // pointer would have been the hit instead). Resolve to the nearest
661
+ // child slot so the drop lands between children with the insertion
662
+ // LINE, instead of placement:"inside" append-after-last.
663
+ var betweenChildren = nearestChildInsertionTarget(
664
+ cursor,
665
+ clientX,
666
+ clientY,
667
+ );
668
+ if (betweenChildren) return betweenChildren;
334
669
  return {
335
670
  anchor: cursor,
336
671
  placement: "inside",
@@ -408,6 +743,21 @@
408
743
  var result = resolveHitTarget(x, y);
409
744
  if (e.data.preview) showInsertionGuideFor(result);
410
745
  var anchorNodeId: string = result ? getNodeId(result.anchor) : "";
746
+ // Id-on-demand fallback (see file header): only mint when there is a
747
+ // real resolved anchor with no stable id — never for a null/no-target
748
+ // result. getOrMintPendingNodeId is idempotent per-element (reuses the
749
+ // existing data-an-pending-node-id if already stamped), so repeated
750
+ // hover-phase hit-tests over the same anchor do not re-mint or spam
751
+ // attribute writes; a HOST caller decides whether/when to persist it.
752
+ var pendingNodeId: string =
753
+ result && !anchorNodeId ? getOrMintPendingNodeId(result.anchor) : "";
754
+ // Only computed alongside a minted pendingNodeId — it exists so a host
755
+ // can persist that pending id into the stored document (see the file
756
+ // header's anchorSelector contract). "" (omitted) when the anchor is an
757
+ // Alpine-generated instance with no source node.
758
+ var anchorSelector: string = pendingNodeId
759
+ ? buildSourceEquivalentSelector(result ? result.anchor : null)
760
+ : "";
411
761
  var placement: string = result ? result.placement : "inside";
412
762
  var axis: string = result ? result.axis : "y";
413
763
  var dropMode: string = result ? result.dropMode : "flow-insert";
@@ -418,6 +768,8 @@
418
768
  type: "agent-native:hit-test-result",
419
769
  correlationId: correlationId,
420
770
  anchorNodeId: anchorNodeId,
771
+ pendingNodeId: pendingNodeId || undefined,
772
+ anchorSelector: anchorSelector || undefined,
421
773
  placement: placement,
422
774
  axis: axis,
423
775
  dropMode: dropMode,
@@ -342,3 +342,27 @@ export function defaultGradientLayer(
342
342
  ) {
343
343
  return buildGradientLayer(type, defaultGradientStops(colorValue));
344
344
  }
345
+
346
+ /**
347
+ * Atomic patch for switching a solid fill to a gradient paint type: prepends
348
+ * a default gradient layer built from the current solid color AND clears the
349
+ * solid backgroundColor underneath it.
350
+ */
351
+ export function solidToGradientPatch(
352
+ colorValue: string,
353
+ backgroundLayers: string[],
354
+ type: DesignGradientType,
355
+ ): { backgroundImage: string; backgroundColor: string } {
356
+ return {
357
+ backgroundImage: joinCssLayers([
358
+ defaultGradientLayer(type, cssColorOrFallback(colorValue, "#000000")),
359
+ ...backgroundLayers,
360
+ ]),
361
+ // Convert the solid fill into the gradient instead of stacking the
362
+ // gradient on top of it — leaving backgroundColor set kept a second
363
+ // real fill alive (the default gradient fades to alpha-0, so the old
364
+ // solid showed through) and the Fill panel correctly-but-confusingly
365
+ // listed two rows for what the user meant as one type switch.
366
+ backgroundColor: "transparent",
367
+ };
368
+ }
@@ -37,6 +37,7 @@ import {
37
37
  joinCssLayers,
38
38
  parseGradientLayer,
39
39
  SOLID_FILL_ID,
40
+ solidToGradientPatch,
40
41
  splitCssLayers,
41
42
  } from "./fill-gradient-helpers";
42
43
  import { colorHasVisibleAlpha, cssColorOrFallback } from "./position-helpers";
@@ -478,9 +479,23 @@ export function ColorInput({
478
479
  const handlePaintTypeChange = (type: DesignPaintType) => {
479
480
  const selectedLayer = fillLayerIndex(selectedFillId);
480
481
  if (type === "solid") {
482
+ // Prefer the removed gradient's first stop color. Switching solid ->
483
+ // gradient converts the fill (backgroundColor becomes "transparent",
484
+ // see solidToGradientPatch below), so on the way back draft/value
485
+ // would be "transparent" and cssColorOrFallback would land on black;
486
+ // the first stop still holds the color the gradient was built from.
487
+ const removedGradient =
488
+ selectedLayer !== null
489
+ ? parseGradientLayer(backgroundLayers[selectedLayer] || "")
490
+ : null;
481
491
  if (selectedLayer !== null) removeBackgroundLayer(selectedLayer);
482
492
  setSelectedFillId(SOLID_FILL_ID);
483
- setNext(cssColorOrFallback(draft || value, "#000000"));
493
+ setNext(
494
+ cssColorOrFallback(
495
+ removedGradient?.stops[0]?.color || draft || value,
496
+ "#000000",
497
+ ),
498
+ );
484
499
  return;
485
500
  }
486
501
  if (type === "none") {
@@ -515,12 +530,18 @@ export function ColorInput({
515
530
  return;
516
531
  }
517
532
 
518
- onBackgroundImageChange(
519
- joinCssLayers([
520
- defaultGradientLayer(nextType, draft || value || "#000000"),
521
- ...backgroundLayers,
522
- ]),
533
+ const patch = solidToGradientPatch(
534
+ draft || value || "#000000",
535
+ backgroundLayers,
536
+ nextType,
523
537
  );
538
+ onBackgroundImageChange(patch.backgroundImage);
539
+ // Clear the solid base fill in the same switch — this is a convert
540
+ // (the mirror of the gradient -> solid branch above), not a stack.
541
+ // Leaving backgroundColor set kept a second real fill alive under the
542
+ // alpha-0 tail of the default gradient, so the panel listed a phantom
543
+ // extra row for what the user meant as one paint-type change.
544
+ setNext(patch.backgroundColor);
524
545
  setSelectedFillId(fillLayerId(0));
525
546
  setSelectedStopId("stop-0");
526
547
  };
@@ -73,6 +73,24 @@ export function resolveTextStrokeColor(
73
73
  return cssColorOrFallback(strokeColor, "#000000");
74
74
  }
75
75
 
76
+ /**
77
+ * The style patch the Stroke section's "Add layer" button commits for text
78
+ * elements: seed a 1px glyph outline in the resolved stroke color.
79
+ *
80
+ * Keys MUST stay kebab-case: camelCase webkit props (webkitTextStrokeWidth)
81
+ * get mangled by code-layer.ts normalizeStyleProperty — its camel→kebab pass
82
+ * yields `webkit-text-stroke-width` WITHOUT the required leading dash, which
83
+ * fails the style allow-list and silently persists nothing.
84
+ */
85
+ export function textStrokeAddPatch(
86
+ strokeColor: string | undefined,
87
+ ): Record<string, string> {
88
+ return {
89
+ "-webkit-text-stroke-width": "1px",
90
+ "-webkit-text-stroke-color": resolveTextStrokeColor(strokeColor),
91
+ };
92
+ }
93
+
76
94
  /**
77
95
  * R94 fix — reads a text stroke's width/color out of `element.computedStyles`
78
96
  * regardless of which of two shapes that map is in:
@@ -37,6 +37,7 @@ import {
37
37
  roundToOneDecimal,
38
38
  strokeHiddenByColor,
39
39
  strokeIsVisible,
40
+ textStrokeAddPatch,
40
41
  textStrokeIsVisible,
41
42
  } from "./position-helpers";
42
43
  import { isMixedValue } from "./selection-helpers";
@@ -533,11 +534,11 @@ function TextStrokeProperties({
533
534
  <SectionIconButton
534
535
  label={t("editPanel.labels.addLayer")}
535
536
  onClick={() => {
537
+ // Kebab-case keys required: camelCase webkit props get mangled by
538
+ // normalizeStyleProperty (camel→kebab drops the leading dash) and
539
+ // silently fail the persist allow-list — see textStrokeAddPatch.
536
540
  commitStylePatch(
537
- {
538
- webkitTextStrokeWidth: "1px",
539
- webkitTextStrokeColor: resolveTextStrokeColor(color),
540
- },
541
+ textStrokeAddPatch(color),
541
542
  onStyleChange,
542
543
  onStylesChange,
543
544
  );