@agent-native/core 0.114.2 → 0.114.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +30 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/a2a/artifact-response.ts +42 -16
- package/corpus/core/src/a2a/client.ts +4 -0
- package/corpus/core/src/a2a/handlers.ts +133 -2
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/types.ts +14 -0
- package/corpus/core/src/db/request-telemetry.ts +32 -8
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/pending-tasks-store.ts +89 -0
- package/corpus/core/src/integrations/webhook-handler.ts +17 -7
- package/corpus/core/src/scripts/call-agent.ts +54 -10
- package/corpus/core/src/server/agent-discovery.ts +22 -0
- package/corpus/core/src/settings/store.ts +15 -6
- package/corpus/core/src/shared/optional-node-builtins.ts +66 -0
- package/corpus/core/src/templates/default/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/core/src/templates/headless/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/core/src/vite/client.ts +5 -0
- package/corpus/templates/analytics/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +2 -2
- package/corpus/templates/brain/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/clips/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/clips/shared/feature-flags.ts +1 -1
- package/corpus/templates/content/.agents/skills/content/SKILL.md +19 -5
- package/corpus/templates/content/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/content/changelog/2026-07-17-slack-created-database-entries-now-record-slack-as-their-sub.md +6 -0
- package/corpus/templates/design/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/design/actions/update-file.ts +10 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +30 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +14 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +547 -50
- package/corpus/templates/design/app/components/design/dnd-debug.ts +27 -0
- package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +19 -2
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/app/lib/design-save-outbox.ts +46 -2
- package/corpus/templates/design/app/pages/design-editor/canvas-primitive-insert.ts +4 -9
- package/corpus/templates/design/app/pages/design-editor/history.ts +14 -1
- package/corpus/templates/design/changelog/2026-07-17-drawn-shapes-stay-free-positioned-when-dragged.md +6 -0
- package/corpus/templates/design/changelog/2026-07-17-shapes-show-a-live-outline-while-you-draw.md +6 -0
- package/corpus/templates/design/shared/drag-reflow.ts +375 -0
- package/corpus/templates/design/shared/full-app.ts +1 -1
- package/corpus/templates/dispatch/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/feature-flags/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/feature-flags/SKILL.md +1 -1
- package/dist/a2a/artifact-response.d.ts +5 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +20 -5
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/a2a/client.d.ts +3 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +2 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/types.d.ts +12 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/request-telemetry.d.ts.map +1 -1
- package/dist/db/request-telemetry.js +24 -6
- package/dist/db/request-telemetry.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +9 -0
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +60 -0
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +13 -7
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +39 -10
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-discovery.d.ts +2 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +19 -0
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/settings/store.d.ts +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +14 -6
- package/dist/settings/store.js.map +1 -1
- package/dist/shared/optional-node-builtins.d.ts +23 -0
- package/dist/shared/optional-node-builtins.d.ts.map +1 -0
- package/dist/shared/optional-node-builtins.js +35 -0
- package/dist/shared/optional-node-builtins.js.map +1 -0
- package/dist/templates/chat/.agents/skills/feature-flags/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/feature-flags/SKILL.md +1 -1
- package/dist/templates/headless/.agents/skills/feature-flags/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +2 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +2 -1
- package/src/a2a/artifact-response.ts +42 -16
- package/src/a2a/client.ts +4 -0
- package/src/a2a/handlers.ts +133 -2
- package/src/a2a/index.ts +1 -0
- package/src/a2a/types.ts +14 -0
- package/src/db/request-telemetry.ts +32 -8
- package/src/integrations/index.ts +6 -0
- package/src/integrations/pending-tasks-store.ts +89 -0
- package/src/integrations/webhook-handler.ts +17 -7
- package/src/scripts/call-agent.ts +54 -10
- package/src/server/agent-discovery.ts +22 -0
- package/src/settings/store.ts +15 -6
- package/src/shared/optional-node-builtins.ts +66 -0
- package/src/templates/chat/.agents/skills/feature-flags/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/feature-flags/SKILL.md +1 -1
- package/src/templates/headless/.agents/skills/feature-flags/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +1 -1
- package/src/vite/client.ts +5 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Drag-and-drop debug logging (host side). Mirrors the bridge's own [dnd:*]
|
|
2
|
+
// console timeline (editor-chrome.bridge.ts) so a single gesture reads
|
|
3
|
+
// end-to-end across the prototype iframe and the parent React app.
|
|
4
|
+
//
|
|
5
|
+
// The iframe and the host are separate windows, so each has its OWN toggle:
|
|
6
|
+
// • bridge (in the iframe console): window.__DND_DEBUG = true
|
|
7
|
+
// • host (in the top-frame console): window.__DND_DEBUG = true
|
|
8
|
+
// Both default OFF (opt in at runtime). Host lines are cyan and prefixed
|
|
9
|
+
// [dnd:host:*]; bridge lines are purple and prefixed [dnd:*].
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
__DND_DEBUG?: boolean;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function dndHostLog(phase: string, data?: unknown): void {
|
|
17
|
+
if (typeof window === "undefined") return;
|
|
18
|
+
if (!window.__DND_DEBUG) return;
|
|
19
|
+
try {
|
|
20
|
+
const tag = `%c[dnd:host:${phase}]`;
|
|
21
|
+
const style = "color:#0ea5e9;font-weight:bold";
|
|
22
|
+
if (data === undefined) console.log(tag, style);
|
|
23
|
+
else console.log(tag, style, data);
|
|
24
|
+
} catch {
|
|
25
|
+
/* logging must never break a drag */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { DEFAULT_LINE_STROKE_WIDTH_PX } from "../canvas-primitive-style";
|
|
14
14
|
import { boardPointToScreenLocalPoint } from "./coordinate-transforms";
|
|
15
|
-
import { getFrameCenter } from "./frame-geometry";
|
|
15
|
+
import { getFrameCenter, getScreenPreviewViewport } from "./frame-geometry";
|
|
16
16
|
import type {
|
|
17
17
|
CanvasPrimitiveInsert,
|
|
18
18
|
DraftCreationPreview,
|
|
@@ -229,10 +229,27 @@ export function draftPrimitiveToInsert(
|
|
|
229
229
|
frameGeometry: FrameGeometry,
|
|
230
230
|
metadata?: ResolvedScreenMetadata,
|
|
231
231
|
): CanvasPrimitiveInsert {
|
|
232
|
-
const
|
|
232
|
+
const metadataViewport = {
|
|
233
233
|
width: metadata?.width ?? frameGeometry.width,
|
|
234
234
|
height: metadata?.height ?? frameGeometry.height,
|
|
235
235
|
};
|
|
236
|
+
// A draw must serialize in the same viewport the content is laid out in.
|
|
237
|
+
// getScreenPreviewViewport is the source of truth: an inline screen renders at
|
|
238
|
+
// metadata dims and CSS-scales when the frame's aspect matches, but reflows to
|
|
239
|
+
// the frame when it differs (so the metadata 1280×2560 default no longer
|
|
240
|
+
// stretches shapes on a non-portrait frame). Fixed-viewport sources
|
|
241
|
+
// (localhost/fusion) always use their own metadata viewport.
|
|
242
|
+
const isInline = !metadata || metadata.source === "inline";
|
|
243
|
+
const previewViewport = getScreenPreviewViewport(metadataViewport, {
|
|
244
|
+
width: frameGeometry.width,
|
|
245
|
+
height: frameGeometry.height,
|
|
246
|
+
});
|
|
247
|
+
const viewport = isInline
|
|
248
|
+
? {
|
|
249
|
+
width: previewViewport.viewportWidth,
|
|
250
|
+
height: previewViewport.viewportHeight,
|
|
251
|
+
}
|
|
252
|
+
: metadataViewport;
|
|
236
253
|
const scaleX = viewport.width / Math.max(1, frameGeometry.width);
|
|
237
254
|
const scaleY = viewport.height / Math.max(1, frameGeometry.height);
|
|
238
255
|
const roundLocal = (value: number) => Math.round(value) || 0;
|
|
@@ -1191,6 +1191,8 @@ const enUS = {
|
|
|
1191
1191
|
escToExit: "Esc to exit",
|
|
1192
1192
|
tellAgentWhatToChange: "Tell the agent what to change…",
|
|
1193
1193
|
changesSaveWhenReconnected: "Changes will save when reconnected",
|
|
1194
|
+
changesDiscarded:
|
|
1195
|
+
"Some changes couldn't be saved because the file no longer exists and were discarded.",
|
|
1194
1196
|
offline: "Offline",
|
|
1195
1197
|
saving: "Saving...",
|
|
1196
1198
|
clearedAllAnnotations: "Cleared all annotations",
|
|
@@ -5458,6 +5460,7 @@ const designRawLiteralOverrides = {
|
|
|
5458
5460
|
escToExit: "Esc 退出",
|
|
5459
5461
|
tellAgentWhatToChange: "告诉代理要更改什么...",
|
|
5460
5462
|
changesSaveWhenReconnected: "重新连接时将保存更改",
|
|
5463
|
+
changesDiscarded: "部分更改因文件不存在而无法保存,已被丢弃。",
|
|
5461
5464
|
offline: "离线",
|
|
5462
5465
|
saving: "正在保存...",
|
|
5463
5466
|
clearedAllAnnotations: "已清除所有批注",
|
|
@@ -5618,6 +5621,8 @@ const designRawLiteralOverrides = {
|
|
|
5618
5621
|
tellAgentWhatToChange: "Dígale al agente qué cambiar...",
|
|
5619
5622
|
changesSaveWhenReconnected:
|
|
5620
5623
|
"Los cambios se guardarán cuando se vuelva a conectar",
|
|
5624
|
+
changesDiscarded:
|
|
5625
|
+
"Algunos cambios no se pudieron guardar porque el archivo ya no existe y se descartaron.",
|
|
5621
5626
|
offline: "Sin conexión",
|
|
5622
5627
|
saving: "Guardando...",
|
|
5623
5628
|
clearedAllAnnotations: "Se borraron todas las anotaciones",
|
|
@@ -5779,6 +5784,8 @@ const designRawLiteralOverrides = {
|
|
|
5779
5784
|
tellAgentWhatToChange: "Dites à l’agent ce qu’il faut changer…",
|
|
5780
5785
|
changesSaveWhenReconnected:
|
|
5781
5786
|
"Les modifications seront enregistrées une fois reconnecté",
|
|
5787
|
+
changesDiscarded:
|
|
5788
|
+
"Certaines modifications n’ont pas pu être enregistrées car le fichier n’existe plus et ont été ignorées.",
|
|
5782
5789
|
offline: "Hors ligne",
|
|
5783
5790
|
saving: "Enregistrement...",
|
|
5784
5791
|
clearedAllAnnotations: "Toutes les annotations ont été effacées",
|
|
@@ -5941,6 +5948,8 @@ const designRawLiteralOverrides = {
|
|
|
5941
5948
|
tellAgentWhatToChange: "Sagen Sie dem Agenten, was er ändern soll ...",
|
|
5942
5949
|
changesSaveWhenReconnected:
|
|
5943
5950
|
"Änderungen werden gespeichert, wenn die Verbindung wiederhergestellt wird",
|
|
5951
|
+
changesDiscarded:
|
|
5952
|
+
"Einige Änderungen konnten nicht gespeichert werden, da die Datei nicht mehr existiert, und wurden verworfen.",
|
|
5944
5953
|
offline: "Offline",
|
|
5945
5954
|
saving: "Speichern...",
|
|
5946
5955
|
clearedAllAnnotations: "Alle Anmerkungen gelöscht",
|
|
@@ -6099,6 +6108,8 @@ const designRawLiteralOverrides = {
|
|
|
6099
6108
|
escToExit: "Esc 終了",
|
|
6100
6109
|
tellAgentWhatToChange: "何を変更するかをエージェントに伝えてください...",
|
|
6101
6110
|
changesSaveWhenReconnected: "変更は再接続時に保存されます",
|
|
6111
|
+
changesDiscarded:
|
|
6112
|
+
"ファイルが存在しないため、一部の変更を保存できず破棄しました。",
|
|
6102
6113
|
offline: "オフライン",
|
|
6103
6114
|
saving: "保存中...",
|
|
6104
6115
|
clearedAllAnnotations: "すべての注釈を消去しました",
|
|
@@ -6255,6 +6266,8 @@ const designRawLiteralOverrides = {
|
|
|
6255
6266
|
escToExit: "Esc 종료",
|
|
6256
6267
|
tellAgentWhatToChange: "상담사에게 무엇을 변경해야 할지 알려주세요...",
|
|
6257
6268
|
changesSaveWhenReconnected: "다시 연결되면 변경사항이 저장됩니다.",
|
|
6269
|
+
changesDiscarded:
|
|
6270
|
+
"파일이 더 이상 존재하지 않아 일부 변경사항을 저장하지 못하고 삭제했습니다.",
|
|
6258
6271
|
offline: "오프라인",
|
|
6259
6272
|
saving: "저장 중...",
|
|
6260
6273
|
clearedAllAnnotations: "모든 주석이 지워졌습니다",
|
|
@@ -6415,6 +6428,8 @@ const designRawLiteralOverrides = {
|
|
|
6415
6428
|
tellAgentWhatToChange: "Diga ao agente o que mudar…",
|
|
6416
6429
|
changesSaveWhenReconnected:
|
|
6417
6430
|
"As alterações serão salvas quando reconectadas",
|
|
6431
|
+
changesDiscarded:
|
|
6432
|
+
"Algumas alterações não puderam ser salvas porque o arquivo não existe mais e foram descartadas.",
|
|
6418
6433
|
offline: "Off-line",
|
|
6419
6434
|
saving: "Salvando...",
|
|
6420
6435
|
clearedAllAnnotations: "Todas as anotações foram limpas",
|
|
@@ -6572,6 +6587,8 @@ const designRawLiteralOverrides = {
|
|
|
6572
6587
|
escToExit: "बाहर निकलने के लिए Esc",
|
|
6573
6588
|
tellAgentWhatToChange: "एजेंट को बताएं कि क्या बदलना है...",
|
|
6574
6589
|
changesSaveWhenReconnected: "पुन: कनेक्ट होने पर परिवर्तन सहेजे जाएंगे",
|
|
6590
|
+
changesDiscarded:
|
|
6591
|
+
"कुछ परिवर्तन सहेजे नहीं जा सके क्योंकि फ़ाइल अब मौजूद नहीं है, और उन्हें छोड़ दिया गया।",
|
|
6575
6592
|
offline: "ऑफ़लाइन",
|
|
6576
6593
|
saving: "सहेजा जा रहा है...",
|
|
6577
6594
|
clearedAllAnnotations: "सभी एनोटेशन साफ़ किए गए",
|
|
@@ -6728,6 +6745,8 @@ const designRawLiteralOverrides = {
|
|
|
6728
6745
|
escToExit: "Esc للخروج",
|
|
6729
6746
|
tellAgentWhatToChange: "أخبر الوكيل بما يجب تغييره...",
|
|
6730
6747
|
changesSaveWhenReconnected: "سيتم حفظ التغييرات عند إعادة الاتصال",
|
|
6748
|
+
changesDiscarded:
|
|
6749
|
+
"تعذّر حفظ بعض التغييرات لأن الملف لم يعد موجودًا، وتم تجاهلها.",
|
|
6731
6750
|
offline: "غير متصل",
|
|
6732
6751
|
saving: "جارٍ الحفظ...",
|
|
6733
6752
|
clearedAllAnnotations: "تم مسح جميع التعليقات التوضيحية",
|
|
@@ -29,6 +29,33 @@ export interface DesignSaveOutboxStorage {
|
|
|
29
29
|
export interface DrainDesignSaveOutboxResult {
|
|
30
30
|
saved: DesignSaveOutboxEntry[];
|
|
31
31
|
failed: Array<{ entry: DesignSaveOutboxEntry; error: unknown }>;
|
|
32
|
+
/**
|
|
33
|
+
* Entries dropped because retrying can never succeed — the target file no
|
|
34
|
+
* longer exists (deleted or never created). Retained-and-retried forever they
|
|
35
|
+
* turn one orphaned screen into a 500 storm that jams every save; dropping
|
|
36
|
+
* them self-heals stale outbox residue. Separate from `failed`, which is for
|
|
37
|
+
* transient/conflict errors that SHOULD be retried.
|
|
38
|
+
*/
|
|
39
|
+
dropped: Array<{ entry: DesignSaveOutboxEntry; error: unknown }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A save failure that can never succeed on retry: the server reports the target
|
|
44
|
+
* file is gone (HTTP 404, or a "File not found" message from update-file's
|
|
45
|
+
* missing-row guard). Distinct from 409 conflicts and network errors, which are
|
|
46
|
+
* transient and must stay queued.
|
|
47
|
+
*/
|
|
48
|
+
export function isTerminalSaveError(error: unknown): boolean {
|
|
49
|
+
if (!error || typeof error !== "object") return false;
|
|
50
|
+
const candidate = error as { status?: unknown; message?: unknown };
|
|
51
|
+
// Terminal only when an explicit 404 ALSO names a missing file. A bare 404 can
|
|
52
|
+
// be a transient route-not-found (e.g. a cold-start action route), so both
|
|
53
|
+
// signals are required — dropping an edit is unrecoverable, a retry is not.
|
|
54
|
+
return (
|
|
55
|
+
candidate.status === 404 &&
|
|
56
|
+
typeof candidate.message === "string" &&
|
|
57
|
+
/file not found/i.test(candidate.message)
|
|
58
|
+
);
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
const DATABASE_NAME = "agent-native-design-save-outbox";
|
|
@@ -293,7 +320,11 @@ async function drainEntries(
|
|
|
293
320
|
) => Promise<unknown>,
|
|
294
321
|
storage: DesignSaveOutboxStorage,
|
|
295
322
|
): Promise<DrainDesignSaveOutboxResult> {
|
|
296
|
-
const result: DrainDesignSaveOutboxResult = {
|
|
323
|
+
const result: DrainDesignSaveOutboxResult = {
|
|
324
|
+
saved: [],
|
|
325
|
+
failed: [],
|
|
326
|
+
dropped: [],
|
|
327
|
+
};
|
|
297
328
|
for (const entry of await storage.list(designId, actorScope)) {
|
|
298
329
|
try {
|
|
299
330
|
if (
|
|
@@ -332,7 +363,20 @@ async function drainEntries(
|
|
|
332
363
|
await storage.deleteIfRevision(entry);
|
|
333
364
|
result.saved.push(entry);
|
|
334
365
|
} catch (error) {
|
|
335
|
-
|
|
366
|
+
if (isTerminalSaveError(error)) {
|
|
367
|
+
// The target file is gone (deleted/never created) — retrying can never
|
|
368
|
+
// succeed. Drop the entry so one orphaned screen can't loop update-file
|
|
369
|
+
// 500s forever and jam every save. Logged, never silently swallowed.
|
|
370
|
+
await storage.deleteIfRevision(entry);
|
|
371
|
+
result.dropped.push({ entry, error });
|
|
372
|
+
if (typeof console !== "undefined") {
|
|
373
|
+
console.warn(
|
|
374
|
+
`[design-save-outbox] dropped unrecoverable save for ${entry.actionName} ${entry.resourceId} (file no longer exists)`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
result.failed.push({ entry, error });
|
|
379
|
+
}
|
|
336
380
|
}
|
|
337
381
|
}
|
|
338
382
|
return result;
|
|
@@ -62,6 +62,9 @@ export function nextBlankScreenFilename(files: DesignFile[]): string {
|
|
|
62
62
|
export function blankScreenHtml(title: string): string {
|
|
63
63
|
const safeTitle = escapeHtmlText(title);
|
|
64
64
|
const safeTitleAttribute = escapeHtmlAttributeValue(title);
|
|
65
|
+
// Blank screen = free canvas: <body> is the positioned root and drawn shapes
|
|
66
|
+
// are absolute children (x,y in the HTML). A centering grid / <main> wrapper
|
|
67
|
+
// trapped shapes at center and got auto-layout-converted on drop.
|
|
65
68
|
return `<!DOCTYPE html>
|
|
66
69
|
<html lang="en">
|
|
67
70
|
<head>
|
|
@@ -77,17 +80,9 @@ export function blankScreenHtml(title: string): string {
|
|
|
77
80
|
color: var(--color-text, #111827);
|
|
78
81
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
79
82
|
}
|
|
80
|
-
main {
|
|
81
|
-
min-height: 100vh;
|
|
82
|
-
display: grid;
|
|
83
|
-
place-items: center;
|
|
84
|
-
padding: 48px;
|
|
85
|
-
}
|
|
86
83
|
</style>
|
|
87
84
|
</head>
|
|
88
|
-
<body>
|
|
89
|
-
<main data-agent-native-layer-name="${safeTitleAttribute}">
|
|
90
|
-
</main>
|
|
85
|
+
<body data-agent-native-layer-name="${safeTitleAttribute}">
|
|
91
86
|
</body>
|
|
92
87
|
</html>`;
|
|
93
88
|
}
|
|
@@ -173,6 +173,11 @@ export interface ContentHistoryChange {
|
|
|
173
173
|
fileId: string;
|
|
174
174
|
before: string;
|
|
175
175
|
after: string;
|
|
176
|
+
/** Agent-authored replacement checkpoint. Prevents the next user edit's
|
|
177
|
+
* fallback mirror from coalescing backward through this entry, which would
|
|
178
|
+
* collapse the agent state out of the undo stack and make a single Cmd+Z
|
|
179
|
+
* jump past all AI-generated content to the pre-agent baseline. */
|
|
180
|
+
isCheckpoint?: boolean;
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
export interface ContentHistoryGroup {
|
|
@@ -261,7 +266,15 @@ export function mergeLocalContentHistoryFallback(
|
|
|
261
266
|
): ContentHistoryChange[] {
|
|
262
267
|
if (change.before === change.after) return stack;
|
|
263
268
|
const last = stack[stack.length - 1];
|
|
264
|
-
if (
|
|
269
|
+
if (
|
|
270
|
+
last &&
|
|
271
|
+
last.fileId === change.fileId &&
|
|
272
|
+
last.after === change.before &&
|
|
273
|
+
// An incoming checkpoint must stay its own undo boundary, or one Cmd+Z
|
|
274
|
+
// reverts an agent edit together with the preceding user edit.
|
|
275
|
+
!last.isCheckpoint &&
|
|
276
|
+
!change.isCheckpoint
|
|
277
|
+
) {
|
|
265
278
|
return [...stack.slice(0, -1), { ...last, after: change.after }];
|
|
266
279
|
}
|
|
267
280
|
return [...stack.slice(-(MAX_DESIGN_UNDO_STACK - 1)), change];
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for Figma-parity in-screen drag & drop.
|
|
3
|
+
*
|
|
4
|
+
* This module is the typed, unit-tested source of truth for the drag
|
|
5
|
+
* behaviors added in the "live reflow" work (Phase 0 + Phase 1 of the
|
|
6
|
+
* auto-layout DnD plan). It is deliberately free of DOM access so it can be
|
|
7
|
+
* exercised directly by vitest.
|
|
8
|
+
*
|
|
9
|
+
* The editor bridge (`app/components/design/bridge/editor-chrome.bridge.ts`)
|
|
10
|
+
* cannot `import` anything — it is compiled to a self-contained IIFE by
|
|
11
|
+
* `bridge/codegen.ts`. So, exactly like `shared/canvas-math.ts` →
|
|
12
|
+
* `computeMoveSnapOffset`, the bridge carries a hand-ported copy of these
|
|
13
|
+
* functions. Keep the two in sync; this file is the reference implementation
|
|
14
|
+
* and the place where behavior is proven.
|
|
15
|
+
*
|
|
16
|
+
* Four independent concerns live here:
|
|
17
|
+
* 1. Hysteresis — stabilize the resolved drop target so the insertion
|
|
18
|
+
* preview never strobes on a boundary (Phase 0.1).
|
|
19
|
+
* 2. Size guard — refuse to auto-nest a large element into a smaller
|
|
20
|
+
* container, Figma-style (Phase 0.2).
|
|
21
|
+
* 3. Packed-container detection — decide whether a flex container is simple
|
|
22
|
+
* enough that live sibling reflow can be modeled by a constant slot
|
|
23
|
+
* shift; everything else falls back to an indicator only (Phase 1).
|
|
24
|
+
* 4. Reflow offsets — the per-sibling translate deltas that visually open /
|
|
25
|
+
* close a slot while dragging, for the packed case (Phase 1).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// 1. Hysteresis
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Identity of a resolved drop target. `containerKey` is any stable string id
|
|
34
|
+
* for the target container (the bridge uses a per-drag element→key map);
|
|
35
|
+
* `index` is the insertion index within that container.
|
|
36
|
+
*/
|
|
37
|
+
export interface DragTargetKey {
|
|
38
|
+
containerKey: string;
|
|
39
|
+
index: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A freshly resolved (raw) drop-target candidate for the current pointer
|
|
44
|
+
* position, plus the geometry the hysteresis gate needs to decide whether to
|
|
45
|
+
* accept it. All coordinates are in one consistent space (the bridge uses
|
|
46
|
+
* iframe-local client coordinates — see canvas-math note about the host
|
|
47
|
+
* scaling the whole iframe uniformly).
|
|
48
|
+
*/
|
|
49
|
+
export interface CandidatePoint {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DragTargetCandidate {
|
|
55
|
+
/** The resolved target, or null when the pointer is over no valid target. */
|
|
56
|
+
key: DragTargetKey | null;
|
|
57
|
+
/** Pointer position this tick, in a consistent space, for the movement
|
|
58
|
+
* deadband (distance from where the current target was committed). */
|
|
59
|
+
pointer: CandidatePoint;
|
|
60
|
+
/** How far the pointer has penetrated a *different* container from its
|
|
61
|
+
* entering edge, px. Pass Infinity for a same-container change. */
|
|
62
|
+
containerPenetrationPx: number;
|
|
63
|
+
/** True when the candidate container is an ancestor of the committed one
|
|
64
|
+
* (leaving toward a parent) — reverses instantly. */
|
|
65
|
+
isLeave: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface HysteresisState {
|
|
69
|
+
key: DragTargetKey;
|
|
70
|
+
committedAt: number;
|
|
71
|
+
committedPointer: CandidatePoint;
|
|
72
|
+
/** The differing candidate currently being timed out, and since when. */
|
|
73
|
+
pendingKey: DragTargetKey | null;
|
|
74
|
+
pendingAt: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface HysteresisOptions {
|
|
78
|
+
/** Pointer must move at least this far from the last commit before a
|
|
79
|
+
* same-container slot change is accepted. Default 8. */
|
|
80
|
+
movePx?: number;
|
|
81
|
+
/** …or the new candidate must persist this long (ms). Default 60. */
|
|
82
|
+
dwellMs?: number;
|
|
83
|
+
/** Pointer must penetrate a different container this far, px. Default 10. */
|
|
84
|
+
containerPenetrationPx?: number;
|
|
85
|
+
/** …or the new container must persist this long (ms). Default 80. */
|
|
86
|
+
containerDwellMs?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface HysteresisResult {
|
|
90
|
+
key: DragTargetKey | null;
|
|
91
|
+
changed: boolean;
|
|
92
|
+
state: HysteresisState | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const DEFAULT_HYSTERESIS: Required<HysteresisOptions> = {
|
|
96
|
+
movePx: 8,
|
|
97
|
+
dwellMs: 60,
|
|
98
|
+
containerPenetrationPx: 10,
|
|
99
|
+
containerDwellMs: 80,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function keysEqual(a: DragTargetKey | null, b: DragTargetKey | null): boolean {
|
|
103
|
+
if (a === null || b === null) return a === b;
|
|
104
|
+
return a.containerKey === b.containerKey && a.index === b.index;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function commitTarget(
|
|
108
|
+
key: DragTargetKey,
|
|
109
|
+
pointer: CandidatePoint,
|
|
110
|
+
now: number,
|
|
111
|
+
): HysteresisResult {
|
|
112
|
+
return {
|
|
113
|
+
key,
|
|
114
|
+
changed: true,
|
|
115
|
+
state: {
|
|
116
|
+
key,
|
|
117
|
+
committedAt: now,
|
|
118
|
+
committedPointer: pointer,
|
|
119
|
+
pendingKey: null,
|
|
120
|
+
pendingAt: 0,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Stabilize a raw drop-target candidate against the committed one so the
|
|
127
|
+
* preview only transitions when the pointer deliberately moves to a new
|
|
128
|
+
* slot/container. A change is accepted when the pointer has moved `movePx`
|
|
129
|
+
* from the last commit (deliberate move) OR the *new* candidate has persisted
|
|
130
|
+
* `dwellMs` — dwell is timed from when the candidate first appeared, not from
|
|
131
|
+
* the committed target's age. Cross-container leaves reverse instantly; other
|
|
132
|
+
* container entries need penetration or dwell. Pure: `now` is passed in.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveTargetHysteresis(
|
|
135
|
+
prev: HysteresisState | null,
|
|
136
|
+
candidate: DragTargetCandidate,
|
|
137
|
+
now: number,
|
|
138
|
+
options: HysteresisOptions = {},
|
|
139
|
+
): HysteresisResult {
|
|
140
|
+
const opts = { ...DEFAULT_HYSTERESIS, ...options };
|
|
141
|
+
|
|
142
|
+
if (candidate.key === null) {
|
|
143
|
+
return { key: null, changed: prev !== null, state: null };
|
|
144
|
+
}
|
|
145
|
+
if (prev === null) {
|
|
146
|
+
return commitTarget(candidate.key, candidate.pointer, now);
|
|
147
|
+
}
|
|
148
|
+
if (keysEqual(candidate.key, prev.key)) {
|
|
149
|
+
return {
|
|
150
|
+
key: prev.key,
|
|
151
|
+
changed: false,
|
|
152
|
+
state: { ...prev, pendingKey: null, pendingAt: 0 },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const pendingContinues = keysEqual(candidate.key, prev.pendingKey);
|
|
157
|
+
const pendingKey = candidate.key;
|
|
158
|
+
const pendingAt = pendingContinues ? prev.pendingAt : now;
|
|
159
|
+
const dwelledFor = now - pendingAt;
|
|
160
|
+
const sameContainer = candidate.key.containerKey === prev.key.containerKey;
|
|
161
|
+
|
|
162
|
+
let accept: boolean;
|
|
163
|
+
if (sameContainer) {
|
|
164
|
+
const movedPx = Math.hypot(
|
|
165
|
+
candidate.pointer.x - prev.committedPointer.x,
|
|
166
|
+
candidate.pointer.y - prev.committedPointer.y,
|
|
167
|
+
);
|
|
168
|
+
accept = movedPx >= opts.movePx || dwelledFor >= opts.dwellMs;
|
|
169
|
+
} else if (candidate.isLeave) {
|
|
170
|
+
accept = true;
|
|
171
|
+
} else {
|
|
172
|
+
accept =
|
|
173
|
+
candidate.containerPenetrationPx >= opts.containerPenetrationPx ||
|
|
174
|
+
dwelledFor >= opts.containerDwellMs;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (accept) {
|
|
178
|
+
return commitTarget(candidate.key, candidate.pointer, now);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
key: prev.key,
|
|
182
|
+
changed: false,
|
|
183
|
+
state: { ...prev, pendingKey, pendingAt },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// 2. Size guard
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export interface SizeGuardBox {
|
|
192
|
+
width: number;
|
|
193
|
+
height: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface SizeGuardOptions {
|
|
197
|
+
/** Cmd/⌘ held — user override, always allow. Default false. */
|
|
198
|
+
bypass?: boolean;
|
|
199
|
+
/** Slack in px before a container counts as "too small". Default 0. */
|
|
200
|
+
tolerancePx?: number;
|
|
201
|
+
/**
|
|
202
|
+
* Axis on which the container hugs its content (and would therefore grow to
|
|
203
|
+
* fit a larger child, so it should NOT be rejected on that axis). Matches
|
|
204
|
+
* Figma "Hug contents". Default "none".
|
|
205
|
+
*/
|
|
206
|
+
hugAxis?: "none" | "main" | "cross" | "both" | "width" | "height";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Figma's "don't drop a large image into a button" guard: reject a container
|
|
211
|
+
* whose content box is smaller than the dragged element on an axis it cannot
|
|
212
|
+
* grow on. Bypassed by ⌘ (the same modifier that disables snapping).
|
|
213
|
+
*/
|
|
214
|
+
export function isContainerTooSmallForDrag(
|
|
215
|
+
containerContentBox: SizeGuardBox,
|
|
216
|
+
draggedRect: SizeGuardBox,
|
|
217
|
+
options: SizeGuardOptions = {},
|
|
218
|
+
): boolean {
|
|
219
|
+
if (options.bypass) return false;
|
|
220
|
+
const tol = options.tolerancePx ?? 0;
|
|
221
|
+
const hug = options.hugAxis ?? "none";
|
|
222
|
+
const hugsWidth = hug === "both" || hug === "width" || hug === "main";
|
|
223
|
+
const hugsHeight = hug === "both" || hug === "height" || hug === "cross";
|
|
224
|
+
|
|
225
|
+
const tooNarrow =
|
|
226
|
+
!hugsWidth && containerContentBox.width + tol < draggedRect.width;
|
|
227
|
+
const tooShort =
|
|
228
|
+
!hugsHeight && containerContentBox.height + tol < draggedRect.height;
|
|
229
|
+
return tooNarrow || tooShort;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// 3. Packed-container detection (Phase 1, option-1 restriction)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
export interface PackedContainerInfo {
|
|
237
|
+
/** Computed `display`. */
|
|
238
|
+
display: string;
|
|
239
|
+
/** Computed `flex-direction`. */
|
|
240
|
+
flexDirection: string;
|
|
241
|
+
/** Computed `flex-wrap`. */
|
|
242
|
+
flexWrap: string;
|
|
243
|
+
/** Computed `justify-content`. */
|
|
244
|
+
justifyContent: string;
|
|
245
|
+
/** Resolved main-axis gap in px (row-gap or column-gap as appropriate). */
|
|
246
|
+
gap: number;
|
|
247
|
+
/** True if any direct child has `flex-grow` > 0 (it absorbs space instead
|
|
248
|
+
* of translating, so a constant slot shift would misrepresent the drop). */
|
|
249
|
+
hasFlexGrowChild: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const START_JUSTIFY = new Set(["flex-start", "start", "normal", "left", ""]);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether a container is simple enough that a same-magnitude per-sibling
|
|
256
|
+
* translate exactly reproduces the post-drop layout (see
|
|
257
|
+
* `computeReorderOffsets`). Only START-aligned, non-wrapping, fixed-gap,
|
|
258
|
+
* non-reverse flex rows/columns with no growing child qualify.
|
|
259
|
+
*
|
|
260
|
+
* Everything else (space-between/around/evenly, center/end, wrap, grid,
|
|
261
|
+
* reverse, or a flex-grow child) must fall back to an indicator-only preview,
|
|
262
|
+
* because the real reflow is NOT a uniform shift and animating a constant
|
|
263
|
+
* translate would show a preview that does not match where the item lands.
|
|
264
|
+
*/
|
|
265
|
+
export function isSimplePackedContainer(info: PackedContainerInfo): boolean {
|
|
266
|
+
const isFlex = info.display === "flex" || info.display === "inline-flex";
|
|
267
|
+
if (!isFlex) return false;
|
|
268
|
+
if (info.flexDirection !== "row" && info.flexDirection !== "column") {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (info.flexWrap !== "nowrap") return false;
|
|
272
|
+
if (!START_JUSTIFY.has(info.justifyContent)) return false;
|
|
273
|
+
if (!Number.isFinite(info.gap) || info.gap < 0) return false;
|
|
274
|
+
if (info.hasFlexGrowChild) return false;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The main flow axis (x/y) for a packed container's flex-direction. */
|
|
279
|
+
export function mainAxisForDirection(flexDirection: string): "x" | "y" {
|
|
280
|
+
return flexDirection === "column" || flexDirection === "column-reverse"
|
|
281
|
+
? "y"
|
|
282
|
+
: "x";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// 4. Reflow offsets (packed case)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
export interface ReorderOffsetsInput {
|
|
290
|
+
/** Number of direct children currently in the container (incl. dragged). */
|
|
291
|
+
count: number;
|
|
292
|
+
/** The dragged element's current index. */
|
|
293
|
+
originIndex: number;
|
|
294
|
+
/**
|
|
295
|
+
* Insertion slot in current indexing: the item lands *before* the child at
|
|
296
|
+
* `targetSlot` (0..count, where `count` means "at the end").
|
|
297
|
+
*/
|
|
298
|
+
targetSlot: number;
|
|
299
|
+
/** Dragged element's main-axis size + the container gap, in px. */
|
|
300
|
+
slotMain: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Per-sibling translate offsets (px, signed along the main axis) that preview
|
|
305
|
+
* a same-container reorder by opening the destination slot and closing the
|
|
306
|
+
* origin slot. Negative = toward the container start (up/left); positive =
|
|
307
|
+
* toward the end (down/right). The dragged element's own index is always 0
|
|
308
|
+
* here (it follows the cursor separately).
|
|
309
|
+
*
|
|
310
|
+
* For a packed container, moving the dragged item (size s, plus one gap g)
|
|
311
|
+
* from `originIndex` to `targetSlot` shifts exactly the siblings between the
|
|
312
|
+
* two positions by ±(s + g) — the shift magnitude is independent of each
|
|
313
|
+
* sibling's own size, which is why the constant model is exact for this case.
|
|
314
|
+
*/
|
|
315
|
+
export function computeReorderOffsets(input: ReorderOffsetsInput): number[] {
|
|
316
|
+
const { count, originIndex, targetSlot, slotMain } = input;
|
|
317
|
+
const offsets = new Array(count).fill(0);
|
|
318
|
+
if (targetSlot > originIndex + 1) {
|
|
319
|
+
// Moving later: siblings between move toward the start to fill the origin.
|
|
320
|
+
for (let i = originIndex + 1; i <= targetSlot - 1; i += 1) {
|
|
321
|
+
offsets[i] = -slotMain;
|
|
322
|
+
}
|
|
323
|
+
} else if (targetSlot < originIndex) {
|
|
324
|
+
// Moving earlier: siblings from targetSlot..originIndex-1 move toward end.
|
|
325
|
+
for (let i = targetSlot; i <= originIndex - 1; i += 1) {
|
|
326
|
+
offsets[i] = slotMain;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// targetSlot === originIndex or originIndex + 1 → no net movement.
|
|
330
|
+
return offsets;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface VacateOffsetsInput {
|
|
334
|
+
count: number;
|
|
335
|
+
originIndex: number;
|
|
336
|
+
slotMain: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Offsets for the ORIGIN container when the dragged item leaves it entirely
|
|
341
|
+
* (drag-out or cross-container move): every following sibling closes the gap
|
|
342
|
+
* by shifting toward the start. This is what erases the "hole" the old
|
|
343
|
+
* relative-promotion approach left behind.
|
|
344
|
+
*/
|
|
345
|
+
export function computeVacateOffsets(input: VacateOffsetsInput): number[] {
|
|
346
|
+
const { count, originIndex, slotMain } = input;
|
|
347
|
+
const offsets = new Array(count).fill(0);
|
|
348
|
+
for (let i = originIndex + 1; i <= count - 1; i += 1) {
|
|
349
|
+
offsets[i] = -slotMain;
|
|
350
|
+
}
|
|
351
|
+
return offsets;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface InsertOffsetsInput {
|
|
355
|
+
/** Number of children currently in the TARGET container (excl. incoming). */
|
|
356
|
+
count: number;
|
|
357
|
+
/** Insertion slot: incoming item lands before child at `targetSlot`. */
|
|
358
|
+
targetSlot: number;
|
|
359
|
+
/** Incoming element's main-axis size + the target container's gap, in px. */
|
|
360
|
+
slotMain: number;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Offsets for a TARGET container the dragged item is entering: children at and
|
|
365
|
+
* after the insertion slot shift toward the end to open a slot sized to the
|
|
366
|
+
* incoming element.
|
|
367
|
+
*/
|
|
368
|
+
export function computeInsertOffsets(input: InsertOffsetsInput): number[] {
|
|
369
|
+
const { count, targetSlot, slotMain } = input;
|
|
370
|
+
const offsets = new Array(count).fill(0);
|
|
371
|
+
for (let i = targetSlot; i <= count - 1; i += 1) {
|
|
372
|
+
offsets[i] = slotMain;
|
|
373
|
+
}
|
|
374
|
+
return offsets;
|
|
375
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* a design as app-backed.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { defineFeatureFlag } from "@agent-native/core/feature-flags";
|
|
16
|
+
import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Runtime rollout for full app building in the Design app. Builder
|