@agent-native/core 0.80.11 → 0.81.1
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 +53 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +419 -19
- package/corpus/core/src/client/AgentPanel.tsx +33 -1
- package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
- package/corpus/core/src/client/chat-view-transition.ts +33 -2
- package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
- package/corpus/core/src/db/client.ts +29 -2
- package/corpus/core/src/provider-api/index.ts +29 -0
- package/corpus/core/src/server/core-routes-plugin.ts +58 -20
- package/corpus/core/src/styles/agent-native.css +54 -0
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
- package/corpus/templates/analytics/app/global.css +8 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
- package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
- package/corpus/templates/assets/app/global.css +15 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +90 -16
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
- package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
- package/corpus/templates/design/AGENTS.md +27 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
- package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
- package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
- package/corpus/templates/design/actions/capture-design-state.ts +27 -4
- package/corpus/templates/design/actions/connect-localhost.ts +59 -22
- package/corpus/templates/design/actions/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +5 -19
- package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
- package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
- package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
- package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
- package/corpus/templates/design/actions/navigate.ts +18 -3
- package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
- package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
- package/corpus/templates/design/actions/provider-api-request.ts +3 -3
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
- package/corpus/templates/design/actions/run-design-audit.ts +53 -4
- package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/actions/write-local-file.ts +213 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
- package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
- package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
- package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
- package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/global.css +7 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
- package/corpus/templates/design/app/i18n-data.ts +358 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
- package/corpus/templates/design/app/pages/Index.tsx +5 -1
- package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +168 -65
- package/corpus/templates/design/package.json +3 -1
- package/corpus/templates/design/server/db/schema.ts +23 -0
- package/corpus/templates/design/server/handlers/uploads.ts +22 -1
- package/corpus/templates/design/server/lib/provider-api.ts +7 -4
- package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
- package/corpus/templates/design/server/plugins/db.ts +39 -0
- package/corpus/templates/design/server/register-secrets.ts +38 -0
- package/corpus/templates/design/shared/board-file.ts +228 -0
- package/corpus/templates/design/shared/board-objects.ts +288 -0
- package/corpus/templates/design/shared/code-layer.ts +52 -0
- package/corpus/templates/design/shared/motion-compiler.ts +58 -7
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- package/corpus/templates/design/tsconfig.json +2 -1
- package/corpus/templates/slides/AGENTS.md +9 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
- package/corpus/templates/slides/app/vite-env.d.ts +18 -0
- package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/vite.config.ts +1 -0
- package/dist/cli/design-connect.d.ts +25 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +357 -18
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +18 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -0
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +5 -5
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/chat-view-transition.d.ts +6 -0
- package/dist/client/chat-view-transition.d.ts.map +1 -1
- package/dist/client/chat-view-transition.js +23 -2
- package/dist/client/chat-view-transition.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +3 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +11 -4
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -3
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +20 -6
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +28 -2
- package/dist/db/client.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/progress/routes.d.ts +1 -1
- package/dist/provider-api/index.d.ts +5 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -0
- package/dist/provider-api/index.js.map +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +43 -18
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +54 -0
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,7 +48,20 @@ import { prettyScreenName } from "@/lib/screen-names";
|
|
|
48
48
|
import { cn } from "@/lib/utils";
|
|
49
49
|
|
|
50
50
|
import { canvasPrimitiveReactStyle } from "./canvas-primitive-style";
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
DesignCanvas,
|
|
53
|
+
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT,
|
|
54
|
+
appendHitTestResponder,
|
|
55
|
+
} from "./DesignCanvas";
|
|
56
|
+
import {
|
|
57
|
+
DEVICE_FRAME_VIEWPORTS,
|
|
58
|
+
type DeviceFrameType,
|
|
59
|
+
type ElementInfo,
|
|
60
|
+
} from "./types";
|
|
61
|
+
|
|
62
|
+
// Re-export so consumers of MultiScreenCanvas can use the same script without
|
|
63
|
+
// importing DesignCanvas directly.
|
|
64
|
+
export { LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT };
|
|
52
65
|
|
|
53
66
|
interface ScreenFile {
|
|
54
67
|
id: string;
|
|
@@ -204,15 +217,112 @@ interface MultiScreenCanvasProps {
|
|
|
204
217
|
* Called when the user drags an element out of the active screen's iframe
|
|
205
218
|
* and drops it onto a different screen. The bridge in the source iframe
|
|
206
219
|
* posts { type:"agent-native:cross-screen-drag" } messages; the host
|
|
207
|
-
* translates them to board coords, finds the target frame,
|
|
208
|
-
* prop with the resolved
|
|
220
|
+
* translates them to board coords, finds the target frame, runs a hit-test
|
|
221
|
+
* in the target iframe (50ms timeout), and calls this prop with the resolved
|
|
222
|
+
* ids and anchor placement.
|
|
209
223
|
*/
|
|
210
224
|
onCrossScreenElementDrop?: (args: {
|
|
211
225
|
sourceSelector: string;
|
|
212
226
|
sourceNodeId?: string;
|
|
213
227
|
sourceScreenId: string;
|
|
214
228
|
targetScreenId: string;
|
|
229
|
+
/** data-agent-native-node-id of the deepest container at the drop point
|
|
230
|
+
* inside the target screen iframe (undefined when hit-test timed out). */
|
|
231
|
+
targetAnchorNodeId?: string;
|
|
232
|
+
/** DOM insertion placement relative to the anchor node. */
|
|
233
|
+
targetAnchorPlacement?: "before" | "after" | "inside";
|
|
215
234
|
}) => void;
|
|
235
|
+
// ── Board file (new model) ───────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* The id of the reserved "__board__.html" design file.
|
|
238
|
+
* When provided, a full-surface board <DesignCanvas> is rendered below
|
|
239
|
+
* the screen iframes so board elements are editable through the bridge.
|
|
240
|
+
*/
|
|
241
|
+
boardFileId?: string;
|
|
242
|
+
/**
|
|
243
|
+
* The current HTML content of the board file.
|
|
244
|
+
* Passed as `content` to the board <DesignCanvas> instance.
|
|
245
|
+
*/
|
|
246
|
+
boardFileContent?: string;
|
|
247
|
+
/**
|
|
248
|
+
* The logical geometry of the board iframe in canvas coordinates.
|
|
249
|
+
* Should be { x:0, y:0, width:totalSurfaceWidth, height:totalSurfaceHeight }.
|
|
250
|
+
* Used to translate cross-screen-drag coords when the source is the board.
|
|
251
|
+
*/
|
|
252
|
+
boardFrameGeometry?: FrameGeometry;
|
|
253
|
+
/**
|
|
254
|
+
* Called when a draft primitive is committed outside all screen frames
|
|
255
|
+
* (and there is more than one screen). The caller should append the
|
|
256
|
+
* primitive into the board file's HTML content.
|
|
257
|
+
*
|
|
258
|
+
* Replaces the legacy onCreateBoardObject.
|
|
259
|
+
*/
|
|
260
|
+
onBoardDrawPrimitive?: (primitive: CanvasPrimitiveInsert) => boolean | string;
|
|
261
|
+
// ── Board edit callbacks (active-target model) ───────────────────────────
|
|
262
|
+
/**
|
|
263
|
+
* When true the board <DesignCanvas> is in edit mode.
|
|
264
|
+
* Pass `canEditDesign` from DesignEditor. Defaults to false.
|
|
265
|
+
*/
|
|
266
|
+
boardEditMode?: boolean;
|
|
267
|
+
/**
|
|
268
|
+
* Called when the user selects an element on the board surface.
|
|
269
|
+
* DesignEditor should set boardFileId as the active file and push the
|
|
270
|
+
* selection to the inspector.
|
|
271
|
+
*/
|
|
272
|
+
onBoardElementSelect?: (info: ElementInfo) => void;
|
|
273
|
+
/**
|
|
274
|
+
* Called when the user hovers an element on the board surface.
|
|
275
|
+
*/
|
|
276
|
+
onBoardElementHover?: (info: ElementInfo | null) => void;
|
|
277
|
+
/**
|
|
278
|
+
* Called when a drag / reorder / reparent / drop-into-container or delete
|
|
279
|
+
* occurs on a board element. Target file is boardFileId.
|
|
280
|
+
*/
|
|
281
|
+
onBoardVisualStructureChange?: (
|
|
282
|
+
selector: string,
|
|
283
|
+
anchorSelector: string,
|
|
284
|
+
placement: "before" | "after" | "inside",
|
|
285
|
+
info?: ElementInfo,
|
|
286
|
+
details?: {
|
|
287
|
+
sourceId?: string;
|
|
288
|
+
anchorSourceId?: string;
|
|
289
|
+
requestId?: string;
|
|
290
|
+
},
|
|
291
|
+
) => boolean | void;
|
|
292
|
+
/**
|
|
293
|
+
* Called when a style property changes on a board element.
|
|
294
|
+
* Target file is boardFileId.
|
|
295
|
+
*/
|
|
296
|
+
onBoardVisualStyleChange?: (
|
|
297
|
+
selector: string,
|
|
298
|
+
styles: Record<string, string>,
|
|
299
|
+
info?: ElementInfo,
|
|
300
|
+
) => void;
|
|
301
|
+
/**
|
|
302
|
+
* Called when an alt-drag clone is created on the board surface.
|
|
303
|
+
* Target file is boardFileId.
|
|
304
|
+
*/
|
|
305
|
+
onBoardVisualDuplicateChange?: (
|
|
306
|
+
selector: string,
|
|
307
|
+
cloneHtml: string,
|
|
308
|
+
info?: ElementInfo,
|
|
309
|
+
details?: {
|
|
310
|
+
sourceId?: string;
|
|
311
|
+
anchorSelector?: string;
|
|
312
|
+
anchorSourceId?: string;
|
|
313
|
+
placement?: "before" | "after" | "inside";
|
|
314
|
+
},
|
|
315
|
+
) => boolean | void;
|
|
316
|
+
/**
|
|
317
|
+
* Called when inline text is edited on a board element.
|
|
318
|
+
* Target file is boardFileId.
|
|
319
|
+
*/
|
|
320
|
+
onBoardTextContentChange?: (
|
|
321
|
+
selector: string,
|
|
322
|
+
value: string,
|
|
323
|
+
info?: ElementInfo,
|
|
324
|
+
details?: { html?: string },
|
|
325
|
+
) => void;
|
|
216
326
|
}
|
|
217
327
|
|
|
218
328
|
/**
|
|
@@ -224,7 +334,7 @@ const SCREEN_WIDTH = OVERVIEW_FRAME_WIDTH;
|
|
|
224
334
|
const SCREEN_HEIGHT = 640;
|
|
225
335
|
const SCREEN_CARD_HEIGHT = SCREEN_HEIGHT + 26;
|
|
226
336
|
const SCREEN_GAP = 56;
|
|
227
|
-
const SURFACE_PADDING = 240;
|
|
337
|
+
export const SURFACE_PADDING = 240;
|
|
228
338
|
const DUPLICATE_DRAG_THRESHOLD = 6;
|
|
229
339
|
const DRAG_THRESHOLD = 3;
|
|
230
340
|
const FRAME_LABEL_HEIGHT = 28;
|
|
@@ -555,6 +665,17 @@ export function MultiScreenCanvas({
|
|
|
555
665
|
onActiveBreakpointChange,
|
|
556
666
|
onSelectionChange,
|
|
557
667
|
onCrossScreenElementDrop,
|
|
668
|
+
boardFileId,
|
|
669
|
+
boardFileContent,
|
|
670
|
+
boardFrameGeometry,
|
|
671
|
+
onBoardDrawPrimitive,
|
|
672
|
+
boardEditMode = false,
|
|
673
|
+
onBoardElementSelect,
|
|
674
|
+
onBoardElementHover,
|
|
675
|
+
onBoardVisualStructureChange,
|
|
676
|
+
onBoardVisualStyleChange,
|
|
677
|
+
onBoardVisualDuplicateChange,
|
|
678
|
+
onBoardTextContentChange,
|
|
558
679
|
}: MultiScreenCanvasProps) {
|
|
559
680
|
const t = useT();
|
|
560
681
|
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
@@ -628,7 +749,15 @@ export function MultiScreenCanvas({
|
|
|
628
749
|
selector: string;
|
|
629
750
|
sourceId?: string;
|
|
630
751
|
} | null>(null);
|
|
752
|
+
/** Board-space point from the last cross-screen-drag "move" message. */
|
|
753
|
+
const crossScreenLastBoardPointRef = useRef<{ x: number; y: number } | null>(
|
|
754
|
+
null,
|
|
755
|
+
);
|
|
631
756
|
const onCrossScreenElementDropRef = useRef(onCrossScreenElementDrop);
|
|
757
|
+
const onBoardDrawPrimitiveRef = useRef(onBoardDrawPrimitive);
|
|
758
|
+
// Ref wrapper for finishDrag so callbacks declared before finishDrag can
|
|
759
|
+
// reference it via the ref without hitting the const TDZ.
|
|
760
|
+
const finishDragRef = useRef<() => void>(() => {});
|
|
632
761
|
const suppressNextPick = useRef(false);
|
|
633
762
|
const feedbackTimerRef = useRef<number | null>(null);
|
|
634
763
|
const pendingWheelGestureRef = useRef<PendingWheelGesture | null>(null);
|
|
@@ -668,6 +797,10 @@ export function MultiScreenCanvas({
|
|
|
668
797
|
onCrossScreenElementDropRef.current = onCrossScreenElementDrop;
|
|
669
798
|
}, [onCrossScreenElementDrop]);
|
|
670
799
|
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
onBoardDrawPrimitiveRef.current = onBoardDrawPrimitive;
|
|
802
|
+
}, [onBoardDrawPrimitive]);
|
|
803
|
+
|
|
671
804
|
useEffect(() => {
|
|
672
805
|
screensRef.current = screens;
|
|
673
806
|
}, [screens]);
|
|
@@ -1083,22 +1216,37 @@ export function MultiScreenCanvas({
|
|
|
1083
1216
|
// Board math mirrors primitiveLocalToBoardRect:
|
|
1084
1217
|
// boardX = frame.x + iframeX * (frame.width / metadata.width)
|
|
1085
1218
|
// boardY = frame.y + iframeY * (frame.height / metadata.height)
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1092
|
-
|
|
1219
|
+
|
|
1220
|
+
let boardX: number;
|
|
1221
|
+
let boardY: number;
|
|
1222
|
+
|
|
1223
|
+
if (sourceScreenId === boardFileId && boardFrameGeometry) {
|
|
1224
|
+
// The board iframe is pixel-exact: 1 iframe pixel == 1 canvas unit.
|
|
1225
|
+
// iframeX/iframeY are already in canvas space (no scale needed).
|
|
1226
|
+
boardX = boardFrameGeometry.x + iframeX;
|
|
1227
|
+
boardY = boardFrameGeometry.y + iframeY;
|
|
1228
|
+
} else {
|
|
1229
|
+
const sourceScreen = screensRef.current.find(
|
|
1230
|
+
(s) => s.id === sourceScreenId,
|
|
1231
|
+
);
|
|
1232
|
+
const sourceGeometry = frameGeometryRef.current[sourceScreenId];
|
|
1233
|
+
if (!sourceScreen || !sourceGeometry) {
|
|
1234
|
+
clearCrossScreenDrag();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const sourceMetadata = getResolvedMetadata(sourceScreen);
|
|
1238
|
+
const scaleX =
|
|
1239
|
+
sourceGeometry.width / Math.max(1, sourceMetadata.width);
|
|
1240
|
+
const scaleY =
|
|
1241
|
+
sourceGeometry.height / Math.max(1, sourceMetadata.height);
|
|
1242
|
+
boardX = sourceGeometry.x + iframeX * scaleX;
|
|
1243
|
+
boardY = sourceGeometry.y + iframeY * scaleY;
|
|
1093
1244
|
}
|
|
1094
|
-
const sourceMetadata = getResolvedMetadata(sourceScreen);
|
|
1095
|
-
const scaleX = sourceGeometry.width / Math.max(1, sourceMetadata.width);
|
|
1096
|
-
const scaleY =
|
|
1097
|
-
sourceGeometry.height / Math.max(1, sourceMetadata.height);
|
|
1098
|
-
const boardX = sourceGeometry.x + iframeX * scaleX;
|
|
1099
|
-
const boardY = sourceGeometry.y + iframeY * scaleY;
|
|
1100
1245
|
const boardPoint = { x: boardX, y: boardY };
|
|
1101
1246
|
|
|
1247
|
+
// Remember the latest board-space position for use in the "end" handler.
|
|
1248
|
+
crossScreenLastBoardPointRef.current = boardPoint;
|
|
1249
|
+
|
|
1102
1250
|
setCrossScreenGhost({ boardX, boardY });
|
|
1103
1251
|
|
|
1104
1252
|
// Find which screen frame the board point falls in.
|
|
@@ -1124,18 +1272,91 @@ export function MultiScreenCanvas({
|
|
|
1124
1272
|
selector: msg.selector ?? "",
|
|
1125
1273
|
sourceId: msg.sourceId,
|
|
1126
1274
|
};
|
|
1275
|
+
const lastBoardPoint = crossScreenLastBoardPointRef.current;
|
|
1127
1276
|
clearCrossScreenDrag();
|
|
1277
|
+
crossScreenLastBoardPointRef.current = null;
|
|
1128
1278
|
// Guard: require at least one stable identifier (selector or sourceId)
|
|
1129
1279
|
// so the drop handler in DesignEditor can resolve the node. An empty
|
|
1130
1280
|
// selector AND a missing sourceId would produce nodeAttrId="" and
|
|
1131
1281
|
// moveNodeBetweenDocuments would fail with a generic error toast.
|
|
1132
1282
|
const hasIdentifier = !!(payload.selector || payload.sourceId);
|
|
1133
1283
|
if (candidate && hasIdentifier && sourceScreenId) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
+
});
|
|
1139
1360
|
});
|
|
1140
1361
|
}
|
|
1141
1362
|
}
|
|
@@ -1147,6 +1368,8 @@ export function MultiScreenCanvas({
|
|
|
1147
1368
|
};
|
|
1148
1369
|
}, [
|
|
1149
1370
|
activeId,
|
|
1371
|
+
boardFileId,
|
|
1372
|
+
boardFrameGeometry,
|
|
1150
1373
|
getFrameEntryAtPoint,
|
|
1151
1374
|
getResolvedMetadata,
|
|
1152
1375
|
onCrossScreenElementDrop,
|
|
@@ -1202,6 +1425,7 @@ export function MultiScreenCanvas({
|
|
|
1202
1425
|
(
|
|
1203
1426
|
handleMouseMove: (ev: MouseEvent) => void,
|
|
1204
1427
|
handleMouseUp: (ev: MouseEvent) => void,
|
|
1428
|
+
handleCancel?: () => void,
|
|
1205
1429
|
) => {
|
|
1206
1430
|
dragCleanup.current?.();
|
|
1207
1431
|
const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
|
|
@@ -1218,8 +1442,13 @@ export function MultiScreenCanvas({
|
|
|
1218
1442
|
ev.preventDefault();
|
|
1219
1443
|
handleMouseUp(ev);
|
|
1220
1444
|
};
|
|
1221
|
-
const cleanupOnBlur = () =>
|
|
1445
|
+
const cleanupOnBlur = () => {
|
|
1446
|
+
if (handleCancel) {
|
|
1447
|
+
handleCancel();
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1222
1450
|
handleMouseUp(lastMouseEvent ?? new MouseEvent("mouseup"));
|
|
1451
|
+
};
|
|
1223
1452
|
dragCleanup.current = () => {
|
|
1224
1453
|
window.removeEventListener("mousemove", move);
|
|
1225
1454
|
window.removeEventListener("mouseup", up);
|
|
@@ -1323,6 +1552,10 @@ export function MultiScreenCanvas({
|
|
|
1323
1552
|
dragCleanup.current?.();
|
|
1324
1553
|
}, []);
|
|
1325
1554
|
|
|
1555
|
+
// Keep the finishDragRef in sync so board-object callbacks declared before
|
|
1556
|
+
// finishDrag can call it via the ref without a TDZ forward-reference issue.
|
|
1557
|
+
finishDragRef.current = finishDrag;
|
|
1558
|
+
|
|
1326
1559
|
const cancelActiveDrag = useCallback(() => {
|
|
1327
1560
|
let cancelled = false;
|
|
1328
1561
|
const state = dragState.current;
|
|
@@ -1475,6 +1708,7 @@ export function MultiScreenCanvas({
|
|
|
1475
1708
|
),
|
|
1476
1709
|
)
|
|
1477
1710
|
.map((entry) => entry.id);
|
|
1711
|
+
|
|
1478
1712
|
updateSelectedIds(() =>
|
|
1479
1713
|
state.additive
|
|
1480
1714
|
? dedupeIds([...state.baseSelectedIds, ...hitIds])
|
|
@@ -1541,17 +1775,18 @@ export function MultiScreenCanvas({
|
|
|
1541
1775
|
|
|
1542
1776
|
// Fallback: if no frame contains the center (shape drawn outside all
|
|
1543
1777
|
// frames), use the nearest frame so the shape still persists rather than
|
|
1544
|
-
// becoming a lost draft primitive.
|
|
1545
|
-
//
|
|
1778
|
+
// becoming a lost draft primitive.
|
|
1779
|
+
//
|
|
1780
|
+
// Exception: when there is MORE THAN ONE screen, a draft whose center
|
|
1781
|
+
// falls outside all frames becomes a board object (floating on the
|
|
1782
|
+
// infinite canvas surface), not a primitive inside any screen. In that
|
|
1783
|
+
// case return undefined so the caller can route to onCreateBoardObject.
|
|
1784
|
+
// With a single screen there is no meaningful "outside", so we always
|
|
1785
|
+
// absorb the draft into the only available frame (existing behaviour).
|
|
1546
1786
|
if (entries.length === 0) return undefined;
|
|
1547
1787
|
if (entries.length === 1) return entries[0];
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const cb = getFrameCenter(b.geometry);
|
|
1551
|
-
const da = Math.hypot(draftCenter.x - ca.x, draftCenter.y - ca.y);
|
|
1552
|
-
const db = Math.hypot(draftCenter.x - cb.x, draftCenter.y - cb.y);
|
|
1553
|
-
return da - db;
|
|
1554
|
-
})[0];
|
|
1788
|
+
// Multiple screens: draft drawn outside all frames → board object.
|
|
1789
|
+
return undefined;
|
|
1555
1790
|
},
|
|
1556
1791
|
[getCurrentFrameEntries],
|
|
1557
1792
|
);
|
|
@@ -1562,7 +1797,39 @@ export function MultiScreenCanvas({
|
|
|
1562
1797
|
preferredFrameId?: string,
|
|
1563
1798
|
): PersistedDraftPrimitive | null => {
|
|
1564
1799
|
const targetFrame = getTargetFrameForDraft(draft, preferredFrameId);
|
|
1565
|
-
|
|
1800
|
+
|
|
1801
|
+
// When the draft center is outside ALL frames (and screens.length > 1),
|
|
1802
|
+
// getTargetFrameForDraft returns undefined. Route to onBoardDrawPrimitive
|
|
1803
|
+
// so the board file captures the new element.
|
|
1804
|
+
if (!targetFrame) {
|
|
1805
|
+
const handler = onBoardDrawPrimitiveRef.current;
|
|
1806
|
+
if (handler) {
|
|
1807
|
+
// Convert the draft into a board-space CanvasPrimitiveInsert.
|
|
1808
|
+
// The board uses a 1:1 coordinate mapping (no frame scaling needed).
|
|
1809
|
+
const boardPrimitive = draftPrimitiveToInsert(draft, {
|
|
1810
|
+
x: 0,
|
|
1811
|
+
y: 0,
|
|
1812
|
+
width: 1,
|
|
1813
|
+
height: 1,
|
|
1814
|
+
});
|
|
1815
|
+
const persisted = handler(boardPrimitive);
|
|
1816
|
+
if (!persisted) return null;
|
|
1817
|
+
// Return a sentinel PersistedDraftPrimitive so the caller can remove
|
|
1818
|
+
// the draft. The sentinel frameId "__board__" is detected downstream.
|
|
1819
|
+
return {
|
|
1820
|
+
frameId: "__board__",
|
|
1821
|
+
nodeId:
|
|
1822
|
+
(typeof persisted === "string"
|
|
1823
|
+
? persisted
|
|
1824
|
+
: boardPrimitive.nodeId) ?? draft.id,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if (!onCreatePrimitive) {
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1566
1833
|
const targetScreen = screens.find(
|
|
1567
1834
|
(screen) => screen.id === targetFrame.id,
|
|
1568
1835
|
);
|
|
@@ -1580,7 +1847,9 @@ export function MultiScreenCanvas({
|
|
|
1580
1847
|
targetMetadata,
|
|
1581
1848
|
);
|
|
1582
1849
|
const persisted = onCreatePrimitive(targetFrame.id, localPrimitive);
|
|
1583
|
-
if (!persisted)
|
|
1850
|
+
if (!persisted) {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1584
1853
|
return {
|
|
1585
1854
|
frameId: targetFrame.id,
|
|
1586
1855
|
nodeId:
|
|
@@ -1606,7 +1875,9 @@ export function MultiScreenCanvas({
|
|
|
1606
1875
|
);
|
|
1607
1876
|
updateSelectedDraftIds(() => []);
|
|
1608
1877
|
updateSelectedIds(() => []);
|
|
1609
|
-
|
|
1878
|
+
if (persisted.frameId !== "__board__") {
|
|
1879
|
+
onPrimitiveCreated?.(persisted.frameId, persisted.nodeId);
|
|
1880
|
+
}
|
|
1610
1881
|
return;
|
|
1611
1882
|
}
|
|
1612
1883
|
|
|
@@ -1623,6 +1894,51 @@ export function MultiScreenCanvas({
|
|
|
1623
1894
|
],
|
|
1624
1895
|
);
|
|
1625
1896
|
|
|
1897
|
+
const retryPersistedDraftPrimitives = useCallback(() => {
|
|
1898
|
+
const drafts = draftPrimitivesRef.current;
|
|
1899
|
+
if (drafts.length === 0 || !onCreatePrimitive) return;
|
|
1900
|
+
|
|
1901
|
+
const persistedByDraftId = new Map<string, PersistedDraftPrimitive>();
|
|
1902
|
+
drafts.forEach((draft) => {
|
|
1903
|
+
const persisted = persistDraftPrimitive(draft);
|
|
1904
|
+
if (persisted) persistedByDraftId.set(draft.id, persisted);
|
|
1905
|
+
});
|
|
1906
|
+
if (persistedByDraftId.size === 0) return;
|
|
1907
|
+
|
|
1908
|
+
const selectedDraftIds = selectedDraftIdsRef.current;
|
|
1909
|
+
updateDraftPrimitives((current) =>
|
|
1910
|
+
current.filter((draft) => !persistedByDraftId.has(draft.id)),
|
|
1911
|
+
);
|
|
1912
|
+
updateSelectedDraftIds((current) =>
|
|
1913
|
+
current.filter((id) => !persistedByDraftId.has(id)),
|
|
1914
|
+
);
|
|
1915
|
+
updateSelectedIds(() => []);
|
|
1916
|
+
|
|
1917
|
+
const selectedPersisted = selectedDraftIds
|
|
1918
|
+
.map((id) => persistedByDraftId.get(id))
|
|
1919
|
+
.filter((entry): entry is PersistedDraftPrimitive => Boolean(entry));
|
|
1920
|
+
const persistedEntries =
|
|
1921
|
+
selectedPersisted.length > 0
|
|
1922
|
+
? selectedPersisted
|
|
1923
|
+
: Array.from(persistedByDraftId.values());
|
|
1924
|
+
const lastPersisted = persistedEntries[persistedEntries.length - 1];
|
|
1925
|
+
// Do not call onPrimitiveCreated for board objects (sentinel frameId).
|
|
1926
|
+
if (lastPersisted && lastPersisted.frameId !== "__board__") {
|
|
1927
|
+
onPrimitiveCreated?.(lastPersisted.frameId, lastPersisted.nodeId);
|
|
1928
|
+
}
|
|
1929
|
+
}, [
|
|
1930
|
+
onCreatePrimitive,
|
|
1931
|
+
onPrimitiveCreated,
|
|
1932
|
+
persistDraftPrimitive,
|
|
1933
|
+
updateDraftPrimitives,
|
|
1934
|
+
updateSelectedDraftIds,
|
|
1935
|
+
updateSelectedIds,
|
|
1936
|
+
]);
|
|
1937
|
+
|
|
1938
|
+
useEffect(() => {
|
|
1939
|
+
retryPersistedDraftPrimitives();
|
|
1940
|
+
}, [frameGeometry, retryPersistedDraftPrimitives, screens]);
|
|
1941
|
+
|
|
1626
1942
|
const clearActivePenPath = useCallback(() => {
|
|
1627
1943
|
setActivePenPath(null);
|
|
1628
1944
|
setPenGesturePreview(null);
|
|
@@ -1784,7 +2100,18 @@ export function MultiScreenCanvas({
|
|
|
1784
2100
|
finishDrag();
|
|
1785
2101
|
};
|
|
1786
2102
|
|
|
1787
|
-
|
|
2103
|
+
const cancelPenGesture = () => {
|
|
2104
|
+
const state = dragState.current;
|
|
2105
|
+
if (state?.type === "pen-node") {
|
|
2106
|
+
setActivePenPath(state.pathBefore);
|
|
2107
|
+
}
|
|
2108
|
+
setPenGesturePreview(null);
|
|
2109
|
+
setPenPointer(null);
|
|
2110
|
+
setPenCloseHover(false);
|
|
2111
|
+
finishDrag();
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
installDragListeners(handleMouseMove, handleMouseUp, cancelPenGesture);
|
|
1788
2115
|
},
|
|
1789
2116
|
[
|
|
1790
2117
|
finishDrag,
|
|
@@ -1804,15 +2131,12 @@ export function MultiScreenCanvas({
|
|
|
1804
2131
|
|
|
1805
2132
|
const originCanvas = getCanvasPoint(e.clientX, e.clientY);
|
|
1806
2133
|
const originFrameId = getFrameEntryAtPoint(originCanvas)?.id;
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
tool === "pen" || tool === "line" || tool === "arrow"
|
|
1814
|
-
? getDraftGeometryForTool(tool, originCanvas, originCanvas)
|
|
1815
|
-
: { x: originCanvas.x, y: originCanvas.y, width: 0, height: 0 };
|
|
2134
|
+
const initialGeometry = getDraftPreviewGeometryForTool(
|
|
2135
|
+
tool,
|
|
2136
|
+
originCanvas,
|
|
2137
|
+
originCanvas,
|
|
2138
|
+
false,
|
|
2139
|
+
);
|
|
1816
2140
|
const initialPoints =
|
|
1817
2141
|
tool === "pen"
|
|
1818
2142
|
? [originCanvas]
|
|
@@ -1871,10 +2195,11 @@ export function MultiScreenCanvas({
|
|
|
1871
2195
|
|
|
1872
2196
|
setCreationPreview({
|
|
1873
2197
|
tool,
|
|
1874
|
-
geometry:
|
|
2198
|
+
geometry: getDraftPreviewGeometryForTool(
|
|
1875
2199
|
tool,
|
|
1876
2200
|
state.originCanvas,
|
|
1877
2201
|
nextCanvas,
|
|
2202
|
+
state.hasMoved,
|
|
1878
2203
|
),
|
|
1879
2204
|
points:
|
|
1880
2205
|
state.tool === "line" || state.tool === "arrow"
|
|
@@ -3395,7 +3720,7 @@ export function MultiScreenCanvas({
|
|
|
3395
3720
|
<div
|
|
3396
3721
|
ref={surfaceRef}
|
|
3397
3722
|
className="relative h-full w-full select-none overflow-hidden"
|
|
3398
|
-
|
|
3723
|
+
onMouseDownCapture={handleMouseDown}
|
|
3399
3724
|
onMouseMove={handleMouseMove}
|
|
3400
3725
|
style={{ cursor: surfaceCursor, touchAction: "none" }}
|
|
3401
3726
|
>
|
|
@@ -3429,6 +3754,55 @@ export function MultiScreenCanvas({
|
|
|
3429
3754
|
transformOrigin: "top left",
|
|
3430
3755
|
}}
|
|
3431
3756
|
>
|
|
3757
|
+
{boardFileId &&
|
|
3758
|
+
boardFileContent !== undefined &&
|
|
3759
|
+
(() => {
|
|
3760
|
+
const boardGeo = boardFrameGeometry ?? {
|
|
3761
|
+
x: 0,
|
|
3762
|
+
y: 0,
|
|
3763
|
+
width: 8192,
|
|
3764
|
+
height: 8192,
|
|
3765
|
+
};
|
|
3766
|
+
// Clamp board canvas to the DesignCanvas max safe dimension.
|
|
3767
|
+
const boardW = Math.min(boardGeo.width, 16384);
|
|
3768
|
+
const boardH = Math.min(boardGeo.height, 16384);
|
|
3769
|
+
return (
|
|
3770
|
+
// Overflow-hidden wrapper so the board iframe never bleeds outside
|
|
3771
|
+
// its declared logical surface. z-index 0 keeps it below screen
|
|
3772
|
+
// iframes (which have their own stacking context above this).
|
|
3773
|
+
<div
|
|
3774
|
+
style={{
|
|
3775
|
+
position: "absolute",
|
|
3776
|
+
left: SURFACE_PADDING,
|
|
3777
|
+
top: SURFACE_PADDING,
|
|
3778
|
+
width: boardW,
|
|
3779
|
+
height: boardH,
|
|
3780
|
+
overflow: "hidden",
|
|
3781
|
+
pointerEvents: "auto",
|
|
3782
|
+
// Board sits behind all screen iframes.
|
|
3783
|
+
zIndex: 0,
|
|
3784
|
+
}}
|
|
3785
|
+
>
|
|
3786
|
+
<DesignCanvas
|
|
3787
|
+
content={boardFileContent}
|
|
3788
|
+
contentKey={boardFileId}
|
|
3789
|
+
zoom={100}
|
|
3790
|
+
deviceFrame="none"
|
|
3791
|
+
editMode={boardEditMode}
|
|
3792
|
+
interactMode={false}
|
|
3793
|
+
registerRuntimeBridge={false}
|
|
3794
|
+
onElementSelect={onBoardElementSelect ?? (() => {})}
|
|
3795
|
+
onElementHover={onBoardElementHover ?? (() => {})}
|
|
3796
|
+
onVisualStructureChange={onBoardVisualStructureChange}
|
|
3797
|
+
onVisualStyleChange={onBoardVisualStyleChange}
|
|
3798
|
+
onVisualDuplicateChange={onBoardVisualDuplicateChange}
|
|
3799
|
+
onTextContentChange={onBoardTextContentChange}
|
|
3800
|
+
tweakValues={{}}
|
|
3801
|
+
/>
|
|
3802
|
+
</div>
|
|
3803
|
+
);
|
|
3804
|
+
})()}
|
|
3805
|
+
|
|
3432
3806
|
{canvasFrames.map(({ screen, metadata, geometry }) => {
|
|
3433
3807
|
return (
|
|
3434
3808
|
<Screen
|
|
@@ -3575,6 +3949,14 @@ export function MultiScreenCanvas({
|
|
|
3575
3949
|
))}
|
|
3576
3950
|
</div>
|
|
3577
3951
|
|
|
3952
|
+
{penActive || creationToolActive ? (
|
|
3953
|
+
<div
|
|
3954
|
+
data-canvas-creation-shield
|
|
3955
|
+
className="pointer-events-auto absolute inset-0 z-30 cursor-crosshair"
|
|
3956
|
+
aria-hidden="true"
|
|
3957
|
+
/>
|
|
3958
|
+
) : null}
|
|
3959
|
+
|
|
3578
3960
|
{marquee ? (
|
|
3579
3961
|
<span
|
|
3580
3962
|
className="pointer-events-none absolute z-40 border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)]"
|
|
@@ -3731,6 +4113,7 @@ function DraftPrimitiveLayer({
|
|
|
3731
4113
|
return (
|
|
3732
4114
|
<button
|
|
3733
4115
|
data-frame-shell
|
|
4116
|
+
data-screen-shell
|
|
3734
4117
|
type="button"
|
|
3735
4118
|
className={cn(
|
|
3736
4119
|
"group/artboard pointer-events-auto absolute block overflow-visible text-left outline-none",
|
|
@@ -4109,17 +4492,31 @@ const Screen = memo(function Screen({
|
|
|
4109
4492
|
(directlyHovered || isDirectlyHovered) &&
|
|
4110
4493
|
!creationToolActive &&
|
|
4111
4494
|
!canvasGestureActive;
|
|
4495
|
+
const suppressFrameChromeForChild =
|
|
4496
|
+
hasHoveredChild && !directlyHovered && !isDirectlyHovered;
|
|
4112
4497
|
const emphasized = isSelected || frameDirectlyHovered;
|
|
4113
4498
|
const fullViewVisible = emphasized || showFullView;
|
|
4114
4499
|
const activeOrEmphasized = isActive || emphasized;
|
|
4115
4500
|
const selectionOutlined = isSelected && !groupSelected;
|
|
4116
4501
|
const showHoverChrome =
|
|
4117
|
-
frameDirectlyHovered &&
|
|
4502
|
+
frameDirectlyHovered &&
|
|
4503
|
+
!isSelected &&
|
|
4504
|
+
!groupSelected &&
|
|
4505
|
+
!suppressFrameChromeForChild;
|
|
4118
4506
|
const screenContentInteractive =
|
|
4119
4507
|
Boolean(screenContent) &&
|
|
4120
4508
|
!penActive &&
|
|
4121
4509
|
!creationToolActive &&
|
|
4122
4510
|
!canvasGestureActive;
|
|
4511
|
+
// Memoize the srcdoc with the hit-test responder injected so we don't
|
|
4512
|
+
// rebuild the string every render (that would reload the iframe).
|
|
4513
|
+
// Keyed only on screen.content; the hit-test script itself is constant.
|
|
4514
|
+
const srcdocWithHitTest = useMemo(
|
|
4515
|
+
() => appendHitTestResponder(screen.content),
|
|
4516
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4517
|
+
[screen.content],
|
|
4518
|
+
);
|
|
4519
|
+
|
|
4123
4520
|
const updateDirectHover = useCallback((next: boolean) => {
|
|
4124
4521
|
setDirectlyHovered((current) => (current === next ? current : next));
|
|
4125
4522
|
}, []);
|
|
@@ -4148,6 +4545,7 @@ const Screen = memo(function Screen({
|
|
|
4148
4545
|
return (
|
|
4149
4546
|
<div
|
|
4150
4547
|
data-frame-shell
|
|
4548
|
+
data-screen-shell
|
|
4151
4549
|
className="group/frame pointer-events-auto absolute"
|
|
4152
4550
|
style={{
|
|
4153
4551
|
left: SURFACE_PADDING + geometry.x,
|
|
@@ -4338,17 +4736,15 @@ const Screen = memo(function Screen({
|
|
|
4338
4736
|
<span
|
|
4339
4737
|
data-screen-content
|
|
4340
4738
|
className={cn(
|
|
4341
|
-
"relative block h-full w-full overflow-hidden rounded-
|
|
4342
|
-
showHoverChrome
|
|
4343
|
-
? "border-[var(--design-editor-accent-color)]"
|
|
4344
|
-
: "border-border",
|
|
4739
|
+
"relative block h-full w-full overflow-hidden rounded-[inherit] bg-white shadow-2xl ring-1 ring-inset ring-border transition-colors",
|
|
4345
4740
|
)}
|
|
4346
4741
|
style={{ pointerEvents: screenContentInteractive ? "auto" : "none" }}
|
|
4347
4742
|
>
|
|
4348
4743
|
{screenContent ?? (
|
|
4349
4744
|
<iframe
|
|
4745
|
+
data-screen-iframe-id={screen.id}
|
|
4350
4746
|
src={previewUrl}
|
|
4351
|
-
srcDoc={previewUrl ? undefined :
|
|
4747
|
+
srcDoc={previewUrl ? undefined : srcdocWithHitTest}
|
|
4352
4748
|
sandbox="allow-scripts"
|
|
4353
4749
|
loading="lazy"
|
|
4354
4750
|
className="pointer-events-none border-0"
|
|
@@ -4388,8 +4784,20 @@ const Screen = memo(function Screen({
|
|
|
4388
4784
|
aria-hidden="true"
|
|
4389
4785
|
/>
|
|
4390
4786
|
) : null}
|
|
4391
|
-
<span className="pointer-events-none absolute inset-0 rounded-[7px] border border-black/5" />
|
|
4392
4787
|
</span>
|
|
4788
|
+
<span
|
|
4789
|
+
data-screen-hover-outline
|
|
4790
|
+
className={cn(
|
|
4791
|
+
"pointer-events-none absolute inset-0 z-10 rounded-[inherit] border border-[var(--design-editor-accent-color)] transition-opacity",
|
|
4792
|
+
showHoverChrome ? "opacity-100" : "opacity-0",
|
|
4793
|
+
)}
|
|
4794
|
+
style={{
|
|
4795
|
+
borderWidth: 1.5 * chromeScale,
|
|
4796
|
+
transition: getChromeBorderTransition(chromeSettling),
|
|
4797
|
+
}}
|
|
4798
|
+
aria-hidden="true"
|
|
4799
|
+
/>
|
|
4800
|
+
<span className="pointer-events-none absolute inset-0 rounded-[inherit] border border-black/5" />
|
|
4393
4801
|
<ResizeHandles
|
|
4394
4802
|
active={false}
|
|
4395
4803
|
enabled={
|
|
@@ -4417,6 +4825,7 @@ const Screen = memo(function Screen({
|
|
|
4417
4825
|
screen={screen}
|
|
4418
4826
|
primaryGeometry={geometry}
|
|
4419
4827
|
previewUrl={previewUrl}
|
|
4828
|
+
srcdocWithHitTest={srcdocWithHitTest}
|
|
4420
4829
|
activeBreakpointWidth={screen.activeBreakpointWidth}
|
|
4421
4830
|
penActive={penActive}
|
|
4422
4831
|
creationToolActive={creationToolActive}
|
|
@@ -4465,6 +4874,7 @@ function BreakpointPreviewRow({
|
|
|
4465
4874
|
screen,
|
|
4466
4875
|
primaryGeometry,
|
|
4467
4876
|
previewUrl,
|
|
4877
|
+
srcdocWithHitTest,
|
|
4468
4878
|
activeBreakpointWidth,
|
|
4469
4879
|
penActive,
|
|
4470
4880
|
creationToolActive,
|
|
@@ -4475,6 +4885,13 @@ function BreakpointPreviewRow({
|
|
|
4475
4885
|
screen: ScreenFile;
|
|
4476
4886
|
primaryGeometry: FrameGeometry;
|
|
4477
4887
|
previewUrl: string | undefined;
|
|
4888
|
+
/**
|
|
4889
|
+
* The primary screen's srcdoc with the lightweight hit-test responder already
|
|
4890
|
+
* injected (memoised in the parent Screen component). Passed down so
|
|
4891
|
+
* breakpoint sub-iframes carry the same responder and can be found via
|
|
4892
|
+
* [data-screen-iframe-id] by the cross-screen drop-into-container handler.
|
|
4893
|
+
*/
|
|
4894
|
+
srcdocWithHitTest: string;
|
|
4478
4895
|
activeBreakpointWidth: number | undefined;
|
|
4479
4896
|
penActive: boolean;
|
|
4480
4897
|
creationToolActive: boolean;
|
|
@@ -4563,8 +4980,9 @@ function BreakpointPreviewRow({
|
|
|
4563
4980
|
style={{ width: frameWidth, height: frameHeight }}
|
|
4564
4981
|
>
|
|
4565
4982
|
<iframe
|
|
4983
|
+
data-screen-iframe-id={screen.id}
|
|
4566
4984
|
src={previewUrl}
|
|
4567
|
-
srcDoc={previewUrl ? undefined :
|
|
4985
|
+
srcDoc={previewUrl ? undefined : srcdocWithHitTest}
|
|
4568
4986
|
sandbox="allow-scripts"
|
|
4569
4987
|
loading="lazy"
|
|
4570
4988
|
className="pointer-events-none border-0"
|
|
@@ -5240,6 +5658,23 @@ function isDraftPrimitive(
|
|
|
5240
5658
|
return Boolean(value);
|
|
5241
5659
|
}
|
|
5242
5660
|
|
|
5661
|
+
export function getDraftPreviewGeometryForTool(
|
|
5662
|
+
tool: DraftCreationTool,
|
|
5663
|
+
start: Point,
|
|
5664
|
+
end: Point,
|
|
5665
|
+
hasMoved: boolean,
|
|
5666
|
+
): FrameGeometry {
|
|
5667
|
+
if (tool === "pen" || tool === "line" || tool === "arrow") {
|
|
5668
|
+
return getDraftGeometryForTool(tool, start, end);
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5671
|
+
if (!hasMoved) {
|
|
5672
|
+
return { x: start.x, y: start.y, width: 0, height: 0 };
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
return getDraftGeometryForTool(tool, start, end);
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5243
5678
|
function getDraftGeometryForTool(
|
|
5244
5679
|
tool: DraftCreationTool,
|
|
5245
5680
|
start: Point,
|