@agent-native/core 0.92.9 → 0.92.11
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 +29 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +181 -15
- package/corpus/core/src/agent/run-manager.ts +31 -4
- package/corpus/core/src/agent/run-store.ts +80 -0
- package/corpus/core/src/cli/design-connect.ts +433 -151
- package/corpus/core/src/cli/skills.ts +14 -0
- package/corpus/core/src/client/AssistantChat.tsx +25 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
- package/corpus/core/src/client/agent-chat.ts +165 -1
- package/corpus/core/src/client/chat/index.ts +9 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
- package/corpus/core/src/collab/awareness-store.ts +23 -12
- package/corpus/core/src/collab/client.ts +16 -5
- package/corpus/core/src/collab/index.ts +4 -0
- package/corpus/core/src/collab/recent-edits.ts +65 -2
- package/corpus/core/src/deploy/build.ts +133 -5
- package/corpus/core/src/index.browser.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
- package/corpus/core/src/server/builder-design-systems.ts +30 -4
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
- package/corpus/templates/clips/AGENTS.md +4 -0
- package/corpus/templates/clips/actions/create-recording.ts +2 -0
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
- package/corpus/templates/clips/actions/request-transcript.ts +102 -33
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +48 -4
- package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
- package/corpus/templates/clips/app/routes/record.tsx +17 -4
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +12 -2
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
- package/corpus/templates/clips/desktop/src/styles.css +6 -3
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
- package/corpus/templates/clips/shared/recording-core.ts +12 -0
- package/corpus/templates/content/AGENTS.md +7 -6
- package/corpus/templates/content/actions/_document-flush.ts +132 -17
- package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
- package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
- package/corpus/templates/content/actions/link-notion-page.ts +2 -0
- package/corpus/templates/content/actions/pull-document.ts +5 -7
- package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
- package/corpus/templates/content/actions/push-notion-page.ts +10 -0
- package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
- package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
- package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/content/app/i18n-data.ts +18 -0
- package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
- package/corpus/templates/design/AGENTS.md +45 -10
- package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
- package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
- package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
- package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
- package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
- package/corpus/templates/design/actions/generate-design.ts +79 -12
- package/corpus/templates/design/actions/generate-screens.ts +57 -12
- package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
- package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
- package/corpus/templates/design/actions/import-design-source.ts +4 -31
- package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
- package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
- package/corpus/templates/design/actions/list-design-components.ts +104 -0
- package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
- package/corpus/templates/design/actions/open-component-source.ts +2 -4
- package/corpus/templates/design/actions/present-design-variants.ts +412 -16
- package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
- package/corpus/templates/design/actions/run-design-audit.ts +25 -3
- package/corpus/templates/design/actions/show-design-questions.ts +10 -0
- package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
- package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
- package/corpus/templates/design/actions/write-local-file.ts +25 -1
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
- package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
- package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
- package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
- package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
- package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
- package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
- package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
- package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
- package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
- package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
- package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
- package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
- package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
- package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
- package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
- package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
- package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
- package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
- package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
- package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
- package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
- package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
- package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
- package/corpus/templates/design/app/components/design/types.ts +33 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
- package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
- package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
- package/corpus/templates/design/app/i18n-data.ts +1217 -10
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
- package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
- package/corpus/templates/design/app/lib/design-import.ts +24 -0
- package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
- package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
- package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
- package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
- package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
- package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
- package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -6
- package/corpus/templates/design/e2e/helpers.ts +27 -6
- package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
- package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
- package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
- package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
- package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
- package/corpus/templates/design/server/plugins/db.ts +41 -0
- package/corpus/templates/design/server/source-workspace.ts +64 -0
- package/corpus/templates/design/shared/board-file.ts +13 -2
- package/corpus/templates/design/shared/canvas-math.ts +231 -33
- package/corpus/templates/design/shared/code-layer.ts +122 -8
- package/corpus/templates/design/shared/component-library.ts +161 -0
- package/corpus/templates/design/shared/db-conflict.ts +27 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
- package/corpus/templates/design/shared/figma-url.ts +149 -0
- package/corpus/templates/design/shared/pen-path.ts +16 -7
- package/corpus/templates/design/shared/source-mode.ts +1 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
- package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
- package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
- package/dist/agent/production-agent.d.ts +79 -6
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +161 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +29 -5
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +46 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +61 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +336 -139
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +14 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +19 -3
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +38 -18
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +60 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +122 -1
- package/dist/client/agent-chat.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/extensions/EmbeddedExtension.d.ts +12 -0
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +36 -2
- package/dist/client/extensions/EmbeddedExtension.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/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/collab/awareness-store.d.ts +5 -0
- package/dist/collab/awareness-store.d.ts.map +1 -1
- package/dist/collab/awareness-store.js +19 -12
- package/dist/collab/awareness-store.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +13 -5
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.d.ts +1 -0
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +1 -0
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/recent-edits.d.ts +3 -1
- package/dist/collab/recent-edits.d.ts.map +1 -1
- package/dist/collab/recent-edits.js +57 -2
- package/dist/collab/recent-edits.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +24 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +102 -5
- package/dist/deploy/build.js.map +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +87 -10
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +15 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +9 -4
- package/dist/server/builder-design-systems.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getResizeCursorForHandle,
|
|
16
16
|
resizeFrameGroupFromDelta,
|
|
17
17
|
resizeFrameGroupToBounds,
|
|
18
|
-
|
|
18
|
+
resizeRotatedFrameFromDeltaWithSnap,
|
|
19
19
|
rotateFrameGroupAroundCenter,
|
|
20
20
|
rotatedRectIntersects,
|
|
21
21
|
screenToCanvasPoint,
|
|
@@ -152,6 +152,7 @@ const EMPTY_SELECTED_LAYER_SELECTOR_GROUPS_BY_SCREEN: Record<
|
|
|
152
152
|
string,
|
|
153
153
|
string[][]
|
|
154
154
|
> = {};
|
|
155
|
+
const EMPTY_SCREEN_IDS: readonly string[] = [];
|
|
155
156
|
// Shared with canvas-math.ts (DEFAULT_CANVAS_MIN_ZOOM/DEFAULT_CANVAS_MAX_ZOOM)
|
|
156
157
|
// so this surface's zoom clamp lives in one place instead of being
|
|
157
158
|
// redeclared locally and drifting from the shared constant.
|
|
@@ -165,7 +166,9 @@ import {
|
|
|
165
166
|
BOARD_SURFACE_BACKGROUND,
|
|
166
167
|
getBoardContentKey,
|
|
167
168
|
getBoardContentLayerSignature,
|
|
169
|
+
getBoardSurfaceContentBounds,
|
|
168
170
|
getBoardSurfaceRenderContent,
|
|
171
|
+
getBoardSurfaceStaticPreviewContent,
|
|
169
172
|
hasBoardSurfaceContent,
|
|
170
173
|
} from "./multi-screen/board-surface-html";
|
|
171
174
|
import {
|
|
@@ -197,9 +200,14 @@ import {
|
|
|
197
200
|
isBreakpointSelectionTarget,
|
|
198
201
|
} from "./multi-screen/iframe-targeting";
|
|
199
202
|
import {
|
|
203
|
+
boardPointToBoardSurfaceLocalPoint,
|
|
204
|
+
boardSurfaceLocalPointToBoardPoint,
|
|
205
|
+
getBoardSurfaceRenderGeometry,
|
|
200
206
|
getBoardSurfaceLayerStyle,
|
|
207
|
+
getBoardSurfaceStaticPreviewViewport,
|
|
201
208
|
isLineupShrinkOnlyChange,
|
|
202
209
|
OVERVIEW_FRAME_WIDTH,
|
|
210
|
+
shouldRenderBoardSurfaceStaticPreview,
|
|
203
211
|
shouldSuppressLineupRecenter,
|
|
204
212
|
SURFACE_PADDING,
|
|
205
213
|
type LineupRecenterDuplicateArm,
|
|
@@ -280,6 +288,7 @@ import {
|
|
|
280
288
|
findTopFrameEntryAtPoint,
|
|
281
289
|
frameGeometryWithOverrides,
|
|
282
290
|
frameStyleLeftTop,
|
|
291
|
+
geometryContainsPoint,
|
|
283
292
|
getBreakpointFrameGeometry,
|
|
284
293
|
getFrameCenter,
|
|
285
294
|
getInitialFrameGeometry,
|
|
@@ -302,17 +311,84 @@ import {
|
|
|
302
311
|
} from "./multi-screen/gradient-overlay-geometry";
|
|
303
312
|
import {
|
|
304
313
|
getPrimitiveDropTargetForPoint,
|
|
314
|
+
getPrimitiveLowZoomHitRect,
|
|
315
|
+
parsePrimitivesFromScreen,
|
|
305
316
|
resolveNodeScreenId,
|
|
306
317
|
type PrimitiveDropTarget,
|
|
307
318
|
} from "./multi-screen/primitive-drop-target";
|
|
308
319
|
import type { AlignmentGuide, CanvasFrameEntry } from "./multi-screen/types";
|
|
309
320
|
import { vectorEditCanvasToLocalPoint } from "./multi-screen/vector-edit-geometry";
|
|
310
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Imperatively writes a draft primitive's full visual state onto its cached
|
|
324
|
+
* DOM node — the outer box (left/top/width/height/rotation, matching
|
|
325
|
+
* DraftPrimitiveLayer's own inline style) plus, for kinds whose rendered
|
|
326
|
+
* content depends on geometry rather than plain CSS 100%-sizing (path/line/
|
|
327
|
+
* arrow's <path d>+viewBox, polygon/star's <polygon points>+viewBox), the
|
|
328
|
+
* inner SVG content too. Used by beginDraftResize's live mousemove tick (PERF9:
|
|
329
|
+
* ref-only writes instead of setDraftPrimitives per tick) and by
|
|
330
|
+
* cancelActiveDrag's Escape-cancel restore, so both paths stay in sync with
|
|
331
|
+
* exactly what DraftPrimitiveLayer/DraftPrimitiveContent would have rendered.
|
|
332
|
+
* rect/ellipse/text/frame kinds need no extra work here: their content divs
|
|
333
|
+
* are `size-full` and already track the outer box via normal CSS layout.
|
|
334
|
+
*/
|
|
335
|
+
function applyDraftPrimitiveToDom(
|
|
336
|
+
element: HTMLElement,
|
|
337
|
+
draft: DraftPrimitive,
|
|
338
|
+
): void {
|
|
339
|
+
const { geometry } = draft;
|
|
340
|
+
const { left, top } = frameStyleLeftTop(geometry);
|
|
341
|
+
element.style.left = `${left}px`;
|
|
342
|
+
element.style.top = `${top}px`;
|
|
343
|
+
element.style.width = `${geometry.width}px`;
|
|
344
|
+
element.style.height = `${geometry.height}px`;
|
|
345
|
+
element.style.transform = geometry.rotation
|
|
346
|
+
? `rotate(${geometry.rotation}deg)`
|
|
347
|
+
: "";
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
draft.kind === "path" ||
|
|
351
|
+
draft.kind === "line" ||
|
|
352
|
+
draft.kind === "arrow"
|
|
353
|
+
) {
|
|
354
|
+
const svgEl = element.querySelector("svg");
|
|
355
|
+
const pathEl = element.querySelector("path");
|
|
356
|
+
svgEl?.setAttribute(
|
|
357
|
+
"viewBox",
|
|
358
|
+
`${geometry.x} ${geometry.y} ${geometry.width} ${geometry.height}`,
|
|
359
|
+
);
|
|
360
|
+
if (pathEl) {
|
|
361
|
+
const pathData =
|
|
362
|
+
draft.pathData ??
|
|
363
|
+
(draft.penPath
|
|
364
|
+
? serializePenPath(draft.penPath)
|
|
365
|
+
: pointsToPath(draft.points ?? []));
|
|
366
|
+
pathEl.setAttribute("d", pathData);
|
|
367
|
+
if (draft.strokeWidth !== undefined) {
|
|
368
|
+
pathEl.setAttribute("stroke-width", String(draft.strokeWidth));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if (draft.kind === "polygon" || draft.kind === "star") {
|
|
372
|
+
const svgEl = element.querySelector("svg");
|
|
373
|
+
const polygonEl = element.querySelector("polygon");
|
|
374
|
+
svgEl?.setAttribute(
|
|
375
|
+
"viewBox",
|
|
376
|
+
`0 0 ${Math.max(1, geometry.width)} ${Math.max(1, geometry.height)}`,
|
|
377
|
+
);
|
|
378
|
+
polygonEl?.setAttribute(
|
|
379
|
+
"points",
|
|
380
|
+
polygonPointsForBox(draft.kind, geometry.width, geometry.height),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
311
385
|
export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
312
386
|
screens,
|
|
313
387
|
zoom,
|
|
314
388
|
activeId,
|
|
315
389
|
selectedScreenIds,
|
|
390
|
+
hiddenScreenIds = EMPTY_SCREEN_IDS,
|
|
391
|
+
lockedScreenIds = EMPTY_SCREEN_IDS,
|
|
316
392
|
fullViewScreenIds,
|
|
317
393
|
activeScreenHasHoveredChild = false,
|
|
318
394
|
hoveredChildScreenId,
|
|
@@ -431,9 +507,179 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
431
507
|
// error, not just a stale-closure risk). Kept in sync by the effect right
|
|
432
508
|
// below the other prop/state-mirroring refs (see onGeometryChangeRef etc.).
|
|
433
509
|
const effectiveToolRef = useRef<MultiScreenCanvasTool>("move");
|
|
434
|
-
const
|
|
435
|
-
|
|
510
|
+
const hiddenScreenIdSet = useMemo(
|
|
511
|
+
() => new Set(hiddenScreenIds),
|
|
512
|
+
[hiddenScreenIds],
|
|
513
|
+
);
|
|
514
|
+
const lockedScreenIdSet = useMemo(
|
|
515
|
+
() => new Set(lockedScreenIds),
|
|
516
|
+
[lockedScreenIds],
|
|
517
|
+
);
|
|
518
|
+
const renderedScreens = useMemo(
|
|
519
|
+
() => screens.filter((screen) => !hiddenScreenIdSet.has(screen.id)),
|
|
520
|
+
[hiddenScreenIdSet, screens],
|
|
521
|
+
);
|
|
522
|
+
const selectableScreens = useMemo(
|
|
523
|
+
() => renderedScreens.filter((screen) => !lockedScreenIdSet.has(screen.id)),
|
|
524
|
+
[lockedScreenIdSet, renderedScreens],
|
|
525
|
+
);
|
|
526
|
+
const [selectedIds, setSelectedIds] = useState<string[]>(() =>
|
|
527
|
+
(selectedScreenIds ?? []).filter((id) =>
|
|
528
|
+
selectableScreens.some((screen) => screen.id === id),
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
const screenIndexById = useMemo(
|
|
532
|
+
() => new Map(screens.map((screen, index) => [screen.id, index] as const)),
|
|
533
|
+
[screens],
|
|
534
|
+
);
|
|
535
|
+
const [boardSurfaceFocusPoint, setBoardSurfaceFocusPoint] =
|
|
536
|
+
useState<Point | null>(null);
|
|
537
|
+
const pendingStaticBoardSelectionRef = useRef<{
|
|
538
|
+
nodeId: string;
|
|
539
|
+
point: Point;
|
|
540
|
+
} | null>(null);
|
|
541
|
+
const cancelPendingStaticBoardSelection = useCallback(() => {
|
|
542
|
+
pendingStaticBoardSelectionRef.current = null;
|
|
543
|
+
setBoardSurfaceFocusPoint(null);
|
|
544
|
+
}, []);
|
|
545
|
+
const boardSurfaceContentBounds = useMemo(
|
|
546
|
+
() => getBoardSurfaceContentBounds(boardFileContent),
|
|
547
|
+
[boardFileContent],
|
|
548
|
+
);
|
|
549
|
+
const boardViewportGeometry = useMemo((): FrameGeometry | undefined => {
|
|
550
|
+
if (surfaceSize.width <= 0 || surfaceSize.height <= 0) return undefined;
|
|
551
|
+
const scale = Math.max(0.0001, canvasZoom / 100);
|
|
552
|
+
return {
|
|
553
|
+
x: -pan.x / scale - SURFACE_PADDING,
|
|
554
|
+
y: -pan.y / scale - SURFACE_PADDING,
|
|
555
|
+
width: surfaceSize.width / scale,
|
|
556
|
+
height: surfaceSize.height / scale,
|
|
557
|
+
};
|
|
558
|
+
}, [canvasZoom, pan.x, pan.y, surfaceSize.height, surfaceSize.width]);
|
|
559
|
+
const boardSurfaceRenderGeometry = useMemo(() => {
|
|
560
|
+
if (!boardFrameGeometry) return undefined;
|
|
561
|
+
const focusGeometry = boardSurfaceFocusPoint
|
|
562
|
+
? {
|
|
563
|
+
x: boardSurfaceFocusPoint.x,
|
|
564
|
+
y: boardSurfaceFocusPoint.y,
|
|
565
|
+
width: 1,
|
|
566
|
+
height: 1,
|
|
567
|
+
}
|
|
568
|
+
: (boardViewportGeometry ?? boardSurfaceContentBounds);
|
|
569
|
+
return getBoardSurfaceRenderGeometry({
|
|
570
|
+
logicalGeometry: boardFrameGeometry,
|
|
571
|
+
contentBounds: boardSurfaceContentBounds,
|
|
572
|
+
screenGeometries: [
|
|
573
|
+
...Object.values(frameGeometry),
|
|
574
|
+
...(boardViewportGeometry ? [boardViewportGeometry] : []),
|
|
575
|
+
],
|
|
576
|
+
focus: focusGeometry ? getFrameCenter(focusGeometry) : undefined,
|
|
577
|
+
});
|
|
578
|
+
}, [
|
|
579
|
+
boardFrameGeometry,
|
|
580
|
+
boardSurfaceContentBounds,
|
|
581
|
+
boardSurfaceFocusPoint,
|
|
582
|
+
boardViewportGeometry,
|
|
583
|
+
frameGeometry,
|
|
584
|
+
]);
|
|
585
|
+
const boardStaticPreviewViewport = useMemo(
|
|
586
|
+
() =>
|
|
587
|
+
boardFrameGeometry
|
|
588
|
+
? getBoardSurfaceStaticPreviewViewport(boardFrameGeometry)
|
|
589
|
+
: null,
|
|
590
|
+
[boardFrameGeometry],
|
|
436
591
|
);
|
|
592
|
+
const boardStaticPreviewContent = useMemo(() => {
|
|
593
|
+
if (
|
|
594
|
+
!boardFrameGeometry ||
|
|
595
|
+
!boardStaticPreviewViewport ||
|
|
596
|
+
!boardFileContent
|
|
597
|
+
) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
return getBoardSurfaceStaticPreviewContent({
|
|
601
|
+
html: boardFileContent,
|
|
602
|
+
logicalGeometry: boardFrameGeometry,
|
|
603
|
+
viewport: boardStaticPreviewViewport,
|
|
604
|
+
});
|
|
605
|
+
}, [boardFileContent, boardFrameGeometry, boardStaticPreviewViewport]);
|
|
606
|
+
const showBoardStaticPreview = Boolean(
|
|
607
|
+
boardFrameGeometry &&
|
|
608
|
+
boardSurfaceRenderGeometry &&
|
|
609
|
+
boardStaticPreviewContent &&
|
|
610
|
+
shouldRenderBoardSurfaceStaticPreview({
|
|
611
|
+
zoom: canvasZoom,
|
|
612
|
+
viewportGeometry: boardViewportGeometry,
|
|
613
|
+
renderGeometry: boardSurfaceRenderGeometry,
|
|
614
|
+
}),
|
|
615
|
+
);
|
|
616
|
+
const boardStaticPrimitives = useMemo(() => {
|
|
617
|
+
if (!boardFileId || !boardFileContent) return [];
|
|
618
|
+
return parsePrimitivesFromScreen({
|
|
619
|
+
id: boardFileId,
|
|
620
|
+
filename: "__board__.html",
|
|
621
|
+
content: boardFileContent,
|
|
622
|
+
});
|
|
623
|
+
}, [boardFileContent, boardFileId]);
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
cancelPendingStaticBoardSelection();
|
|
626
|
+
}, [cancelPendingStaticBoardSelection, canvasZoom, pan.x, pan.y]);
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
cancelPendingStaticBoardSelection();
|
|
629
|
+
}, [
|
|
630
|
+
activeTool,
|
|
631
|
+
boardFileContent,
|
|
632
|
+
boardFileId,
|
|
633
|
+
boardFrameGeometry?.height,
|
|
634
|
+
boardFrameGeometry?.width,
|
|
635
|
+
boardFrameGeometry?.x,
|
|
636
|
+
boardFrameGeometry?.y,
|
|
637
|
+
cancelPendingStaticBoardSelection,
|
|
638
|
+
localActiveTool,
|
|
639
|
+
]);
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
if (!showBoardStaticPreview) cancelPendingStaticBoardSelection();
|
|
642
|
+
}, [cancelPendingStaticBoardSelection, showBoardStaticPreview]);
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
const pending = pendingStaticBoardSelectionRef.current;
|
|
645
|
+
if (
|
|
646
|
+
!pending ||
|
|
647
|
+
!boardFileId ||
|
|
648
|
+
!boardSurfaceRenderGeometry ||
|
|
649
|
+
!geometryContainsPoint(boardSurfaceRenderGeometry, pending.point)
|
|
650
|
+
) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const selector = `[data-agent-native-node-id="${CSS.escape(pending.nodeId)}"]`;
|
|
654
|
+
let secondFrame: number | null = null;
|
|
655
|
+
const firstFrame = window.requestAnimationFrame(() => {
|
|
656
|
+
secondFrame = window.requestAnimationFrame(() => {
|
|
657
|
+
if (pendingStaticBoardSelectionRef.current !== pending) return;
|
|
658
|
+
const iframe = findCanvasIframeForScreen(
|
|
659
|
+
surfaceRef.current,
|
|
660
|
+
boardFileId,
|
|
661
|
+
boardFileId,
|
|
662
|
+
);
|
|
663
|
+
const targetWindow = iframe?.contentWindow;
|
|
664
|
+
if (!targetWindow) return;
|
|
665
|
+
targetWindow.postMessage(
|
|
666
|
+
{
|
|
667
|
+
type: "select-element",
|
|
668
|
+
selector,
|
|
669
|
+
selectorCandidates: [selector],
|
|
670
|
+
},
|
|
671
|
+
"*",
|
|
672
|
+
);
|
|
673
|
+
if (pendingStaticBoardSelectionRef.current === pending) {
|
|
674
|
+
pendingStaticBoardSelectionRef.current = null;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
return () => {
|
|
679
|
+
window.cancelAnimationFrame(firstFrame);
|
|
680
|
+
if (secondFrame !== null) window.cancelAnimationFrame(secondFrame);
|
|
681
|
+
};
|
|
682
|
+
}, [boardFileId, boardSurfaceRenderGeometry]);
|
|
437
683
|
const selectedIdsRef = useRef(selectedIds);
|
|
438
684
|
const dragState = useRef<DragState | null>(null);
|
|
439
685
|
const dragCleanup = useRef<(() => void) | null>(null);
|
|
@@ -508,6 +754,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
508
754
|
);
|
|
509
755
|
const [duplicatePreview, setDuplicatePreview] =
|
|
510
756
|
useState<DuplicatePreview | null>(null);
|
|
757
|
+
// PERF9: DOM node backing the alt-drag duplicate ghost, cached so
|
|
758
|
+
// beginDuplicateGesture's live mousemove tick can write left/top directly
|
|
759
|
+
// instead of calling setDuplicatePreview (a full re-render) every frame —
|
|
760
|
+
// same "mutate the DOM now, commit React state only when something
|
|
761
|
+
// conditional actually changes" discipline as the frame/draft drag paths.
|
|
762
|
+
const duplicatePreviewElRef = useRef<HTMLDivElement | null>(null);
|
|
511
763
|
const [transformBadge, setTransformBadge] = useState<TransformBadge | null>(
|
|
512
764
|
null,
|
|
513
765
|
);
|
|
@@ -965,7 +1217,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
965
1217
|
// to the first selected/first-on-board screen in that case. Mirror the
|
|
966
1218
|
// same fallback chain so the anchor matches whichever frame actually
|
|
967
1219
|
// drove the zoom-scale recompute.
|
|
968
|
-
const referenceId =
|
|
1220
|
+
const referenceId =
|
|
1221
|
+
(activeId && renderedScreens.some((screen) => screen.id === activeId)
|
|
1222
|
+
? activeId
|
|
1223
|
+
: undefined) ??
|
|
1224
|
+
selectedIds[0] ??
|
|
1225
|
+
renderedScreens[0]?.id;
|
|
969
1226
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
970
1227
|
// Prefer the `geometryById` prop over `frameGeometryRef` here: a screen
|
|
971
1228
|
// just created by the frame tool has its geometry committed straight
|
|
@@ -1018,10 +1275,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1018
1275
|
// P18: an externally-driven zoom change (toolbar/keyboard) also moves
|
|
1019
1276
|
// the canvas-space mapping the pen ghost preview was computed from.
|
|
1020
1277
|
recomputePenPointerForViewChangeRef.current();
|
|
1021
|
-
}, [zoom, activeId]);
|
|
1278
|
+
}, [zoom, activeId, renderedScreens, selectedIds]);
|
|
1022
1279
|
|
|
1023
1280
|
useEffect(() => {
|
|
1024
|
-
const
|
|
1281
|
+
const selectableIds = new Set(selectableScreens.map((screen) => screen.id));
|
|
1025
1282
|
// B5-9: see resolveFrameGeometrySync's doc comment — this used to notify
|
|
1026
1283
|
// the parent (onGeometryChange -> queueFrameGeometrySave) with a brand
|
|
1027
1284
|
// new screen's disposable getInitialFrameGeometry() fallback before its
|
|
@@ -1046,13 +1303,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1046
1303
|
}
|
|
1047
1304
|
}
|
|
1048
1305
|
updateSelectedIds((current) => {
|
|
1049
|
-
const next = current.filter((id) =>
|
|
1306
|
+
const next = current.filter((id) => selectableIds.has(id));
|
|
1050
1307
|
return next.length === current.length ? current : next;
|
|
1051
1308
|
});
|
|
1052
1309
|
}, [
|
|
1053
1310
|
geometryById,
|
|
1054
1311
|
getResolvedMetadata,
|
|
1055
1312
|
screens,
|
|
1313
|
+
selectableScreens,
|
|
1056
1314
|
updateFrameGeometry,
|
|
1057
1315
|
updateFrameGeometryRefOnly,
|
|
1058
1316
|
updateSelectedIds,
|
|
@@ -1087,11 +1345,15 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1087
1345
|
|
|
1088
1346
|
useEffect(() => {
|
|
1089
1347
|
if (!selectedScreenIds) return;
|
|
1348
|
+
const selectableIds = new Set(selectableScreens.map((screen) => screen.id));
|
|
1349
|
+
const nextSelection = selectedScreenIds.filter((id) =>
|
|
1350
|
+
selectableIds.has(id),
|
|
1351
|
+
);
|
|
1090
1352
|
// Remember the selection we're pushing in from the parent so the report
|
|
1091
1353
|
// effects above can recognise (and not echo back) the resulting change.
|
|
1092
|
-
propSyncedSelectionRef.current =
|
|
1093
|
-
updateSelectedIds(() =>
|
|
1094
|
-
}, [
|
|
1354
|
+
propSyncedSelectionRef.current = nextSelection;
|
|
1355
|
+
updateSelectedIds(() => nextSelection);
|
|
1356
|
+
}, [selectableScreens, selectedScreenIds, updateSelectedIds]);
|
|
1095
1357
|
|
|
1096
1358
|
useEffect(() => {
|
|
1097
1359
|
if (
|
|
@@ -1102,8 +1364,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1102
1364
|
}
|
|
1103
1365
|
handledSelectAllRequestRef.current = selectAllRequest;
|
|
1104
1366
|
updateSelectedDraftIds(() => []);
|
|
1105
|
-
updateSelectedIds(() =>
|
|
1106
|
-
}, [
|
|
1367
|
+
updateSelectedIds(() => selectableScreens.map((screen) => screen.id));
|
|
1368
|
+
}, [
|
|
1369
|
+
selectAllRequest,
|
|
1370
|
+
selectableScreens,
|
|
1371
|
+
updateSelectedDraftIds,
|
|
1372
|
+
updateSelectedIds,
|
|
1373
|
+
]);
|
|
1107
1374
|
|
|
1108
1375
|
useEffect(() => {
|
|
1109
1376
|
if (
|
|
@@ -1158,14 +1425,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1158
1425
|
}
|
|
1159
1426
|
return;
|
|
1160
1427
|
}
|
|
1161
|
-
if (!surfaceRef.current ||
|
|
1428
|
+
if (!surfaceRef.current || renderedScreens.length === 0) return;
|
|
1162
1429
|
const rect = surfaceRef.current.getBoundingClientRect();
|
|
1163
1430
|
const scale = zoomRef.current / 100;
|
|
1164
|
-
const frames =
|
|
1431
|
+
const frames = renderedScreens.map((screen) => {
|
|
1165
1432
|
const metadata = getResolvedMetadata(screen);
|
|
1166
1433
|
const currentGeometry =
|
|
1167
1434
|
frameGeometryRef.current[screen.id] ??
|
|
1168
|
-
getInitialFrameGeometry(
|
|
1435
|
+
getInitialFrameGeometry(screenIndexById.get(screen.id) ?? 0, metadata);
|
|
1169
1436
|
return getPreviewDeviceFrameGeometry({
|
|
1170
1437
|
currentGeometry,
|
|
1171
1438
|
metadata,
|
|
@@ -1174,7 +1441,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1174
1441
|
});
|
|
1175
1442
|
const bounds = getFrameGroupBounds(
|
|
1176
1443
|
frames.map((geometry, index) => ({
|
|
1177
|
-
id:
|
|
1444
|
+
id: renderedScreens[index]?.id ?? String(index),
|
|
1178
1445
|
geometry,
|
|
1179
1446
|
})),
|
|
1180
1447
|
);
|
|
@@ -1194,7 +1461,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1194
1461
|
// Leave a Figma-like board gutter beside the last frame for quick drops/draws,
|
|
1195
1462
|
// and fit tall single frames so lower canvas interactions remain reachable.
|
|
1196
1463
|
const widthFitScale =
|
|
1197
|
-
|
|
1464
|
+
renderedScreens.length > 1 && totalWidth > 0
|
|
1198
1465
|
? Math.max(0.1, (rect.width - 180) / totalWidth)
|
|
1199
1466
|
: scale;
|
|
1200
1467
|
const heightFitScale =
|
|
@@ -1273,16 +1540,27 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1273
1540
|
|
|
1274
1541
|
const getCurrentFrameEntries = useCallback(
|
|
1275
1542
|
() =>
|
|
1276
|
-
|
|
1543
|
+
renderedScreens.map((screen) => {
|
|
1277
1544
|
const metadata = getResolvedMetadata(screen);
|
|
1278
1545
|
return {
|
|
1279
1546
|
id: screen.id,
|
|
1280
1547
|
geometry:
|
|
1281
1548
|
frameGeometryRef.current[screen.id] ??
|
|
1282
|
-
getInitialFrameGeometry(
|
|
1549
|
+
getInitialFrameGeometry(
|
|
1550
|
+
screenIndexById.get(screen.id) ?? 0,
|
|
1551
|
+
metadata,
|
|
1552
|
+
),
|
|
1283
1553
|
};
|
|
1284
1554
|
}),
|
|
1285
|
-
[getResolvedMetadata,
|
|
1555
|
+
[getResolvedMetadata, renderedScreens, screenIndexById],
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
const getSelectableFrameEntries = useCallback(
|
|
1559
|
+
() =>
|
|
1560
|
+
getCurrentFrameEntries().filter(
|
|
1561
|
+
(entry) => !lockedScreenIdSet.has(entry.id),
|
|
1562
|
+
),
|
|
1563
|
+
[getCurrentFrameEntries, lockedScreenIdSet],
|
|
1286
1564
|
);
|
|
1287
1565
|
|
|
1288
1566
|
const getCurrentDraftEntries = useCallback(
|
|
@@ -1301,7 +1579,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1301
1579
|
|
|
1302
1580
|
const getFrameEntryAtPoint = useCallback(
|
|
1303
1581
|
(point: Point) =>
|
|
1304
|
-
findTopFrameEntryAtPoint(
|
|
1582
|
+
findTopFrameEntryAtPoint(getSelectableFrameEntries(), point, {
|
|
1305
1583
|
// Screen wrappers give this same id a large z-index boost. Geometry
|
|
1306
1584
|
// hit testing must mirror it or drops/draws on overlapping frames can
|
|
1307
1585
|
// persist into a visually obscured sibling.
|
|
@@ -1313,7 +1591,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1313
1591
|
? activeId
|
|
1314
1592
|
: screensRef.current[0]?.id),
|
|
1315
1593
|
}),
|
|
1316
|
-
[activeId,
|
|
1594
|
+
[activeId, getSelectableFrameEntries],
|
|
1317
1595
|
);
|
|
1318
1596
|
|
|
1319
1597
|
// Mirrors getFrameEntryAtPoint above, but hit-tests draft primitives
|
|
@@ -1453,8 +1731,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1453
1731
|
const iframeId = targetScreen
|
|
1454
1732
|
? getActiveScreenIframeId(targetScreen)
|
|
1455
1733
|
: targetId;
|
|
1456
|
-
const targetIframe =
|
|
1457
|
-
|
|
1734
|
+
const targetIframe = findCanvasIframeForScreen(
|
|
1735
|
+
surfaceRef.current,
|
|
1736
|
+
iframeId,
|
|
1737
|
+
boardFileId,
|
|
1458
1738
|
);
|
|
1459
1739
|
targetIframe?.contentWindow?.postMessage(
|
|
1460
1740
|
{ type: "agent-native:hit-test-preview-clear" },
|
|
@@ -1525,21 +1805,44 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1525
1805
|
const targetScreen = screensRef.current.find(
|
|
1526
1806
|
(s) => s.id === candidate.id,
|
|
1527
1807
|
);
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1808
|
+
const targetIsBoard = candidate.id === boardFileId;
|
|
1809
|
+
if (!targetScreen && !targetIsBoard) return Promise.resolve({});
|
|
1810
|
+
if (targetIsBoard && !boardSurfaceRenderGeometry) {
|
|
1811
|
+
return Promise.resolve({});
|
|
1812
|
+
}
|
|
1813
|
+
const targetIframe = targetIsBoard
|
|
1814
|
+
? findCanvasIframeForScreen(
|
|
1815
|
+
surfaceRef.current,
|
|
1816
|
+
candidate.id,
|
|
1817
|
+
boardFileId,
|
|
1818
|
+
)
|
|
1819
|
+
: surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1820
|
+
`[data-screen-iframe-id="${CSS.escape(
|
|
1821
|
+
getActiveScreenIframeId(targetScreen!),
|
|
1822
|
+
)}"]`,
|
|
1823
|
+
);
|
|
1534
1824
|
const targetContentWindow = targetIframe?.contentWindow;
|
|
1535
1825
|
if (!targetContentWindow) return Promise.resolve({});
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1826
|
+
const localPoint =
|
|
1827
|
+
targetIsBoard && boardSurfaceRenderGeometry
|
|
1828
|
+
? boardPointToBoardSurfaceLocalPoint(
|
|
1829
|
+
boardPoint,
|
|
1830
|
+
boardSurfaceRenderGeometry,
|
|
1831
|
+
)
|
|
1832
|
+
: (() => {
|
|
1833
|
+
const {
|
|
1834
|
+
width: targetViewportWidth,
|
|
1835
|
+
height: targetViewportHeight,
|
|
1836
|
+
} = getTargetViewportMetadata(targetScreen!, targetIframe);
|
|
1837
|
+
return boardPointToScreenLocalPoint(
|
|
1838
|
+
boardPoint,
|
|
1839
|
+
candidate.geometry,
|
|
1840
|
+
{
|
|
1841
|
+
width: targetViewportWidth,
|
|
1842
|
+
height: targetViewportHeight,
|
|
1843
|
+
},
|
|
1844
|
+
);
|
|
1845
|
+
})();
|
|
1543
1846
|
|
|
1544
1847
|
const correlationId = `hit-${Date.now()}-${Math.random()
|
|
1545
1848
|
.toString(36)
|
|
@@ -1652,14 +1955,20 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1652
1955
|
const targetScreen = screensRef.current.find(
|
|
1653
1956
|
(s) => s.id === candidate.id,
|
|
1654
1957
|
);
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
: null;
|
|
1658
|
-
const targetIframe = targetIframeId
|
|
1958
|
+
const targetIsBoard = candidate.id === boardFileId;
|
|
1959
|
+
const targetIframe = targetScreen
|
|
1659
1960
|
? surfaceRef.current?.querySelector<HTMLIFrameElement>(
|
|
1660
|
-
`[data-screen-iframe-id="${
|
|
1961
|
+
`[data-screen-iframe-id="${CSS.escape(
|
|
1962
|
+
getActiveScreenIframeId(targetScreen),
|
|
1963
|
+
)}"]`,
|
|
1661
1964
|
)
|
|
1662
|
-
:
|
|
1965
|
+
: targetIsBoard
|
|
1966
|
+
? findCanvasIframeForScreen(
|
|
1967
|
+
surfaceRef.current,
|
|
1968
|
+
candidate.id,
|
|
1969
|
+
boardFileId,
|
|
1970
|
+
)
|
|
1971
|
+
: null;
|
|
1663
1972
|
const guide = targetScreen
|
|
1664
1973
|
? getCrossScreenDropGuideForHitTest({
|
|
1665
1974
|
hit,
|
|
@@ -1669,7 +1978,20 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1669
1978
|
targetIframe,
|
|
1670
1979
|
),
|
|
1671
1980
|
})
|
|
1672
|
-
:
|
|
1981
|
+
: targetIsBoard && boardSurfaceRenderGeometry
|
|
1982
|
+
? getCrossScreenDropGuideForHitTest({
|
|
1983
|
+
hit,
|
|
1984
|
+
targetGeometry: boardSurfaceRenderGeometry,
|
|
1985
|
+
targetMetadata: {
|
|
1986
|
+
width:
|
|
1987
|
+
targetIframe?.clientWidth ||
|
|
1988
|
+
boardSurfaceRenderGeometry.width,
|
|
1989
|
+
height:
|
|
1990
|
+
targetIframe?.clientHeight ||
|
|
1991
|
+
boardSurfaceRenderGeometry.height,
|
|
1992
|
+
},
|
|
1993
|
+
})
|
|
1994
|
+
: null;
|
|
1673
1995
|
setCrossScreenDropGuide(guide);
|
|
1674
1996
|
});
|
|
1675
1997
|
};
|
|
@@ -1706,6 +2028,20 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1706
2028
|
: { boardX: boardPoint.x, boardY: boardPoint.y },
|
|
1707
2029
|
);
|
|
1708
2030
|
requestCrossScreenDropGuide(nextTarget, boardPoint);
|
|
2031
|
+
} else if (
|
|
2032
|
+
sourceScreenId !== boardFileId &&
|
|
2033
|
+
boardFileId &&
|
|
2034
|
+
boardFrameGeometry &&
|
|
2035
|
+
geometryContainsPoint(boardFrameGeometry, boardPoint)
|
|
2036
|
+
) {
|
|
2037
|
+
const nextTarget = { id: boardFileId, geometry: boardFrameGeometry };
|
|
2038
|
+
if (crossScreenTargetRef.current?.id !== boardFileId) {
|
|
2039
|
+
clearCrossScreenPreviewGuide();
|
|
2040
|
+
}
|
|
2041
|
+
crossScreenTargetRef.current = nextTarget;
|
|
2042
|
+
setCrossScreenTarget(nextTarget);
|
|
2043
|
+
setCrossScreenGhost({ boardX: boardPoint.x, boardY: boardPoint.y });
|
|
2044
|
+
requestCrossScreenDropGuide(nextTarget, boardPoint);
|
|
1709
2045
|
} else {
|
|
1710
2046
|
clearCrossScreenPreviewGuide();
|
|
1711
2047
|
crossScreenTargetRef.current = null;
|
|
@@ -1741,16 +2077,45 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1741
2077
|
if (!targetCandidate) return;
|
|
1742
2078
|
|
|
1743
2079
|
if (targetCandidate.id === boardFileId) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2080
|
+
void runHitTest(targetCandidate, lastBoardPoint).then(
|
|
2081
|
+
({
|
|
2082
|
+
anchorNodeId,
|
|
2083
|
+
pendingNodeId,
|
|
2084
|
+
anchorSelector,
|
|
2085
|
+
placement,
|
|
2086
|
+
dropMode,
|
|
2087
|
+
anchorRect,
|
|
2088
|
+
}) => {
|
|
2089
|
+
const hasAnchor = Boolean(
|
|
2090
|
+
anchorNodeId || pendingNodeId || anchorSelector,
|
|
2091
|
+
);
|
|
2092
|
+
onCrossScreenElementDropRef.current?.({
|
|
2093
|
+
sourceSelector: payload.selector,
|
|
2094
|
+
sourceNodeId: payload.sourceId,
|
|
2095
|
+
sourceScreenId,
|
|
2096
|
+
targetScreenId: targetCandidate.id,
|
|
2097
|
+
targetAnchorNodeId: anchorNodeId,
|
|
2098
|
+
targetAnchorPendingNodeId: pendingNodeId,
|
|
2099
|
+
targetAnchorSelector: anchorSelector,
|
|
2100
|
+
targetAnchorPlacement: placement,
|
|
2101
|
+
targetDropMode: dropMode,
|
|
2102
|
+
targetAnchorRect: anchorRect,
|
|
2103
|
+
targetCanvasPoint: lastBoardPoint,
|
|
2104
|
+
// Anchor rects come from the finite board iframe and are local
|
|
2105
|
+
// to its render window. Use the same local space for nested
|
|
2106
|
+
// placement; root-level board drops keep persisted board coords.
|
|
2107
|
+
targetLocalPoint:
|
|
2108
|
+
hasAnchor && boardSurfaceRenderGeometry
|
|
2109
|
+
? boardPointToBoardSurfaceLocalPoint(
|
|
2110
|
+
lastBoardPoint,
|
|
2111
|
+
boardSurfaceRenderGeometry,
|
|
2112
|
+
)
|
|
2113
|
+
: lastBoardPoint,
|
|
2114
|
+
sourcePointerOffset: payload.sourcePointerOffset,
|
|
2115
|
+
styleSnapshot: payload.styleSnapshot,
|
|
2116
|
+
});
|
|
2117
|
+
},
|
|
2118
|
+
);
|
|
1754
2119
|
return;
|
|
1755
2120
|
}
|
|
1756
2121
|
|
|
@@ -2048,11 +2413,17 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2048
2413
|
let boardX: number;
|
|
2049
2414
|
let boardY: number;
|
|
2050
2415
|
|
|
2051
|
-
if (sourceScreenId === boardFileId &&
|
|
2416
|
+
if (sourceScreenId === boardFileId && boardSurfaceRenderGeometry) {
|
|
2052
2417
|
// The board iframe is pixel-exact: 1 iframe pixel == 1 canvas unit.
|
|
2053
|
-
//
|
|
2054
|
-
|
|
2055
|
-
|
|
2418
|
+
// Its finite paint window can start anywhere within the much larger
|
|
2419
|
+
// logical board, so add the render origin (not the logical board's
|
|
2420
|
+
// fixed -65536 origin) to recover persisted canvas coordinates.
|
|
2421
|
+
const boardPoint = boardSurfaceLocalPointToBoardPoint(
|
|
2422
|
+
{ x: iframeX, y: iframeY },
|
|
2423
|
+
boardSurfaceRenderGeometry,
|
|
2424
|
+
);
|
|
2425
|
+
boardX = boardPoint.x;
|
|
2426
|
+
boardY = boardPoint.y;
|
|
2056
2427
|
} else {
|
|
2057
2428
|
const sourceScreen = screensRef.current.find(
|
|
2058
2429
|
(s) => s.id === sourceScreenId,
|
|
@@ -2107,6 +2478,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2107
2478
|
activeId,
|
|
2108
2479
|
boardFileId,
|
|
2109
2480
|
boardFrameGeometry,
|
|
2481
|
+
boardSurfaceRenderGeometry,
|
|
2110
2482
|
getFrameEntryAtPoint,
|
|
2111
2483
|
getCanvasPoint,
|
|
2112
2484
|
getResolvedMetadata,
|
|
@@ -2305,7 +2677,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2305
2677
|
* to collect every screen. */
|
|
2306
2678
|
const collectLayerMarqueeCandidates = useCallback(
|
|
2307
2679
|
async (screenIds?: Set<string>) => {
|
|
2308
|
-
const frameEntries =
|
|
2680
|
+
const frameEntries = getSelectableFrameEntries().filter(
|
|
2309
2681
|
(entry) => !screenIds || screenIds.has(entry.id),
|
|
2310
2682
|
);
|
|
2311
2683
|
const frameCandidates = await Promise.all(
|
|
@@ -2340,7 +2712,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2340
2712
|
);
|
|
2341
2713
|
const boardCandidates =
|
|
2342
2714
|
boardFileId &&
|
|
2343
|
-
|
|
2715
|
+
boardSurfaceRenderGeometry &&
|
|
2344
2716
|
(!screenIds || screenIds.has(boardFileId))
|
|
2345
2717
|
? await (async () => {
|
|
2346
2718
|
const infos = await requestSelectableElementInfos(boardFileId);
|
|
@@ -2354,13 +2726,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2354
2726
|
width: info.boundingRect.width,
|
|
2355
2727
|
height: info.boundingRect.height,
|
|
2356
2728
|
},
|
|
2357
|
-
|
|
2729
|
+
boardSurfaceRenderGeometry,
|
|
2358
2730
|
{
|
|
2359
|
-
width:
|
|
2360
|
-
height:
|
|
2731
|
+
width: boardSurfaceRenderGeometry.width,
|
|
2732
|
+
height: boardSurfaceRenderGeometry.height,
|
|
2361
2733
|
},
|
|
2362
2734
|
),
|
|
2363
|
-
frameGeometry:
|
|
2735
|
+
frameGeometry: boardSurfaceRenderGeometry,
|
|
2364
2736
|
}));
|
|
2365
2737
|
})()
|
|
2366
2738
|
: [];
|
|
@@ -2368,8 +2740,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2368
2740
|
},
|
|
2369
2741
|
[
|
|
2370
2742
|
boardFileId,
|
|
2371
|
-
|
|
2372
|
-
|
|
2743
|
+
boardSurfaceRenderGeometry,
|
|
2744
|
+
getSelectableFrameEntries,
|
|
2373
2745
|
getResolvedMetadata,
|
|
2374
2746
|
requestSelectableElementInfos,
|
|
2375
2747
|
],
|
|
@@ -2576,6 +2948,52 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2576
2948
|
const position = frameStyleLeftTop(geometry, labelHeight);
|
|
2577
2949
|
element.style.left = `${position.left}px`;
|
|
2578
2950
|
element.style.top = `${position.top}px`;
|
|
2951
|
+
if (kind !== "frame") {
|
|
2952
|
+
// A draft resize (unlike a plain draft move) can also leave width/
|
|
2953
|
+
// height/rotation, and for path/line/arrow/polygon/star kinds the
|
|
2954
|
+
// inner SVG viewBox + path/polygon content, imperatively mutated
|
|
2955
|
+
// mid-gesture (see applyDraftPrimitiveToDom, used by
|
|
2956
|
+
// beginDraftResize's own live mousemove tick). Restoring it here
|
|
2957
|
+
// unconditionally is a harmless no-op for a plain draft move (whose
|
|
2958
|
+
// origin geometry never had a different width/height/rotation to
|
|
2959
|
+
// begin with) and fixes an Escape mid-draft-resize leaving the
|
|
2960
|
+
// shape's box/content visually stuck at its last dragged size.
|
|
2961
|
+
if ("kind" in origin) {
|
|
2962
|
+
applyDraftPrimitiveToDom(element, origin);
|
|
2963
|
+
}
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
// Resize/rotate (unlike plain move) can also leave width/height/
|
|
2967
|
+
// rotation imperatively mutated mid-gesture — restore those here too
|
|
2968
|
+
// so an Escape mid-resize/rotate doesn't leave the frame visually
|
|
2969
|
+
// stuck at its last dragged size/angle even though the geometry
|
|
2970
|
+
// state itself was already rolled back above.
|
|
2971
|
+
element.style.width = `${geometry.width}px`;
|
|
2972
|
+
element.style.transform = geometry.rotation
|
|
2973
|
+
? `rotate(${geometry.rotation}deg)`
|
|
2974
|
+
: "";
|
|
2975
|
+
element.style.transformOrigin = `${geometry.width / 2}px ${
|
|
2976
|
+
labelHeight + geometry.height / 2
|
|
2977
|
+
}px`;
|
|
2978
|
+
const cardEl = element.querySelector<HTMLElement>("[data-screen-card]");
|
|
2979
|
+
if (cardEl) {
|
|
2980
|
+
cardEl.style.width = `${geometry.width}px`;
|
|
2981
|
+
cardEl.style.height = `${geometry.height}px`;
|
|
2982
|
+
}
|
|
2983
|
+
const iframeEl = element.querySelector<HTMLIFrameElement>(
|
|
2984
|
+
`[data-screen-iframe-id="${CSS.escape(targetId)}"]`,
|
|
2985
|
+
);
|
|
2986
|
+
const screen = screensRef.current.find((s) => s.id === targetId);
|
|
2987
|
+
if (iframeEl && screen) {
|
|
2988
|
+
const viewport = getScreenPreviewViewport(
|
|
2989
|
+
getResolvedMetadata(screen),
|
|
2990
|
+
geometry,
|
|
2991
|
+
);
|
|
2992
|
+
iframeEl.style.width = `${viewport.viewportWidth}px`;
|
|
2993
|
+
iframeEl.style.height = `${viewport.viewportHeight}px`;
|
|
2994
|
+
iframeEl.style.transform =
|
|
2995
|
+
viewport.scale === 1 ? "" : `scale(${viewport.scale})`;
|
|
2996
|
+
}
|
|
2579
2997
|
});
|
|
2580
2998
|
|
|
2581
2999
|
const selectionBox = surfaceRef.current?.querySelector<HTMLElement>(
|
|
@@ -2602,6 +3020,17 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2602
3020
|
const position = frameStyleLeftTop(bounds);
|
|
2603
3021
|
selectionBox.style.left = `${position.left}px`;
|
|
2604
3022
|
selectionBox.style.top = `${position.top}px`;
|
|
3023
|
+
// Plain move never touches the selection box's width/height/rotation
|
|
3024
|
+
// (only left/top), but resize/rotate/group-rotate do — restore those
|
|
3025
|
+
// too so Escape can't leave the box's chrome a stale dragged size.
|
|
3026
|
+
selectionBox.style.width = `${bounds.width}px`;
|
|
3027
|
+
selectionBox.style.height = `${bounds.height}px`;
|
|
3028
|
+
const boxRotation =
|
|
3029
|
+
originGeometries.length === 1 ? (originGeometries[0].rotation ?? 0) : 0;
|
|
3030
|
+
selectionBox.style.transform = boxRotation
|
|
3031
|
+
? `rotate(${boxRotation}deg)`
|
|
3032
|
+
: "";
|
|
3033
|
+
selectionBox.style.transformOrigin = `${bounds.width / 2}px ${bounds.height / 2}px`;
|
|
2605
3034
|
};
|
|
2606
3035
|
|
|
2607
3036
|
if (state) {
|
|
@@ -2611,33 +3040,36 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2611
3040
|
state.type === "resize" ||
|
|
2612
3041
|
state.type === "group-rotate"
|
|
2613
3042
|
) {
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
);
|
|
2624
|
-
}
|
|
3043
|
+
// Move/resize/group-rotate all mutate the frame(s)' DOM directly
|
|
3044
|
+
// (left/top for move; also width/height/rotation for resize and
|
|
3045
|
+
// group-rotate — see restoreImperativeMoveDom's comment) while React
|
|
3046
|
+
// deliberately keeps its pre-drag props. A state rollback to those
|
|
3047
|
+
// same values does not make React rewrite the externally-mutated
|
|
3048
|
+
// styles, so Escape must restore the frame + selection-box DOM
|
|
3049
|
+
// state explicitly for every one of these gesture types, not just
|
|
3050
|
+
// move.
|
|
3051
|
+
restoreImperativeMoveDom(state.originFrames, state.targetIds, "frame");
|
|
2625
3052
|
updateFrameGeometry((current) =>
|
|
2626
3053
|
frameGeometryWithOverrides(current, state.originFrames),
|
|
2627
3054
|
);
|
|
2628
3055
|
} else if (state.type === "rotate") {
|
|
3056
|
+
// Single-frame rotate also mutates the frame's transform directly —
|
|
3057
|
+
// see the matching comment above.
|
|
3058
|
+
restoreImperativeMoveDom(
|
|
3059
|
+
{ [state.frameId]: state.originFrame },
|
|
3060
|
+
[state.frameId],
|
|
3061
|
+
"frame",
|
|
3062
|
+
);
|
|
2629
3063
|
updateFrameGeometry((current) => ({
|
|
2630
3064
|
...current,
|
|
2631
3065
|
[state.frameId]: { ...state.originFrame },
|
|
2632
3066
|
}));
|
|
2633
3067
|
} else if (state.type === "draft-move" || state.type === "draft-resize") {
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
);
|
|
2640
|
-
}
|
|
3068
|
+
// Both draft-move (left/top only) and draft-resize (also width/
|
|
3069
|
+
// height/rotation + inner SVG content, see restoreImperativeMoveDom's
|
|
3070
|
+
// draft-kind branch) mutate the draft's DOM node directly mid-gesture
|
|
3071
|
+
// — restore it here the same way move/resize/rotate do for frames.
|
|
3072
|
+
restoreImperativeMoveDom(state.originDrafts, state.targetIds, "draft");
|
|
2641
3073
|
updateDraftPrimitives((current) =>
|
|
2642
3074
|
current.map((draft) => {
|
|
2643
3075
|
const origin = state.originDrafts[draft.id];
|
|
@@ -2689,6 +3121,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2689
3121
|
return false;
|
|
2690
3122
|
}, [
|
|
2691
3123
|
finishDrag,
|
|
3124
|
+
getResolvedMetadata,
|
|
2692
3125
|
updateDraftPrimitives,
|
|
2693
3126
|
updateFrameGeometry,
|
|
2694
3127
|
updateSelectedDraftIds,
|
|
@@ -2725,6 +3158,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2725
3158
|
(e: React.MouseEvent) => {
|
|
2726
3159
|
e.preventDefault();
|
|
2727
3160
|
e.stopPropagation();
|
|
3161
|
+
cancelPendingStaticBoardSelection();
|
|
2728
3162
|
dragState.current = {
|
|
2729
3163
|
type: "pan",
|
|
2730
3164
|
originClient: { x: e.clientX, y: e.clientY },
|
|
@@ -2760,7 +3194,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2760
3194
|
|
|
2761
3195
|
installDragListeners(handleMouseMove, handlePanEnd);
|
|
2762
3196
|
},
|
|
2763
|
-
[finishDrag, installDragListeners],
|
|
3197
|
+
[cancelPendingStaticBoardSelection, finishDrag, installDragListeners],
|
|
2764
3198
|
);
|
|
2765
3199
|
|
|
2766
3200
|
const beginMarquee = useCallback(
|
|
@@ -2875,10 +3309,22 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2875
3309
|
) {
|
|
2876
3310
|
state.hasMoved = true;
|
|
2877
3311
|
}
|
|
3312
|
+
|
|
3313
|
+
// Never commit a live marquee rect/selection before the drag
|
|
3314
|
+
// threshold is crossed — matches every other gesture in this file
|
|
3315
|
+
// (move/resize/rotate/draft-move/draft-resize all gate on hasMoved
|
|
3316
|
+
// here). Without this, a shift-click that jitters a couple px near an
|
|
3317
|
+
// already-selected frame's edge runs xorMarqueeSelection against a
|
|
3318
|
+
// near-zero-size rect and can toggle that frame OUT of the selection
|
|
3319
|
+
// — a corruption that mouseup's shouldClearSelectionOnEmptyCanvasClick
|
|
3320
|
+
// never rolls back for the additive (shift) case, since it only
|
|
3321
|
+
// restores selection when the gesture is non-additive.
|
|
3322
|
+
if (!state.hasMoved) return;
|
|
3323
|
+
|
|
2878
3324
|
setMarquee(rect);
|
|
2879
3325
|
|
|
2880
3326
|
const chromeScale = chromeScaleFromZoom(zoomRef.current);
|
|
2881
|
-
const hitIds =
|
|
3327
|
+
const hitIds = getSelectableFrameEntries()
|
|
2882
3328
|
.filter((entry) =>
|
|
2883
3329
|
rotatedRectIntersects(
|
|
2884
3330
|
rect,
|
|
@@ -2940,7 +3386,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
2940
3386
|
finishDrag,
|
|
2941
3387
|
getCanvasPoint,
|
|
2942
3388
|
getCurrentDraftEntries,
|
|
2943
|
-
|
|
3389
|
+
getSelectableFrameEntries,
|
|
2944
3390
|
installDragListeners,
|
|
2945
3391
|
onLayerMarqueeSelectionChange,
|
|
2946
3392
|
updateSelectedDraftIds,
|
|
@@ -3160,6 +3606,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3160
3606
|
undefined,
|
|
3161
3607
|
{ nextTool: "pen" },
|
|
3162
3608
|
);
|
|
3609
|
+
// Keep the Pen tool armed after Enter/Escape/closing a path, matching
|
|
3610
|
+
// Figma. The parent selection callback also receives nextTool="pen",
|
|
3611
|
+
// but board primitives intentionally bypass that generic callback and
|
|
3612
|
+
// asynchronous selection reconciliation can otherwise paint Move for a
|
|
3613
|
+
// frame. Drive the controlled tool explicitly at the commit boundary.
|
|
3614
|
+
onActiveToolChange?.("pen");
|
|
3163
3615
|
clearActivePenPath();
|
|
3164
3616
|
},
|
|
3165
3617
|
[clearActivePenPath, commitDraftPrimitive, onActiveToolChange, toolProps],
|
|
@@ -4025,13 +4477,16 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4025
4477
|
current.filter((draftId) => !persistedDraftIds.has(draftId)),
|
|
4026
4478
|
);
|
|
4027
4479
|
// Reparent each persisted node into the container primitive.
|
|
4480
|
+
// When the drop resolved to a before/after auto-layout slot
|
|
4481
|
+
// (see findAutoLayoutInsertionAnchor), anchor against that
|
|
4482
|
+
// sibling instead of always appending inside the container.
|
|
4028
4483
|
persisted.forEach((entry) => {
|
|
4029
4484
|
onPrimitiveReparentRef.current?.({
|
|
4030
4485
|
sourceNodeId: entry.nodeId,
|
|
4031
4486
|
sourceScreenId: entry.frameId,
|
|
4032
|
-
targetNodeId: dropTarget.nodeId,
|
|
4487
|
+
targetNodeId: dropTarget.anchorNodeId ?? dropTarget.nodeId,
|
|
4033
4488
|
targetScreenId: dropTarget.screenId,
|
|
4034
|
-
placement: "inside",
|
|
4489
|
+
placement: dropTarget.placement ?? "inside",
|
|
4035
4490
|
});
|
|
4036
4491
|
});
|
|
4037
4492
|
const lastPersisted = persisted[persisted.length - 1];
|
|
@@ -4165,6 +4620,23 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4165
4620
|
setIsDragging(true);
|
|
4166
4621
|
setDragCursor(getResizeCursor(handle));
|
|
4167
4622
|
|
|
4623
|
+
// PERF9: cache each resized draft's DOM node (and the selection-box
|
|
4624
|
+
// overlay tracking it) once, up front — mirrors beginDraftDrag/
|
|
4625
|
+
// beginFrameDrag/beginResize's imperative "mutate now, commit once"
|
|
4626
|
+
// discipline. This used to call updateDraftPrimitives (setDraftPrimitives)
|
|
4627
|
+
// on every mousemove tick, forcing a full MultiScreenCanvas re-render
|
|
4628
|
+
// per rAF frame during a drag.
|
|
4629
|
+
const resizedDraftEls = new Map<string, HTMLElement>();
|
|
4630
|
+
targetIds.forEach((targetId) => {
|
|
4631
|
+
const el = surfaceRef.current?.querySelector<HTMLElement>(
|
|
4632
|
+
`[data-draft-id="${CSS.escape(targetId)}"]`,
|
|
4633
|
+
);
|
|
4634
|
+
if (el) resizedDraftEls.set(targetId, el);
|
|
4635
|
+
});
|
|
4636
|
+
const selectionBoxEl = surfaceRef.current?.querySelector<HTMLElement>(
|
|
4637
|
+
"[data-frame-selection-box]",
|
|
4638
|
+
);
|
|
4639
|
+
|
|
4168
4640
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
4169
4641
|
const state = dragState.current;
|
|
4170
4642
|
if (!state || state.type !== "draft-resize") return;
|
|
@@ -4182,7 +4654,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4182
4654
|
}
|
|
4183
4655
|
|
|
4184
4656
|
// A resize handle click (or normal 1-2px hand jitter) must not resize
|
|
4185
|
-
// the draft. Frame resize already enforces this same threshold.
|
|
4657
|
+
// the draft. Frame resize already enforces this same threshold. Draft
|
|
4658
|
+
// resizes use direct DOM mutation now (see below), so applying even a
|
|
4659
|
+
// 1px delta here would leave a phantom visual nudge React never
|
|
4660
|
+
// learns it needs to overwrite on mouseup — same rationale as
|
|
4661
|
+
// beginDraftDrag's matching guard.
|
|
4186
4662
|
if (!state.hasMoved) return;
|
|
4187
4663
|
|
|
4188
4664
|
const originEntries = state.targetIds.map((targetId) => ({
|
|
@@ -4229,7 +4705,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4229
4705
|
// drag-start and must see whichever tool is active on THIS tick.
|
|
4230
4706
|
const scaleK = effectiveToolRef.current === "scale";
|
|
4231
4707
|
|
|
4232
|
-
|
|
4708
|
+
// PERF9: ref-only geometry write (no setDraftPrimitives) + direct DOM
|
|
4709
|
+
// mutation via applyDraftPrimitiveToDom, same "imperative now, commit
|
|
4710
|
+
// once" discipline as beginDraftDrag/beginFrameDrag/beginResize above.
|
|
4711
|
+
updateDraftPrimitivesRefOnly((current) =>
|
|
4233
4712
|
current.map((draft) => {
|
|
4234
4713
|
const origin = state.originDrafts[draft.id];
|
|
4235
4714
|
const geometry = resizedById[draft.id];
|
|
@@ -4237,6 +4716,54 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4237
4716
|
return applyDraftGeometry(origin, geometry, scaleK);
|
|
4238
4717
|
}),
|
|
4239
4718
|
);
|
|
4719
|
+
state.targetIds.forEach((targetId) => {
|
|
4720
|
+
const draft = draftPrimitivesRef.current.find(
|
|
4721
|
+
(candidate) => candidate.id === targetId,
|
|
4722
|
+
);
|
|
4723
|
+
const el = resizedDraftEls.get(targetId);
|
|
4724
|
+
if (draft && el) applyDraftPrimitiveToDom(el, draft);
|
|
4725
|
+
});
|
|
4726
|
+
if (selectionBoxEl) {
|
|
4727
|
+
const targetGeometries = state.targetIds
|
|
4728
|
+
.map(
|
|
4729
|
+
(targetId) =>
|
|
4730
|
+
draftPrimitivesRef.current.find(
|
|
4731
|
+
(candidate) => candidate.id === targetId,
|
|
4732
|
+
)?.geometry,
|
|
4733
|
+
)
|
|
4734
|
+
.filter((geometry): geometry is FrameGeometry => Boolean(geometry));
|
|
4735
|
+
const bounds =
|
|
4736
|
+
targetGeometries.length === 1
|
|
4737
|
+
? targetGeometries[0]
|
|
4738
|
+
: (() => {
|
|
4739
|
+
const groupBounds = getFrameGroupBounds(
|
|
4740
|
+
targetGeometries.map((geometry) => ({ id: "", geometry })),
|
|
4741
|
+
);
|
|
4742
|
+
return groupBounds
|
|
4743
|
+
? {
|
|
4744
|
+
x: groupBounds.left,
|
|
4745
|
+
y: groupBounds.top,
|
|
4746
|
+
width: groupBounds.width,
|
|
4747
|
+
height: groupBounds.height,
|
|
4748
|
+
}
|
|
4749
|
+
: null;
|
|
4750
|
+
})();
|
|
4751
|
+
if (bounds) {
|
|
4752
|
+
const { left, top } = frameStyleLeftTop(bounds);
|
|
4753
|
+
selectionBoxEl.style.left = `${left}px`;
|
|
4754
|
+
selectionBoxEl.style.top = `${top}px`;
|
|
4755
|
+
selectionBoxEl.style.width = `${bounds.width}px`;
|
|
4756
|
+
selectionBoxEl.style.height = `${bounds.height}px`;
|
|
4757
|
+
const rotation =
|
|
4758
|
+
targetGeometries.length === 1
|
|
4759
|
+
? (targetGeometries[0].rotation ?? 0)
|
|
4760
|
+
: 0;
|
|
4761
|
+
selectionBoxEl.style.transform = rotation
|
|
4762
|
+
? `rotate(${rotation}deg)`
|
|
4763
|
+
: "";
|
|
4764
|
+
selectionBoxEl.style.transformOrigin = `${bounds.width / 2}px ${bounds.height / 2}px`;
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4240
4767
|
setAlignmentGuides(snap.guides);
|
|
4241
4768
|
showTransformFeedback(
|
|
4242
4769
|
`${Math.round(snap.frame.width)} x ${Math.round(snap.frame.height)}`,
|
|
@@ -4245,7 +4772,19 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4245
4772
|
);
|
|
4246
4773
|
};
|
|
4247
4774
|
|
|
4248
|
-
|
|
4775
|
+
const handleMouseUp = () => {
|
|
4776
|
+
const state = dragState.current;
|
|
4777
|
+
if (state?.type === "draft-resize" && state.hasMoved) {
|
|
4778
|
+
// PERF9: the live resize above only wrote draftPrimitivesRef (no
|
|
4779
|
+
// setDraftPrimitives per tick) — reconcile React state with the
|
|
4780
|
+
// ref's final values here, exactly once, mirroring beginDraftDrag's
|
|
4781
|
+
// matching handleMouseUp comment.
|
|
4782
|
+
updateDraftPrimitives(() => draftPrimitivesRef.current);
|
|
4783
|
+
}
|
|
4784
|
+
finishDrag();
|
|
4785
|
+
};
|
|
4786
|
+
|
|
4787
|
+
installDragListeners(handleMouseMove, handleMouseUp);
|
|
4249
4788
|
},
|
|
4250
4789
|
[
|
|
4251
4790
|
finishDrag,
|
|
@@ -4253,6 +4792,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4253
4792
|
installDragListeners,
|
|
4254
4793
|
showTransformFeedback,
|
|
4255
4794
|
updateDraftPrimitives,
|
|
4795
|
+
updateDraftPrimitivesRefOnly,
|
|
4256
4796
|
updateSelectedDraftIds,
|
|
4257
4797
|
updateSelectedIds,
|
|
4258
4798
|
],
|
|
@@ -4285,7 +4825,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4285
4825
|
|
|
4286
4826
|
const beginFrameDrag = useCallback(
|
|
4287
4827
|
(id: string, e: React.MouseEvent) => {
|
|
4288
|
-
if (e.button !== 0 || e.shiftKey) return;
|
|
4828
|
+
if (e.button !== 0 || e.shiftKey || lockedScreenIdSet.has(id)) return;
|
|
4289
4829
|
e.preventDefault();
|
|
4290
4830
|
e.stopPropagation();
|
|
4291
4831
|
// Frame mousedowns stop propagation, so they never reach handleMouseDown.
|
|
@@ -4326,6 +4866,24 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4326
4866
|
// Figma parity: object drags keep the default arrow cursor, never a
|
|
4327
4867
|
// grabbing hand — see the matching comment in beginDraftDrag above.
|
|
4328
4868
|
|
|
4869
|
+
// The surface itself never moves mid-gesture, so its bounding rect is
|
|
4870
|
+
// invariant for the whole drag. Cache it once instead of letting the
|
|
4871
|
+
// allCommitted/getCanvasPoint branch below call getBoundingClientRect
|
|
4872
|
+
// on every tick — that read would force a synchronous layout reflow
|
|
4873
|
+
// right after this same tick's direct style writes (a classic
|
|
4874
|
+
// write-then-read thrash), scoped to primitive/layer drags specifically
|
|
4875
|
+
// (the only path that reaches that branch).
|
|
4876
|
+
const cachedSurfaceRect = surfaceRef.current?.getBoundingClientRect();
|
|
4877
|
+
const getCanvasPointFromCachedRect = (clientX: number, clientY: number) =>
|
|
4878
|
+
cachedSurfaceRect
|
|
4879
|
+
? screenToCanvasPoint(
|
|
4880
|
+
{ x: clientX, y: clientY },
|
|
4881
|
+
{ ...panRef.current, zoom: zoomRef.current },
|
|
4882
|
+
{ x: cachedSurfaceRect.left, y: cachedSurfaceRect.top },
|
|
4883
|
+
SURFACE_PADDING,
|
|
4884
|
+
)
|
|
4885
|
+
: getCanvasPoint(clientX, clientY);
|
|
4886
|
+
|
|
4329
4887
|
// PERF9: cache each dragged screen's DOM node (and the selection-box
|
|
4330
4888
|
// overlay tracking it) once, up front, instead of querying the DOM on
|
|
4331
4889
|
// every rAF tick. onStartFrameDrag only ever fires with screen.id
|
|
@@ -4520,7 +5078,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4520
5078
|
(targetId) => !currentFrameIds.includes(targetId),
|
|
4521
5079
|
);
|
|
4522
5080
|
if (allCommitted) {
|
|
4523
|
-
const canvasPoint =
|
|
5081
|
+
const canvasPoint = getCanvasPointFromCachedRect(
|
|
5082
|
+
ev.clientX,
|
|
5083
|
+
ev.clientY,
|
|
5084
|
+
);
|
|
4524
5085
|
updatePrimitiveDropTarget(
|
|
4525
5086
|
findPrimitiveDropTarget(canvasPoint, state.primaryId),
|
|
4526
5087
|
);
|
|
@@ -4549,12 +5110,15 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4549
5110
|
if (allCommitted && dropTarget) {
|
|
4550
5111
|
const sourceScreenId = resolvePrimitiveScreenId(state.primaryId);
|
|
4551
5112
|
if (sourceScreenId) {
|
|
5113
|
+
// When the drop resolved to a before/after auto-layout slot
|
|
5114
|
+
// (see findAutoLayoutInsertionAnchor), anchor against that
|
|
5115
|
+
// sibling instead of always appending inside the container.
|
|
4552
5116
|
onPrimitiveReparentRef.current?.({
|
|
4553
5117
|
sourceNodeId: state.primaryId,
|
|
4554
5118
|
sourceScreenId,
|
|
4555
|
-
targetNodeId: dropTarget.nodeId,
|
|
5119
|
+
targetNodeId: dropTarget.anchorNodeId ?? dropTarget.nodeId,
|
|
4556
5120
|
targetScreenId: dropTarget.screenId,
|
|
4557
|
-
placement: "inside",
|
|
5121
|
+
placement: dropTarget.placement ?? "inside",
|
|
4558
5122
|
});
|
|
4559
5123
|
suppressNextPick.current = true;
|
|
4560
5124
|
finishDrag();
|
|
@@ -4591,6 +5155,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4591
5155
|
getCanvasPoint,
|
|
4592
5156
|
getCurrentFrameEntries,
|
|
4593
5157
|
installDragListeners,
|
|
5158
|
+
lockedScreenIdSet,
|
|
4594
5159
|
onPick,
|
|
4595
5160
|
resolvePrimitiveScreenId,
|
|
4596
5161
|
showTransformFeedback,
|
|
@@ -4604,7 +5169,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4604
5169
|
|
|
4605
5170
|
const beginResize = useCallback(
|
|
4606
5171
|
(id: string, handle: ResizeHandle, e: React.MouseEvent) => {
|
|
4607
|
-
if (e.button !== 0) return;
|
|
5172
|
+
if (e.button !== 0 || lockedScreenIdSet.has(id)) return;
|
|
4608
5173
|
e.preventDefault();
|
|
4609
5174
|
e.stopPropagation();
|
|
4610
5175
|
suppressNextPick.current = true;
|
|
@@ -4650,6 +5215,131 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4650
5215
|
: getResizeCursor(handle),
|
|
4651
5216
|
);
|
|
4652
5217
|
|
|
5218
|
+
// PERF9: mirror beginFrameDrag's imperative-DOM discipline for resize
|
|
5219
|
+
// (previously missing here — every tick called the state-committing
|
|
5220
|
+
// updateFrameGeometry, forcing a full MultiScreenCanvas re-render on
|
|
5221
|
+
// every native mousemove, unthrottled by rAF). Cache each resized
|
|
5222
|
+
// screen's DOM nodes up front, write geometry directly to them per
|
|
5223
|
+
// tick via updateFrameGeometryRefOnly (ref-only, no setFrameGeometry),
|
|
5224
|
+
// and reconcile React state with one real commit in handleMouseUp.
|
|
5225
|
+
const frameLabelHeight =
|
|
5226
|
+
FRAME_LABEL_HEIGHT * chromeScaleFromZoom(zoomRef.current);
|
|
5227
|
+
const resizedFrameEls = new Map<string, HTMLElement>();
|
|
5228
|
+
const resizedScreenCardEls = new Map<string, HTMLElement>();
|
|
5229
|
+
const resizedContentIframeEls = new Map<string, HTMLIFrameElement>();
|
|
5230
|
+
const resizedMetadataById = new Map<
|
|
5231
|
+
string,
|
|
5232
|
+
{ width: number; height: number }
|
|
5233
|
+
>();
|
|
5234
|
+
originEntries.forEach((entry) => {
|
|
5235
|
+
const frameEl = surfaceRef.current?.querySelector<HTMLElement>(
|
|
5236
|
+
`[data-frame-id="${CSS.escape(entry.id)}"]`,
|
|
5237
|
+
);
|
|
5238
|
+
if (frameEl) {
|
|
5239
|
+
resizedFrameEls.set(entry.id, frameEl);
|
|
5240
|
+
const cardEl =
|
|
5241
|
+
frameEl.querySelector<HTMLElement>("[data-screen-card]");
|
|
5242
|
+
if (cardEl) resizedScreenCardEls.set(entry.id, cardEl);
|
|
5243
|
+
// Only the plain preview iframe (no custom screenContent, e.g. a
|
|
5244
|
+
// fusion/localhost DesignCanvas) carries this attribute at this
|
|
5245
|
+
// render layer — its own iframe is 100%-sized and follows the
|
|
5246
|
+
// screen-card resize above via CSS with no extra write needed.
|
|
5247
|
+
const iframeEl = frameEl.querySelector<HTMLIFrameElement>(
|
|
5248
|
+
`[data-screen-iframe-id="${CSS.escape(entry.id)}"]`,
|
|
5249
|
+
);
|
|
5250
|
+
if (iframeEl) resizedContentIframeEls.set(entry.id, iframeEl);
|
|
5251
|
+
}
|
|
5252
|
+
const screen = screensRef.current.find((s) => s.id === entry.id);
|
|
5253
|
+
if (screen) {
|
|
5254
|
+
resizedMetadataById.set(entry.id, getResolvedMetadata(screen));
|
|
5255
|
+
}
|
|
5256
|
+
});
|
|
5257
|
+
const resizeSelectionBoxEl =
|
|
5258
|
+
surfaceRef.current?.querySelector<HTMLElement>(
|
|
5259
|
+
"[data-frame-selection-box]",
|
|
5260
|
+
);
|
|
5261
|
+
|
|
5262
|
+
const applyResizedGeometryToDom = (
|
|
5263
|
+
ids: string[],
|
|
5264
|
+
geometryById: FrameGeometryById,
|
|
5265
|
+
) => {
|
|
5266
|
+
ids.forEach((targetId) => {
|
|
5267
|
+
const geometry = geometryById[targetId];
|
|
5268
|
+
if (!geometry) return;
|
|
5269
|
+
const frameEl = resizedFrameEls.get(targetId);
|
|
5270
|
+
if (frameEl) {
|
|
5271
|
+
const { left, top } = frameStyleLeftTop(geometry, frameLabelHeight);
|
|
5272
|
+
frameEl.style.left = `${left}px`;
|
|
5273
|
+
frameEl.style.top = `${top}px`;
|
|
5274
|
+
frameEl.style.width = `${geometry.width}px`;
|
|
5275
|
+
frameEl.style.transform = geometry.rotation
|
|
5276
|
+
? `rotate(${geometry.rotation}deg)`
|
|
5277
|
+
: "";
|
|
5278
|
+
frameEl.style.transformOrigin = `${geometry.width / 2}px ${
|
|
5279
|
+
frameLabelHeight + geometry.height / 2
|
|
5280
|
+
}px`;
|
|
5281
|
+
}
|
|
5282
|
+
const cardEl = resizedScreenCardEls.get(targetId);
|
|
5283
|
+
if (cardEl) {
|
|
5284
|
+
cardEl.style.width = `${geometry.width}px`;
|
|
5285
|
+
cardEl.style.height = `${geometry.height}px`;
|
|
5286
|
+
}
|
|
5287
|
+
const iframeEl = resizedContentIframeEls.get(targetId);
|
|
5288
|
+
const metadata = resizedMetadataById.get(targetId);
|
|
5289
|
+
if (iframeEl && metadata) {
|
|
5290
|
+
const viewport = getScreenPreviewViewport(metadata, geometry);
|
|
5291
|
+
iframeEl.style.width = `${viewport.viewportWidth}px`;
|
|
5292
|
+
iframeEl.style.height = `${viewport.viewportHeight}px`;
|
|
5293
|
+
iframeEl.style.transform =
|
|
5294
|
+
viewport.scale === 1 ? "" : `scale(${viewport.scale})`;
|
|
5295
|
+
}
|
|
5296
|
+
});
|
|
5297
|
+
|
|
5298
|
+
// Keep the selection outline + resize/rotate handles glued to the
|
|
5299
|
+
// live geometry — see the matching comment in beginFrameDrag's move
|
|
5300
|
+
// handler for why SelectionBox's own geometry prop can't do this on
|
|
5301
|
+
// its own during a ref-only tick.
|
|
5302
|
+
if (resizeSelectionBoxEl) {
|
|
5303
|
+
const targetGeometries = ids
|
|
5304
|
+
.map((targetId) => geometryById[targetId])
|
|
5305
|
+
.filter((geometry): geometry is FrameGeometry => Boolean(geometry));
|
|
5306
|
+
const bounds =
|
|
5307
|
+
targetGeometries.length === 1
|
|
5308
|
+
? targetGeometries[0]
|
|
5309
|
+
: (() => {
|
|
5310
|
+
const groupBounds = getFrameGroupBounds(
|
|
5311
|
+
targetGeometries.map((geometry) => ({
|
|
5312
|
+
id: "",
|
|
5313
|
+
geometry,
|
|
5314
|
+
})),
|
|
5315
|
+
);
|
|
5316
|
+
return groupBounds
|
|
5317
|
+
? {
|
|
5318
|
+
x: groupBounds.left,
|
|
5319
|
+
y: groupBounds.top,
|
|
5320
|
+
width: groupBounds.width,
|
|
5321
|
+
height: groupBounds.height,
|
|
5322
|
+
}
|
|
5323
|
+
: null;
|
|
5324
|
+
})();
|
|
5325
|
+
if (bounds) {
|
|
5326
|
+
const { left, top } = frameStyleLeftTop(bounds);
|
|
5327
|
+
resizeSelectionBoxEl.style.left = `${left}px`;
|
|
5328
|
+
resizeSelectionBoxEl.style.top = `${top}px`;
|
|
5329
|
+
resizeSelectionBoxEl.style.width = `${bounds.width}px`;
|
|
5330
|
+
resizeSelectionBoxEl.style.height = `${bounds.height}px`;
|
|
5331
|
+
const rotation =
|
|
5332
|
+
targetGeometries.length === 1
|
|
5333
|
+
? (targetGeometries[0].rotation ?? 0)
|
|
5334
|
+
: 0;
|
|
5335
|
+
resizeSelectionBoxEl.style.transform = rotation
|
|
5336
|
+
? `rotate(${rotation}deg)`
|
|
5337
|
+
: "";
|
|
5338
|
+
resizeSelectionBoxEl.style.transformOrigin = `${bounds.width / 2}px ${bounds.height / 2}px`;
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
};
|
|
5342
|
+
|
|
4653
5343
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
4654
5344
|
const state = dragState.current;
|
|
4655
5345
|
if (!state || state.type !== "resize") return;
|
|
@@ -4704,26 +5394,52 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4704
5394
|
: null;
|
|
4705
5395
|
|
|
4706
5396
|
if (singleRotatedFrame) {
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
5397
|
+
// Rotation-aware resize snapping (WORK ITEM 2): snap against the
|
|
5398
|
+
// same stationary siblings the unrotated group-resize path below
|
|
5399
|
+
// uses. See resizeRotatedFrameFromDeltaWithSnap's doc comment for
|
|
5400
|
+
// the documented approximation (snaps the frame's own unrotated
|
|
5401
|
+
// local bounds, not a full rotated-edge projection).
|
|
5402
|
+
const { frame: resizedGeometry, guides: rotatedSnapGuides } =
|
|
5403
|
+
resizeRotatedFrameFromDeltaWithSnap(
|
|
5404
|
+
singleRotatedFrame.geometry,
|
|
5405
|
+
state.handle,
|
|
5406
|
+
dx,
|
|
5407
|
+
dy,
|
|
5408
|
+
getCurrentFrameEntries().filter(
|
|
5409
|
+
(entry) => !state.targetIds.includes(entry.id),
|
|
5410
|
+
),
|
|
5411
|
+
{
|
|
5412
|
+
thresholdScreenPx: DEFAULT_SNAP_THRESHOLD_SCREEN_PX,
|
|
5413
|
+
zoom: zoomRef.current,
|
|
5414
|
+
bypass: ev.metaKey || ev.ctrlKey,
|
|
5415
|
+
preserveAspectRatio: lockAspectRatio,
|
|
5416
|
+
},
|
|
5417
|
+
{
|
|
5418
|
+
preserveAspectRatio: lockAspectRatio,
|
|
5419
|
+
resizeFromCenter: ev.altKey,
|
|
5420
|
+
minWidth: 1,
|
|
5421
|
+
minHeight: 1,
|
|
5422
|
+
},
|
|
5423
|
+
);
|
|
5424
|
+
let nextGeometryById: FrameGeometryById | null = null;
|
|
5425
|
+
updateFrameGeometryRefOnly((current) => {
|
|
5426
|
+
const next = {
|
|
5427
|
+
...current,
|
|
5428
|
+
[singleRotatedFrame.id]: {
|
|
5429
|
+
...state.originFrames[singleRotatedFrame.id],
|
|
5430
|
+
...resizedGeometry,
|
|
5431
|
+
},
|
|
5432
|
+
};
|
|
5433
|
+
nextGeometryById = next;
|
|
5434
|
+
return next;
|
|
5435
|
+
});
|
|
5436
|
+
if (nextGeometryById) {
|
|
5437
|
+
applyResizedGeometryToDom(
|
|
5438
|
+
[singleRotatedFrame.id],
|
|
5439
|
+
nextGeometryById,
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
setAlignmentGuides(rotatedSnapGuides);
|
|
4727
5443
|
showTransformFeedback(
|
|
4728
5444
|
`${Math.round(resizedGeometry.width)} x ${Math.round(resizedGeometry.height)}`,
|
|
4729
5445
|
ev.clientX,
|
|
@@ -4767,7 +5483,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4767
5483
|
state.originBounds,
|
|
4768
5484
|
snap.frame,
|
|
4769
5485
|
);
|
|
4770
|
-
|
|
5486
|
+
let nextGeometryById: FrameGeometryById | null = null;
|
|
5487
|
+
updateFrameGeometryRefOnly((current) => {
|
|
4771
5488
|
const next = { ...current };
|
|
4772
5489
|
resizedEntries.forEach((entry) => {
|
|
4773
5490
|
next[entry.id] = {
|
|
@@ -4775,8 +5492,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4775
5492
|
...entry.geometry,
|
|
4776
5493
|
};
|
|
4777
5494
|
});
|
|
5495
|
+
nextGeometryById = next;
|
|
4778
5496
|
return next;
|
|
4779
5497
|
});
|
|
5498
|
+
if (nextGeometryById) {
|
|
5499
|
+
applyResizedGeometryToDom(state.targetIds, nextGeometryById);
|
|
5500
|
+
}
|
|
4780
5501
|
setAlignmentGuides(snap.guides);
|
|
4781
5502
|
showTransformFeedback(
|
|
4782
5503
|
`${Math.round(snap.frame.width)} x ${Math.round(snap.frame.height)}`,
|
|
@@ -4794,7 +5515,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4794
5515
|
);
|
|
4795
5516
|
}
|
|
4796
5517
|
if (state?.type === "resize" && state.hasMoved) {
|
|
5518
|
+
// PERF9: the live resize above only wrote frameGeometryRef (ref-only,
|
|
5519
|
+
// no setFrameGeometry per tick) — reconcile React state with the
|
|
5520
|
+
// ref's final values here, exactly once, mirroring beginFrameDrag's
|
|
5521
|
+
// matching handleMouseUp comment.
|
|
4797
5522
|
const after = cloneFrameGeometryById(frameGeometryRef.current);
|
|
5523
|
+
setFrameGeometry(after);
|
|
5524
|
+
onGeometryChangeRef.current?.(after);
|
|
4798
5525
|
onGeometryCommitRef.current?.(
|
|
4799
5526
|
frameGeometryWithOverrides(after, state.originFrames),
|
|
4800
5527
|
after,
|
|
@@ -4809,10 +5536,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4809
5536
|
activeId,
|
|
4810
5537
|
finishDrag,
|
|
4811
5538
|
getCurrentFrameEntries,
|
|
5539
|
+
getResolvedMetadata,
|
|
4812
5540
|
installDragListeners,
|
|
5541
|
+
lockedScreenIdSet,
|
|
4813
5542
|
onPick,
|
|
4814
5543
|
showTransformFeedback,
|
|
4815
5544
|
updateFrameGeometry,
|
|
5545
|
+
updateFrameGeometryRefOnly,
|
|
4816
5546
|
updateSelectedIds,
|
|
4817
5547
|
],
|
|
4818
5548
|
);
|
|
@@ -4828,7 +5558,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4828
5558
|
|
|
4829
5559
|
const beginRotate = useCallback(
|
|
4830
5560
|
(id: string, e: React.MouseEvent) => {
|
|
4831
|
-
if (e.button !== 0) return;
|
|
5561
|
+
if (e.button !== 0 || lockedScreenIdSet.has(id)) return;
|
|
4832
5562
|
e.preventDefault();
|
|
4833
5563
|
e.stopPropagation();
|
|
4834
5564
|
suppressNextPick.current = true;
|
|
@@ -4863,6 +5593,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4863
5593
|
rotateCursorDataUri(quantizeAngleTo8Buckets(originPointerAngle)),
|
|
4864
5594
|
);
|
|
4865
5595
|
|
|
5596
|
+
// PERF9: same imperative-DOM discipline as beginFrameDrag/beginResize.
|
|
5597
|
+
// Rotation never changes width/height, so only the frame shell's own
|
|
5598
|
+
// transform (plus the glued selection box) needs a live write per
|
|
5599
|
+
// tick — no screen-card/iframe involvement like resize needs.
|
|
5600
|
+
const rotateFrameEl = surfaceRef.current?.querySelector<HTMLElement>(
|
|
5601
|
+
`[data-frame-id="${CSS.escape(id)}"]`,
|
|
5602
|
+
);
|
|
5603
|
+
const rotateSelectionBoxEl =
|
|
5604
|
+
surfaceRef.current?.querySelector<HTMLElement>(
|
|
5605
|
+
"[data-frame-selection-box]",
|
|
5606
|
+
);
|
|
5607
|
+
|
|
4866
5608
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
4867
5609
|
const state = dragState.current;
|
|
4868
5610
|
if (!state || state.type !== "rotate") return;
|
|
@@ -4885,13 +5627,19 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4885
5627
|
const raw =
|
|
4886
5628
|
state.originRotation + pointerAngle - state.originPointerAngle;
|
|
4887
5629
|
const rotation = ev.shiftKey ? Math.round(raw / 15) * 15 : raw;
|
|
4888
|
-
|
|
5630
|
+
const roundedRotation = Math.round(rotation * 10) / 10;
|
|
5631
|
+
updateFrameGeometryRefOnly((current) => ({
|
|
4889
5632
|
...current,
|
|
4890
5633
|
[state.frameId]: {
|
|
4891
5634
|
...state.originFrame,
|
|
4892
|
-
rotation:
|
|
5635
|
+
rotation: roundedRotation,
|
|
4893
5636
|
},
|
|
4894
5637
|
}));
|
|
5638
|
+
const rotateTransform = `rotate(${roundedRotation}deg)`;
|
|
5639
|
+
if (rotateFrameEl) rotateFrameEl.style.transform = rotateTransform;
|
|
5640
|
+
if (rotateSelectionBoxEl) {
|
|
5641
|
+
rotateSelectionBoxEl.style.transform = rotateTransform;
|
|
5642
|
+
}
|
|
4895
5643
|
setDragCursor(
|
|
4896
5644
|
rotateCursorDataUri(quantizeAngleTo8Buckets(pointerAngle)),
|
|
4897
5645
|
);
|
|
@@ -4912,7 +5660,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4912
5660
|
}));
|
|
4913
5661
|
}
|
|
4914
5662
|
if (state?.type === "rotate" && state.hasMoved) {
|
|
5663
|
+
// PERF9: the live rotate above only wrote frameGeometryRef
|
|
5664
|
+
// (ref-only, no setFrameGeometry per tick) — reconcile React state
|
|
5665
|
+
// with the ref's final values here, exactly once, mirroring
|
|
5666
|
+
// beginFrameDrag's matching handleMouseUp comment.
|
|
4915
5667
|
const after = cloneFrameGeometryById(frameGeometryRef.current);
|
|
5668
|
+
setFrameGeometry(after);
|
|
5669
|
+
onGeometryChangeRef.current?.(after);
|
|
4916
5670
|
onGeometryCommitRef.current?.(
|
|
4917
5671
|
frameGeometryWithOverrides(after, {
|
|
4918
5672
|
[state.frameId]: state.originFrame,
|
|
@@ -4932,9 +5686,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4932
5686
|
getCanvasPoint,
|
|
4933
5687
|
getCurrentFrameEntries,
|
|
4934
5688
|
installDragListeners,
|
|
5689
|
+
lockedScreenIdSet,
|
|
4935
5690
|
onPick,
|
|
4936
5691
|
showTransformFeedback,
|
|
4937
5692
|
updateFrameGeometry,
|
|
5693
|
+
updateFrameGeometryRefOnly,
|
|
4938
5694
|
updateSelectedIds,
|
|
4939
5695
|
],
|
|
4940
5696
|
);
|
|
@@ -4979,6 +5735,29 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
4979
5735
|
rotateCursorDataUri(quantizeAngleTo8Buckets(originPointerAngle)),
|
|
4980
5736
|
);
|
|
4981
5737
|
|
|
5738
|
+
// PERF9: same imperative-DOM discipline as beginRotate/beginResize.
|
|
5739
|
+
// Unlike single rotate, orbiting around a shared center also changes
|
|
5740
|
+
// each member's x/y, so every frame shell needs a live left/top write,
|
|
5741
|
+
// not just transform. The individual PassiveSelectionBox outlines are
|
|
5742
|
+
// intentionally left to the next real render (they briefly lag behind
|
|
5743
|
+
// during the gesture, self-correcting at mouseup) — group rotate is a
|
|
5744
|
+
// rarer gesture and threading a live update through each of those too
|
|
5745
|
+
// is a larger change than this fix covers; the shared GroupSelectionBox
|
|
5746
|
+
// (the interactive handle the user is actually dragging) is kept glued.
|
|
5747
|
+
const groupRotateFrameEls = new Map<string, HTMLElement>();
|
|
5748
|
+
originEntries.forEach((entry) => {
|
|
5749
|
+
const frameEl = surfaceRef.current?.querySelector<HTMLElement>(
|
|
5750
|
+
`[data-frame-id="${CSS.escape(entry.id)}"]`,
|
|
5751
|
+
);
|
|
5752
|
+
if (frameEl) groupRotateFrameEls.set(entry.id, frameEl);
|
|
5753
|
+
});
|
|
5754
|
+
const groupRotateSelectionBoxEl =
|
|
5755
|
+
surfaceRef.current?.querySelector<HTMLElement>(
|
|
5756
|
+
"[data-frame-selection-box]",
|
|
5757
|
+
);
|
|
5758
|
+
const groupFrameLabelHeight =
|
|
5759
|
+
FRAME_LABEL_HEIGHT * chromeScaleFromZoom(zoomRef.current);
|
|
5760
|
+
|
|
4982
5761
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
4983
5762
|
const state = dragState.current;
|
|
4984
5763
|
if (!state || state.type !== "group-rotate") return;
|
|
@@ -5012,7 +5791,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5012
5791
|
state.groupCenter,
|
|
5013
5792
|
delta,
|
|
5014
5793
|
);
|
|
5015
|
-
|
|
5794
|
+
let nextGeometryById: FrameGeometryById | null = null;
|
|
5795
|
+
updateFrameGeometryRefOnly((current) => {
|
|
5016
5796
|
const next = { ...current };
|
|
5017
5797
|
rotated.forEach((entry) => {
|
|
5018
5798
|
next[entry.id] = {
|
|
@@ -5020,8 +5800,38 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5020
5800
|
...entry.geometry,
|
|
5021
5801
|
};
|
|
5022
5802
|
});
|
|
5803
|
+
nextGeometryById = next;
|
|
5023
5804
|
return next;
|
|
5024
5805
|
});
|
|
5806
|
+
if (nextGeometryById) {
|
|
5807
|
+
rotated.forEach((entry) => {
|
|
5808
|
+
const geometry = nextGeometryById![entry.id];
|
|
5809
|
+
const frameEl = groupRotateFrameEls.get(entry.id);
|
|
5810
|
+
if (!geometry || !frameEl) return;
|
|
5811
|
+
const { left, top } = frameStyleLeftTop(
|
|
5812
|
+
geometry,
|
|
5813
|
+
groupFrameLabelHeight,
|
|
5814
|
+
);
|
|
5815
|
+
frameEl.style.left = `${left}px`;
|
|
5816
|
+
frameEl.style.top = `${top}px`;
|
|
5817
|
+
frameEl.style.transform = geometry.rotation
|
|
5818
|
+
? `rotate(${geometry.rotation}deg)`
|
|
5819
|
+
: "";
|
|
5820
|
+
});
|
|
5821
|
+
if (groupRotateSelectionBoxEl) {
|
|
5822
|
+
const groupBoundsNow = getFrameGroupBounds(rotated);
|
|
5823
|
+
if (groupBoundsNow) {
|
|
5824
|
+
const { left, top } = frameStyleLeftTop({
|
|
5825
|
+
x: groupBoundsNow.left,
|
|
5826
|
+
y: groupBoundsNow.top,
|
|
5827
|
+
});
|
|
5828
|
+
groupRotateSelectionBoxEl.style.left = `${left}px`;
|
|
5829
|
+
groupRotateSelectionBoxEl.style.top = `${top}px`;
|
|
5830
|
+
groupRotateSelectionBoxEl.style.width = `${groupBoundsNow.width}px`;
|
|
5831
|
+
groupRotateSelectionBoxEl.style.height = `${groupBoundsNow.height}px`;
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5025
5835
|
showTransformFeedback(
|
|
5026
5836
|
`${Math.round(delta)}deg`,
|
|
5027
5837
|
ev.clientX,
|
|
@@ -5038,7 +5848,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5038
5848
|
);
|
|
5039
5849
|
}
|
|
5040
5850
|
if (state?.type === "group-rotate" && state.hasMoved) {
|
|
5851
|
+
// PERF9: the live group-rotate above only wrote frameGeometryRef
|
|
5852
|
+
// (ref-only, no setFrameGeometry per tick) — reconcile React state
|
|
5853
|
+
// with the ref's final values here, exactly once, mirroring
|
|
5854
|
+
// beginFrameDrag's matching handleMouseUp comment.
|
|
5041
5855
|
const after = cloneFrameGeometryById(frameGeometryRef.current);
|
|
5856
|
+
setFrameGeometry(after);
|
|
5857
|
+
onGeometryChangeRef.current?.(after);
|
|
5042
5858
|
onGeometryCommitRef.current?.(
|
|
5043
5859
|
frameGeometryWithOverrides(after, state.originFrames),
|
|
5044
5860
|
after,
|
|
@@ -5057,12 +5873,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5057
5873
|
installDragListeners,
|
|
5058
5874
|
showTransformFeedback,
|
|
5059
5875
|
updateFrameGeometry,
|
|
5876
|
+
updateFrameGeometryRefOnly,
|
|
5060
5877
|
],
|
|
5061
5878
|
);
|
|
5062
5879
|
|
|
5063
5880
|
const handleFrameClick = useCallback(
|
|
5064
5881
|
(id: string, e: React.MouseEvent<HTMLElement>) => {
|
|
5065
5882
|
e.stopPropagation();
|
|
5883
|
+
if (lockedScreenIdSet.has(id)) return;
|
|
5066
5884
|
if (suppressNextPick.current) {
|
|
5067
5885
|
suppressNextPick.current = false;
|
|
5068
5886
|
return;
|
|
@@ -5091,24 +5909,42 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5091
5909
|
updateSelectedIds(() => [id]);
|
|
5092
5910
|
onPick(id);
|
|
5093
5911
|
},
|
|
5094
|
-
[
|
|
5912
|
+
[
|
|
5913
|
+
activeId,
|
|
5914
|
+
lockedScreenIdSet,
|
|
5915
|
+
onPick,
|
|
5916
|
+
updateSelectedDraftIds,
|
|
5917
|
+
updateSelectedIds,
|
|
5918
|
+
],
|
|
5095
5919
|
);
|
|
5096
5920
|
|
|
5097
5921
|
const handleFrameDoubleClick = useCallback(
|
|
5098
5922
|
(id: string, e: React.MouseEvent<HTMLElement>) => {
|
|
5099
5923
|
e.preventDefault();
|
|
5100
5924
|
e.stopPropagation();
|
|
5925
|
+
if (lockedScreenIdSet.has(id)) {
|
|
5926
|
+
onEdit?.(id);
|
|
5927
|
+
return;
|
|
5928
|
+
}
|
|
5101
5929
|
updateSelectedDraftIds(() => []);
|
|
5102
5930
|
updateSelectedIds(() => [id]);
|
|
5103
5931
|
onPick(id);
|
|
5104
5932
|
onEdit?.(id);
|
|
5105
5933
|
},
|
|
5106
|
-
[
|
|
5934
|
+
[
|
|
5935
|
+
lockedScreenIdSet,
|
|
5936
|
+
onEdit,
|
|
5937
|
+
onPick,
|
|
5938
|
+
updateSelectedDraftIds,
|
|
5939
|
+
updateSelectedIds,
|
|
5940
|
+
],
|
|
5107
5941
|
);
|
|
5108
5942
|
|
|
5109
5943
|
const beginDuplicateGesture = useCallback(
|
|
5110
5944
|
(screen: ScreenFile, display: string, e: React.MouseEvent<HTMLElement>) => {
|
|
5111
|
-
if (e.button !== 0 || !e.altKey)
|
|
5945
|
+
if (e.button !== 0 || !e.altKey || lockedScreenIdSet.has(screen.id)) {
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5112
5948
|
e.preventDefault();
|
|
5113
5949
|
e.stopPropagation();
|
|
5114
5950
|
duplicateCleanup.current?.();
|
|
@@ -5155,25 +5991,60 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5155
5991
|
// duplicate on mouseup.
|
|
5156
5992
|
setDragCursor(e.altKey ? "copy" : null);
|
|
5157
5993
|
|
|
5994
|
+
// PERF: track the last committed canDuplicate/moved/cursor values in
|
|
5995
|
+
// plain closure locals (not state) so the mousemove tick below can tell
|
|
5996
|
+
// whether anything conditional actually changed without reading back
|
|
5997
|
+
// through React state (which the imperative writes below intentionally
|
|
5998
|
+
// stop keeping fresh every tick — see duplicatePreviewElRef).
|
|
5999
|
+
let lastCanDuplicate = !!onDuplicate;
|
|
6000
|
+
let lastMoved = false;
|
|
6001
|
+
let lastCursor: string | null = e.altKey ? "copy" : null;
|
|
6002
|
+
|
|
5158
6003
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
5159
6004
|
const dx = ev.clientX - origin.x;
|
|
5160
6005
|
const dy = ev.clientY - origin.y;
|
|
5161
6006
|
const moved = Math.hypot(dx, dy) >= DUPLICATE_DRAG_THRESHOLD;
|
|
5162
6007
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
6008
|
+
const x = rect ? ev.clientX - rect.left + 16 : ev.clientX;
|
|
6009
|
+
const y = rect ? ev.clientY - rect.top + 16 : ev.clientY;
|
|
6010
|
+
// Live alt state, not just capability: if the user releases alt
|
|
6011
|
+
// mid-drag the preview should visibly fall back to its "not armed"
|
|
6012
|
+
// dashed/preview styling, matching that mouseup will then cancel
|
|
6013
|
+
// the duplicate instead of creating one (see handleMouseUp below).
|
|
6014
|
+
const canDuplicate = !!onDuplicate && ev.altKey;
|
|
6015
|
+
|
|
6016
|
+
// PERF9: the ghost's position is a direct DOM write every tick
|
|
6017
|
+
// instead of a setDuplicatePreview (full re-render) — same
|
|
6018
|
+
// "imperative now, commit state only when something conditional
|
|
6019
|
+
// changes" discipline as the frame/draft drag paths above. Falls
|
|
6020
|
+
// back to setDuplicatePreview itself if the node hasn't mounted yet
|
|
6021
|
+
// (e.g. the very first tick right after mousedown, before React has
|
|
6022
|
+
// committed the initial preview).
|
|
6023
|
+
const el = duplicatePreviewElRef.current;
|
|
6024
|
+
if (el) {
|
|
6025
|
+
el.style.left = `${x}px`;
|
|
6026
|
+
el.style.top = `${y}px`;
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
if (!el || canDuplicate !== lastCanDuplicate || moved !== lastMoved) {
|
|
6030
|
+
lastCanDuplicate = canDuplicate;
|
|
6031
|
+
lastMoved = moved;
|
|
6032
|
+
setDuplicatePreview({
|
|
6033
|
+
display,
|
|
6034
|
+
x,
|
|
6035
|
+
y,
|
|
6036
|
+
width: previewWidth,
|
|
6037
|
+
height: previewHeight,
|
|
6038
|
+
canDuplicate,
|
|
6039
|
+
moved,
|
|
6040
|
+
});
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
const cursor = ev.altKey ? "copy" : null;
|
|
6044
|
+
if (cursor !== lastCursor) {
|
|
6045
|
+
lastCursor = cursor;
|
|
6046
|
+
setDragCursor(cursor);
|
|
6047
|
+
}
|
|
5177
6048
|
};
|
|
5178
6049
|
|
|
5179
6050
|
const cleanupDuplicateGesture = () => {
|
|
@@ -5247,6 +6118,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5247
6118
|
finishDrag,
|
|
5248
6119
|
getCurrentFrameEntries,
|
|
5249
6120
|
installDragListeners,
|
|
6121
|
+
lockedScreenIdSet,
|
|
5250
6122
|
onDuplicate,
|
|
5251
6123
|
onPick,
|
|
5252
6124
|
],
|
|
@@ -5330,6 +6202,39 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5330
6202
|
beginDraftCreation(creationTool, e);
|
|
5331
6203
|
return;
|
|
5332
6204
|
}
|
|
6205
|
+
if (
|
|
6206
|
+
e.button === 0 &&
|
|
6207
|
+
tool === "move" &&
|
|
6208
|
+
!onFrame &&
|
|
6209
|
+
showBoardStaticPreview &&
|
|
6210
|
+
boardSurfaceRenderGeometry
|
|
6211
|
+
) {
|
|
6212
|
+
const point = getCanvasPoint(e.clientX, e.clientY);
|
|
6213
|
+
if (!geometryContainsPoint(boardSurfaceRenderGeometry, point)) {
|
|
6214
|
+
const hit = [...boardStaticPrimitives]
|
|
6215
|
+
.reverse()
|
|
6216
|
+
.find((primitive) =>
|
|
6217
|
+
geometryContainsPoint(
|
|
6218
|
+
getPrimitiveLowZoomHitRect(primitive, zoomRef.current),
|
|
6219
|
+
point,
|
|
6220
|
+
),
|
|
6221
|
+
);
|
|
6222
|
+
if (hit) {
|
|
6223
|
+
e.preventDefault();
|
|
6224
|
+
e.stopPropagation();
|
|
6225
|
+
pendingStaticBoardSelectionRef.current = {
|
|
6226
|
+
nodeId: hit.nodeId,
|
|
6227
|
+
point,
|
|
6228
|
+
};
|
|
6229
|
+
// Re-window the one live Alpine iframe around the clicked static
|
|
6230
|
+
// primitive. DesignCanvas updates its content offset in place, so
|
|
6231
|
+
// the iframe node/srcdoc/runtime survive; the effect above selects
|
|
6232
|
+
// the same node through that live bridge after the offset settles.
|
|
6233
|
+
setBoardSurfaceFocusPoint(point);
|
|
6234
|
+
return;
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
5333
6238
|
if (e.button === 0 && !onFrame) {
|
|
5334
6239
|
beginMarquee(e);
|
|
5335
6240
|
}
|
|
@@ -5342,10 +6247,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5342
6247
|
beginPenNodeCreation,
|
|
5343
6248
|
beginVectorAnchorDrag,
|
|
5344
6249
|
beginVectorHandleDrag,
|
|
6250
|
+
boardStaticPrimitives,
|
|
6251
|
+
boardSurfaceRenderGeometry,
|
|
5345
6252
|
claimKeyboardFocus,
|
|
5346
6253
|
getCanvasPoint,
|
|
5347
6254
|
localActiveTool,
|
|
5348
6255
|
setAltHoverMeasurement,
|
|
6256
|
+
showBoardStaticPreview,
|
|
5349
6257
|
toggleVectorNodeType,
|
|
5350
6258
|
vectorEdit,
|
|
5351
6259
|
],
|
|
@@ -5576,6 +6484,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5576
6484
|
// it's an acceptable trade for keeping gesture start/end render-free.
|
|
5577
6485
|
const markWheelGestureActive = useCallback(() => {
|
|
5578
6486
|
if (wheelGestureActiveRef.current) return;
|
|
6487
|
+
cancelPendingStaticBoardSelection();
|
|
5579
6488
|
wheelGestureActiveRef.current = true;
|
|
5580
6489
|
setWheelCameraGestureActive(true);
|
|
5581
6490
|
const surface = surfaceRef.current;
|
|
@@ -5594,7 +6503,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
5594
6503
|
element.style.pointerEvents = "none";
|
|
5595
6504
|
});
|
|
5596
6505
|
wheelGestureMutedElementsRef.current = muted;
|
|
5597
|
-
}, []);
|
|
6506
|
+
}, [cancelPendingStaticBoardSelection]);
|
|
5598
6507
|
|
|
5599
6508
|
const flushPendingWheelGesture = useCallback(() => {
|
|
5600
6509
|
wheelGestureFrameRef.current = null;
|
|
@@ -6164,11 +7073,12 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6164
7073
|
const canvasFrames = useMemo(() => {
|
|
6165
7074
|
const cache = canvasFrameEntryCacheRef.current;
|
|
6166
7075
|
const nextIds = new Set<string>();
|
|
6167
|
-
const next =
|
|
7076
|
+
const next = renderedScreens.map((screen) => {
|
|
6168
7077
|
nextIds.add(screen.id);
|
|
6169
7078
|
const metadata = getResolvedMetadata(screen);
|
|
6170
7079
|
const geometry =
|
|
6171
|
-
frameGeometry[screen.id] ??
|
|
7080
|
+
frameGeometry[screen.id] ??
|
|
7081
|
+
getInitialFrameGeometry(screenIndexById.get(screen.id) ?? 0, metadata);
|
|
6172
7082
|
const prior = cache.get(screen.id);
|
|
6173
7083
|
if (
|
|
6174
7084
|
prior &&
|
|
@@ -6192,7 +7102,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6192
7102
|
}
|
|
6193
7103
|
pruneResolvedMetadataCache(resolvedMetadataCacheRef.current, nextIds);
|
|
6194
7104
|
return next;
|
|
6195
|
-
}, [frameGeometry, getResolvedMetadata,
|
|
7105
|
+
}, [frameGeometry, getResolvedMetadata, renderedScreens, screenIndexById]);
|
|
6196
7106
|
// Interaction-protected screens are never eligible for LRU eviction. Active
|
|
6197
7107
|
// screen/frame selection are the primary signals; layer selection, native
|
|
6198
7108
|
// file-drop targeting, gradient editing, and in-flight frame transforms are
|
|
@@ -6441,6 +7351,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6441
7351
|
|
|
6442
7352
|
<div
|
|
6443
7353
|
ref={worldRef}
|
|
7354
|
+
data-multi-screen-canvas-world
|
|
6444
7355
|
className="pointer-events-none absolute"
|
|
6445
7356
|
style={{
|
|
6446
7357
|
left: 0,
|
|
@@ -6456,11 +7367,51 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6456
7367
|
transformOrigin: "top left",
|
|
6457
7368
|
}}
|
|
6458
7369
|
>
|
|
7370
|
+
{showBoardStaticPreview &&
|
|
7371
|
+
boardFrameGeometry &&
|
|
7372
|
+
boardStaticPreviewViewport &&
|
|
7373
|
+
boardStaticPreviewContent ? (
|
|
7374
|
+
<div
|
|
7375
|
+
data-board-static-preview
|
|
7376
|
+
aria-hidden="true"
|
|
7377
|
+
style={{
|
|
7378
|
+
position: "absolute",
|
|
7379
|
+
left: SURFACE_PADDING + boardFrameGeometry.x,
|
|
7380
|
+
top: SURFACE_PADDING + boardFrameGeometry.y,
|
|
7381
|
+
width: boardFrameGeometry.width,
|
|
7382
|
+
height: boardFrameGeometry.height,
|
|
7383
|
+
overflow: "hidden",
|
|
7384
|
+
pointerEvents: "none",
|
|
7385
|
+
background: "transparent",
|
|
7386
|
+
zIndex: 0,
|
|
7387
|
+
}}
|
|
7388
|
+
>
|
|
7389
|
+
<iframe
|
|
7390
|
+
data-board-static-preview-iframe
|
|
7391
|
+
aria-hidden="true"
|
|
7392
|
+
tabIndex={-1}
|
|
7393
|
+
sandbox=""
|
|
7394
|
+
referrerPolicy="no-referrer"
|
|
7395
|
+
srcDoc={boardStaticPreviewContent}
|
|
7396
|
+
style={{
|
|
7397
|
+
display: "block",
|
|
7398
|
+
width: boardStaticPreviewViewport.width,
|
|
7399
|
+
height: boardStaticPreviewViewport.height,
|
|
7400
|
+
border: 0,
|
|
7401
|
+
pointerEvents: "none",
|
|
7402
|
+
transform: `scale(${boardFrameGeometry.width / boardStaticPreviewViewport.width}, ${boardFrameGeometry.height / boardStaticPreviewViewport.height})`,
|
|
7403
|
+
transformOrigin: "top left",
|
|
7404
|
+
background: "transparent",
|
|
7405
|
+
}}
|
|
7406
|
+
/>
|
|
7407
|
+
</div>
|
|
7408
|
+
) : null}
|
|
7409
|
+
|
|
6459
7410
|
{boardFileId &&
|
|
6460
7411
|
boardFileContent !== undefined &&
|
|
6461
7412
|
hasBoardSurfaceContent(boardFileContent) &&
|
|
6462
7413
|
(() => {
|
|
6463
|
-
const boardGeo =
|
|
7414
|
+
const boardGeo = boardSurfaceRenderGeometry ?? {
|
|
6464
7415
|
x: 0,
|
|
6465
7416
|
y: 0,
|
|
6466
7417
|
width: 8192,
|
|
@@ -6569,6 +7520,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6569
7520
|
screen={screen}
|
|
6570
7521
|
metadata={metadata}
|
|
6571
7522
|
geometry={geometry}
|
|
7523
|
+
locked={lockedScreenIdSet.has(screen.id)}
|
|
6572
7524
|
screenContent={screenContentById.get(screen.id)}
|
|
6573
7525
|
cullTier={screenCullTierById.get(screen.id) ?? "visible"}
|
|
6574
7526
|
isActive={screen.id === activeId}
|
|
@@ -6904,33 +7856,57 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6904
7856
|
) : null}
|
|
6905
7857
|
|
|
6906
7858
|
{primitiveDropTarget ? (
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
7859
|
+
primitiveDropTarget.placement === "before" ||
|
|
7860
|
+
primitiveDropTarget.placement === "after" ? (
|
|
7861
|
+
// Auto-layout flow-insert: reuse the exact same insertion-LINE
|
|
7862
|
+
// renderer the cross-screen hit-test guide uses (see
|
|
7863
|
+
// getCrossScreenDropGuideStyle) so both drop paths draw an
|
|
7864
|
+
// identical Figma-style indicator between siblings.
|
|
7865
|
+
<span
|
|
7866
|
+
data-primitive-drop-target
|
|
7867
|
+
data-primitive-drop-placement={primitiveDropTarget.placement}
|
|
7868
|
+
className="pointer-events-none absolute z-40 rounded-sm"
|
|
7869
|
+
style={getCrossScreenDropGuideStyle({
|
|
7870
|
+
guide: {
|
|
7871
|
+
placement: primitiveDropTarget.placement,
|
|
7872
|
+
axis: primitiveDropTarget.axis ?? "y",
|
|
7873
|
+
boardRect: primitiveDropTarget.boardRect,
|
|
7874
|
+
},
|
|
7875
|
+
pan,
|
|
7876
|
+
scale,
|
|
7877
|
+
})}
|
|
7878
|
+
/>
|
|
7879
|
+
) : (
|
|
7880
|
+
<span
|
|
7881
|
+
data-primitive-drop-target
|
|
7882
|
+
className="pointer-events-none absolute z-40 rounded-sm"
|
|
7883
|
+
style={{
|
|
7884
|
+
// Surface position = pan + (SURFACE_PADDING + canvasCoord) * scale
|
|
7885
|
+
left:
|
|
7886
|
+
pan.x +
|
|
7887
|
+
(SURFACE_PADDING + primitiveDropTarget.boardRect.x) * scale,
|
|
7888
|
+
top:
|
|
7889
|
+
pan.y +
|
|
7890
|
+
(SURFACE_PADDING + primitiveDropTarget.boardRect.y) * scale,
|
|
7891
|
+
width: Math.max(1, primitiveDropTarget.boardRect.width * scale),
|
|
7892
|
+
height: Math.max(1, primitiveDropTarget.boardRect.height * scale),
|
|
7893
|
+
transform: primitiveDropTarget.boardRect.rotation
|
|
7894
|
+
? `rotate(${primitiveDropTarget.boardRect.rotation}deg)`
|
|
7895
|
+
: undefined,
|
|
7896
|
+
// Match the in-screen inside-guide style: 2px accent border + 14%
|
|
7897
|
+
// accent fill. Uses the same CSS variable as the DesignCanvas guide.
|
|
7898
|
+
border: "2px solid var(--design-editor-accent-color)",
|
|
7899
|
+
background:
|
|
7900
|
+
"color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)",
|
|
7901
|
+
}}
|
|
7902
|
+
/>
|
|
7903
|
+
)
|
|
6930
7904
|
) : null}
|
|
6931
7905
|
|
|
6932
7906
|
{duplicatePreview ? (
|
|
6933
7907
|
<div
|
|
7908
|
+
ref={duplicatePreviewElRef}
|
|
7909
|
+
data-duplicate-preview-ghost
|
|
6934
7910
|
className={cn(
|
|
6935
7911
|
"pointer-events-none absolute z-20 rounded-lg border bg-background/90 shadow-2xl backdrop-blur-sm transition-colors",
|
|
6936
7912
|
duplicatePreview.canDuplicate
|
|
@@ -6938,6 +7914,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6938
7914
|
: "border-dashed border-muted-foreground/45",
|
|
6939
7915
|
)}
|
|
6940
7916
|
style={{
|
|
7917
|
+
// PERF9: left/top stay driven by state (matches the last value
|
|
7918
|
+
// beginDuplicateGesture's mousemove tick imperatively wrote, kept
|
|
7919
|
+
// in sync — see setDuplicatePreview's canDuplicate/moved-gated
|
|
7920
|
+
// calls below), but the DOM node's own style.left/top is what
|
|
7921
|
+
// actually moves every tick, not this re-render.
|
|
6941
7922
|
left: duplicatePreview.x,
|
|
6942
7923
|
top: duplicatePreview.y,
|
|
6943
7924
|
width: duplicatePreview.width * Math.min(scale, 1),
|
|
@@ -7805,6 +8786,7 @@ interface ScreenProps {
|
|
|
7805
8786
|
screen: ScreenFile;
|
|
7806
8787
|
metadata: ResolvedScreenMetadata;
|
|
7807
8788
|
geometry: FrameGeometry;
|
|
8789
|
+
locked: boolean;
|
|
7808
8790
|
isActive: boolean;
|
|
7809
8791
|
isSelected: boolean;
|
|
7810
8792
|
isTopScreen: boolean;
|
|
@@ -7870,6 +8852,7 @@ const Screen = memo(function Screen({
|
|
|
7870
8852
|
screen,
|
|
7871
8853
|
metadata,
|
|
7872
8854
|
geometry,
|
|
8855
|
+
locked,
|
|
7873
8856
|
isActive,
|
|
7874
8857
|
isSelected,
|
|
7875
8858
|
isTopScreen,
|
|
@@ -7919,6 +8902,7 @@ const Screen = memo(function Screen({
|
|
|
7919
8902
|
const [directlyHovered, setDirectlyHovered] = useState(false);
|
|
7920
8903
|
const frameDirectlyHovered =
|
|
7921
8904
|
(directlyHovered || isDirectlyHovered) &&
|
|
8905
|
+
!locked &&
|
|
7922
8906
|
!creationToolActive &&
|
|
7923
8907
|
!canvasGestureActive;
|
|
7924
8908
|
const childHoverActive =
|
|
@@ -7949,6 +8933,7 @@ const Screen = memo(function Screen({
|
|
|
7949
8933
|
!suppressFrameChromeForChild;
|
|
7950
8934
|
const screenContentInteractive =
|
|
7951
8935
|
Boolean(screenContent) &&
|
|
8936
|
+
!locked &&
|
|
7952
8937
|
!penActive &&
|
|
7953
8938
|
!creationToolActive &&
|
|
7954
8939
|
!canvasGestureActive;
|