@agent-native/core 0.82.0 → 0.83.0
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 +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
|
|
37
37
|
import { builderDocsBlockConfigs } from "./builder-docs-blocks";
|
|
38
38
|
import { inlineDatabaseBlockConfig } from "./inline-database-block";
|
|
39
|
+
import { sourceComponentBlockConfig } from "./source-component-block";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Register the content block library (the dev-doc + OpenAPI + standard
|
|
@@ -62,6 +63,7 @@ export function registerContentBlocks(registry: BlockRegistry): void {
|
|
|
62
63
|
for (const block of builderDocsBlockConfigs) {
|
|
63
64
|
registry.register(block);
|
|
64
65
|
}
|
|
66
|
+
registry.register(sourceComponentBlockConfig);
|
|
65
67
|
registry.register(inlineDatabaseBlockConfig);
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineBlock,
|
|
3
|
+
type BlockMdxConfig,
|
|
4
|
+
type BlockSpec,
|
|
5
|
+
} from "@agent-native/core/blocks/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export interface SourceComponentData {
|
|
9
|
+
provider: string;
|
|
10
|
+
componentName: string;
|
|
11
|
+
rawRef: string;
|
|
12
|
+
rawHash: string;
|
|
13
|
+
sourceLabel?: string;
|
|
14
|
+
previewStatus?: "available" | "unavailable" | "warning";
|
|
15
|
+
previewKind?: "summary" | "table" | "embed" | "symbol" | "component";
|
|
16
|
+
previewUrl?: string;
|
|
17
|
+
previewItems?: string[];
|
|
18
|
+
preview?: SourceComponentPreview;
|
|
19
|
+
title?: string;
|
|
20
|
+
summary?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SourceComponentPreview {
|
|
24
|
+
status: "available" | "unavailable" | "warning";
|
|
25
|
+
kind: "summary" | "table" | "embed" | "symbol" | "component";
|
|
26
|
+
label: string;
|
|
27
|
+
summary?: string;
|
|
28
|
+
fields?: SourceComponentPreviewField[];
|
|
29
|
+
table?: SourceComponentPreviewTable;
|
|
30
|
+
url?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SourceComponentPreviewField {
|
|
34
|
+
label: string;
|
|
35
|
+
value: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SourceComponentPreviewTable {
|
|
39
|
+
columns: SourceComponentPreviewTableColumn[];
|
|
40
|
+
rows: Record<string, string>[];
|
|
41
|
+
truncated?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SourceComponentPreviewTableColumn {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sourceComponentPreviewFieldSchema = z.object({
|
|
50
|
+
label: z.string().trim().min(1).max(80),
|
|
51
|
+
value: z.string().trim().max(200),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const sourceComponentPreviewTableColumnSchema = z.object({
|
|
55
|
+
id: z.string().trim().min(1).max(80),
|
|
56
|
+
label: z.string().trim().min(1).max(120),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const sourceComponentPreviewTableSchema = z.object({
|
|
60
|
+
columns: z.array(sourceComponentPreviewTableColumnSchema).max(12),
|
|
61
|
+
rows: z.array(z.record(z.string(), z.string().max(500))).max(6),
|
|
62
|
+
truncated: z.boolean().optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sourceComponentPreviewSchema = z.object({
|
|
66
|
+
status: z.enum(["available", "unavailable", "warning"]),
|
|
67
|
+
kind: z.enum(["summary", "table", "embed", "symbol", "component"]),
|
|
68
|
+
label: z.string().trim().min(1).max(160),
|
|
69
|
+
summary: z.string().trim().max(500).optional(),
|
|
70
|
+
fields: z.array(sourceComponentPreviewFieldSchema).max(8).optional(),
|
|
71
|
+
table: sourceComponentPreviewTableSchema.optional(),
|
|
72
|
+
url: z.string().trim().max(2_000).optional(),
|
|
73
|
+
}) satisfies z.ZodType<SourceComponentPreview>;
|
|
74
|
+
|
|
75
|
+
export const sourceComponentSchema = z.object({
|
|
76
|
+
provider: z.string().trim().min(1).max(80),
|
|
77
|
+
componentName: z.string().trim().min(1).max(160),
|
|
78
|
+
rawRef: z.string().trim().min(1),
|
|
79
|
+
rawHash: z.string().trim().min(1),
|
|
80
|
+
sourceLabel: z.string().trim().max(200).optional(),
|
|
81
|
+
previewStatus: z.enum(["available", "unavailable", "warning"]).optional(),
|
|
82
|
+
previewKind: z
|
|
83
|
+
.enum(["summary", "table", "embed", "symbol", "component"])
|
|
84
|
+
.optional(),
|
|
85
|
+
previewUrl: z.string().trim().max(2_000).optional(),
|
|
86
|
+
previewItems: z.array(z.string().trim().max(160)).max(8).optional(),
|
|
87
|
+
preview: sourceComponentPreviewSchema.optional(),
|
|
88
|
+
title: z.string().trim().max(200).optional(),
|
|
89
|
+
summary: z.string().trim().max(500).optional(),
|
|
90
|
+
}) as unknown as z.ZodType<SourceComponentData>;
|
|
91
|
+
|
|
92
|
+
export const sourceComponentMdx: BlockMdxConfig<SourceComponentData> = {
|
|
93
|
+
tag: "SourceComponent",
|
|
94
|
+
toAttrs: (data) => ({
|
|
95
|
+
provider: data.provider,
|
|
96
|
+
componentName: data.componentName,
|
|
97
|
+
rawRef: data.rawRef,
|
|
98
|
+
rawHash: data.rawHash,
|
|
99
|
+
sourceLabel: data.sourceLabel,
|
|
100
|
+
previewStatus: data.previewStatus,
|
|
101
|
+
previewKind: data.previewKind,
|
|
102
|
+
previewUrl: data.previewUrl,
|
|
103
|
+
previewItems: data.previewItems,
|
|
104
|
+
preview: data.preview as Record<string, unknown> | undefined,
|
|
105
|
+
previewTitle: data.title,
|
|
106
|
+
summary: data.summary,
|
|
107
|
+
}),
|
|
108
|
+
fromAttrs: (attrs) => ({
|
|
109
|
+
provider: attrs.string("provider") ?? "",
|
|
110
|
+
componentName: attrs.string("componentName") ?? "",
|
|
111
|
+
rawRef: attrs.string("rawRef") ?? "",
|
|
112
|
+
rawHash: attrs.string("rawHash") ?? "",
|
|
113
|
+
sourceLabel: attrs.string("sourceLabel"),
|
|
114
|
+
previewStatus: attrs.string("previewStatus") as
|
|
115
|
+
| SourceComponentData["previewStatus"]
|
|
116
|
+
| undefined,
|
|
117
|
+
previewKind: attrs.string("previewKind") as
|
|
118
|
+
| SourceComponentData["previewKind"]
|
|
119
|
+
| undefined,
|
|
120
|
+
previewUrl: attrs.string("previewUrl"),
|
|
121
|
+
previewItems: attrs.array<string>("previewItems"),
|
|
122
|
+
preview: attrs.object<SourceComponentPreview>("preview"),
|
|
123
|
+
title: attrs.string("previewTitle") ?? attrs.string("title"),
|
|
124
|
+
summary: attrs.string("summary"),
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const ServerReadStub = () => null;
|
|
129
|
+
|
|
130
|
+
export const sourceComponentBlockConfig: BlockSpec<SourceComponentData> =
|
|
131
|
+
defineBlock<SourceComponentData>({
|
|
132
|
+
type: "source-component",
|
|
133
|
+
schema: sourceComponentSchema,
|
|
134
|
+
mdx: sourceComponentMdx,
|
|
135
|
+
Read: ServerReadStub,
|
|
136
|
+
placement: ["block"],
|
|
137
|
+
editSurface: "none",
|
|
138
|
+
label: "Source component",
|
|
139
|
+
description:
|
|
140
|
+
"A read-only source-native component marker that preserves provider-specific body blocks for round-trip sync.",
|
|
141
|
+
});
|
|
@@ -596,7 +596,11 @@ export function DesignCanvas({
|
|
|
596
596
|
)
|
|
597
597
|
.replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
|
|
598
598
|
.replace("__EDITOR_CHROME_SCALE_X__", String(editorChromeScaleX))
|
|
599
|
-
.replace("__EDITOR_CHROME_SCALE_Y__", String(editorChromeScaleY))
|
|
599
|
+
.replace("__EDITOR_CHROME_SCALE_Y__", String(editorChromeScaleY))
|
|
600
|
+
.replace(
|
|
601
|
+
"__DESIGN_CANVAS_SCREEN_ID__",
|
|
602
|
+
JSON.stringify(screenId ?? contentKey ?? ""),
|
|
603
|
+
);
|
|
600
604
|
const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
|
|
601
605
|
"__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
|
|
602
606
|
isEmbeddedFrame ? "true" : "false",
|
|
@@ -749,6 +749,7 @@ export function MultiScreenCanvas({
|
|
|
749
749
|
selector: string;
|
|
750
750
|
sourceId?: string;
|
|
751
751
|
} | null>(null);
|
|
752
|
+
const crossScreenParentDragCleanupRef = useRef<(() => void) | null>(null);
|
|
752
753
|
/** Board-space point from the last cross-screen-drag "move" message. */
|
|
753
754
|
const crossScreenLastBoardPointRef = useRef<{ x: number; y: number } | null>(
|
|
754
755
|
null,
|
|
@@ -1144,20 +1145,132 @@ export function MultiScreenCanvas({
|
|
|
1144
1145
|
useEffect(() => {
|
|
1145
1146
|
if (!onCrossScreenElementDrop) return;
|
|
1146
1147
|
|
|
1148
|
+
const stopParentCrossScreenDrag = () => {
|
|
1149
|
+
crossScreenParentDragCleanupRef.current?.();
|
|
1150
|
+
crossScreenParentDragCleanupRef.current = null;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1147
1153
|
const clearCrossScreenDrag = () => {
|
|
1154
|
+
stopParentCrossScreenDrag();
|
|
1148
1155
|
setCrossScreenGhost(null);
|
|
1149
1156
|
setCrossScreenTarget(null);
|
|
1150
1157
|
crossScreenTargetRef.current = null;
|
|
1151
1158
|
crossScreenDragMsgRef.current = null;
|
|
1152
1159
|
};
|
|
1153
1160
|
|
|
1161
|
+
const updateCrossScreenTargetFromBoardPoint = (
|
|
1162
|
+
boardPoint: Point,
|
|
1163
|
+
sourceScreenId: string,
|
|
1164
|
+
) => {
|
|
1165
|
+
crossScreenLastBoardPointRef.current = boardPoint;
|
|
1166
|
+
setCrossScreenGhost({ boardX: boardPoint.x, boardY: boardPoint.y });
|
|
1167
|
+
const target = getFrameEntryAtPoint(boardPoint);
|
|
1168
|
+
if (target && target.id !== sourceScreenId) {
|
|
1169
|
+
const nextTarget = { id: target.id, geometry: target.geometry };
|
|
1170
|
+
crossScreenTargetRef.current = nextTarget;
|
|
1171
|
+
setCrossScreenTarget(nextTarget);
|
|
1172
|
+
} else {
|
|
1173
|
+
crossScreenTargetRef.current = null;
|
|
1174
|
+
setCrossScreenTarget(null);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const finalizeCrossScreenDrop = (
|
|
1179
|
+
sourceScreenId: string,
|
|
1180
|
+
candidate: CrossScreenDragTarget | null,
|
|
1181
|
+
payload: { selector: string; sourceId?: string },
|
|
1182
|
+
lastBoardPoint: Point | null,
|
|
1183
|
+
) => {
|
|
1184
|
+
clearCrossScreenDrag();
|
|
1185
|
+
crossScreenLastBoardPointRef.current = null;
|
|
1186
|
+
const hasIdentifier = !!(payload.selector || payload.sourceId);
|
|
1187
|
+
if (!candidate || !hasIdentifier || !sourceScreenId) return;
|
|
1188
|
+
|
|
1189
|
+
const runHitTest = (): Promise<{
|
|
1190
|
+
anchorNodeId?: string;
|
|
1191
|
+
placement?: "before" | "after" | "inside";
|
|
1192
|
+
}> => {
|
|
1193
|
+
if (!lastBoardPoint) return Promise.resolve({});
|
|
1194
|
+
const targetScreen = screensRef.current.find(
|
|
1195
|
+
(s) => s.id === candidate.id,
|
|
1196
|
+
);
|
|
1197
|
+
if (!targetScreen) return Promise.resolve({});
|
|
1198
|
+
const targetGeometry = candidate.geometry;
|
|
1199
|
+
const targetIframe =
|
|
1200
|
+
surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1201
|
+
`[data-screen-iframe-id="${CSS.escape(candidate.id)}"]`,
|
|
1202
|
+
);
|
|
1203
|
+
const targetContentWindow = targetIframe?.contentWindow;
|
|
1204
|
+
if (!targetContentWindow) return Promise.resolve({});
|
|
1205
|
+
const targetViewportWidth =
|
|
1206
|
+
targetIframe.clientWidth || getResolvedMetadata(targetScreen).width;
|
|
1207
|
+
const targetViewportHeight =
|
|
1208
|
+
targetIframe.clientHeight || getResolvedMetadata(targetScreen).height;
|
|
1209
|
+
const scaleX = targetViewportWidth / Math.max(1, targetGeometry.width);
|
|
1210
|
+
const scaleY =
|
|
1211
|
+
targetViewportHeight / Math.max(1, targetGeometry.height);
|
|
1212
|
+
const localX = (lastBoardPoint.x - targetGeometry.x) * scaleX;
|
|
1213
|
+
const localY = (lastBoardPoint.y - targetGeometry.y) * scaleY;
|
|
1214
|
+
|
|
1215
|
+
const correlationId = `hit-${Date.now()}-${Math.random()
|
|
1216
|
+
.toString(36)
|
|
1217
|
+
.slice(2, 6)}`;
|
|
1218
|
+
|
|
1219
|
+
return new Promise((resolve) => {
|
|
1220
|
+
const timer = window.setTimeout(() => {
|
|
1221
|
+
window.removeEventListener("message", hitListener);
|
|
1222
|
+
resolve({});
|
|
1223
|
+
}, 50);
|
|
1224
|
+
|
|
1225
|
+
const hitListener = (ev: MessageEvent) => {
|
|
1226
|
+
if (
|
|
1227
|
+
!ev.data ||
|
|
1228
|
+
ev.data.type !== "agent-native:hit-test-result" ||
|
|
1229
|
+
ev.data.correlationId !== correlationId
|
|
1230
|
+
) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
window.clearTimeout(timer);
|
|
1234
|
+
window.removeEventListener("message", hitListener);
|
|
1235
|
+
resolve({
|
|
1236
|
+
anchorNodeId: ev.data.anchorNodeId ?? undefined,
|
|
1237
|
+
placement: ev.data.placement ?? undefined,
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1240
|
+
window.addEventListener("message", hitListener);
|
|
1241
|
+
|
|
1242
|
+
targetContentWindow.postMessage(
|
|
1243
|
+
{
|
|
1244
|
+
type: "agent-native:hit-test",
|
|
1245
|
+
correlationId,
|
|
1246
|
+
x: localX,
|
|
1247
|
+
y: localY,
|
|
1248
|
+
},
|
|
1249
|
+
"*",
|
|
1250
|
+
);
|
|
1251
|
+
});
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
void runHitTest().then(({ anchorNodeId, placement }) => {
|
|
1255
|
+
onCrossScreenElementDropRef.current?.({
|
|
1256
|
+
sourceSelector: payload.selector,
|
|
1257
|
+
sourceNodeId: payload.sourceId,
|
|
1258
|
+
sourceScreenId,
|
|
1259
|
+
targetScreenId: candidate.id,
|
|
1260
|
+
targetAnchorNodeId: anchorNodeId,
|
|
1261
|
+
targetAnchorPlacement: placement,
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1154
1266
|
const handleMessage = (event: MessageEvent) => {
|
|
1155
1267
|
if (!event.data || event.data.type !== "agent-native:cross-screen-drag") {
|
|
1156
1268
|
return;
|
|
1157
1269
|
}
|
|
1158
1270
|
const msg = event.data as {
|
|
1159
1271
|
type: string;
|
|
1160
|
-
phase: "move" | "end" | "cancel";
|
|
1272
|
+
phase: "start" | "move" | "end" | "cancel";
|
|
1273
|
+
screenId?: string;
|
|
1161
1274
|
selector?: string;
|
|
1162
1275
|
sourceId?: string;
|
|
1163
1276
|
iframeX?: number;
|
|
@@ -1171,9 +1284,13 @@ export function MultiScreenCanvas({
|
|
|
1171
1284
|
return;
|
|
1172
1285
|
}
|
|
1173
1286
|
|
|
1174
|
-
//
|
|
1175
|
-
// a
|
|
1176
|
-
const sourceScreenId =
|
|
1287
|
+
// Prefer the iframe-supplied source id. In overview, activeId can point
|
|
1288
|
+
// at a different screen than the layer currently being dragged.
|
|
1289
|
+
const sourceScreenId =
|
|
1290
|
+
msg.screenId &&
|
|
1291
|
+
(msg.screenId === boardFileId || frameGeometryRef.current[msg.screenId])
|
|
1292
|
+
? msg.screenId
|
|
1293
|
+
: activeId;
|
|
1177
1294
|
if (!sourceScreenId) {
|
|
1178
1295
|
// Always clear visual state (ghost + highlight) when we have no active
|
|
1179
1296
|
// screen to attribute the drag to — regardless of the phase. Without
|
|
@@ -1183,6 +1300,62 @@ export function MultiScreenCanvas({
|
|
|
1183
1300
|
return;
|
|
1184
1301
|
}
|
|
1185
1302
|
|
|
1303
|
+
if (msg.phase === "start") {
|
|
1304
|
+
crossScreenDragMsgRef.current = {
|
|
1305
|
+
selector: msg.selector ?? "",
|
|
1306
|
+
sourceId: msg.sourceId,
|
|
1307
|
+
};
|
|
1308
|
+
stopParentCrossScreenDrag();
|
|
1309
|
+
const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
|
|
1310
|
+
surfaceRef.current,
|
|
1311
|
+
);
|
|
1312
|
+
let didCleanup = false;
|
|
1313
|
+
const activateParentDrag = (ev: MouseEvent) => {
|
|
1314
|
+
ev.preventDefault();
|
|
1315
|
+
updateCrossScreenTargetFromBoardPoint(
|
|
1316
|
+
getCanvasPoint(ev.clientX, ev.clientY),
|
|
1317
|
+
sourceScreenId,
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
1320
|
+
const handleParentMouseMove = (ev: MouseEvent) => {
|
|
1321
|
+
activateParentDrag(ev);
|
|
1322
|
+
};
|
|
1323
|
+
const handleParentMouseUp = (ev: MouseEvent) => {
|
|
1324
|
+
activateParentDrag(ev);
|
|
1325
|
+
const candidate = crossScreenTargetRef.current;
|
|
1326
|
+
const payload = crossScreenDragMsgRef.current ?? {
|
|
1327
|
+
selector: msg.selector ?? "",
|
|
1328
|
+
sourceId: msg.sourceId,
|
|
1329
|
+
};
|
|
1330
|
+
const lastBoardPoint = crossScreenLastBoardPointRef.current;
|
|
1331
|
+
finalizeCrossScreenDrop(
|
|
1332
|
+
sourceScreenId,
|
|
1333
|
+
candidate,
|
|
1334
|
+
payload,
|
|
1335
|
+
lastBoardPoint,
|
|
1336
|
+
);
|
|
1337
|
+
};
|
|
1338
|
+
const handleParentWindowBlur = () => {
|
|
1339
|
+
clearCrossScreenDrag();
|
|
1340
|
+
};
|
|
1341
|
+
const cleanup = () => {
|
|
1342
|
+
if (didCleanup) return;
|
|
1343
|
+
didCleanup = true;
|
|
1344
|
+
window.removeEventListener("mousemove", handleParentMouseMove, true);
|
|
1345
|
+
window.removeEventListener("mouseup", handleParentMouseUp, true);
|
|
1346
|
+
window.removeEventListener("blur", handleParentWindowBlur, true);
|
|
1347
|
+
restorePreviewPointerEvents();
|
|
1348
|
+
if (crossScreenParentDragCleanupRef.current === cleanup) {
|
|
1349
|
+
crossScreenParentDragCleanupRef.current = null;
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
crossScreenParentDragCleanupRef.current = cleanup;
|
|
1353
|
+
window.addEventListener("mousemove", handleParentMouseMove, true);
|
|
1354
|
+
window.addEventListener("mouseup", handleParentMouseUp, true);
|
|
1355
|
+
window.addEventListener("blur", handleParentWindowBlur, true);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1186
1359
|
if (msg.phase === "move") {
|
|
1187
1360
|
const { iframeX, iframeY, viewportW, viewportH, selector, sourceId } =
|
|
1188
1361
|
msg;
|
|
@@ -1208,14 +1381,16 @@ export function MultiScreenCanvas({
|
|
|
1208
1381
|
iframeX <= viewportW &&
|
|
1209
1382
|
iframeY <= viewportH
|
|
1210
1383
|
) {
|
|
1211
|
-
|
|
1384
|
+
setCrossScreenGhost(null);
|
|
1385
|
+
setCrossScreenTarget(null);
|
|
1386
|
+
crossScreenTargetRef.current = null;
|
|
1387
|
+
crossScreenLastBoardPointRef.current = null;
|
|
1212
1388
|
return;
|
|
1213
1389
|
}
|
|
1214
1390
|
|
|
1215
|
-
// Translate iframe coords → board coords using
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
// boardY = frame.y + iframeY * (frame.height / metadata.height)
|
|
1391
|
+
// Translate iframe coords → board coords using the live embedded
|
|
1392
|
+
// viewport from the bridge. In overview, the iframe viewport may be the
|
|
1393
|
+
// frame geometry rather than the screen metadata width.
|
|
1219
1394
|
|
|
1220
1395
|
let boardX: number;
|
|
1221
1396
|
let boardY: number;
|
|
@@ -1234,31 +1409,13 @@ export function MultiScreenCanvas({
|
|
|
1234
1409
|
clearCrossScreenDrag();
|
|
1235
1410
|
return;
|
|
1236
1411
|
}
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1239
|
-
sourceGeometry.width / Math.max(1, sourceMetadata.width);
|
|
1240
|
-
const scaleY =
|
|
1241
|
-
sourceGeometry.height / Math.max(1, sourceMetadata.height);
|
|
1412
|
+
const scaleX = sourceGeometry.width / Math.max(1, viewportW);
|
|
1413
|
+
const scaleY = sourceGeometry.height / Math.max(1, viewportH);
|
|
1242
1414
|
boardX = sourceGeometry.x + iframeX * scaleX;
|
|
1243
1415
|
boardY = sourceGeometry.y + iframeY * scaleY;
|
|
1244
1416
|
}
|
|
1245
1417
|
const boardPoint = { x: boardX, y: boardY };
|
|
1246
|
-
|
|
1247
|
-
// Remember the latest board-space position for use in the "end" handler.
|
|
1248
|
-
crossScreenLastBoardPointRef.current = boardPoint;
|
|
1249
|
-
|
|
1250
|
-
setCrossScreenGhost({ boardX, boardY });
|
|
1251
|
-
|
|
1252
|
-
// Find which screen frame the board point falls in.
|
|
1253
|
-
const target = getFrameEntryAtPoint(boardPoint);
|
|
1254
|
-
if (target && target.id !== sourceScreenId) {
|
|
1255
|
-
const nextTarget = { id: target.id, geometry: target.geometry };
|
|
1256
|
-
crossScreenTargetRef.current = nextTarget;
|
|
1257
|
-
setCrossScreenTarget(nextTarget);
|
|
1258
|
-
} else {
|
|
1259
|
-
crossScreenTargetRef.current = null;
|
|
1260
|
-
setCrossScreenTarget(null);
|
|
1261
|
-
}
|
|
1418
|
+
updateCrossScreenTargetFromBoardPoint(boardPoint, sourceScreenId);
|
|
1262
1419
|
return;
|
|
1263
1420
|
}
|
|
1264
1421
|
|
|
@@ -1273,97 +1430,18 @@ export function MultiScreenCanvas({
|
|
|
1273
1430
|
sourceId: msg.sourceId,
|
|
1274
1431
|
};
|
|
1275
1432
|
const lastBoardPoint = crossScreenLastBoardPointRef.current;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const hasIdentifier = !!(payload.selector || payload.sourceId);
|
|
1283
|
-
if (candidate && hasIdentifier && sourceScreenId) {
|
|
1284
|
-
// Translate the board-space drop point to the target iframe's local
|
|
1285
|
-
// coordinate space, then run a lightweight hit-test (postMessage +
|
|
1286
|
-
// 50 ms timeout) to find the deepest container at the drop site.
|
|
1287
|
-
const runHitTest = (): Promise<{
|
|
1288
|
-
anchorNodeId?: string;
|
|
1289
|
-
placement?: "before" | "after" | "inside";
|
|
1290
|
-
}> => {
|
|
1291
|
-
if (!lastBoardPoint) return Promise.resolve({});
|
|
1292
|
-
const targetScreen = screensRef.current.find(
|
|
1293
|
-
(s) => s.id === candidate.id,
|
|
1294
|
-
);
|
|
1295
|
-
if (!targetScreen) return Promise.resolve({});
|
|
1296
|
-
const targetGeometry = candidate.geometry;
|
|
1297
|
-
const targetMetadata = getResolvedMetadata(targetScreen);
|
|
1298
|
-
// Board → target iframe local coords
|
|
1299
|
-
const scaleX =
|
|
1300
|
-
targetMetadata.width / Math.max(1, targetGeometry.width);
|
|
1301
|
-
const scaleY =
|
|
1302
|
-
targetMetadata.height / Math.max(1, targetGeometry.height);
|
|
1303
|
-
const localX = (lastBoardPoint.x - targetGeometry.x) * scaleX;
|
|
1304
|
-
const localY = (lastBoardPoint.y - targetGeometry.y) * scaleY;
|
|
1305
|
-
|
|
1306
|
-
const targetIframe =
|
|
1307
|
-
surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1308
|
-
`[data-screen-iframe-id="${CSS.escape(candidate.id)}"]`,
|
|
1309
|
-
);
|
|
1310
|
-
const targetContentWindow = targetIframe?.contentWindow;
|
|
1311
|
-
if (!targetContentWindow) return Promise.resolve({});
|
|
1312
|
-
|
|
1313
|
-
const correlationId = `hit-${Date.now()}-${Math.random()
|
|
1314
|
-
.toString(36)
|
|
1315
|
-
.slice(2, 6)}`;
|
|
1316
|
-
|
|
1317
|
-
return new Promise((resolve) => {
|
|
1318
|
-
const timer = window.setTimeout(() => {
|
|
1319
|
-
window.removeEventListener("message", hitListener);
|
|
1320
|
-
resolve({});
|
|
1321
|
-
}, 50);
|
|
1322
|
-
|
|
1323
|
-
const hitListener = (ev: MessageEvent) => {
|
|
1324
|
-
if (
|
|
1325
|
-
!ev.data ||
|
|
1326
|
-
ev.data.type !== "agent-native:hit-test-result" ||
|
|
1327
|
-
ev.data.correlationId !== correlationId
|
|
1328
|
-
)
|
|
1329
|
-
return;
|
|
1330
|
-
window.clearTimeout(timer);
|
|
1331
|
-
window.removeEventListener("message", hitListener);
|
|
1332
|
-
resolve({
|
|
1333
|
-
anchorNodeId: ev.data.anchorNodeId ?? undefined,
|
|
1334
|
-
placement: ev.data.placement ?? undefined,
|
|
1335
|
-
});
|
|
1336
|
-
};
|
|
1337
|
-
window.addEventListener("message", hitListener);
|
|
1338
|
-
|
|
1339
|
-
targetContentWindow.postMessage(
|
|
1340
|
-
{
|
|
1341
|
-
type: "agent-native:hit-test",
|
|
1342
|
-
correlationId,
|
|
1343
|
-
x: localX,
|
|
1344
|
-
y: localY,
|
|
1345
|
-
},
|
|
1346
|
-
"*",
|
|
1347
|
-
);
|
|
1348
|
-
});
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
|
-
void runHitTest().then(({ anchorNodeId, placement }) => {
|
|
1352
|
-
onCrossScreenElementDropRef.current?.({
|
|
1353
|
-
sourceSelector: payload.selector,
|
|
1354
|
-
sourceNodeId: payload.sourceId,
|
|
1355
|
-
sourceScreenId,
|
|
1356
|
-
targetScreenId: candidate.id,
|
|
1357
|
-
targetAnchorNodeId: anchorNodeId,
|
|
1358
|
-
targetAnchorPlacement: placement,
|
|
1359
|
-
});
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1433
|
+
finalizeCrossScreenDrop(
|
|
1434
|
+
sourceScreenId,
|
|
1435
|
+
candidate,
|
|
1436
|
+
payload,
|
|
1437
|
+
lastBoardPoint,
|
|
1438
|
+
);
|
|
1362
1439
|
}
|
|
1363
1440
|
};
|
|
1364
1441
|
|
|
1365
1442
|
window.addEventListener("message", handleMessage);
|
|
1366
1443
|
return () => {
|
|
1444
|
+
stopParentCrossScreenDrag();
|
|
1367
1445
|
window.removeEventListener("message", handleMessage);
|
|
1368
1446
|
};
|
|
1369
1447
|
}, [
|
|
@@ -1371,6 +1449,7 @@ export function MultiScreenCanvas({
|
|
|
1371
1449
|
boardFileId,
|
|
1372
1450
|
boardFrameGeometry,
|
|
1373
1451
|
getFrameEntryAtPoint,
|
|
1452
|
+
getCanvasPoint,
|
|
1374
1453
|
getResolvedMetadata,
|
|
1375
1454
|
onCrossScreenElementDrop,
|
|
1376
1455
|
]);
|
|
@@ -1940,6 +2019,7 @@ export function MultiScreenCanvas({
|
|
|
1940
2019
|
}, [frameGeometry, retryPersistedDraftPrimitives, screens]);
|
|
1941
2020
|
|
|
1942
2021
|
const clearActivePenPath = useCallback(() => {
|
|
2022
|
+
activePenPathRef.current = null;
|
|
1943
2023
|
setActivePenPath(null);
|
|
1944
2024
|
setPenGesturePreview(null);
|
|
1945
2025
|
setPenPointer(null);
|
|
@@ -1965,6 +2045,25 @@ export function MultiScreenCanvas({
|
|
|
1965
2045
|
[clearActivePenPath, commitDraftPrimitive, onActiveToolChange, toolProps],
|
|
1966
2046
|
);
|
|
1967
2047
|
|
|
2048
|
+
const undoActivePenPathSegment = useCallback(() => {
|
|
2049
|
+
const path = activePenPathRef.current;
|
|
2050
|
+
if (!path) return false;
|
|
2051
|
+
|
|
2052
|
+
const remainingNodes = path.nodes.slice(0, -1);
|
|
2053
|
+
if (remainingNodes.length === 0) {
|
|
2054
|
+
clearActivePenPath();
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const nextPath: PenPath = { nodes: remainingNodes, closed: false };
|
|
2059
|
+
activePenPathRef.current = nextPath;
|
|
2060
|
+
setActivePenPath(nextPath);
|
|
2061
|
+
setPenGesturePreview(null);
|
|
2062
|
+
setPenPointer(null);
|
|
2063
|
+
setPenCloseHover(false);
|
|
2064
|
+
return true;
|
|
2065
|
+
}, [clearActivePenPath]);
|
|
2066
|
+
|
|
1968
2067
|
const getPenAnchorPoint = useCallback(
|
|
1969
2068
|
(
|
|
1970
2069
|
clientX: number,
|
|
@@ -3615,15 +3714,21 @@ export function MultiScreenCanvas({
|
|
|
3615
3714
|
useEffect(() => {
|
|
3616
3715
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3617
3716
|
const path = activePenPathRef.current;
|
|
3618
|
-
if (
|
|
3619
|
-
!path ||
|
|
3620
|
-
event.metaKey ||
|
|
3621
|
-
event.ctrlKey ||
|
|
3622
|
-
isEditableHotkeyTarget(event.target)
|
|
3623
|
-
) {
|
|
3717
|
+
if (!path || isEditableHotkeyTarget(event.target)) {
|
|
3624
3718
|
return;
|
|
3625
3719
|
}
|
|
3626
3720
|
|
|
3721
|
+
const primaryKey = event.metaKey || event.ctrlKey;
|
|
3722
|
+
if (primaryKey && !event.shiftKey && event.key.toLowerCase() === "z") {
|
|
3723
|
+
event.preventDefault();
|
|
3724
|
+
event.stopPropagation();
|
|
3725
|
+
event.stopImmediatePropagation();
|
|
3726
|
+
undoActivePenPathSegment();
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
if (primaryKey) return;
|
|
3731
|
+
|
|
3627
3732
|
if (event.key === "Enter") {
|
|
3628
3733
|
event.preventDefault();
|
|
3629
3734
|
event.stopPropagation();
|
|
@@ -3644,14 +3749,7 @@ export function MultiScreenCanvas({
|
|
|
3644
3749
|
event.preventDefault();
|
|
3645
3750
|
event.stopPropagation();
|
|
3646
3751
|
event.stopImmediatePropagation();
|
|
3647
|
-
|
|
3648
|
-
if (remainingNodes.length === 0) {
|
|
3649
|
-
clearActivePenPath();
|
|
3650
|
-
return;
|
|
3651
|
-
}
|
|
3652
|
-
setActivePenPath({ nodes: remainingNodes, closed: false });
|
|
3653
|
-
setPenPointer(null);
|
|
3654
|
-
setPenCloseHover(false);
|
|
3752
|
+
undoActivePenPathSegment();
|
|
3655
3753
|
}
|
|
3656
3754
|
};
|
|
3657
3755
|
|
|
@@ -3659,7 +3757,7 @@ export function MultiScreenCanvas({
|
|
|
3659
3757
|
return () => {
|
|
3660
3758
|
window.removeEventListener("keydown", handleKeyDown, true);
|
|
3661
3759
|
};
|
|
3662
|
-
}, [clearActivePenPath, finishPenPath]);
|
|
3760
|
+
}, [clearActivePenPath, finishPenPath, undoActivePenPathSegment]);
|
|
3663
3761
|
|
|
3664
3762
|
useEffect(() => {
|
|
3665
3763
|
const tool = normalizeCanvasTool(activeTool ?? localActiveTool);
|