@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.
- package/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +20 -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/client/use-db-sync.ts +99 -1
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +156 -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/design/.agents/skills/design-generation/SKILL.md +9 -0
- 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 +181 -1
- 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-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/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/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/deploy/build.d.ts +2 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +96 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- 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
|
@@ -30,6 +30,34 @@ export default defineAction({
|
|
|
30
30
|
const workspace = await resolveSourceWorkspace(designId, {
|
|
31
31
|
includeContent: true,
|
|
32
32
|
});
|
|
33
|
+
// The board overlay file is a reserved canvas-model document, not a
|
|
34
|
+
// source file the code workbench edits — resolveSourceWorkspace already
|
|
35
|
+
// excludes it from `files`. Asking for "the board's source" (e.g. a UI
|
|
36
|
+
// surface that follows setActiveFileId to whatever screen a cross-screen
|
|
37
|
+
// drop just landed on, including the board) is a legitimate no-op query,
|
|
38
|
+
// not an error: return an empty/readonly placeholder instead of letting
|
|
39
|
+
// findSourceWorkspaceFile throw a 404-as-500 for every such request.
|
|
40
|
+
if (fileId && workspace.boardFileId && fileId === workspace.boardFileId) {
|
|
41
|
+
return {
|
|
42
|
+
designId,
|
|
43
|
+
path: "__board__.html",
|
|
44
|
+
displayName: "__board__.html",
|
|
45
|
+
fileId,
|
|
46
|
+
sourceType: workspace.sourceType,
|
|
47
|
+
backendKind: "virtual-inline",
|
|
48
|
+
readonly: true,
|
|
49
|
+
language: "html",
|
|
50
|
+
content: "",
|
|
51
|
+
versionHash: "",
|
|
52
|
+
updatedAt: null,
|
|
53
|
+
provenance: {
|
|
54
|
+
kind: "design-file" as const,
|
|
55
|
+
designId,
|
|
56
|
+
fileId,
|
|
57
|
+
filename: "__board__.html",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
33
61
|
const file = findSourceWorkspaceFile(workspace.files, { fileId, path });
|
|
34
62
|
const live = await readLiveSourceFile(file);
|
|
35
63
|
return {
|
|
@@ -33,8 +33,14 @@ function rowsAffected(result: unknown): number | undefined {
|
|
|
33
33
|
* `skippedStaleMirror?: true` is added to the result ONLY in the narrow
|
|
34
34
|
* SQL-mirror-only staleness case: `content` was provided, `syncCollab` was
|
|
35
35
|
* explicitly `false`, a live collaboration document exists for this file,
|
|
36
|
-
* and the caller's `expectedVersionHash`
|
|
37
|
-
*
|
|
36
|
+
* and the caller's `expectedVersionHash` matches NEITHER the live collab
|
|
37
|
+
* text, NOR the `content` being written (an own edit that raced ahead via
|
|
38
|
+
* Yjs), NOR the current SQL mirror content. A caller whose hash matches the
|
|
39
|
+
* current mirror is the mirror column's own lineage (mirror-lineage rescue):
|
|
40
|
+
* it proceeds instead of skipping, advancing the mirror AND diff-merging its
|
|
41
|
+
* content into the live doc.
|
|
42
|
+
* In the genuinely-stale case the content column is intentionally left
|
|
43
|
+
* untouched (the live
|
|
38
44
|
* collab document remains the source of truth) while any `filename`/
|
|
39
45
|
* `fileType` updates in the same call still apply, and the action returns
|
|
40
46
|
* success instead of throwing. The field is omitted (not `false`) in every
|
|
@@ -163,7 +169,24 @@ export default defineAction({
|
|
|
163
169
|
// trying to sync into collab in the first place. Every other
|
|
164
170
|
// expectedVersionHash combination (syncCollab true/default, or no live
|
|
165
171
|
// collab state, or a matching hash, or no hash at all) is UNCHANGED.
|
|
172
|
+
//
|
|
173
|
+
// Own-edit false-positive fix: a single client's own edit reaches the
|
|
174
|
+
// live collab doc via TWO independent, unordered paths — the Yjs
|
|
175
|
+
// update (~80ms client debounce, applied to the server's in-memory doc
|
|
176
|
+
// as soon as its POST lands) and this guarded update-file call (~400ms
|
|
177
|
+
// client debounce). The Yjs path usually wins the race, so by the time
|
|
178
|
+
// this call's hash check runs, `liveContent` often already equals the
|
|
179
|
+
// very `content` this call is trying to write — that is NOT a
|
|
180
|
+
// divergent concurrent edit, it's the same edit having arrived early
|
|
181
|
+
// by a different transport. Comparing hashes first would reject that
|
|
182
|
+
// as "stale" and permanently skip the SQL mirror write (there is no
|
|
183
|
+
// background job that later reconciles design_files.content from the
|
|
184
|
+
// live collab doc — see hasCollabState below), silently losing writes
|
|
185
|
+
// on every edit after the first in a session. Check content equality
|
|
186
|
+
// BEFORE the hash comparison so this exact-match case always proceeds
|
|
187
|
+
// as a normal write instead of hitting either the skip or throw path.
|
|
166
188
|
let skipContentWrite = false;
|
|
189
|
+
let mirrorLineageCollabSync = false;
|
|
167
190
|
if (expectedVersionHash !== undefined && content !== undefined) {
|
|
168
191
|
const collabExists = await hasCollabState(id);
|
|
169
192
|
let liveContent: string;
|
|
@@ -177,10 +200,52 @@ export default defineAction({
|
|
|
177
200
|
.limit(1);
|
|
178
201
|
liveContent = current?.content ?? "";
|
|
179
202
|
}
|
|
180
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
liveContent !== content &&
|
|
205
|
+
sourceContentHash(liveContent) !== expectedVersionHash
|
|
206
|
+
) {
|
|
181
207
|
if (syncCollab === false && collabExists) {
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
// Mirror-lineage rescue (sequential-edit data-loss fix, verified
|
|
209
|
+
// live): the client's Yjs transact and its guarded update-file
|
|
210
|
+
// call ride two independent transports, and the Yjs pipe can lag
|
|
211
|
+
// or silently die — reproduced with a live collab doc that never
|
|
212
|
+
// received EITHER of two sequential scrub edits while the HTTP
|
|
213
|
+
// saves advanced the SQL mirror normally. Comparing the caller
|
|
214
|
+
// only against that stale live text mis-classified the SECOND
|
|
215
|
+
// save as a divergent writer and silently dropped it. When the
|
|
216
|
+
// caller's expectedVersionHash matches the CURRENT SQL mirror,
|
|
217
|
+
// the caller is the mirror's own uninterrupted lineage (a plain
|
|
218
|
+
// CAS success against the column this write updates) while the
|
|
219
|
+
// live doc is the diverging party — a dead/lagging client Yjs
|
|
220
|
+
// pipe or a concurrent live-only editor. Do NOT skip, and do NOT
|
|
221
|
+
// silently drop either side: proceed with the mirror write AND
|
|
222
|
+
// push `content` through the collab layer exactly like
|
|
223
|
+
// syncCollab:true does (mirrorLineageCollabSync below). The
|
|
224
|
+
// applyText char-diff merge folds the caller's change into the
|
|
225
|
+
// live doc as a CRDT diff, so no one's edits are dropped: the
|
|
226
|
+
// mirror advances with the caller, and the live doc receives the
|
|
227
|
+
// caller's change as a diff-merge that preserves any other
|
|
228
|
+
// editor's live edits. Only callers matching NEITHER the live
|
|
229
|
+
// text NOR the mirror are genuinely stale and still hit the skip
|
|
230
|
+
// below.
|
|
231
|
+
const [mirrorRow] = await db
|
|
232
|
+
.select({ content: schema.designFiles.content })
|
|
233
|
+
.from(schema.designFiles)
|
|
234
|
+
.where(eq(schema.designFiles.id, id))
|
|
235
|
+
.limit(1);
|
|
236
|
+
if (
|
|
237
|
+
sourceContentHash(mirrorRow?.content ?? "") ===
|
|
238
|
+
expectedVersionHash
|
|
239
|
+
) {
|
|
240
|
+
// Caller is exactly at the persisted mirror's tip — the live
|
|
241
|
+
// collab doc is the lagging party, not the caller. Write the
|
|
242
|
+
// mirror normally and also sync the caller's content into the
|
|
243
|
+
// live doc via the collab diff-merge below.
|
|
244
|
+
mirrorLineageCollabSync = true;
|
|
245
|
+
} else {
|
|
246
|
+
skipContentWrite = true;
|
|
247
|
+
skippedStaleMirror = true;
|
|
248
|
+
}
|
|
184
249
|
} else {
|
|
185
250
|
throw new Error(
|
|
186
251
|
"File changed since it was read. Re-read the file and retry.",
|
|
@@ -263,8 +328,12 @@ export default defineAction({
|
|
|
263
328
|
}
|
|
264
329
|
}
|
|
265
330
|
|
|
266
|
-
// Push content through the collab layer so live editors see the change
|
|
267
|
-
|
|
331
|
+
// Push content through the collab layer so live editors see the change.
|
|
332
|
+
// mirrorLineageCollabSync: a syncCollab:false caller whose hash matched
|
|
333
|
+
// the current SQL mirror (mirror-lineage rescue) also syncs here, so a
|
|
334
|
+
// dead/lagging live doc receives the caller's change as a CRDT
|
|
335
|
+
// diff-merge instead of silently diverging from the mirror.
|
|
336
|
+
if (content !== undefined && (syncCollab || mirrorLineageCollabSync)) {
|
|
268
337
|
const collabExists = await hasCollabState(id);
|
|
269
338
|
if (collabExists) {
|
|
270
339
|
await applyText(id, content, "content", "agent");
|
|
@@ -988,9 +988,38 @@ export function getEmbeddedIframeBackgroundColor(args: {
|
|
|
988
988
|
: (args.embeddedFrameBackground ?? "transparent");
|
|
989
989
|
}
|
|
990
990
|
|
|
991
|
+
/**
|
|
992
|
+
* CSS rule that shifts embedded-frame (board) content so board-coordinate
|
|
993
|
+
* (0,0) lands at the center of the board iframe's oversized surface.
|
|
994
|
+
*
|
|
995
|
+
* The selector is deliberately scoped to DIRECT `<body>` children only.
|
|
996
|
+
* `translate` composes per element, so a blanket
|
|
997
|
+
* `[data-agent-native-node-id]{translate:…}` rule shifted every NESTED
|
|
998
|
+
* node-id-bearing descendant by the surface offset AGAIN (+65536px per
|
|
999
|
+
* nesting level) — any board child nested inside another board primitive
|
|
1000
|
+
* rendered tens of thousands of px outside its parent (i.e. visually
|
|
1001
|
+
* vanished) even when its persisted parent-relative left/top were perfectly
|
|
1002
|
+
* correct, and every rect-based coordinate computation on nested board
|
|
1003
|
+
* content was poisoned by the extra offset.
|
|
1004
|
+
*
|
|
1005
|
+
* Scoping (rather than wrapping content in a single offset container) is the
|
|
1006
|
+
* robust fix here because board elements are DOCUMENTED direct `<body>`
|
|
1007
|
+
* children (see shared/board-file.ts module doc + emptyBoardHtml): every
|
|
1008
|
+
* creation path appends at body level (appendCanvasPrimitiveToHtml,
|
|
1009
|
+
* boardObjectEntryToHtmlFragment, moveNodeBetweenDocuments' body-append),
|
|
1010
|
+
* and several consumers key on that structure with `body > …` selectors
|
|
1011
|
+
* (BOARD_SURFACE_RENDER_STYLE) and depth-1 walks
|
|
1012
|
+
* (backfillBoardPrimitiveMarkers, getBoardContentLayerSignature). A wrapper
|
|
1013
|
+
* element would silently break all of those structural contracts.
|
|
1014
|
+
*
|
|
1015
|
+
* With this rule, the offset applies exactly once — to top-level board
|
|
1016
|
+
* elements — and nested children position purely via parent-relative
|
|
1017
|
+
* left/top inside their (absolutely positioned) parent, like any normal
|
|
1018
|
+
* CSS containing-block hierarchy.
|
|
1019
|
+
*/
|
|
991
1020
|
function embeddedContentOffsetStyle(x: number, y: number): string {
|
|
992
1021
|
if (x === 0 && y === 0) return "";
|
|
993
|
-
return `<style data-agent-native-content-offset>[data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
|
|
1022
|
+
return `<style data-agent-native-content-offset>body > [data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
|
|
994
1023
|
}
|
|
995
1024
|
|
|
996
1025
|
function injectEmbeddedFrameStyle(content: string, style: string): string {
|
|
@@ -1046,6 +1075,60 @@ export interface IframeContextMenuPayload {
|
|
|
1046
1075
|
info?: ElementInfo | null;
|
|
1047
1076
|
}
|
|
1048
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* Creation-race keystroke routing: after the host calls `beginTextEdit(nodeId)`
|
|
1080
|
+
* for a JUST-created text element, there is a window (persist round-trip +
|
|
1081
|
+
* bridge activation) where the browser focus still sits on the HOST document.
|
|
1082
|
+
* Keystrokes typed in that window used to fall into host keyboard shortcuts —
|
|
1083
|
+
* letters switched tools, digits zoomed, and Delete/Backspace DELETED the
|
|
1084
|
+
* selected layer/screen — while the characters themselves were silently lost.
|
|
1085
|
+
*
|
|
1086
|
+
* While a text edit is pending, every host keydown is routed through this
|
|
1087
|
+
* pure helper:
|
|
1088
|
+
* - printable characters are BUFFERED (replayed into the editable once the
|
|
1089
|
+
* bridge reports the session active) and swallowed,
|
|
1090
|
+
* - Backspace edits the buffer, Delete/Enter/Tab/arrows are swallowed so they
|
|
1091
|
+
* can never reach host layer-deletion/navigation,
|
|
1092
|
+
* - Escape clears the pending buffer (user aborted) and is swallowed,
|
|
1093
|
+
* - IME composition and Cmd/Ctrl chords (undo!) pass through untouched.
|
|
1094
|
+
*
|
|
1095
|
+
* Exported for unit tests (DesignCanvas.pending-text-edit.test.ts).
|
|
1096
|
+
*/
|
|
1097
|
+
export type PendingTextEditKeyAction =
|
|
1098
|
+
| { action: "buffer"; char: string }
|
|
1099
|
+
| { action: "drop-last" }
|
|
1100
|
+
| { action: "swallow" }
|
|
1101
|
+
| { action: "clear-and-swallow" }
|
|
1102
|
+
| { action: "pass" };
|
|
1103
|
+
|
|
1104
|
+
export function routePendingTextEditKey(e: {
|
|
1105
|
+
key: string;
|
|
1106
|
+
metaKey?: boolean;
|
|
1107
|
+
ctrlKey?: boolean;
|
|
1108
|
+
altKey?: boolean;
|
|
1109
|
+
isComposing?: boolean;
|
|
1110
|
+
}): PendingTextEditKeyAction {
|
|
1111
|
+
if (e.isComposing) return { action: "pass" };
|
|
1112
|
+
if (e.metaKey || e.ctrlKey) return { action: "pass" };
|
|
1113
|
+
if (e.key === "Escape") return { action: "clear-and-swallow" };
|
|
1114
|
+
if (e.key === "Backspace") return { action: "drop-last" };
|
|
1115
|
+
if (
|
|
1116
|
+
e.key === "Delete" ||
|
|
1117
|
+
e.key === "Enter" ||
|
|
1118
|
+
e.key === "Tab" ||
|
|
1119
|
+
e.key.startsWith("Arrow")
|
|
1120
|
+
) {
|
|
1121
|
+
return { action: "swallow" };
|
|
1122
|
+
}
|
|
1123
|
+
if (e.key.length === 1) return { action: "buffer", char: e.key };
|
|
1124
|
+
return { action: "pass" };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** How long a pending text edit may wait for bridge activation before the
|
|
1128
|
+
* keystroke routing above stands down (matches the bridge's own ~2s
|
|
1129
|
+
* begin-text-edit retry window, plus round-trip slack). */
|
|
1130
|
+
export const PENDING_TEXT_EDIT_TIMEOUT_MS = 3000;
|
|
1131
|
+
|
|
1049
1132
|
export function DesignCanvas({
|
|
1050
1133
|
content,
|
|
1051
1134
|
contentKey,
|
|
@@ -1900,7 +1983,44 @@ export function DesignCanvas({
|
|
|
1900
1983
|
}
|
|
1901
1984
|
return;
|
|
1902
1985
|
}
|
|
1986
|
+
if (e.data.type === "text-edit-pending") {
|
|
1987
|
+
// The bridge is waiting for a just-created node before it can enter
|
|
1988
|
+
// text-edit mode (begin-text-edit arrived first). Arm the host-side
|
|
1989
|
+
// keystroke buffer for the window — repeated pending:true re-posts
|
|
1990
|
+
// for the same node (DesignEditor's T6 retry loop) only extend the
|
|
1991
|
+
// wait, never reset an in-progress buffer.
|
|
1992
|
+
const pendingNodeId =
|
|
1993
|
+
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
1994
|
+
const currentPending = pendingTextEditRef.current;
|
|
1995
|
+
if (e.data.pending && pendingNodeId) {
|
|
1996
|
+
if (currentPending && currentPending.nodeId === pendingNodeId) {
|
|
1997
|
+
currentPending.startedAt = Date.now();
|
|
1998
|
+
} else {
|
|
1999
|
+
pendingTextEditRef.current = {
|
|
2000
|
+
nodeId: pendingNodeId,
|
|
2001
|
+
buffer: "",
|
|
2002
|
+
startedAt: Date.now(),
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
} else if (currentPending?.nodeId === pendingNodeId) {
|
|
2006
|
+
pendingTextEditRef.current = null;
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
1903
2010
|
if (e.data.type === "text-editing-state") {
|
|
2011
|
+
// Creation-race replay: the bridge just armed the session for the
|
|
2012
|
+
// node we were waiting on — flush any keystrokes the host buffered
|
|
2013
|
+
// during the round-trip window into the now-live editable.
|
|
2014
|
+
if (e.data.active && pendingTextEditRef.current) {
|
|
2015
|
+
const pendingBuffer = pendingTextEditRef.current.buffer;
|
|
2016
|
+
pendingTextEditRef.current = null;
|
|
2017
|
+
if (pendingBuffer) {
|
|
2018
|
+
postOneShotBridgeMessage({
|
|
2019
|
+
type: "text-edit-insert-text",
|
|
2020
|
+
text: pendingBuffer,
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
1904
2024
|
onTextEditingStateChange?.({
|
|
1905
2025
|
active: Boolean(e.data.active),
|
|
1906
2026
|
selector:
|
|
@@ -2091,6 +2211,7 @@ export function DesignCanvas({
|
|
|
2091
2211
|
bridgeUrl,
|
|
2092
2212
|
fusionUrl,
|
|
2093
2213
|
flushPendingOneShotMessages,
|
|
2214
|
+
postOneShotBridgeMessage,
|
|
2094
2215
|
]);
|
|
2095
2216
|
|
|
2096
2217
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -2390,6 +2511,18 @@ export function DesignCanvas({
|
|
|
2390
2511
|
(nodeId: string) => {
|
|
2391
2512
|
const iframe = iframeRef.current;
|
|
2392
2513
|
if (!iframe?.contentWindow || !nodeId) return;
|
|
2514
|
+
// Arm the creation-race keystroke buffer (see routePendingTextEditKey):
|
|
2515
|
+
// until the bridge reports text-editing-state active for this command,
|
|
2516
|
+
// host keystrokes are buffered/swallowed instead of hitting host
|
|
2517
|
+
// shortcuts, then replayed into the editable. Re-arming for the same
|
|
2518
|
+
// node preserves an in-progress buffer.
|
|
2519
|
+
if (pendingTextEditRef.current?.nodeId !== nodeId) {
|
|
2520
|
+
pendingTextEditRef.current = {
|
|
2521
|
+
nodeId,
|
|
2522
|
+
buffer: "",
|
|
2523
|
+
startedAt: Date.now(),
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2393
2526
|
postOneShotBridgeMessage({
|
|
2394
2527
|
type: "begin-text-edit",
|
|
2395
2528
|
nodeId,
|
|
@@ -2399,6 +2532,53 @@ export function DesignCanvas({
|
|
|
2399
2532
|
[postOneShotBridgeMessage],
|
|
2400
2533
|
);
|
|
2401
2534
|
|
|
2535
|
+
// Creation-race keystroke routing (host side). Active only while a
|
|
2536
|
+
// beginTextEdit() command is pending; see routePendingTextEditKey's doc
|
|
2537
|
+
// comment for the exact policy. Capture-phase on window so it runs before
|
|
2538
|
+
// the editor's own shortcut handlers.
|
|
2539
|
+
const pendingTextEditRef = useRef<{
|
|
2540
|
+
nodeId: string;
|
|
2541
|
+
buffer: string;
|
|
2542
|
+
startedAt: number;
|
|
2543
|
+
} | null>(null);
|
|
2544
|
+
useEffect(() => {
|
|
2545
|
+
function onPendingTextEditKeyDown(e: KeyboardEvent) {
|
|
2546
|
+
const pending = pendingTextEditRef.current;
|
|
2547
|
+
if (!pending) return;
|
|
2548
|
+
if (Date.now() - pending.startedAt > PENDING_TEXT_EDIT_TIMEOUT_MS) {
|
|
2549
|
+
pendingTextEditRef.current = null;
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const routed = routePendingTextEditKey(e);
|
|
2553
|
+
if (routed.action === "pass") return;
|
|
2554
|
+
e.preventDefault();
|
|
2555
|
+
e.stopPropagation();
|
|
2556
|
+
e.stopImmediatePropagation();
|
|
2557
|
+
if (routed.action === "buffer") {
|
|
2558
|
+
pending.buffer += routed.char;
|
|
2559
|
+
} else if (routed.action === "drop-last") {
|
|
2560
|
+
pending.buffer = pending.buffer.slice(0, -1);
|
|
2561
|
+
} else if (routed.action === "clear-and-swallow") {
|
|
2562
|
+
pendingTextEditRef.current = null;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function onPendingTextEditPointerDown() {
|
|
2566
|
+
// The user clicked somewhere else in the host — stand down so buffered
|
|
2567
|
+
// keys are never replayed into an edit they've abandoned.
|
|
2568
|
+
pendingTextEditRef.current = null;
|
|
2569
|
+
}
|
|
2570
|
+
window.addEventListener("keydown", onPendingTextEditKeyDown, true);
|
|
2571
|
+
window.addEventListener("pointerdown", onPendingTextEditPointerDown, true);
|
|
2572
|
+
return () => {
|
|
2573
|
+
window.removeEventListener("keydown", onPendingTextEditKeyDown, true);
|
|
2574
|
+
window.removeEventListener(
|
|
2575
|
+
"pointerdown",
|
|
2576
|
+
onPendingTextEditPointerDown,
|
|
2577
|
+
true,
|
|
2578
|
+
);
|
|
2579
|
+
};
|
|
2580
|
+
}, []);
|
|
2581
|
+
|
|
2402
2582
|
const sendStyleChange = useCallback(
|
|
2403
2583
|
(
|
|
2404
2584
|
selector: string,
|
|
@@ -213,6 +213,7 @@ import {
|
|
|
213
213
|
parseGradientLayer,
|
|
214
214
|
removeFillLayerAtIndex,
|
|
215
215
|
SOLID_FILL_ID,
|
|
216
|
+
solidToGradientPatch,
|
|
216
217
|
splitCssLayers,
|
|
217
218
|
withLayerSizeMarker,
|
|
218
219
|
} from "./edit-panel/fill-gradient-helpers";
|
|
@@ -356,11 +357,14 @@ export {
|
|
|
356
357
|
truncateOpeningTag,
|
|
357
358
|
};
|
|
358
359
|
export {
|
|
360
|
+
averageGradientOpacity,
|
|
359
361
|
buildGradientLayer,
|
|
362
|
+
defaultGradientStops,
|
|
360
363
|
isLayerHiddenBySize,
|
|
361
364
|
joinCssLayers,
|
|
362
365
|
parseGradientLayer,
|
|
363
366
|
removeFillLayerAtIndex,
|
|
367
|
+
solidToGradientPatch,
|
|
364
368
|
splitCssLayers,
|
|
365
369
|
withLayerSizeMarker,
|
|
366
370
|
type FillLayerArrays,
|