@agent-native/core 0.81.2 → 0.81.3
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 +19 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/frame.ts +55 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
- package/corpus/templates/design/app/pages/DesignEditor.tsx +557 -211
- package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -1
- package/corpus/templates/design/e2e/helpers.ts +4 -1
- package/corpus/templates/design/playwright.config.ts +4 -1
- package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
- package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/visual-answer.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
- package/corpus/templates/plan/app/entry.client.tsx +10 -0
- package/corpus/templates/plan/app/global.css +28 -0
- package/corpus/templates/plan/shared/plan-content.ts +10 -6
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +59 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +31 -4
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +33 -18
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.81.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3807702: Fix MCP App embed rendering in ChatGPT/Codex hosts:
|
|
8
|
+
- Stop the `openai:set_globals` storm that left embeds stuck blanking by
|
|
9
|
+
guarding the bridge sync on a signature that ignores host `maxHeight`, and
|
|
10
|
+
by not re-blanking once the app frame has launched.
|
|
11
|
+
- Size embeds to their content: the embedded app reports its real
|
|
12
|
+
`scrollHeight` via `agentNative.contentHeight` and the shell sizes the iframe
|
|
13
|
+
to that (plus chrome) instead of the host max, so plans no longer render far
|
|
14
|
+
too tall or too short.
|
|
15
|
+
- Suppress empty embeds: when a tool whose descriptor declares an embed widget
|
|
16
|
+
produces no embeddable content, the result is marked `isError` so the host
|
|
17
|
+
shows the text result without an empty widget box. This also keeps read-only
|
|
18
|
+
and comment-mutation tools from rendering an embed for results that produce
|
|
19
|
+
no plan surface.
|
|
20
|
+
- Bump the embed shell resource version so hosts refetch the updated shell.
|
|
21
|
+
|
|
3
22
|
## 0.81.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.3",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -83,6 +83,60 @@ export function isTrustedFrameMessage(event: MessageEvent): boolean {
|
|
|
83
83
|
return event.source === window.parent || event.source === window;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Content-height reporting (for MCP App embeds)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
//
|
|
90
|
+
// The embed shell renders this app in a cross-origin iframe and can't measure
|
|
91
|
+
// its content, so we post the real content height for the shell to size to.
|
|
92
|
+
// Only shrinks when the document flows to its natural height — embed layouts
|
|
93
|
+
// must not force `100vh`/`h-screen` on the root, or scrollHeight just equals
|
|
94
|
+
// the iframe height.
|
|
95
|
+
let _contentHeightReportingStarted = false;
|
|
96
|
+
let _lastReportedContentHeight = 0;
|
|
97
|
+
|
|
98
|
+
function measureContentHeight(): number {
|
|
99
|
+
if (typeof document === "undefined") return 0;
|
|
100
|
+
const doc = document.documentElement;
|
|
101
|
+
const body = document.body;
|
|
102
|
+
return Math.ceil(
|
|
103
|
+
Math.max(doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function reportContentHeight(): void {
|
|
108
|
+
if (typeof window === "undefined" || window.parent === window) return;
|
|
109
|
+
const height = measureContentHeight();
|
|
110
|
+
if (!height || Math.abs(height - _lastReportedContentHeight) < 2) return;
|
|
111
|
+
_lastReportedContentHeight = height;
|
|
112
|
+
sendToFrame("agentNative.contentHeight", { height });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function startContentHeightReporting(): void {
|
|
116
|
+
if (_contentHeightReportingStarted || typeof window === "undefined") return;
|
|
117
|
+
if (window.parent === window) return;
|
|
118
|
+
_contentHeightReportingStarted = true;
|
|
119
|
+
let scheduled = false;
|
|
120
|
+
const schedule = () => {
|
|
121
|
+
if (scheduled) return;
|
|
122
|
+
scheduled = true;
|
|
123
|
+
window.requestAnimationFrame(() => {
|
|
124
|
+
scheduled = false;
|
|
125
|
+
reportContentHeight();
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
schedule();
|
|
129
|
+
for (const delay of [100, 300, 800, 1500]) window.setTimeout(schedule, delay);
|
|
130
|
+
try {
|
|
131
|
+
const observer = new ResizeObserver(schedule);
|
|
132
|
+
if (document.documentElement) observer.observe(document.documentElement);
|
|
133
|
+
if (document.body) observer.observe(document.body);
|
|
134
|
+
} catch {
|
|
135
|
+
window.addEventListener("resize", schedule, { passive: true });
|
|
136
|
+
}
|
|
137
|
+
window.addEventListener("load", schedule, { passive: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
86
140
|
// Listen for frame origin message and cache it.
|
|
87
141
|
// Only accept from the direct parent frame, and only set once.
|
|
88
142
|
if (typeof window !== "undefined") {
|
|
@@ -102,6 +156,7 @@ if (typeof window !== "undefined") {
|
|
|
102
156
|
{ type: "agentNative.embeddedAppReady" },
|
|
103
157
|
getFramePostMessageTargetOrigin() ?? window.location.origin,
|
|
104
158
|
);
|
|
159
|
+
startContentHeightReporting();
|
|
105
160
|
}
|
|
106
161
|
});
|
|
107
162
|
}
|
|
@@ -448,6 +448,18 @@ function purgeEmbedStartUrls(value: unknown, depth = 0): unknown {
|
|
|
448
448
|
return value;
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
// True when a tool result carries SOME content. An errored/no-plan result comes
|
|
452
|
+
// back empty (`{}` / null) and would render an empty embed box. We gate on "has
|
|
453
|
+
// content", not "has a URL" — valid embeds often carry data but no URL (the
|
|
454
|
+
// shell mints the embed-start itself).
|
|
455
|
+
function mcpResultHasContent(result: unknown): boolean {
|
|
456
|
+
if (result == null) return false;
|
|
457
|
+
if (typeof result === "string") return result.trim().length > 0;
|
|
458
|
+
if (Array.isArray(result)) return result.length > 0;
|
|
459
|
+
if (typeof result === "object") return Object.keys(result).length > 0;
|
|
460
|
+
return Boolean(result);
|
|
461
|
+
}
|
|
462
|
+
|
|
451
463
|
function mcpAppEmbedOpenLinkMeta(
|
|
452
464
|
result: unknown,
|
|
453
465
|
resource: ResolvedMcpAppResource,
|
|
@@ -750,7 +762,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
|
|
|
750
762
|
|
|
751
763
|
// ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
|
|
752
764
|
// when the shared shell changes in a way that must invalidate host caches.
|
|
753
|
-
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-
|
|
765
|
+
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v64";
|
|
754
766
|
|
|
755
767
|
function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
|
|
756
768
|
const app = safeUiSegment(config.appId ?? config.name, "agent-native");
|
|
@@ -1546,12 +1558,24 @@ export async function createMCPServerForRequest(
|
|
|
1546
1558
|
// branch below degrades to the plain deep-link artifacts the tool would
|
|
1547
1559
|
// otherwise return — no `openai/outputTemplate`, no minted embed-start,
|
|
1548
1560
|
// no embed structuredContent — so the host shows a link, not an iframe.
|
|
1549
|
-
const
|
|
1561
|
+
const mcpAppResourceCandidate = requestMeta?.inlineMcpApps
|
|
1550
1562
|
? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
|
|
1551
1563
|
: null;
|
|
1552
|
-
const rawResultForClient =
|
|
1564
|
+
const rawResultForClient = mcpAppResourceCandidate
|
|
1553
1565
|
? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
|
|
1554
1566
|
: rawResult;
|
|
1567
|
+
// Only attach the embed widget for a non-error result that has content.
|
|
1568
|
+
const embedHasContent = mcpResultHasContent(rawResultForClient);
|
|
1569
|
+
const mcpAppResource =
|
|
1570
|
+
mcpAppResourceCandidate && !mcpResultIsError && embedHasContent
|
|
1571
|
+
? mcpAppResourceCandidate
|
|
1572
|
+
: null;
|
|
1573
|
+
// `openai/outputTemplate` is declared at the tool level, so the host
|
|
1574
|
+
// renders a widget for every call regardless of result _meta. The only
|
|
1575
|
+
// per-result signal it honors is `isError` (shows error text, no widget),
|
|
1576
|
+
// so treat an embed tool that produced nothing as an error.
|
|
1577
|
+
const embedProducedNothing =
|
|
1578
|
+
!!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
|
|
1555
1579
|
const { block, _meta } = buildLinkArtifacts(
|
|
1556
1580
|
entry,
|
|
1557
1581
|
(args as Record<string, any>) ?? {},
|
|
@@ -1590,7 +1614,9 @@ export async function createMCPServerForRequest(
|
|
|
1590
1614
|
if (block) content.push(block);
|
|
1591
1615
|
return {
|
|
1592
1616
|
content,
|
|
1593
|
-
...(mcpResultIsError
|
|
1617
|
+
...(mcpResultIsError || embedProducedNothing
|
|
1618
|
+
? { isError: true }
|
|
1619
|
+
: {}),
|
|
1594
1620
|
...(structuredContent ? { structuredContent } : {}),
|
|
1595
1621
|
...(Object.keys(responseMeta).length > 0
|
|
1596
1622
|
? { _meta: responseMeta }
|
|
@@ -138,6 +138,7 @@ export function embedApp(
|
|
|
138
138
|
let appFrameLoadTimer = null;
|
|
139
139
|
let lastFrameSrc = "";
|
|
140
140
|
let embedSessionRefreshAttempts = 0;
|
|
141
|
+
let reportedContentHeight = 0;
|
|
141
142
|
|
|
142
143
|
function esc(value) {
|
|
143
144
|
return String(value ?? "")
|
|
@@ -174,10 +175,18 @@ export function embedApp(
|
|
|
174
175
|
function visibleIntrinsicHeight() {
|
|
175
176
|
const context = hostState().context || {};
|
|
176
177
|
const hostMaxHeight = contextMaxHeight(context);
|
|
178
|
+
// Size to the app-reported content height (the shell can't measure the
|
|
179
|
+
// cross-origin frame). Don't clamp to the host maxHeight — on Codex it's a
|
|
180
|
+
// small inline hint (~360) that clips the plan; applyIntrinsicHeight still
|
|
181
|
+
// caps at defaultIntrinsicHeight.
|
|
182
|
+
if (reportedContentHeight > 0) {
|
|
183
|
+
return Math.floor(reportedContentHeight + chromeHeight);
|
|
184
|
+
}
|
|
177
185
|
if (hostMaxHeight) return Math.floor(hostMaxHeight);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
// No host maxHeight and no content height yet: use the CONFIGURED height,
|
|
187
|
+
// never window.innerHeight. Echoing the iframe's own size pins us to the
|
|
188
|
+
// current frame height (circular) and locks the embed at the cap forever.
|
|
189
|
+
return Math.floor(defaultIntrinsicHeight);
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
function applyIntrinsicHeight(nextHeight) {
|
|
@@ -1356,7 +1365,8 @@ export function embedApp(
|
|
|
1356
1365
|
}
|
|
1357
1366
|
|
|
1358
1367
|
function notifyHostHeight() {
|
|
1359
|
-
const
|
|
1368
|
+
const intrinsic = visibleIntrinsicHeight();
|
|
1369
|
+
const height = applyIntrinsicHeight(intrinsic);
|
|
1360
1370
|
if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
|
|
1361
1371
|
if (app && typeof app.sendSizeChanged === "function") {
|
|
1362
1372
|
try {
|
|
@@ -1487,6 +1497,14 @@ export function embedApp(
|
|
|
1487
1497
|
notifyOuterMcpAppReady();
|
|
1488
1498
|
return;
|
|
1489
1499
|
}
|
|
1500
|
+
if (event.data.type === "agentNative.contentHeight") {
|
|
1501
|
+
const next = finiteNumber(data && data.height);
|
|
1502
|
+
if (next && Math.abs(next - reportedContentHeight) >= 1) {
|
|
1503
|
+
reportedContentHeight = next;
|
|
1504
|
+
notifyHostHeightSoon();
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1490
1508
|
if (event.data.type === "agentNative.embedSessionExpired") {
|
|
1491
1509
|
refreshExpiredEmbedSession();
|
|
1492
1510
|
return;
|
|
@@ -1538,12 +1556,9 @@ export function embedApp(
|
|
|
1538
1556
|
const selfNavigate = shouldSelfNavigateToApp();
|
|
1539
1557
|
if (startedFor === launchUrl) return;
|
|
1540
1558
|
startedFor = launchUrl;
|
|
1541
|
-
// Only show
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
// the stage on every redundant launch, so the app rendered and was
|
|
1545
|
-
// instantly blanked back to "Loading app" forever. renderFrame()/
|
|
1546
|
-
// transplant replace the stage themselves when they actually (re)mount.
|
|
1559
|
+
// Only show "Loading app" when no frame is mounted. set_globals
|
|
1560
|
+
// re-launches constantly, so calling it every time would wipe the
|
|
1561
|
+
// just-mounted iframe; renderFrame/transplant own the stage on (re)mount.
|
|
1547
1562
|
if (!appFrame) {
|
|
1548
1563
|
setMessage("Loading app");
|
|
1549
1564
|
}
|
|
@@ -1671,14 +1686,11 @@ export function embedApp(
|
|
|
1671
1686
|
toolResultData = objectValue(data);
|
|
1672
1687
|
openUrl = openLinkFrom(params, data);
|
|
1673
1688
|
openStartUrl = embedStartUrlFrom(params, data);
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
// the host round-trips + (re)launch when something we care about changed.
|
|
1680
|
-
// Deliberately EXCLUDE bridge.maxHeight: the host mutates it on every
|
|
1681
|
-
// set_globals as it echoes our height back, which would defeat the guard.
|
|
1689
|
+
// set_globals fires constantly, and this sync calls notifyHostHeight/
|
|
1690
|
+
// sendHostContext which the host echoes back as another set_globals — an
|
|
1691
|
+
// infinite storm. Only do the host round-trips + (re)launch when something
|
|
1692
|
+
// we care about changed. Exclude maxHeight: the host mutates it every
|
|
1693
|
+
// event, which would defeat the guard.
|
|
1682
1694
|
let signature;
|
|
1683
1695
|
try {
|
|
1684
1696
|
signature = JSON.stringify([
|
|
@@ -1704,6 +1716,9 @@ export function embedApp(
|
|
|
1704
1716
|
if (openUrl || openStartUrl) {
|
|
1705
1717
|
void launchEmbed();
|
|
1706
1718
|
} else if (!appFrame) {
|
|
1719
|
+
// No open URL yet — a content-less result no longer reaches the host as
|
|
1720
|
+
// a widget (build-server marks it isError), so this is the transient
|
|
1721
|
+
// "waiting for the embed to resolve" state, not an empty box.
|
|
1707
1722
|
setMessage("Waiting for app result");
|
|
1708
1723
|
}
|
|
1709
1724
|
return true;
|
|
@@ -374,6 +374,8 @@ interface DesignCanvasProps {
|
|
|
374
374
|
designTitle?: string;
|
|
375
375
|
/** Stable id for comment pins, usually scoped to the active screen. */
|
|
376
376
|
commentContextId?: string;
|
|
377
|
+
/** Stable id of the screen containing this canvas when rendered in overview. */
|
|
378
|
+
screenId?: string;
|
|
377
379
|
/** Human-readable label for comment-pin prompts. */
|
|
378
380
|
commentContextLabel?: string;
|
|
379
381
|
/**
|
|
@@ -506,6 +508,7 @@ export function DesignCanvas({
|
|
|
506
508
|
designId,
|
|
507
509
|
designTitle,
|
|
508
510
|
commentContextId,
|
|
511
|
+
screenId,
|
|
509
512
|
commentContextLabel,
|
|
510
513
|
onPrototypeNavigate,
|
|
511
514
|
motionTracks,
|
|
@@ -1098,7 +1101,10 @@ export function DesignCanvas({
|
|
|
1098
1101
|
const beginTextEdit = useCallback((nodeId: string) => {
|
|
1099
1102
|
const iframe = iframeRef.current;
|
|
1100
1103
|
if (!iframe?.contentWindow || !nodeId) return;
|
|
1101
|
-
iframe.contentWindow.postMessage(
|
|
1104
|
+
iframe.contentWindow.postMessage(
|
|
1105
|
+
{ type: "begin-text-edit", nodeId, force: true },
|
|
1106
|
+
"*",
|
|
1107
|
+
);
|
|
1102
1108
|
}, []);
|
|
1103
1109
|
|
|
1104
1110
|
const sendStyleChange = useCallback(
|
|
@@ -1342,6 +1348,7 @@ export function DesignCanvas({
|
|
|
1342
1348
|
: "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
1343
1349
|
}
|
|
1344
1350
|
data-design-preview-iframe
|
|
1351
|
+
data-screen-iframe-id={screenId ?? undefined}
|
|
1345
1352
|
data-design-source-type={
|
|
1346
1353
|
sourceType ??
|
|
1347
1354
|
(externalPreviewUrl
|
|
@@ -1000,7 +1000,11 @@ function LayerRow({
|
|
|
1000
1000
|
const hideable = node.hideable !== false && Boolean(onToggleHidden);
|
|
1001
1001
|
const dragEligible = selectable && !node.locked && !node.hidden;
|
|
1002
1002
|
const draggable = dragEligible && Boolean(onMoveLayer);
|
|
1003
|
-
const canDropInside = layerCanDropInside(
|
|
1003
|
+
const canDropInside = layerCanDropInside(
|
|
1004
|
+
node,
|
|
1005
|
+
hasChildren,
|
|
1006
|
+
canAcceptChildren,
|
|
1007
|
+
);
|
|
1004
1008
|
const activeDrop =
|
|
1005
1009
|
dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
|
|
1006
1010
|
// Tracks whether the user pressed Escape to cancel rename so that the
|
|
@@ -1868,9 +1872,14 @@ function ElementLayerGlyph({ className }: { className?: string }) {
|
|
|
1868
1872
|
);
|
|
1869
1873
|
}
|
|
1870
1874
|
|
|
1871
|
-
function layerCanDropInside(
|
|
1875
|
+
function layerCanDropInside(
|
|
1876
|
+
node: LayersPanelNode,
|
|
1877
|
+
hasChildren: boolean,
|
|
1878
|
+
canAcceptChildren: boolean,
|
|
1879
|
+
) {
|
|
1872
1880
|
return (
|
|
1873
1881
|
hasChildren ||
|
|
1882
|
+
canAcceptChildren ||
|
|
1874
1883
|
Boolean(node.layout?.isFlexContainer || node.layout?.isGridContainer) ||
|
|
1875
1884
|
node.type === "file" ||
|
|
1876
1885
|
node.type === "screen" ||
|
|
@@ -3365,15 +3365,26 @@ export function MultiScreenCanvas({
|
|
|
3365
3365
|
[flushPendingWheelGesture],
|
|
3366
3366
|
);
|
|
3367
3367
|
|
|
3368
|
-
const
|
|
3369
|
-
(
|
|
3368
|
+
const enqueueWheelGestureFromClient = useCallback(
|
|
3369
|
+
(args: {
|
|
3370
|
+
deltaX: number;
|
|
3371
|
+
deltaY: number;
|
|
3372
|
+
deltaMode: number;
|
|
3373
|
+
clientX: number;
|
|
3374
|
+
clientY: number;
|
|
3375
|
+
ctrlKey: boolean;
|
|
3376
|
+
metaKey: boolean;
|
|
3377
|
+
shiftKey: boolean;
|
|
3378
|
+
}) => {
|
|
3370
3379
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
3371
3380
|
if (!rect) return;
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3381
|
+
const delta = getWheelDeltaFromValues(
|
|
3382
|
+
args.deltaX,
|
|
3383
|
+
args.deltaY,
|
|
3384
|
+
args.deltaMode,
|
|
3385
|
+
);
|
|
3375
3386
|
|
|
3376
|
-
if (
|
|
3387
|
+
if (args.ctrlKey || args.metaKey) {
|
|
3377
3388
|
const zoomDeltaY = clamp(
|
|
3378
3389
|
delta.y,
|
|
3379
3390
|
-MAX_WHEEL_ZOOM_DELTA,
|
|
@@ -3382,20 +3393,23 @@ export function MultiScreenCanvas({
|
|
|
3382
3393
|
enqueueWheelGesture({
|
|
3383
3394
|
mode: "zoom",
|
|
3384
3395
|
deltaY: zoomDeltaY,
|
|
3385
|
-
cursor: {
|
|
3386
|
-
|
|
3387
|
-
|
|
3396
|
+
cursor: {
|
|
3397
|
+
x: args.clientX - rect.left,
|
|
3398
|
+
y: args.clientY - rect.top,
|
|
3399
|
+
},
|
|
3400
|
+
clientX: args.clientX,
|
|
3401
|
+
clientY: args.clientY,
|
|
3388
3402
|
});
|
|
3389
3403
|
return;
|
|
3390
3404
|
}
|
|
3391
3405
|
|
|
3392
3406
|
const deltaX = clamp(
|
|
3393
|
-
|
|
3407
|
+
args.shiftKey && delta.x === 0 ? delta.y : delta.x,
|
|
3394
3408
|
-MAX_WHEEL_PAN_DELTA,
|
|
3395
3409
|
MAX_WHEEL_PAN_DELTA,
|
|
3396
3410
|
);
|
|
3397
3411
|
const deltaY = clamp(
|
|
3398
|
-
|
|
3412
|
+
args.shiftKey && delta.x === 0 ? 0 : delta.y,
|
|
3399
3413
|
-MAX_WHEEL_PAN_DELTA,
|
|
3400
3414
|
MAX_WHEEL_PAN_DELTA,
|
|
3401
3415
|
);
|
|
@@ -3404,6 +3418,24 @@ export function MultiScreenCanvas({
|
|
|
3404
3418
|
[enqueueWheelGesture],
|
|
3405
3419
|
);
|
|
3406
3420
|
|
|
3421
|
+
const handleWheelEvent = useCallback(
|
|
3422
|
+
(event: WheelEvent) => {
|
|
3423
|
+
event.preventDefault();
|
|
3424
|
+
event.stopPropagation();
|
|
3425
|
+
enqueueWheelGestureFromClient({
|
|
3426
|
+
deltaX: event.deltaX,
|
|
3427
|
+
deltaY: event.deltaY,
|
|
3428
|
+
deltaMode: event.deltaMode,
|
|
3429
|
+
clientX: event.clientX,
|
|
3430
|
+
clientY: event.clientY,
|
|
3431
|
+
ctrlKey: event.ctrlKey,
|
|
3432
|
+
metaKey: event.metaKey,
|
|
3433
|
+
shiftKey: event.shiftKey,
|
|
3434
|
+
});
|
|
3435
|
+
},
|
|
3436
|
+
[enqueueWheelGestureFromClient],
|
|
3437
|
+
);
|
|
3438
|
+
|
|
3407
3439
|
useEffect(() => {
|
|
3408
3440
|
const surface = surfaceRef.current;
|
|
3409
3441
|
if (!surface) return;
|
|
@@ -3418,6 +3450,44 @@ export function MultiScreenCanvas({
|
|
|
3418
3450
|
};
|
|
3419
3451
|
}, [handleWheelEvent]);
|
|
3420
3452
|
|
|
3453
|
+
useEffect(() => {
|
|
3454
|
+
const handleEmbeddedWheelMessage = (event: MessageEvent) => {
|
|
3455
|
+
if (!event.data || event.data.type !== "embedded-canvas-wheel") return;
|
|
3456
|
+
const surface = surfaceRef.current;
|
|
3457
|
+
if (!surface) return;
|
|
3458
|
+
const sourceIframe = Array.from(
|
|
3459
|
+
surface.querySelectorAll<HTMLIFrameElement>(
|
|
3460
|
+
"iframe[data-design-preview-iframe]",
|
|
3461
|
+
),
|
|
3462
|
+
).find((iframe) => iframe.contentWindow === event.source);
|
|
3463
|
+
if (!sourceIframe) return;
|
|
3464
|
+
|
|
3465
|
+
const rect = sourceIframe.getBoundingClientRect();
|
|
3466
|
+
const scaleX =
|
|
3467
|
+
sourceIframe.clientWidth > 0
|
|
3468
|
+
? rect.width / sourceIframe.clientWidth
|
|
3469
|
+
: 1;
|
|
3470
|
+
const scaleY =
|
|
3471
|
+
sourceIframe.clientHeight > 0
|
|
3472
|
+
? rect.height / sourceIframe.clientHeight
|
|
3473
|
+
: 1;
|
|
3474
|
+
enqueueWheelGestureFromClient({
|
|
3475
|
+
deltaX: Number(event.data.deltaX) || 0,
|
|
3476
|
+
deltaY: Number(event.data.deltaY) || 0,
|
|
3477
|
+
deltaMode: Number(event.data.deltaMode) || WheelEvent.DOM_DELTA_PIXEL,
|
|
3478
|
+
clientX: rect.left + (Number(event.data.clientX) || 0) * scaleX,
|
|
3479
|
+
clientY: rect.top + (Number(event.data.clientY) || 0) * scaleY,
|
|
3480
|
+
ctrlKey: Boolean(event.data.ctrlKey),
|
|
3481
|
+
metaKey: Boolean(event.data.metaKey),
|
|
3482
|
+
shiftKey: Boolean(event.data.shiftKey),
|
|
3483
|
+
});
|
|
3484
|
+
};
|
|
3485
|
+
|
|
3486
|
+
window.addEventListener("message", handleEmbeddedWheelMessage);
|
|
3487
|
+
return () =>
|
|
3488
|
+
window.removeEventListener("message", handleEmbeddedWheelMessage);
|
|
3489
|
+
}, [enqueueWheelGestureFromClient]);
|
|
3490
|
+
|
|
3421
3491
|
useEffect(() => {
|
|
3422
3492
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3423
3493
|
if (activePenPathRef.current) return;
|
|
@@ -6060,12 +6130,15 @@ function getSelectableBounds(geometry: FrameGeometry): BoundsRect {
|
|
|
6060
6130
|
};
|
|
6061
6131
|
}
|
|
6062
6132
|
|
|
6063
|
-
function
|
|
6064
|
-
|
|
6065
|
-
|
|
6133
|
+
function getWheelDeltaFromValues(
|
|
6134
|
+
deltaX: number,
|
|
6135
|
+
deltaY: number,
|
|
6136
|
+
deltaMode: number,
|
|
6137
|
+
) {
|
|
6138
|
+
const multiplier = deltaMode === 1 ? 16 : deltaMode === 2 ? 800 : 1;
|
|
6066
6139
|
return {
|
|
6067
|
-
x:
|
|
6068
|
-
y:
|
|
6140
|
+
x: deltaX * multiplier,
|
|
6141
|
+
y: deltaY * multiplier,
|
|
6069
6142
|
};
|
|
6070
6143
|
}
|
|
6071
6144
|
|
|
@@ -3468,17 +3468,20 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3468
3468
|
}
|
|
3469
3469
|
}
|
|
3470
3470
|
|
|
3471
|
-
function beginTextEditingFromEvent(e) {
|
|
3471
|
+
function beginTextEditingFromEvent(e, forceTextEditing) {
|
|
3472
3472
|
if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target))
|
|
3473
3473
|
return;
|
|
3474
|
-
if (!textEditingEnabled) {
|
|
3474
|
+
if (!textEditingEnabled && !forceTextEditing) {
|
|
3475
3475
|
stopNativeInteraction(e);
|
|
3476
3476
|
return;
|
|
3477
3477
|
}
|
|
3478
3478
|
stopNativeInteraction(e);
|
|
3479
|
-
var
|
|
3480
|
-
|
|
3481
|
-
|
|
3479
|
+
var eventTarget =
|
|
3480
|
+
e && e.target && e.target.nodeType === 1 ? e.target : null;
|
|
3481
|
+
var target =
|
|
3482
|
+
findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY)) ||
|
|
3483
|
+
findTextEditTarget(eventTarget) ||
|
|
3484
|
+
eventTarget;
|
|
3482
3485
|
if (!target || target.nodeType !== 1) return;
|
|
3483
3486
|
// Anchor the selection identity to the nearest source-backed element. Text
|
|
3484
3487
|
// editing still operates on the actual target text node, but a later
|
|
@@ -3725,7 +3728,8 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3725
3728
|
// text-element creation so the user can type right away and the autosize
|
|
3726
3729
|
// CSS (width:max-content or similar) takes effect from the first keystroke.
|
|
3727
3730
|
if (e.data.type === "begin-text-edit") {
|
|
3728
|
-
|
|
3731
|
+
var forceBeginTextEdit = e.data.force === true;
|
|
3732
|
+
if ((readOnly || !textEditingEnabled) && !forceBeginTextEdit) return;
|
|
3729
3733
|
var nodeId: string =
|
|
3730
3734
|
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
3731
3735
|
if (!nodeId) return;
|
|
@@ -3749,14 +3753,17 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3749
3753
|
var bteCenterY = bteRect.top + bteRect.height / 2;
|
|
3750
3754
|
// Delegate to the canonical path so all state, events, and postMessages
|
|
3751
3755
|
// stay consistent with a normal double-click text edit.
|
|
3752
|
-
beginTextEditingFromEvent(
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3756
|
+
beginTextEditingFromEvent(
|
|
3757
|
+
{
|
|
3758
|
+
clientX: bteCenterX,
|
|
3759
|
+
clientY: bteCenterY,
|
|
3760
|
+
target: textTarget,
|
|
3761
|
+
preventDefault: function () {},
|
|
3762
|
+
stopPropagation: function () {},
|
|
3763
|
+
stopImmediatePropagation: function () {},
|
|
3764
|
+
} as unknown as MouseEvent,
|
|
3765
|
+
forceBeginTextEdit,
|
|
3766
|
+
);
|
|
3760
3767
|
return;
|
|
3761
3768
|
}
|
|
3762
3769
|
if (e.data.type === "set-editor-chrome-scale") {
|