@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.
- package/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +18 -18
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
- package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
- package/corpus/core/src/agent/production-agent.ts +22 -11
- package/corpus/core/src/agent/run-manager.ts +17 -6
- package/corpus/core/src/agent/run-store.ts +16 -11
- package/corpus/core/src/cli/design-connect.ts +16 -2
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
- package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +103 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
- package/corpus/templates/content/actions/_database-utils.ts +2 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
- package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
- package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +31 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
- package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +59 -4
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/connect-localhost.ts +26 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/agent/durable-background.d.ts +12 -12
- package/dist/agent/durable-background.js +18 -18
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +8 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -1
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +7 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +6 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +15 -10
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +12 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +9 -10
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
- package/dist/client/KeepTabOpenNotice.js +8 -4
- package/dist/client/KeepTabOpenNotice.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +14 -13
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +25 -17
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +6 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +57 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- 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,
|
|
15
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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(
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
);
|