@agent-native/core 0.80.7 → 0.80.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/corpus/core/docs/content/template-design.mdx +9 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp.ts +10 -0
- package/corpus/core/src/cli/skills.ts +58 -12
- package/corpus/core/src/client/AssistantChat.tsx +70 -27
- package/corpus/core/src/client/analytics.ts +3 -1
- package/corpus/core/src/client/error-format.ts +25 -0
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
- package/corpus/core/src/client/guided-questions.tsx +32 -4
- package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
- package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
- package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
- package/corpus/core/src/client/session-replay.ts +34 -0
- package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
- package/corpus/core/src/coding-tools/index.ts +1 -1
- package/corpus/core/src/mcp/index.ts +2 -0
- package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
- package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
- package/corpus/core/src/onboarding/types.ts +7 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
- package/corpus/core/src/server/agents-bundle.ts +35 -7
- package/corpus/core/src/styles/agent-native.css +5 -0
- package/corpus/core/src/templates/default/app/global.css +35 -35
- package/corpus/templates/analytics/AGENTS.md +6 -0
- package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
- package/corpus/templates/analytics/app/global.css +144 -40
- package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
- package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
- package/corpus/templates/assets/app/global.css +99 -35
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.tsx +4 -4
- package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/brain/app/global.css +63 -35
- package/corpus/templates/brain/app/routes/search.tsx +1 -1
- package/corpus/templates/brain/app/routes/settings.tsx +1 -1
- package/corpus/templates/brain/app/routes/sources.tsx +2 -2
- package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
- package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
- package/corpus/templates/calendar/app/global.css +69 -35
- package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/global.css +35 -35
- package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/AGENTS.md +13 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
- package/corpus/templates/clips/actions/view-screen.ts +2 -2
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/global.css +47 -35
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
- package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
- package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
- package/corpus/templates/clips/desktop/src/app.tsx +290 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
- package/corpus/templates/clips/desktop/src/styles.css +67 -52
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
- package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
- package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/content/app/global.css +29 -15
- package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
- package/corpus/templates/design/AGENTS.md +24 -14
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
- package/corpus/templates/design/actions/edit-design.ts +6 -3
- package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
- package/corpus/templates/design/actions/generate-design.ts +6 -21
- package/corpus/templates/design/actions/generate-screens.ts +48 -5
- package/corpus/templates/design/actions/get-design.ts +1 -0
- package/corpus/templates/design/actions/present-design-variants.ts +355 -30
- package/corpus/templates/design/actions/update-design.ts +47 -10
- package/corpus/templates/design/actions/update-file.ts +33 -8
- package/corpus/templates/design/actions/view-screen.ts +89 -31
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +389 -71
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +596 -95
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +428 -221
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1197 -228
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +94 -217
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +28 -14
- package/corpus/templates/design/app/components/design/index.ts +0 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +228 -170
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +140 -13
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +39 -8
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +75 -5
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
- package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +17 -4
- package/corpus/templates/design/app/components/design/types.ts +9 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
- package/corpus/templates/design/app/global.css +77 -49
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/design/app/i18n-data.ts +55 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2783 -658
- package/corpus/templates/design/app/root.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
- package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
- package/corpus/templates/design/e2e/README.md +4 -3
- package/corpus/templates/design/e2e/global-setup.ts +24 -23
- package/corpus/templates/design/e2e/helpers.ts +122 -11
- package/corpus/templates/design/playwright.config.ts +19 -4
- package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
- package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
- package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
- package/corpus/templates/design/shared/code-layer.ts +125 -22
- package/corpus/templates/design/shared/color-utils.ts +159 -0
- package/corpus/templates/design/shared/generation-session.ts +42 -0
- package/corpus/templates/dispatch/app/global.css +79 -35
- package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
- package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
- package/corpus/templates/forms/app/global.css +64 -35
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
- package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
- package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
- package/corpus/templates/macros/app/global.css +25 -9
- package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
- package/corpus/templates/mail/app/global.css +42 -36
- package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
- package/corpus/templates/plan/app/global.css +74 -42
- package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
- package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/slides/app/global.css +49 -33
- package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
- package/corpus/templates/videos/app/global.css +35 -35
- package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +10 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +58 -12
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +54 -24
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +2 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +17 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +3 -4
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/guided-questions.d.ts +4 -0
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +16 -7
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/onboarding/OnboardingPanel.js +26 -1
- package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
- package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
- package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
- package/dist/client/resources/use-builtin-capabilities.js +2 -1
- package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +31 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +14 -0
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +36 -7
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/coding-tools/index.js +1 -1
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +6 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
- package/dist/mcp/screen-memory-stdio.js +213 -0
- package/dist/mcp/screen-memory-stdio.js.map +1 -0
- package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
- package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
- package/dist/mcp-client/builtin-capabilities.js +10 -0
- package/dist/mcp-client/builtin-capabilities.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -0
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -0
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +61 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
- package/dist/mcp-client/screen-memory-local.js +336 -0
- package/dist/mcp-client/screen-memory-local.js.map +1 -0
- package/dist/onboarding/types.d.ts +7 -0
- package/dist/onboarding/types.d.ts.map +1 -1
- package/dist/onboarding/types.js.map +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +26 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agents-bundle.d.ts.map +1 -1
- package/dist/server/agents-bundle.js +22 -7
- package/dist/server/agents-bundle.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +5 -0
- package/dist/templates/default/app/global.css +35 -35
- package/docs/content/locales/ar-SA/template-design.mdx +6 -0
- package/docs/content/locales/de-DE/template-design.mdx +8 -0
- package/docs/content/locales/es-ES/template-design.mdx +10 -2
- package/docs/content/locales/fr-FR/template-design.mdx +10 -2
- package/docs/content/locales/hi-IN/template-design.mdx +7 -0
- package/docs/content/locales/ja-JP/template-design.mdx +7 -0
- package/docs/content/locales/ko-KR/template-design.mdx +7 -0
- package/docs/content/locales/pt-BR/template-design.mdx +9 -2
- package/docs/content/locales/zh-CN/template-design.mdx +6 -0
- package/docs/content/locales/zh-TW/template-design.mdx +6 -0
- package/docs/content/template-design.mdx +9 -2
- package/package.json +1 -1
- package/src/templates/default/app/global.css +35 -35
- package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
- package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
- package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -258
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_ASSIGNED_REGION_GAP,
|
|
4
|
+
DEFAULT_ASSIGNED_REGION_HEIGHT,
|
|
5
|
+
DEFAULT_ASSIGNED_REGION_MAX_COLUMNS,
|
|
6
|
+
DEFAULT_ASSIGNED_REGION_WIDTH,
|
|
3
7
|
DEFAULT_SNAP_THRESHOLD_SCREEN_PX,
|
|
4
8
|
appendPolylinePoint,
|
|
5
9
|
computeMoveSnap,
|
|
@@ -30,10 +34,12 @@ import {
|
|
|
30
34
|
} from "@shared/pen-path";
|
|
31
35
|
import { IconCopy, IconMaximize } from "@tabler/icons-react";
|
|
32
36
|
import {
|
|
37
|
+
memo,
|
|
33
38
|
useRef,
|
|
34
39
|
useState,
|
|
35
40
|
useCallback,
|
|
36
41
|
useEffect,
|
|
42
|
+
useMemo,
|
|
37
43
|
type CSSProperties,
|
|
38
44
|
type ReactNode,
|
|
39
45
|
} from "react";
|
|
@@ -41,6 +47,8 @@ import {
|
|
|
41
47
|
import { prettyScreenName } from "@/lib/screen-names";
|
|
42
48
|
import { cn } from "@/lib/utils";
|
|
43
49
|
|
|
50
|
+
import { DEVICE_FRAME_VIEWPORTS, type DeviceFrameType } from "./types";
|
|
51
|
+
|
|
44
52
|
interface ScreenFile {
|
|
45
53
|
id: string;
|
|
46
54
|
filename: string;
|
|
@@ -51,6 +59,7 @@ interface ScreenFile {
|
|
|
51
59
|
previewState?: string;
|
|
52
60
|
status?: string;
|
|
53
61
|
title?: string;
|
|
62
|
+
updatedAt?: string;
|
|
54
63
|
width?: number;
|
|
55
64
|
height?: number;
|
|
56
65
|
url?: string;
|
|
@@ -125,6 +134,11 @@ interface MultiScreenCanvasProps {
|
|
|
125
134
|
screens: ScreenFile[];
|
|
126
135
|
zoom: number;
|
|
127
136
|
activeId?: string | null;
|
|
137
|
+
selectedScreenIds?: string[];
|
|
138
|
+
activeScreenHasHoveredChild?: boolean;
|
|
139
|
+
hoveredChildScreenId?: string | null;
|
|
140
|
+
directlyHoveredScreenId?: string | null;
|
|
141
|
+
previewDeviceFrame?: DeviceFrameType;
|
|
128
142
|
activeTool?: MultiScreenCanvasTool;
|
|
129
143
|
toolProps?: CanvasToolProps;
|
|
130
144
|
onActiveToolChange?: (tool: MultiScreenCanvasTool) => void;
|
|
@@ -143,25 +157,27 @@ interface MultiScreenCanvasProps {
|
|
|
143
157
|
screenId: string,
|
|
144
158
|
primitive: CanvasPrimitiveInsert,
|
|
145
159
|
) => boolean | string;
|
|
160
|
+
onPrimitiveCreated?: (screenId: string, nodeId: string) => void;
|
|
146
161
|
onCreateScreenFrame?: (geometry: FrameGeometry) => void;
|
|
147
162
|
onDeleteSelection?: (ids: string[]) => boolean | void;
|
|
148
163
|
onZoomChange?: (zoom: number) => void;
|
|
149
|
-
onZoomToEdit?: (id: string) => void;
|
|
150
|
-
zoomToEditThreshold?: number;
|
|
151
164
|
renderScreenContent?: (
|
|
152
165
|
screen: ScreenFile,
|
|
153
166
|
metadata: ResolvedScreenMetadata,
|
|
154
167
|
geometry: FrameGeometry,
|
|
155
168
|
) => ReactNode;
|
|
169
|
+
onScreenSelectionChange?: (ids: string[]) => void;
|
|
156
170
|
selectAllRequest?: number;
|
|
157
171
|
clearSelectionRequest?: number;
|
|
172
|
+
onSelectionChange?: (selectedIds: string[]) => void;
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
/**
|
|
161
176
|
* design-editor overview canvas. Renders every file in the design as a movable,
|
|
162
177
|
* resizable frame inside an infinite, pannable surface.
|
|
163
178
|
*/
|
|
164
|
-
const
|
|
179
|
+
export const OVERVIEW_FRAME_WIDTH = 320;
|
|
180
|
+
const SCREEN_WIDTH = OVERVIEW_FRAME_WIDTH;
|
|
165
181
|
const SCREEN_HEIGHT = 640;
|
|
166
182
|
const SCREEN_CARD_HEIGHT = SCREEN_HEIGHT + 26;
|
|
167
183
|
const SCREEN_GAP = 56;
|
|
@@ -169,12 +185,70 @@ const SURFACE_PADDING = 240;
|
|
|
169
185
|
const DUPLICATE_DRAG_THRESHOLD = 6;
|
|
170
186
|
const DRAG_THRESHOLD = 3;
|
|
171
187
|
const FRAME_LABEL_HEIGHT = 28;
|
|
188
|
+
const FRAME_HEADER_BUTTON_OUTSIDE_WIDTH = 260;
|
|
189
|
+
const FRAME_HEADER_BUTTON_RESERVE = 116;
|
|
190
|
+
const FRAME_HEADER_DIMENSIONS_WIDTH = 72;
|
|
191
|
+
const FRAME_HEADER_TITLE_CHAR_WIDTH = 6.5;
|
|
192
|
+
const FRAME_HEADER_MIN_TITLE_WIDTH = 28;
|
|
193
|
+
const FRAME_HEADER_MAX_TITLE_WIDTH = 180;
|
|
194
|
+
const TRANSFORM_BADGE_OFFSET = 12;
|
|
195
|
+
const TRANSFORM_BADGE_EDGE_PADDING = 8;
|
|
196
|
+
const TRANSFORM_BADGE_HEIGHT = 28;
|
|
197
|
+
const TRANSFORM_BADGE_MIN_WIDTH = 64;
|
|
198
|
+
const TRANSFORM_BADGE_MAX_WIDTH = 180;
|
|
172
199
|
const MIN_ZOOM = 2;
|
|
173
200
|
const MAX_ZOOM = 800;
|
|
174
|
-
const ZOOM_SENSITIVITY = 0.
|
|
175
|
-
const MAX_WHEEL_ZOOM_DELTA =
|
|
201
|
+
const ZOOM_SENSITIVITY = 0.01;
|
|
202
|
+
const MAX_WHEEL_ZOOM_DELTA = 120;
|
|
176
203
|
const MAX_WHEEL_PAN_DELTA = 140;
|
|
177
204
|
const PIXEL_GRID_ZOOM = 800;
|
|
205
|
+
// During a zoom gesture the constant-size selection chrome is frozen (we don't
|
|
206
|
+
// re-render); on commit it recomputes to its fixed screen size. These transitions
|
|
207
|
+
// are enabled only for that brief settle, so normal selection, resize, and
|
|
208
|
+
// screen-switch geometry stays pinned to the frame.
|
|
209
|
+
const CHROME_SETTLE_MS = 150;
|
|
210
|
+
const CHROME_OPACITY_TRANSITION = "opacity 150ms ease-out";
|
|
211
|
+
const CHROME_BORDER_SETTLE_TRANSITION = `inset ${CHROME_SETTLE_MS}ms ease-out, border-width ${CHROME_SETTLE_MS}ms ease-out, border-radius ${CHROME_SETTLE_MS}ms ease-out, ${CHROME_OPACITY_TRANSITION}`;
|
|
212
|
+
const SELECTION_BOX_SETTLE_TRANSITION = `border-width ${CHROME_SETTLE_MS}ms ease-out, border-radius ${CHROME_SETTLE_MS}ms ease-out, ${CHROME_OPACITY_TRANSITION}`;
|
|
213
|
+
const CHROME_HANDLE_SETTLE_TRANSITION = `width ${CHROME_SETTLE_MS}ms ease-out, height ${CHROME_SETTLE_MS}ms ease-out, border-width ${CHROME_SETTLE_MS}ms ease-out, top ${CHROME_SETTLE_MS}ms ease-out, bottom ${CHROME_SETTLE_MS}ms ease-out, left ${CHROME_SETTLE_MS}ms ease-out, right ${CHROME_SETTLE_MS}ms ease-out, ${CHROME_OPACITY_TRANSITION}`;
|
|
214
|
+
// Frame header (name + dimensions + "Full view" button) is counter-scaled via
|
|
215
|
+
// transform to stay a fixed screen size; ease that scale on zoom-settle. opacity
|
|
216
|
+
// is included so the button's hover-fade (transition-opacity) keeps working.
|
|
217
|
+
const CHROME_LABEL_SETTLE_TRANSITION = `transform ${CHROME_SETTLE_MS}ms ease-out, ${CHROME_OPACITY_TRANSITION}`;
|
|
218
|
+
|
|
219
|
+
export function getChromeBorderTransition(chromeSettling: boolean) {
|
|
220
|
+
return chromeSettling
|
|
221
|
+
? CHROME_BORDER_SETTLE_TRANSITION
|
|
222
|
+
: CHROME_OPACITY_TRANSITION;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function getSelectionBoxTransition(chromeSettling: boolean) {
|
|
226
|
+
return chromeSettling ? SELECTION_BOX_SETTLE_TRANSITION : "none";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isDirectScreenHoverTarget(
|
|
230
|
+
target: EventTarget | null,
|
|
231
|
+
currentTarget: HTMLElement,
|
|
232
|
+
) {
|
|
233
|
+
if (target === currentTarget) return true;
|
|
234
|
+
const element =
|
|
235
|
+
target && typeof (target as Element).closest === "function"
|
|
236
|
+
? (target as Element)
|
|
237
|
+
: null;
|
|
238
|
+
return !!element && !element.closest("[data-screen-content]");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getChromeHandleTransition(chromeSettling: boolean) {
|
|
242
|
+
return chromeSettling
|
|
243
|
+
? CHROME_HANDLE_SETTLE_TRANSITION
|
|
244
|
+
: CHROME_OPACITY_TRANSITION;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getChromeLabelTransition(chromeSettling: boolean) {
|
|
248
|
+
return chromeSettling
|
|
249
|
+
? CHROME_LABEL_SETTLE_TRANSITION
|
|
250
|
+
: CHROME_OPACITY_TRANSITION;
|
|
251
|
+
}
|
|
178
252
|
const DRAFT_FRAME_WIDTH = 320;
|
|
179
253
|
const DRAFT_FRAME_HEIGHT = 640;
|
|
180
254
|
const DRAFT_RECT_WIDTH = 160;
|
|
@@ -389,10 +463,29 @@ type DragState =
|
|
|
389
463
|
| DraftCreateDragState
|
|
390
464
|
| PenNodeDragState;
|
|
391
465
|
|
|
466
|
+
type PendingWheelGesture =
|
|
467
|
+
| {
|
|
468
|
+
mode: "zoom";
|
|
469
|
+
deltaY: number;
|
|
470
|
+
cursor: Point;
|
|
471
|
+
clientX: number;
|
|
472
|
+
clientY: number;
|
|
473
|
+
}
|
|
474
|
+
| {
|
|
475
|
+
mode: "pan";
|
|
476
|
+
deltaX: number;
|
|
477
|
+
deltaY: number;
|
|
478
|
+
};
|
|
479
|
+
|
|
392
480
|
export function MultiScreenCanvas({
|
|
393
481
|
screens,
|
|
394
482
|
zoom,
|
|
395
483
|
activeId,
|
|
484
|
+
selectedScreenIds,
|
|
485
|
+
activeScreenHasHoveredChild = false,
|
|
486
|
+
hoveredChildScreenId,
|
|
487
|
+
directlyHoveredScreenId,
|
|
488
|
+
previewDeviceFrame = "none",
|
|
396
489
|
activeTool,
|
|
397
490
|
toolProps,
|
|
398
491
|
onActiveToolChange,
|
|
@@ -405,14 +498,15 @@ export function MultiScreenCanvas({
|
|
|
405
498
|
onGeometryChange,
|
|
406
499
|
onGeometryCommit,
|
|
407
500
|
onCreatePrimitive,
|
|
501
|
+
onPrimitiveCreated,
|
|
408
502
|
onCreateScreenFrame,
|
|
409
503
|
onDeleteSelection,
|
|
410
504
|
onZoomChange,
|
|
411
|
-
onZoomToEdit,
|
|
412
|
-
zoomToEditThreshold,
|
|
413
505
|
renderScreenContent,
|
|
506
|
+
onScreenSelectionChange,
|
|
414
507
|
selectAllRequest,
|
|
415
508
|
clearSelectionRequest,
|
|
509
|
+
onSelectionChange,
|
|
416
510
|
}: MultiScreenCanvasProps) {
|
|
417
511
|
const t = useT();
|
|
418
512
|
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
@@ -440,7 +534,7 @@ export function MultiScreenCanvas({
|
|
|
440
534
|
const [localActiveTool, setLocalActiveTool] =
|
|
441
535
|
useState<MultiScreenCanvasTool>("move");
|
|
442
536
|
const [selectedIds, setSelectedIds] = useState<string[]>(
|
|
443
|
-
|
|
537
|
+
selectedScreenIds ?? [],
|
|
444
538
|
);
|
|
445
539
|
const selectedIdsRef = useRef(selectedIds);
|
|
446
540
|
const dragState = useRef<DragState | null>(null);
|
|
@@ -460,6 +554,26 @@ export function MultiScreenCanvas({
|
|
|
460
554
|
const [dragCursor, setDragCursor] = useState<string | null>(null);
|
|
461
555
|
const suppressNextPick = useRef(false);
|
|
462
556
|
const feedbackTimerRef = useRef<number | null>(null);
|
|
557
|
+
const pendingWheelGestureRef = useRef<PendingWheelGesture | null>(null);
|
|
558
|
+
const wheelGestureFrameRef = useRef<number | null>(null);
|
|
559
|
+
const worldRef = useRef<HTMLDivElement>(null);
|
|
560
|
+
const pixelGridRef = useRef<HTMLDivElement>(null);
|
|
561
|
+
const viewCommitTimerRef = useRef<number | null>(null);
|
|
562
|
+
const pendingChromeSettleRef = useRef(false);
|
|
563
|
+
const chromeSettleTimerRef = useRef<number | null>(null);
|
|
564
|
+
const [chromeSettling, setChromeSettling] = useState(false);
|
|
565
|
+
const previousPreviewDeviceFrameRef = useRef(previewDeviceFrame);
|
|
566
|
+
|
|
567
|
+
const getResolvedMetadata = useCallback(
|
|
568
|
+
(screen: ScreenFile) =>
|
|
569
|
+
resolveScreenMetadata(
|
|
570
|
+
screen,
|
|
571
|
+
metadataById?.[screen.id],
|
|
572
|
+
getScreenMetadata?.(screen),
|
|
573
|
+
previewDeviceFrame,
|
|
574
|
+
),
|
|
575
|
+
[getScreenMetadata, metadataById, previewDeviceFrame],
|
|
576
|
+
);
|
|
463
577
|
|
|
464
578
|
useEffect(() => {
|
|
465
579
|
onGeometryChangeRef.current = onGeometryChange;
|
|
@@ -543,10 +657,20 @@ export function MultiScreenCanvas({
|
|
|
543
657
|
frameGeometryRef.current = frameGeometry;
|
|
544
658
|
}, [frameGeometry]);
|
|
545
659
|
|
|
660
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
663
|
+
}, [onSelectionChange]);
|
|
664
|
+
|
|
546
665
|
useEffect(() => {
|
|
547
666
|
selectedIdsRef.current = selectedIds;
|
|
667
|
+
onSelectionChangeRef.current?.(selectedIds);
|
|
548
668
|
}, [selectedIds]);
|
|
549
669
|
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
onScreenSelectionChange?.(selectedIds);
|
|
672
|
+
}, [onScreenSelectionChange, selectedIds]);
|
|
673
|
+
|
|
550
674
|
useEffect(() => {
|
|
551
675
|
draftPrimitivesRef.current = draftPrimitives;
|
|
552
676
|
}, [draftPrimitives]);
|
|
@@ -569,8 +693,9 @@ export function MultiScreenCanvas({
|
|
|
569
693
|
screens.forEach((screen, index) => {
|
|
570
694
|
const existing = current[screen.id];
|
|
571
695
|
const persisted = geometryById?.[screen.id];
|
|
696
|
+
const metadata = getResolvedMetadata(screen);
|
|
572
697
|
const resolved = {
|
|
573
|
-
...getInitialFrameGeometry(index),
|
|
698
|
+
...getInitialFrameGeometry(index, metadata),
|
|
574
699
|
...persisted,
|
|
575
700
|
} as FrameGeometry;
|
|
576
701
|
next[screen.id] = persisted ? resolved : (existing ?? resolved);
|
|
@@ -588,14 +713,47 @@ export function MultiScreenCanvas({
|
|
|
588
713
|
const next = current.filter((id) => currentIds.has(id));
|
|
589
714
|
return next.length === current.length ? current : next;
|
|
590
715
|
});
|
|
591
|
-
}, [
|
|
716
|
+
}, [
|
|
717
|
+
geometryById,
|
|
718
|
+
getResolvedMetadata,
|
|
719
|
+
screens,
|
|
720
|
+
updateFrameGeometry,
|
|
721
|
+
updateSelectedIds,
|
|
722
|
+
]);
|
|
723
|
+
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
const previous = previousPreviewDeviceFrameRef.current;
|
|
726
|
+
previousPreviewDeviceFrameRef.current = previewDeviceFrame;
|
|
727
|
+
if (previous === previewDeviceFrame) return;
|
|
728
|
+
|
|
729
|
+
updateFrameGeometry((current) => {
|
|
730
|
+
const next = { ...current };
|
|
731
|
+
let changed = false;
|
|
732
|
+
|
|
733
|
+
screens.forEach((screen, index) => {
|
|
734
|
+
const metadata = getResolvedMetadata(screen);
|
|
735
|
+
const currentGeometry =
|
|
736
|
+
current[screen.id] ?? getInitialFrameGeometry(index, metadata);
|
|
737
|
+
const nextHeight = getOverviewFrameHeight(
|
|
738
|
+
currentGeometry.width,
|
|
739
|
+
metadata,
|
|
740
|
+
);
|
|
741
|
+
if (currentGeometry.height === nextHeight) return;
|
|
742
|
+
next[screen.id] = {
|
|
743
|
+
...currentGeometry,
|
|
744
|
+
height: nextHeight,
|
|
745
|
+
};
|
|
746
|
+
changed = true;
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
return changed ? next : current;
|
|
750
|
+
});
|
|
751
|
+
}, [getResolvedMetadata, previewDeviceFrame, screens, updateFrameGeometry]);
|
|
592
752
|
|
|
593
753
|
useEffect(() => {
|
|
594
|
-
if (!
|
|
595
|
-
updateSelectedIds((
|
|
596
|
-
|
|
597
|
-
);
|
|
598
|
-
}, [activeId, updateSelectedIds]);
|
|
754
|
+
if (!selectedScreenIds) return;
|
|
755
|
+
updateSelectedIds(() => selectedScreenIds);
|
|
756
|
+
}, [screens, selectedScreenIds, updateSelectedIds]);
|
|
599
757
|
|
|
600
758
|
useEffect(() => {
|
|
601
759
|
if (
|
|
@@ -628,11 +786,18 @@ export function MultiScreenCanvas({
|
|
|
628
786
|
useEffect(() => {
|
|
629
787
|
if (!surfaceRef.current || screens.length === 0) return;
|
|
630
788
|
const rect = surfaceRef.current.getBoundingClientRect();
|
|
631
|
-
const columns = Math.min(screens.length, 3);
|
|
632
|
-
const rows = Math.ceil(screens.length / columns);
|
|
633
789
|
const scale = zoomRef.current / 100;
|
|
634
|
-
const
|
|
635
|
-
|
|
790
|
+
const frames = screens.map((screen, index) =>
|
|
791
|
+
getInitialFrameGeometry(index, getResolvedMetadata(screen)),
|
|
792
|
+
);
|
|
793
|
+
const bounds = getFrameGroupBounds(
|
|
794
|
+
frames.map((geometry, index) => ({
|
|
795
|
+
id: screens[index]?.id ?? String(index),
|
|
796
|
+
geometry,
|
|
797
|
+
})),
|
|
798
|
+
);
|
|
799
|
+
const totalWidth = bounds?.width ?? SCREEN_WIDTH;
|
|
800
|
+
const totalHeight = bounds?.height ?? SCREEN_CARD_HEIGHT;
|
|
636
801
|
const visualLeft = Math.max(24, (rect.width - totalWidth * scale) / 2);
|
|
637
802
|
const visualTop = Math.max(24, (rect.height - totalHeight * scale) / 2);
|
|
638
803
|
const nextPan = {
|
|
@@ -641,9 +806,9 @@ export function MultiScreenCanvas({
|
|
|
641
806
|
};
|
|
642
807
|
panRef.current = nextPan;
|
|
643
808
|
setPan(nextPan);
|
|
644
|
-
// Only on mount
|
|
809
|
+
// Only on mount, screen-count changes, or device-preview changes.
|
|
645
810
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
646
|
-
}, [screens.length]);
|
|
811
|
+
}, [previewDeviceFrame, screens.length]);
|
|
647
812
|
|
|
648
813
|
useEffect(() => {
|
|
649
814
|
return () => {
|
|
@@ -652,6 +817,15 @@ export function MultiScreenCanvas({
|
|
|
652
817
|
if (feedbackTimerRef.current !== null) {
|
|
653
818
|
window.clearTimeout(feedbackTimerRef.current);
|
|
654
819
|
}
|
|
820
|
+
if (wheelGestureFrameRef.current !== null) {
|
|
821
|
+
window.cancelAnimationFrame(wheelGestureFrameRef.current);
|
|
822
|
+
}
|
|
823
|
+
if (viewCommitTimerRef.current !== null) {
|
|
824
|
+
window.clearTimeout(viewCommitTimerRef.current);
|
|
825
|
+
}
|
|
826
|
+
if (chromeSettleTimerRef.current !== null) {
|
|
827
|
+
window.clearTimeout(chromeSettleTimerRef.current);
|
|
828
|
+
}
|
|
655
829
|
};
|
|
656
830
|
}, []);
|
|
657
831
|
|
|
@@ -682,8 +856,17 @@ export function MultiScreenCanvas({
|
|
|
682
856
|
}, []);
|
|
683
857
|
|
|
684
858
|
const getCurrentFrameEntries = useCallback(
|
|
685
|
-
() =>
|
|
686
|
-
|
|
859
|
+
() =>
|
|
860
|
+
screens.map((screen, index) => {
|
|
861
|
+
const metadata = getResolvedMetadata(screen);
|
|
862
|
+
return {
|
|
863
|
+
id: screen.id,
|
|
864
|
+
geometry:
|
|
865
|
+
frameGeometryRef.current[screen.id] ??
|
|
866
|
+
getInitialFrameGeometry(index, metadata),
|
|
867
|
+
};
|
|
868
|
+
}),
|
|
869
|
+
[getResolvedMetadata, screens],
|
|
687
870
|
);
|
|
688
871
|
|
|
689
872
|
const getCurrentDraftEntries = useCallback(
|
|
@@ -704,17 +887,20 @@ export function MultiScreenCanvas({
|
|
|
704
887
|
(point: Point) =>
|
|
705
888
|
getCurrentFrameEntries()
|
|
706
889
|
.map((entry, index) => ({ ...entry, index }))
|
|
707
|
-
.filter((entry) =>
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
890
|
+
.filter((entry) => {
|
|
891
|
+
const bounds = {
|
|
892
|
+
left: entry.geometry.x,
|
|
893
|
+
top: entry.geometry.y,
|
|
894
|
+
right: entry.geometry.x + entry.geometry.width,
|
|
895
|
+
bottom: entry.geometry.y + entry.geometry.height,
|
|
896
|
+
};
|
|
897
|
+
const local = rotatePointAroundCenter(
|
|
715
898
|
point,
|
|
716
|
-
|
|
717
|
-
|
|
899
|
+
getFrameCenter(entry.geometry),
|
|
900
|
+
entry.geometry.rotation ?? 0,
|
|
901
|
+
);
|
|
902
|
+
return rectContainsPoint(bounds, local);
|
|
903
|
+
})
|
|
718
904
|
.sort(
|
|
719
905
|
(a, b) =>
|
|
720
906
|
(b.geometry.z ?? 0) - (a.geometry.z ?? 0) || b.index - a.index,
|
|
@@ -722,14 +908,6 @@ export function MultiScreenCanvas({
|
|
|
722
908
|
[getCurrentFrameEntries],
|
|
723
909
|
);
|
|
724
910
|
|
|
725
|
-
const getFrameAtClientPoint = useCallback(
|
|
726
|
-
(clientX: number, clientY: number) => {
|
|
727
|
-
const point = canvasPointFromClient(clientX, clientY);
|
|
728
|
-
return getFrameEntryAtPoint(point)?.id;
|
|
729
|
-
},
|
|
730
|
-
[canvasPointFromClient, getFrameEntryAtPoint],
|
|
731
|
-
);
|
|
732
|
-
|
|
733
911
|
const deleteSelectedItems = useCallback(() => {
|
|
734
912
|
const frameIds = selectedIdsRef.current.filter(
|
|
735
913
|
(id) => frameGeometryRef.current[id],
|
|
@@ -782,6 +960,9 @@ export function MultiScreenCanvas({
|
|
|
782
960
|
handleMouseUp: (ev: MouseEvent) => void,
|
|
783
961
|
) => {
|
|
784
962
|
dragCleanup.current?.();
|
|
963
|
+
const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
|
|
964
|
+
surfaceRef.current,
|
|
965
|
+
);
|
|
785
966
|
let lastMouseEvent: MouseEvent | null = null;
|
|
786
967
|
const move = (ev: MouseEvent) => {
|
|
787
968
|
lastMouseEvent = ev;
|
|
@@ -799,6 +980,7 @@ export function MultiScreenCanvas({
|
|
|
799
980
|
window.removeEventListener("mousemove", move);
|
|
800
981
|
window.removeEventListener("mouseup", up);
|
|
801
982
|
window.removeEventListener("blur", cleanupOnBlur);
|
|
983
|
+
restorePreviewPointerEvents();
|
|
802
984
|
dragCleanup.current = null;
|
|
803
985
|
};
|
|
804
986
|
window.addEventListener("mousemove", move);
|
|
@@ -821,10 +1003,34 @@ export function MultiScreenCanvas({
|
|
|
821
1003
|
|
|
822
1004
|
const showTransformFeedback = useCallback(
|
|
823
1005
|
(text: string, clientX: number, clientY: number) => {
|
|
1006
|
+
const estimatedWidth = Math.min(
|
|
1007
|
+
TRANSFORM_BADGE_MAX_WIDTH,
|
|
1008
|
+
Math.max(TRANSFORM_BADGE_MIN_WIDTH, text.length * 7 + 16),
|
|
1009
|
+
);
|
|
1010
|
+
const maxX = Math.max(
|
|
1011
|
+
TRANSFORM_BADGE_EDGE_PADDING,
|
|
1012
|
+
window.innerWidth - estimatedWidth - TRANSFORM_BADGE_EDGE_PADDING,
|
|
1013
|
+
);
|
|
1014
|
+
const maxY = Math.max(
|
|
1015
|
+
TRANSFORM_BADGE_EDGE_PADDING,
|
|
1016
|
+
window.innerHeight -
|
|
1017
|
+
TRANSFORM_BADGE_HEIGHT -
|
|
1018
|
+
TRANSFORM_BADGE_EDGE_PADDING,
|
|
1019
|
+
);
|
|
1020
|
+
const preferredX =
|
|
1021
|
+
clientX + TRANSFORM_BADGE_OFFSET + estimatedWidth <=
|
|
1022
|
+
window.innerWidth - TRANSFORM_BADGE_EDGE_PADDING
|
|
1023
|
+
? clientX + TRANSFORM_BADGE_OFFSET
|
|
1024
|
+
: clientX - estimatedWidth - TRANSFORM_BADGE_OFFSET;
|
|
1025
|
+
const preferredY =
|
|
1026
|
+
clientY + TRANSFORM_BADGE_OFFSET + TRANSFORM_BADGE_HEIGHT <=
|
|
1027
|
+
window.innerHeight - TRANSFORM_BADGE_EDGE_PADDING
|
|
1028
|
+
? clientY + TRANSFORM_BADGE_OFFSET
|
|
1029
|
+
: clientY - TRANSFORM_BADGE_HEIGHT - TRANSFORM_BADGE_OFFSET;
|
|
824
1030
|
setTransformBadge({
|
|
825
1031
|
text,
|
|
826
|
-
x:
|
|
827
|
-
y:
|
|
1032
|
+
x: clampNumber(preferredX, TRANSFORM_BADGE_EDGE_PADDING, maxX),
|
|
1033
|
+
y: clampNumber(preferredY, TRANSFORM_BADGE_EDGE_PADDING, maxY),
|
|
828
1034
|
});
|
|
829
1035
|
},
|
|
830
1036
|
[],
|
|
@@ -846,6 +1052,74 @@ export function MultiScreenCanvas({
|
|
|
846
1052
|
dragCleanup.current?.();
|
|
847
1053
|
}, []);
|
|
848
1054
|
|
|
1055
|
+
const cancelActiveDrag = useCallback(() => {
|
|
1056
|
+
let cancelled = false;
|
|
1057
|
+
const state = dragState.current;
|
|
1058
|
+
|
|
1059
|
+
if (state) {
|
|
1060
|
+
cancelled = true;
|
|
1061
|
+
if (state.type === "move" || state.type === "resize") {
|
|
1062
|
+
updateFrameGeometry((current) =>
|
|
1063
|
+
frameGeometryWithOverrides(current, state.originFrames),
|
|
1064
|
+
);
|
|
1065
|
+
} else if (state.type === "rotate") {
|
|
1066
|
+
updateFrameGeometry((current) => ({
|
|
1067
|
+
...current,
|
|
1068
|
+
[state.frameId]: { ...state.originFrame },
|
|
1069
|
+
}));
|
|
1070
|
+
} else if (state.type === "draft-move" || state.type === "draft-resize") {
|
|
1071
|
+
updateDraftPrimitives((current) =>
|
|
1072
|
+
current.map((draft) => {
|
|
1073
|
+
const origin = state.originDrafts[draft.id];
|
|
1074
|
+
return origin ? cloneDraftPrimitive(origin) : draft;
|
|
1075
|
+
}),
|
|
1076
|
+
);
|
|
1077
|
+
} else if (state.type === "marquee") {
|
|
1078
|
+
updateSelectedIds(() => state.baseSelectedIds);
|
|
1079
|
+
updateSelectedDraftIds(() => state.baseSelectedDraftIds);
|
|
1080
|
+
} else if (state.type === "pen-node") {
|
|
1081
|
+
setActivePenPath(
|
|
1082
|
+
state.pathBefore ? clonePenPath(state.pathBefore) : null,
|
|
1083
|
+
);
|
|
1084
|
+
setPenGesturePreview(null);
|
|
1085
|
+
setPenPointer(null);
|
|
1086
|
+
setPenCloseHover(false);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (duplicateCleanup.current) {
|
|
1091
|
+
cancelled = true;
|
|
1092
|
+
duplicateCleanup.current();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (cancelled || dragCleanup.current) {
|
|
1096
|
+
finishDrag();
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
}, [
|
|
1101
|
+
finishDrag,
|
|
1102
|
+
updateDraftPrimitives,
|
|
1103
|
+
updateFrameGeometry,
|
|
1104
|
+
updateSelectedDraftIds,
|
|
1105
|
+
updateSelectedIds,
|
|
1106
|
+
]);
|
|
1107
|
+
|
|
1108
|
+
useEffect(() => {
|
|
1109
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1110
|
+
if (event.key !== "Escape") return;
|
|
1111
|
+
if (!cancelActiveDrag()) return;
|
|
1112
|
+
event.preventDefault();
|
|
1113
|
+
event.stopPropagation();
|
|
1114
|
+
event.stopImmediatePropagation();
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
1118
|
+
return () => {
|
|
1119
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
1120
|
+
};
|
|
1121
|
+
}, [cancelActiveDrag]);
|
|
1122
|
+
|
|
849
1123
|
const beginPan = useCallback(
|
|
850
1124
|
(e: React.MouseEvent) => {
|
|
851
1125
|
e.preventDefault();
|
|
@@ -912,12 +1186,22 @@ export function MultiScreenCanvas({
|
|
|
912
1186
|
|
|
913
1187
|
const hitIds = getCurrentFrameEntries()
|
|
914
1188
|
.filter((entry) =>
|
|
915
|
-
|
|
1189
|
+
rotatedRectIntersects(
|
|
1190
|
+
rect,
|
|
1191
|
+
getSelectableBounds(entry.geometry),
|
|
1192
|
+
getFrameCenter(entry.geometry),
|
|
1193
|
+
entry.geometry.rotation ?? 0,
|
|
1194
|
+
),
|
|
916
1195
|
)
|
|
917
1196
|
.map((entry) => entry.id);
|
|
918
1197
|
const hitDraftIds = getCurrentDraftEntries()
|
|
919
1198
|
.filter((entry) =>
|
|
920
|
-
|
|
1199
|
+
rotatedRectIntersects(
|
|
1200
|
+
rect,
|
|
1201
|
+
getSelectableBounds(entry.geometry),
|
|
1202
|
+
getFrameCenter(entry.geometry),
|
|
1203
|
+
entry.geometry.rotation ?? 0,
|
|
1204
|
+
),
|
|
921
1205
|
)
|
|
922
1206
|
.map((entry) => entry.id);
|
|
923
1207
|
updateSelectedIds(() =>
|
|
@@ -962,19 +1246,41 @@ export function MultiScreenCanvas({
|
|
|
962
1246
|
: undefined;
|
|
963
1247
|
if (preferred) return preferred;
|
|
964
1248
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1249
|
+
const draftCenter = getFrameCenter(draft.geometry);
|
|
1250
|
+
|
|
1251
|
+
// Primary: find the frame whose bounds contain the draft's center point.
|
|
1252
|
+
const containing = entries
|
|
1253
|
+
.filter(({ geometry }) => {
|
|
1254
|
+
const bounds = {
|
|
1255
|
+
left: geometry.x,
|
|
1256
|
+
top: geometry.y,
|
|
1257
|
+
right: geometry.x + geometry.width,
|
|
1258
|
+
bottom: geometry.y + geometry.height,
|
|
1259
|
+
};
|
|
1260
|
+
const local = rotatePointAroundCenter(
|
|
1261
|
+
draftCenter,
|
|
1262
|
+
getFrameCenter(geometry),
|
|
1263
|
+
geometry.rotation ?? 0,
|
|
1264
|
+
);
|
|
1265
|
+
return rectContainsPoint(bounds, local);
|
|
1266
|
+
})
|
|
977
1267
|
.sort((a, b) => (b.geometry.z ?? 0) - (a.geometry.z ?? 0))[0];
|
|
1268
|
+
|
|
1269
|
+
if (containing) return containing;
|
|
1270
|
+
|
|
1271
|
+
// Fallback: if no frame contains the center (shape drawn outside all
|
|
1272
|
+
// frames), use the nearest frame so the shape still persists rather than
|
|
1273
|
+
// becoming a lost draft primitive. With a single screen this is always
|
|
1274
|
+
// that screen; with multiple screens it's the visually closest one.
|
|
1275
|
+
if (entries.length === 0) return undefined;
|
|
1276
|
+
if (entries.length === 1) return entries[0];
|
|
1277
|
+
return entries.slice().sort((a, b) => {
|
|
1278
|
+
const ca = getFrameCenter(a.geometry);
|
|
1279
|
+
const cb = getFrameCenter(b.geometry);
|
|
1280
|
+
const da = Math.hypot(draftCenter.x - ca.x, draftCenter.y - ca.y);
|
|
1281
|
+
const db = Math.hypot(draftCenter.x - cb.x, draftCenter.y - cb.y);
|
|
1282
|
+
return da - db;
|
|
1283
|
+
})[0];
|
|
978
1284
|
},
|
|
979
1285
|
[getCurrentFrameEntries],
|
|
980
1286
|
);
|
|
@@ -1028,7 +1334,8 @@ export function MultiScreenCanvas({
|
|
|
1028
1334
|
current.filter((draft) => draft.id !== nextDraft.id),
|
|
1029
1335
|
);
|
|
1030
1336
|
updateSelectedDraftIds(() => []);
|
|
1031
|
-
updateSelectedIds(() => [
|
|
1337
|
+
updateSelectedIds(() => []);
|
|
1338
|
+
onPrimitiveCreated?.(persisted.frameId, persisted.nodeId);
|
|
1032
1339
|
return;
|
|
1033
1340
|
}
|
|
1034
1341
|
|
|
@@ -1038,6 +1345,7 @@ export function MultiScreenCanvas({
|
|
|
1038
1345
|
},
|
|
1039
1346
|
[
|
|
1040
1347
|
persistDraftPrimitive,
|
|
1348
|
+
onPrimitiveCreated,
|
|
1041
1349
|
updateDraftPrimitives,
|
|
1042
1350
|
updateSelectedDraftIds,
|
|
1043
1351
|
updateSelectedIds,
|
|
@@ -1499,16 +1807,6 @@ export function MultiScreenCanvas({
|
|
|
1499
1807
|
);
|
|
1500
1808
|
const lastNodeId = persisted[persisted.length - 1]?.nodeId;
|
|
1501
1809
|
if (lastNodeId) updateSelectedIds(() => [lastNodeId]);
|
|
1502
|
-
} else {
|
|
1503
|
-
// No draft landed in a frame — restore each moved draft to its
|
|
1504
|
-
// original position so the move is atomic (commit or revert).
|
|
1505
|
-
updateDraftPrimitives((current) =>
|
|
1506
|
-
current.map((draft) => {
|
|
1507
|
-
if (!state.targetIds.includes(draft.id)) return draft;
|
|
1508
|
-
const origin = state.originDrafts[draft.id];
|
|
1509
|
-
return origin ?? draft;
|
|
1510
|
-
}),
|
|
1511
|
-
);
|
|
1512
1810
|
}
|
|
1513
1811
|
}
|
|
1514
1812
|
finishDrag();
|
|
@@ -1679,14 +1977,21 @@ export function MultiScreenCanvas({
|
|
|
1679
1977
|
if (e.button !== 0 || e.shiftKey) return;
|
|
1680
1978
|
e.preventDefault();
|
|
1681
1979
|
e.stopPropagation();
|
|
1980
|
+
// Frame mousedowns stop propagation, so they never reach handleMouseDown.
|
|
1981
|
+
// Clear stale suppression here too so a prior gesture can't swallow this
|
|
1982
|
+
// frame's selecting click. This gesture re-arms suppression on mouse-up
|
|
1983
|
+
// only if it actually moves.
|
|
1984
|
+
suppressNextPick.current = false;
|
|
1682
1985
|
|
|
1683
1986
|
const currentSelectedIds = selectedIdsRef.current;
|
|
1684
1987
|
const targetIds = currentSelectedIds.includes(id)
|
|
1685
1988
|
? currentSelectedIds
|
|
1686
1989
|
: [id];
|
|
1990
|
+
if (activeId !== id) {
|
|
1991
|
+
onPick(id);
|
|
1992
|
+
}
|
|
1687
1993
|
if (!currentSelectedIds.includes(id)) {
|
|
1688
1994
|
updateSelectedIds(() => [id]);
|
|
1689
|
-
onPick(id);
|
|
1690
1995
|
}
|
|
1691
1996
|
updateSelectedDraftIds(() => []);
|
|
1692
1997
|
|
|
@@ -1781,6 +2086,7 @@ export function MultiScreenCanvas({
|
|
|
1781
2086
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
1782
2087
|
},
|
|
1783
2088
|
[
|
|
2089
|
+
activeId,
|
|
1784
2090
|
finishDrag,
|
|
1785
2091
|
getCurrentFrameEntries,
|
|
1786
2092
|
installDragListeners,
|
|
@@ -1799,6 +2105,10 @@ export function MultiScreenCanvas({
|
|
|
1799
2105
|
e.stopPropagation();
|
|
1800
2106
|
suppressNextPick.current = true;
|
|
1801
2107
|
|
|
2108
|
+
if (activeId !== id) {
|
|
2109
|
+
onPick(id);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1802
2112
|
const currentSelectedIds = selectedIdsRef.current;
|
|
1803
2113
|
const targetIds = currentSelectedIds.includes(id)
|
|
1804
2114
|
? currentSelectedIds
|
|
@@ -1907,9 +2217,11 @@ export function MultiScreenCanvas({
|
|
|
1907
2217
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
1908
2218
|
},
|
|
1909
2219
|
[
|
|
2220
|
+
activeId,
|
|
1910
2221
|
finishDrag,
|
|
1911
2222
|
getCurrentFrameEntries,
|
|
1912
2223
|
installDragListeners,
|
|
2224
|
+
onPick,
|
|
1913
2225
|
showTransformFeedback,
|
|
1914
2226
|
updateFrameGeometry,
|
|
1915
2227
|
updateSelectedIds,
|
|
@@ -1932,6 +2244,10 @@ export function MultiScreenCanvas({
|
|
|
1932
2244
|
e.stopPropagation();
|
|
1933
2245
|
suppressNextPick.current = true;
|
|
1934
2246
|
|
|
2247
|
+
if (activeId !== id) {
|
|
2248
|
+
onPick(id);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
1935
2251
|
const originFrame = getCurrentFrameEntries().find(
|
|
1936
2252
|
(entry) => entry.id === id,
|
|
1937
2253
|
)?.geometry;
|
|
@@ -2003,10 +2319,12 @@ export function MultiScreenCanvas({
|
|
|
2003
2319
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
2004
2320
|
},
|
|
2005
2321
|
[
|
|
2322
|
+
activeId,
|
|
2006
2323
|
finishDrag,
|
|
2007
2324
|
getCanvasPoint,
|
|
2008
2325
|
getCurrentFrameEntries,
|
|
2009
2326
|
installDragListeners,
|
|
2327
|
+
onPick,
|
|
2010
2328
|
showTransformFeedback,
|
|
2011
2329
|
updateFrameGeometry,
|
|
2012
2330
|
updateSelectedIds,
|
|
@@ -2023,11 +2341,20 @@ export function MultiScreenCanvas({
|
|
|
2023
2341
|
|
|
2024
2342
|
if (e.shiftKey) {
|
|
2025
2343
|
updateSelectedDraftIds(() => []);
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
);
|
|
2344
|
+
const currentSelectedIds = selectedIdsRef.current;
|
|
2345
|
+
const nextSelectedIds = currentSelectedIds.includes(id)
|
|
2346
|
+
? currentSelectedIds.filter((selectedId) => selectedId !== id)
|
|
2347
|
+
: [...currentSelectedIds, id];
|
|
2348
|
+
updateSelectedIds(() => nextSelectedIds);
|
|
2349
|
+
const nextPrimaryId =
|
|
2350
|
+
nextSelectedIds.length === 0
|
|
2351
|
+
? null
|
|
2352
|
+
: nextSelectedIds.includes(id)
|
|
2353
|
+
? id
|
|
2354
|
+
: (nextSelectedIds[nextSelectedIds.length - 1] ?? null);
|
|
2355
|
+
if (nextPrimaryId && nextPrimaryId !== activeId) {
|
|
2356
|
+
onPick(nextPrimaryId);
|
|
2357
|
+
}
|
|
2031
2358
|
return;
|
|
2032
2359
|
}
|
|
2033
2360
|
|
|
@@ -2035,7 +2362,7 @@ export function MultiScreenCanvas({
|
|
|
2035
2362
|
updateSelectedIds(() => [id]);
|
|
2036
2363
|
onPick(id);
|
|
2037
2364
|
},
|
|
2038
|
-
[onPick, updateSelectedDraftIds, updateSelectedIds],
|
|
2365
|
+
[activeId, onPick, updateSelectedDraftIds, updateSelectedIds],
|
|
2039
2366
|
);
|
|
2040
2367
|
|
|
2041
2368
|
const handleFrameDoubleClick = useCallback(
|
|
@@ -2161,6 +2488,10 @@ export function MultiScreenCanvas({
|
|
|
2161
2488
|
|
|
2162
2489
|
const handleMouseDown = useCallback(
|
|
2163
2490
|
(e: React.MouseEvent) => {
|
|
2491
|
+
// Clear any stale pick-suppression left over from a prior resize/rotate/move
|
|
2492
|
+
// gesture that never received its trailing frame click — otherwise it would
|
|
2493
|
+
// silently swallow this unrelated interaction.
|
|
2494
|
+
suppressNextPick.current = false;
|
|
2164
2495
|
const target = e.target as HTMLElement;
|
|
2165
2496
|
const onFrame = !!target.closest("[data-frame-shell]");
|
|
2166
2497
|
const tool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
@@ -2204,6 +2535,136 @@ export function MultiScreenCanvas({
|
|
|
2204
2535
|
[activeTool, localActiveTool, updatePenPointer],
|
|
2205
2536
|
);
|
|
2206
2537
|
|
|
2538
|
+
// Push the current pan/zoom straight to the DOM. A wheel/pinch gesture must
|
|
2539
|
+
// NEVER re-render React's canvas tree during the gesture: each render re-runs
|
|
2540
|
+
// renderScreenContent (which re-creates the active screen's live DesignCanvas
|
|
2541
|
+
// iframe) and, with React DevTools attached, serializes every render over the
|
|
2542
|
+
// extension bridge — that re-render storm is the real source of zoom jank, not
|
|
2543
|
+
// layout/paint. We mutate the transform directly here and reconcile React
|
|
2544
|
+
// state once, after the gesture settles, via scheduleViewCommit().
|
|
2545
|
+
const applyViewToDom = useCallback(() => {
|
|
2546
|
+
const nextScale = zoomRef.current / 100;
|
|
2547
|
+
const p = panRef.current;
|
|
2548
|
+
const world = worldRef.current;
|
|
2549
|
+
if (world) {
|
|
2550
|
+
// 2D translate (not translate3d) so the layer is not GPU-pinned to a
|
|
2551
|
+
// stale low-res raster — keeps zoomed-in content crisp.
|
|
2552
|
+
world.style.transform = `translate(${p.x}px, ${p.y}px) scale(${nextScale})`;
|
|
2553
|
+
}
|
|
2554
|
+
const grid = pixelGridRef.current;
|
|
2555
|
+
if (grid) {
|
|
2556
|
+
grid.style.backgroundPosition = `${p.x}px ${p.y}px`;
|
|
2557
|
+
grid.style.backgroundSize = `${nextScale}px ${nextScale}px`;
|
|
2558
|
+
}
|
|
2559
|
+
}, []);
|
|
2560
|
+
|
|
2561
|
+
const startChromeSettle = useCallback(() => {
|
|
2562
|
+
if (chromeSettleTimerRef.current !== null) {
|
|
2563
|
+
window.clearTimeout(chromeSettleTimerRef.current);
|
|
2564
|
+
}
|
|
2565
|
+
setChromeSettling(true);
|
|
2566
|
+
chromeSettleTimerRef.current = window.setTimeout(() => {
|
|
2567
|
+
chromeSettleTimerRef.current = null;
|
|
2568
|
+
setChromeSettling(false);
|
|
2569
|
+
}, CHROME_SETTLE_MS);
|
|
2570
|
+
}, []);
|
|
2571
|
+
|
|
2572
|
+
const commitView = useCallback(() => {
|
|
2573
|
+
viewCommitTimerRef.current = null;
|
|
2574
|
+
const shouldSettleChrome = pendingChromeSettleRef.current;
|
|
2575
|
+
pendingChromeSettleRef.current = false;
|
|
2576
|
+
if (shouldSettleChrome) startChromeSettle();
|
|
2577
|
+
setCanvasZoom(zoomRef.current);
|
|
2578
|
+
setPan(panRef.current);
|
|
2579
|
+
onZoomChange?.(zoomRef.current);
|
|
2580
|
+
}, [onZoomChange, startChromeSettle]);
|
|
2581
|
+
|
|
2582
|
+
// Debounced: only commit to React state once the gesture has been idle for a
|
|
2583
|
+
// beat, so a continuous pinch produces zero re-renders until the user pauses.
|
|
2584
|
+
const scheduleViewCommit = useCallback(
|
|
2585
|
+
(options?: { settleChrome?: boolean }) => {
|
|
2586
|
+
if (options?.settleChrome) {
|
|
2587
|
+
pendingChromeSettleRef.current = true;
|
|
2588
|
+
}
|
|
2589
|
+
if (viewCommitTimerRef.current !== null) {
|
|
2590
|
+
window.clearTimeout(viewCommitTimerRef.current);
|
|
2591
|
+
}
|
|
2592
|
+
viewCommitTimerRef.current = window.setTimeout(commitView, 120);
|
|
2593
|
+
},
|
|
2594
|
+
[commitView],
|
|
2595
|
+
);
|
|
2596
|
+
|
|
2597
|
+
const flushPendingWheelGesture = useCallback(() => {
|
|
2598
|
+
wheelGestureFrameRef.current = null;
|
|
2599
|
+
const gesture = pendingWheelGestureRef.current;
|
|
2600
|
+
pendingWheelGestureRef.current = null;
|
|
2601
|
+
if (!gesture) return;
|
|
2602
|
+
|
|
2603
|
+
if (gesture.mode === "zoom") {
|
|
2604
|
+
const currentZoom = zoomRef.current;
|
|
2605
|
+
const zoomDeltaY = clamp(
|
|
2606
|
+
gesture.deltaY,
|
|
2607
|
+
-MAX_WHEEL_ZOOM_DELTA,
|
|
2608
|
+
MAX_WHEEL_ZOOM_DELTA,
|
|
2609
|
+
);
|
|
2610
|
+
const nextZoom = clamp(
|
|
2611
|
+
currentZoom * Math.exp(-zoomDeltaY * ZOOM_SENSITIVITY),
|
|
2612
|
+
MIN_ZOOM,
|
|
2613
|
+
MAX_ZOOM,
|
|
2614
|
+
);
|
|
2615
|
+
if (nextZoom === currentZoom) return;
|
|
2616
|
+
|
|
2617
|
+
const nextPan = getPanForZoomToCursor({
|
|
2618
|
+
pan: panRef.current,
|
|
2619
|
+
cursor: gesture.cursor,
|
|
2620
|
+
oldZoom: currentZoom,
|
|
2621
|
+
nextZoom,
|
|
2622
|
+
});
|
|
2623
|
+
zoomRef.current = nextZoom;
|
|
2624
|
+
panRef.current = nextPan;
|
|
2625
|
+
applyViewToDom();
|
|
2626
|
+
scheduleViewCommit({ settleChrome: true });
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const nextPan = {
|
|
2631
|
+
x: panRef.current.x - gesture.deltaX,
|
|
2632
|
+
y: panRef.current.y - gesture.deltaY,
|
|
2633
|
+
};
|
|
2634
|
+
panRef.current = nextPan;
|
|
2635
|
+
applyViewToDom();
|
|
2636
|
+
scheduleViewCommit();
|
|
2637
|
+
}, [applyViewToDom, scheduleViewCommit]);
|
|
2638
|
+
|
|
2639
|
+
const enqueueWheelGesture = useCallback(
|
|
2640
|
+
(gesture: PendingWheelGesture) => {
|
|
2641
|
+
const pending = pendingWheelGestureRef.current;
|
|
2642
|
+
if (pending?.mode === "zoom" && gesture.mode === "zoom") {
|
|
2643
|
+
pendingWheelGestureRef.current = {
|
|
2644
|
+
mode: "zoom",
|
|
2645
|
+
deltaY: pending.deltaY + gesture.deltaY,
|
|
2646
|
+
cursor: gesture.cursor,
|
|
2647
|
+
clientX: gesture.clientX,
|
|
2648
|
+
clientY: gesture.clientY,
|
|
2649
|
+
};
|
|
2650
|
+
} else if (pending?.mode === "pan" && gesture.mode === "pan") {
|
|
2651
|
+
pendingWheelGestureRef.current = {
|
|
2652
|
+
mode: "pan",
|
|
2653
|
+
deltaX: pending.deltaX + gesture.deltaX,
|
|
2654
|
+
deltaY: pending.deltaY + gesture.deltaY,
|
|
2655
|
+
};
|
|
2656
|
+
} else {
|
|
2657
|
+
pendingWheelGestureRef.current = gesture;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
if (wheelGestureFrameRef.current !== null) return;
|
|
2661
|
+
wheelGestureFrameRef.current = window.requestAnimationFrame(
|
|
2662
|
+
flushPendingWheelGesture,
|
|
2663
|
+
);
|
|
2664
|
+
},
|
|
2665
|
+
[flushPendingWheelGesture],
|
|
2666
|
+
);
|
|
2667
|
+
|
|
2207
2668
|
const handleWheelEvent = useCallback(
|
|
2208
2669
|
(event: WheelEvent) => {
|
|
2209
2670
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
@@ -2213,40 +2674,18 @@ export function MultiScreenCanvas({
|
|
|
2213
2674
|
const delta = getWheelDelta(event);
|
|
2214
2675
|
|
|
2215
2676
|
if (event.ctrlKey || event.metaKey) {
|
|
2216
|
-
const currentZoom = zoomRef.current;
|
|
2217
2677
|
const zoomDeltaY = clamp(
|
|
2218
2678
|
delta.y,
|
|
2219
2679
|
-MAX_WHEEL_ZOOM_DELTA,
|
|
2220
2680
|
MAX_WHEEL_ZOOM_DELTA,
|
|
2221
2681
|
);
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
MAX_ZOOM,
|
|
2226
|
-
);
|
|
2227
|
-
if (nextZoom === currentZoom) return;
|
|
2228
|
-
|
|
2229
|
-
const nextPan = getPanForZoomToCursor({
|
|
2230
|
-
pan: panRef.current,
|
|
2682
|
+
enqueueWheelGesture({
|
|
2683
|
+
mode: "zoom",
|
|
2684
|
+
deltaY: zoomDeltaY,
|
|
2231
2685
|
cursor: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
|
2232
|
-
|
|
2233
|
-
|
|
2686
|
+
clientX: event.clientX,
|
|
2687
|
+
clientY: event.clientY,
|
|
2234
2688
|
});
|
|
2235
|
-
const zoomEditFrameId =
|
|
2236
|
-
zoomToEditThreshold !== undefined &&
|
|
2237
|
-
currentZoom < zoomToEditThreshold &&
|
|
2238
|
-
nextZoom >= zoomToEditThreshold
|
|
2239
|
-
? getFrameAtClientPoint(event.clientX, event.clientY)
|
|
2240
|
-
: undefined;
|
|
2241
|
-
|
|
2242
|
-
zoomRef.current = nextZoom;
|
|
2243
|
-
panRef.current = nextPan;
|
|
2244
|
-
setCanvasZoom(nextZoom);
|
|
2245
|
-
setPan(nextPan);
|
|
2246
|
-
onZoomChange?.(nextZoom);
|
|
2247
|
-
if (zoomEditFrameId) {
|
|
2248
|
-
onZoomToEdit?.(zoomEditFrameId);
|
|
2249
|
-
}
|
|
2250
2689
|
return;
|
|
2251
2690
|
}
|
|
2252
2691
|
|
|
@@ -2260,14 +2699,9 @@ export function MultiScreenCanvas({
|
|
|
2260
2699
|
-MAX_WHEEL_PAN_DELTA,
|
|
2261
2700
|
MAX_WHEEL_PAN_DELTA,
|
|
2262
2701
|
);
|
|
2263
|
-
|
|
2264
|
-
x: panRef.current.x - deltaX,
|
|
2265
|
-
y: panRef.current.y - deltaY,
|
|
2266
|
-
};
|
|
2267
|
-
panRef.current = nextPan;
|
|
2268
|
-
setPan(nextPan);
|
|
2702
|
+
enqueueWheelGesture({ mode: "pan", deltaX, deltaY });
|
|
2269
2703
|
},
|
|
2270
|
-
[
|
|
2704
|
+
[enqueueWheelGesture],
|
|
2271
2705
|
);
|
|
2272
2706
|
|
|
2273
2707
|
useEffect(() => {
|
|
@@ -2462,12 +2896,59 @@ export function MultiScreenCanvas({
|
|
|
2462
2896
|
if (tool !== "pen") clearActivePenPath();
|
|
2463
2897
|
}, [activeTool, clearActivePenPath, localActiveTool]);
|
|
2464
2898
|
|
|
2899
|
+
// Cmd+D / Ctrl+D: duplicate the selected frame with a visible offset so the
|
|
2900
|
+
// copy doesn't land exactly on top of the original (Figma-style behaviour).
|
|
2901
|
+
useEffect(() => {
|
|
2902
|
+
if (!onDuplicate) return;
|
|
2903
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
2904
|
+
if (activePenPathRef.current) return;
|
|
2905
|
+
if (
|
|
2906
|
+
!(event.metaKey || event.ctrlKey) ||
|
|
2907
|
+
event.key.toLowerCase() !== "d" ||
|
|
2908
|
+
isEditableHotkeyTarget(event.target)
|
|
2909
|
+
) {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
// Only act on frame IDs — filter out canvas primitives (sub-elements).
|
|
2913
|
+
const frameIds = selectedIdsRef.current.filter(
|
|
2914
|
+
(id) => frameGeometryRef.current[id],
|
|
2915
|
+
);
|
|
2916
|
+
const targetId = frameIds[0];
|
|
2917
|
+
if (!targetId) return;
|
|
2918
|
+
const screen = screens.find((s) => s.id === targetId);
|
|
2919
|
+
if (!screen) return;
|
|
2920
|
+
const sourceGeometry = frameGeometryRef.current[targetId];
|
|
2921
|
+
if (!sourceGeometry) return;
|
|
2922
|
+
event.preventDefault();
|
|
2923
|
+
event.stopPropagation();
|
|
2924
|
+
event.stopImmediatePropagation();
|
|
2925
|
+
// Offset the duplicate by one grid gap to the right (and slightly down)
|
|
2926
|
+
// so it is visually distinct from the original, mirroring Figma's behaviour.
|
|
2927
|
+
const canvasPosition = {
|
|
2928
|
+
x: sourceGeometry.x + sourceGeometry.width + SCREEN_GAP,
|
|
2929
|
+
y: sourceGeometry.y,
|
|
2930
|
+
};
|
|
2931
|
+
onDuplicate(targetId, {
|
|
2932
|
+
mode: "alt-click",
|
|
2933
|
+
screen,
|
|
2934
|
+
canvasPosition,
|
|
2935
|
+
dropCanvasPosition: canvasPosition,
|
|
2936
|
+
});
|
|
2937
|
+
};
|
|
2938
|
+
|
|
2939
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
2940
|
+
return () => {
|
|
2941
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
2942
|
+
};
|
|
2943
|
+
}, [onDuplicate, screens]);
|
|
2944
|
+
|
|
2465
2945
|
const scale = canvasZoom / 100;
|
|
2466
2946
|
const chromeScale = scale > 0 ? 1 / scale : 1;
|
|
2467
2947
|
const showPixelGrid = canvasZoom >= PIXEL_GRID_ZOOM;
|
|
2468
2948
|
const effectiveTool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
2469
2949
|
const penActive = effectiveTool === "pen";
|
|
2470
2950
|
const creationToolActive = Boolean(getDraftCreationTool(effectiveTool));
|
|
2951
|
+
const canvasGestureActive = isDragging || isPanning;
|
|
2471
2952
|
const displayedPenPath = penGesturePreview
|
|
2472
2953
|
? penGesturePreview
|
|
2473
2954
|
: activePenPath && penPointer && activePenPath.nodes.length > 0
|
|
@@ -2488,10 +2969,29 @@ export function MultiScreenCanvas({
|
|
|
2488
2969
|
: effectiveTool === "hand"
|
|
2489
2970
|
? "grab"
|
|
2490
2971
|
: "default";
|
|
2491
|
-
const canvasFrames = screens.map((screen, index) =>
|
|
2492
|
-
screen
|
|
2493
|
-
|
|
2494
|
-
|
|
2972
|
+
const canvasFrames = screens.map((screen, index) => {
|
|
2973
|
+
const metadata = getResolvedMetadata(screen);
|
|
2974
|
+
return {
|
|
2975
|
+
screen,
|
|
2976
|
+
metadata,
|
|
2977
|
+
geometry:
|
|
2978
|
+
frameGeometry[screen.id] ?? getInitialFrameGeometry(index, metadata),
|
|
2979
|
+
};
|
|
2980
|
+
});
|
|
2981
|
+
const screenContentById = useMemo(() => {
|
|
2982
|
+
if (!renderScreenContent) return new Map<string, ReactNode>();
|
|
2983
|
+
return new Map(
|
|
2984
|
+
screens.map((screen, index) => {
|
|
2985
|
+
const metadata = getResolvedMetadata(screen);
|
|
2986
|
+
const geometry =
|
|
2987
|
+
frameGeometry[screen.id] ?? getInitialFrameGeometry(index, metadata);
|
|
2988
|
+
return [
|
|
2989
|
+
screen.id,
|
|
2990
|
+
renderScreenContent(screen, metadata, geometry),
|
|
2991
|
+
] as const;
|
|
2992
|
+
}),
|
|
2993
|
+
);
|
|
2994
|
+
}, [frameGeometry, getResolvedMetadata, renderScreenContent, screens]);
|
|
2495
2995
|
const selectedFrameEntries = canvasFrames
|
|
2496
2996
|
.filter(({ screen }) => selectedIdSet.has(screen.id))
|
|
2497
2997
|
.map(({ screen, geometry }) => ({ id: screen.id, geometry }));
|
|
@@ -2507,16 +3007,25 @@ export function MultiScreenCanvas({
|
|
|
2507
3007
|
selectedDraftEntries.length > 1
|
|
2508
3008
|
? getFrameGroupBounds(selectedDraftEntries)
|
|
2509
3009
|
: null;
|
|
3010
|
+
const singleSelectedFrame =
|
|
3011
|
+
selectedFrameEntries.length === 1 && !selectedGroupBounds
|
|
3012
|
+
? selectedFrameEntries[0]
|
|
3013
|
+
: null;
|
|
3014
|
+
const singleSelectedDraft =
|
|
3015
|
+
selectedDraftEntries.length === 1 && !selectedDraftGroupBounds
|
|
3016
|
+
? selectedDraftEntries[0]
|
|
3017
|
+
: null;
|
|
2510
3018
|
return (
|
|
2511
3019
|
<div
|
|
2512
3020
|
ref={surfaceRef}
|
|
2513
|
-
className="relative h-full w-full select-none overflow-hidden
|
|
3021
|
+
className="relative h-full w-full select-none overflow-hidden"
|
|
2514
3022
|
onMouseDown={handleMouseDown}
|
|
2515
3023
|
onMouseMove={handleMouseMove}
|
|
2516
3024
|
style={{ cursor: surfaceCursor, touchAction: "none" }}
|
|
2517
3025
|
>
|
|
2518
3026
|
{showPixelGrid ? (
|
|
2519
3027
|
<div
|
|
3028
|
+
ref={pixelGridRef}
|
|
2520
3029
|
className="pointer-events-none absolute inset-0 opacity-60"
|
|
2521
3030
|
style={{
|
|
2522
3031
|
backgroundImage:
|
|
@@ -2528,34 +3037,44 @@ export function MultiScreenCanvas({
|
|
|
2528
3037
|
) : null}
|
|
2529
3038
|
|
|
2530
3039
|
<div
|
|
3040
|
+
ref={worldRef}
|
|
2531
3041
|
className="pointer-events-none absolute"
|
|
2532
3042
|
style={{
|
|
2533
|
-
left:
|
|
2534
|
-
top:
|
|
2535
|
-
transform
|
|
3043
|
+
left: 0,
|
|
3044
|
+
top: 0,
|
|
3045
|
+
// Plain 2D transform — NO will-change / translate3d. Forcing a
|
|
3046
|
+
// compositor layer pins a low-res cached raster that the GPU stretches
|
|
3047
|
+
// when you zoom in, leaving screen content permanently blurry. A 2D
|
|
3048
|
+
// transform lets the browser re-rasterize crisply at rest. Zoom smoothness
|
|
3049
|
+
// comes from never re-rendering React during the gesture (see
|
|
3050
|
+
// flushPendingWheelGesture / applyViewToDom), not from layer promotion —
|
|
3051
|
+
// the trace proved paint/composite was never the bottleneck.
|
|
3052
|
+
transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`,
|
|
2536
3053
|
transformOrigin: "top left",
|
|
2537
3054
|
}}
|
|
2538
3055
|
>
|
|
2539
|
-
{canvasFrames.map(({ screen, geometry }) => {
|
|
2540
|
-
const metadata = resolveScreenMetadata(
|
|
2541
|
-
screen,
|
|
2542
|
-
metadataById?.[screen.id],
|
|
2543
|
-
getScreenMetadata?.(screen),
|
|
2544
|
-
);
|
|
3056
|
+
{canvasFrames.map(({ screen, metadata, geometry }) => {
|
|
2545
3057
|
return (
|
|
2546
3058
|
<Screen
|
|
2547
3059
|
key={screen.id}
|
|
2548
3060
|
screen={screen}
|
|
2549
3061
|
metadata={metadata}
|
|
2550
3062
|
geometry={geometry}
|
|
2551
|
-
screenContent={
|
|
3063
|
+
screenContent={screenContentById.get(screen.id)}
|
|
2552
3064
|
isActive={screen.id === activeId}
|
|
2553
3065
|
isSelected={selectedIdSet.has(screen.id)}
|
|
3066
|
+
isDirectlyHovered={screen.id === directlyHoveredScreenId}
|
|
3067
|
+
hasHoveredChild={
|
|
3068
|
+
(screen.id === activeId && activeScreenHasHoveredChild) ||
|
|
3069
|
+
screen.id === hoveredChildScreenId
|
|
3070
|
+
}
|
|
2554
3071
|
groupSelected={hasGroupSelection}
|
|
2555
3072
|
handlesEnabled={!hasGroupSelection}
|
|
2556
3073
|
penActive={penActive}
|
|
2557
3074
|
creationToolActive={creationToolActive}
|
|
3075
|
+
canvasGestureActive={canvasGestureActive}
|
|
2558
3076
|
chromeScale={chromeScale}
|
|
3077
|
+
chromeSettling={chromeSettling}
|
|
2559
3078
|
onPick={handleFrameClick}
|
|
2560
3079
|
onEdit={handleFrameDoubleClick}
|
|
2561
3080
|
onStartFrameDrag={beginFrameDrag}
|
|
@@ -2574,6 +3093,7 @@ export function MultiScreenCanvas({
|
|
|
2574
3093
|
groupSelected={Boolean(selectedDraftGroupBounds)}
|
|
2575
3094
|
penActive={penActive}
|
|
2576
3095
|
chromeScale={chromeScale}
|
|
3096
|
+
chromeSettling={chromeSettling}
|
|
2577
3097
|
onClick={handleDraftClick}
|
|
2578
3098
|
onStartDrag={beginDraftDrag}
|
|
2579
3099
|
onStartResize={beginDraftResize}
|
|
@@ -2588,6 +3108,7 @@ export function MultiScreenCanvas({
|
|
|
2588
3108
|
groupSelected={false}
|
|
2589
3109
|
penActive={penActive}
|
|
2590
3110
|
chromeScale={chromeScale}
|
|
3111
|
+
chromeSettling={chromeSettling}
|
|
2591
3112
|
onClick={() => {}}
|
|
2592
3113
|
onStartDrag={() => {}}
|
|
2593
3114
|
onStartResize={() => {}}
|
|
@@ -2598,10 +3119,39 @@ export function MultiScreenCanvas({
|
|
|
2598
3119
|
<PenPathOverlay path={displayedPenPath} closeHover={penCloseHover} />
|
|
2599
3120
|
) : null}
|
|
2600
3121
|
|
|
3122
|
+
{singleSelectedFrame ? (
|
|
3123
|
+
<SelectionBox
|
|
3124
|
+
geometry={singleSelectedFrame.geometry}
|
|
3125
|
+
chromeScale={chromeScale}
|
|
3126
|
+
chromeSettling={chromeSettling}
|
|
3127
|
+
showRotate
|
|
3128
|
+
onStartResize={(handle, event) =>
|
|
3129
|
+
beginResize(singleSelectedFrame.id, handle, event)
|
|
3130
|
+
}
|
|
3131
|
+
onStartRotate={(event) =>
|
|
3132
|
+
beginRotate(singleSelectedFrame.id, event)
|
|
3133
|
+
}
|
|
3134
|
+
/>
|
|
3135
|
+
) : null}
|
|
3136
|
+
|
|
3137
|
+
{singleSelectedDraft ? (
|
|
3138
|
+
<SelectionBox
|
|
3139
|
+
geometry={singleSelectedDraft.geometry}
|
|
3140
|
+
chromeScale={chromeScale}
|
|
3141
|
+
chromeSettling={chromeSettling}
|
|
3142
|
+
showRotate={false}
|
|
3143
|
+
onStartResize={(handle, event) =>
|
|
3144
|
+
beginDraftResize(singleSelectedDraft.id, handle, event)
|
|
3145
|
+
}
|
|
3146
|
+
onStartRotate={() => {}}
|
|
3147
|
+
/>
|
|
3148
|
+
) : null}
|
|
3149
|
+
|
|
2601
3150
|
{selectedGroupBounds ? (
|
|
2602
3151
|
<GroupSelectionBox
|
|
2603
3152
|
bounds={selectedGroupBounds}
|
|
2604
3153
|
chromeScale={chromeScale}
|
|
3154
|
+
chromeSettling={chromeSettling}
|
|
2605
3155
|
onStartResize={beginGroupResize}
|
|
2606
3156
|
/>
|
|
2607
3157
|
) : null}
|
|
@@ -2610,6 +3160,7 @@ export function MultiScreenCanvas({
|
|
|
2610
3160
|
<GroupSelectionBox
|
|
2611
3161
|
bounds={selectedDraftGroupBounds}
|
|
2612
3162
|
chromeScale={chromeScale}
|
|
3163
|
+
chromeSettling={chromeSettling}
|
|
2613
3164
|
onStartResize={beginDraftGroupResize}
|
|
2614
3165
|
/>
|
|
2615
3166
|
) : null}
|
|
@@ -2635,20 +3186,23 @@ export function MultiScreenCanvas({
|
|
|
2635
3186
|
}
|
|
2636
3187
|
/>
|
|
2637
3188
|
))}
|
|
2638
|
-
|
|
2639
|
-
{marquee ? (
|
|
2640
|
-
<span
|
|
2641
|
-
className="pointer-events-none absolute z-40 border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)]"
|
|
2642
|
-
style={{
|
|
2643
|
-
left: SURFACE_PADDING + marquee.x,
|
|
2644
|
-
top: SURFACE_PADDING + marquee.y,
|
|
2645
|
-
width: marquee.width,
|
|
2646
|
-
height: marquee.height,
|
|
2647
|
-
}}
|
|
2648
|
-
/>
|
|
2649
|
-
) : null}
|
|
2650
3189
|
</div>
|
|
2651
3190
|
|
|
3191
|
+
{marquee ? (
|
|
3192
|
+
<span
|
|
3193
|
+
className="pointer-events-none absolute z-40 border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)]"
|
|
3194
|
+
style={{
|
|
3195
|
+
// Convert canvas-space marquee to surface-space so this overlay
|
|
3196
|
+
// is never clipped or hidden by the canvas transform container.
|
|
3197
|
+
// Surface position = pan + (SURFACE_PADDING + canvasCoord) * scale
|
|
3198
|
+
left: pan.x + (SURFACE_PADDING + marquee.x) * scale,
|
|
3199
|
+
top: pan.y + (SURFACE_PADDING + marquee.y) * scale,
|
|
3200
|
+
width: Math.max(1, marquee.width * scale),
|
|
3201
|
+
height: Math.max(1, marquee.height * scale),
|
|
3202
|
+
}}
|
|
3203
|
+
/>
|
|
3204
|
+
) : null}
|
|
3205
|
+
|
|
2652
3206
|
{duplicatePreview ? (
|
|
2653
3207
|
<div
|
|
2654
3208
|
className={cn(
|
|
@@ -2684,7 +3238,8 @@ export function MultiScreenCanvas({
|
|
|
2684
3238
|
|
|
2685
3239
|
{transformBadge ? (
|
|
2686
3240
|
<div
|
|
2687
|
-
|
|
3241
|
+
data-transform-badge
|
|
3242
|
+
className="pointer-events-none fixed z-50 rounded border border-border bg-background/95 px-1.5 py-0.5 font-mono text-[11px] leading-5 text-foreground shadow-lg backdrop-blur"
|
|
2688
3243
|
style={{ left: transformBadge.x, top: transformBadge.y }}
|
|
2689
3244
|
>
|
|
2690
3245
|
{transformBadge.text}
|
|
@@ -2700,6 +3255,7 @@ function DraftPrimitiveLayer({
|
|
|
2700
3255
|
groupSelected,
|
|
2701
3256
|
penActive,
|
|
2702
3257
|
chromeScale,
|
|
3258
|
+
chromeSettling,
|
|
2703
3259
|
preview = false,
|
|
2704
3260
|
onClick,
|
|
2705
3261
|
onStartDrag,
|
|
@@ -2710,6 +3266,7 @@ function DraftPrimitiveLayer({
|
|
|
2710
3266
|
groupSelected: boolean;
|
|
2711
3267
|
penActive: boolean;
|
|
2712
3268
|
chromeScale: number;
|
|
3269
|
+
chromeSettling: boolean;
|
|
2713
3270
|
preview?: boolean;
|
|
2714
3271
|
onClick: (id: string, e: React.MouseEvent) => void;
|
|
2715
3272
|
onStartDrag: (id: string, e: React.MouseEvent) => void;
|
|
@@ -2727,7 +3284,7 @@ function DraftPrimitiveLayer({
|
|
|
2727
3284
|
type="button"
|
|
2728
3285
|
className={cn(
|
|
2729
3286
|
"group/artboard pointer-events-auto absolute block overflow-visible text-left outline-none",
|
|
2730
|
-
preview || penActive ? "cursor-crosshair" : "cursor-
|
|
3287
|
+
preview || penActive ? "cursor-crosshair" : "cursor-pointer",
|
|
2731
3288
|
)}
|
|
2732
3289
|
style={{
|
|
2733
3290
|
left: SURFACE_PADDING + geometry.x,
|
|
@@ -2752,20 +3309,24 @@ function DraftPrimitiveLayer({
|
|
|
2752
3309
|
<span
|
|
2753
3310
|
className={cn(
|
|
2754
3311
|
"pointer-events-none absolute rounded-sm border transition-opacity",
|
|
2755
|
-
|
|
3312
|
+
preview
|
|
2756
3313
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
2757
|
-
:
|
|
3314
|
+
: selected
|
|
3315
|
+
? "border-transparent opacity-0"
|
|
3316
|
+
: "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100",
|
|
2758
3317
|
)}
|
|
2759
3318
|
style={{
|
|
2760
3319
|
inset: -5 * chromeScale,
|
|
2761
3320
|
borderWidth: 1.5 * chromeScale,
|
|
3321
|
+
transition: getChromeBorderTransition(chromeSettling),
|
|
2762
3322
|
}}
|
|
2763
3323
|
/>
|
|
2764
3324
|
<ResizeHandles
|
|
2765
|
-
active={
|
|
2766
|
-
enabled={!penActive &&
|
|
3325
|
+
active={preview}
|
|
3326
|
+
enabled={!penActive && preview}
|
|
2767
3327
|
showRotate={false}
|
|
2768
3328
|
chromeScale={chromeScale}
|
|
3329
|
+
chromeSettling={chromeSettling}
|
|
2769
3330
|
onStartResize={(handle, event) =>
|
|
2770
3331
|
onStartResize(draft.id, handle, event)
|
|
2771
3332
|
}
|
|
@@ -3006,35 +3567,21 @@ function isPoint(point: Point | undefined): point is Point {
|
|
|
3006
3567
|
return !!point;
|
|
3007
3568
|
}
|
|
3008
3569
|
|
|
3009
|
-
|
|
3010
|
-
screen,
|
|
3011
|
-
metadata,
|
|
3012
|
-
geometry,
|
|
3013
|
-
isActive,
|
|
3014
|
-
isSelected,
|
|
3015
|
-
groupSelected,
|
|
3016
|
-
handlesEnabled,
|
|
3017
|
-
penActive,
|
|
3018
|
-
creationToolActive,
|
|
3019
|
-
chromeScale,
|
|
3020
|
-
onPick,
|
|
3021
|
-
onEdit,
|
|
3022
|
-
onStartFrameDrag,
|
|
3023
|
-
onStartResize,
|
|
3024
|
-
onStartRotate,
|
|
3025
|
-
onStartDuplicateGesture,
|
|
3026
|
-
screenContent,
|
|
3027
|
-
}: {
|
|
3570
|
+
interface ScreenProps {
|
|
3028
3571
|
screen: ScreenFile;
|
|
3029
3572
|
metadata: ResolvedScreenMetadata;
|
|
3030
3573
|
geometry: FrameGeometry;
|
|
3031
3574
|
isActive: boolean;
|
|
3032
3575
|
isSelected: boolean;
|
|
3576
|
+
isDirectlyHovered: boolean;
|
|
3577
|
+
hasHoveredChild: boolean;
|
|
3033
3578
|
groupSelected: boolean;
|
|
3034
3579
|
handlesEnabled: boolean;
|
|
3035
3580
|
penActive: boolean;
|
|
3036
3581
|
creationToolActive: boolean;
|
|
3582
|
+
canvasGestureActive: boolean;
|
|
3037
3583
|
chromeScale: number;
|
|
3584
|
+
chromeSettling: boolean;
|
|
3038
3585
|
screenContent?: ReactNode;
|
|
3039
3586
|
onPick: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
3040
3587
|
onEdit: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
@@ -3050,15 +3597,75 @@ function Screen({
|
|
|
3050
3597
|
display: string,
|
|
3051
3598
|
e: React.MouseEvent<HTMLElement>,
|
|
3052
3599
|
) => void;
|
|
3053
|
-
}
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
const Screen = memo(function Screen({
|
|
3603
|
+
screen,
|
|
3604
|
+
metadata,
|
|
3605
|
+
geometry,
|
|
3606
|
+
isActive,
|
|
3607
|
+
isSelected,
|
|
3608
|
+
isDirectlyHovered,
|
|
3609
|
+
hasHoveredChild,
|
|
3610
|
+
groupSelected,
|
|
3611
|
+
handlesEnabled,
|
|
3612
|
+
penActive,
|
|
3613
|
+
creationToolActive,
|
|
3614
|
+
canvasGestureActive,
|
|
3615
|
+
chromeScale,
|
|
3616
|
+
chromeSettling,
|
|
3617
|
+
onPick,
|
|
3618
|
+
onEdit,
|
|
3619
|
+
onStartFrameDrag,
|
|
3620
|
+
onStartResize,
|
|
3621
|
+
onStartRotate,
|
|
3622
|
+
onStartDuplicateGesture,
|
|
3623
|
+
screenContent,
|
|
3624
|
+
}: ScreenProps) {
|
|
3054
3625
|
const t = useT();
|
|
3055
3626
|
const display = metadata.title ?? prettyScreenName(screen.filename);
|
|
3056
3627
|
const previewUrl = metadata.previewUrl ?? getPreviewUrl(screen.content);
|
|
3628
|
+
const previewViewport = getScreenPreviewViewport(metadata, geometry);
|
|
3057
3629
|
const suppressNextClick = useRef(false);
|
|
3058
|
-
const
|
|
3630
|
+
const [directlyHovered, setDirectlyHovered] = useState(false);
|
|
3631
|
+
const frameDirectlyHovered =
|
|
3632
|
+
(directlyHovered || isDirectlyHovered) &&
|
|
3633
|
+
!creationToolActive &&
|
|
3634
|
+
!canvasGestureActive;
|
|
3635
|
+
const emphasized = isSelected || frameDirectlyHovered;
|
|
3636
|
+
const activeOrEmphasized = isActive || emphasized;
|
|
3059
3637
|
const selectionOutlined = isSelected && !groupSelected;
|
|
3060
|
-
const
|
|
3061
|
-
|
|
3638
|
+
const showHoverChrome =
|
|
3639
|
+
frameDirectlyHovered && !isSelected && !groupSelected && !hasHoveredChild;
|
|
3640
|
+
const screenContentInteractive =
|
|
3641
|
+
Boolean(screenContent) &&
|
|
3642
|
+
!penActive &&
|
|
3643
|
+
!creationToolActive &&
|
|
3644
|
+
!canvasGestureActive;
|
|
3645
|
+
const updateDirectHover = useCallback((next: boolean) => {
|
|
3646
|
+
setDirectlyHovered((current) => (current === next ? current : next));
|
|
3647
|
+
}, []);
|
|
3648
|
+
const frameLabelHeight = FRAME_LABEL_HEIGHT * chromeScale;
|
|
3649
|
+
const frameScreenWidth = geometry.width / Math.max(chromeScale, 0.001);
|
|
3650
|
+
const fullViewOutsideFrame =
|
|
3651
|
+
frameScreenWidth < FRAME_HEADER_BUTTON_OUTSIDE_WIDTH;
|
|
3652
|
+
const labelInfoMaxWidth = Math.max(
|
|
3653
|
+
64,
|
|
3654
|
+
frameScreenWidth - (fullViewOutsideFrame ? 8 : FRAME_HEADER_BUTTON_RESERVE),
|
|
3655
|
+
);
|
|
3656
|
+
const estimatedTitleWidth = clamp(
|
|
3657
|
+
display.length * FRAME_HEADER_TITLE_CHAR_WIDTH,
|
|
3658
|
+
FRAME_HEADER_MIN_TITLE_WIDTH,
|
|
3659
|
+
FRAME_HEADER_MAX_TITLE_WIDTH,
|
|
3660
|
+
);
|
|
3661
|
+
const showDimensions =
|
|
3662
|
+
frameScreenWidth >=
|
|
3663
|
+
estimatedTitleWidth +
|
|
3664
|
+
FRAME_HEADER_DIMENSIONS_WIDTH +
|
|
3665
|
+
(fullViewOutsideFrame ? 16 : FRAME_HEADER_BUTTON_RESERVE);
|
|
3666
|
+
const fullViewMaxWidth = fullViewOutsideFrame
|
|
3667
|
+
? 120
|
|
3668
|
+
: Math.max(84, Math.min(180, frameScreenWidth * 0.46));
|
|
3062
3669
|
|
|
3063
3670
|
return (
|
|
3064
3671
|
<div
|
|
@@ -3066,29 +3673,58 @@ function Screen({
|
|
|
3066
3673
|
className="group/frame pointer-events-auto absolute"
|
|
3067
3674
|
style={{
|
|
3068
3675
|
left: SURFACE_PADDING + geometry.x,
|
|
3069
|
-
top: SURFACE_PADDING + geometry.y -
|
|
3676
|
+
top: SURFACE_PADDING + geometry.y - frameLabelHeight,
|
|
3070
3677
|
width: geometry.width,
|
|
3071
3678
|
transform: geometry.rotation
|
|
3072
3679
|
? `rotate(${geometry.rotation}deg)`
|
|
3073
3680
|
: undefined,
|
|
3074
|
-
transformOrigin: `${geometry.width / 2}px ${
|
|
3681
|
+
transformOrigin: `${geometry.width / 2}px ${frameLabelHeight + geometry.height / 2}px`,
|
|
3075
3682
|
zIndex: geometry.z,
|
|
3076
3683
|
}}
|
|
3077
3684
|
>
|
|
3078
3685
|
<div
|
|
3079
|
-
className="
|
|
3686
|
+
className="relative w-full cursor-default"
|
|
3687
|
+
style={{ height: frameLabelHeight }}
|
|
3688
|
+
onClick={(e) => {
|
|
3689
|
+
e.stopPropagation();
|
|
3690
|
+
if (suppressNextClick.current) {
|
|
3691
|
+
suppressNextClick.current = false;
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
if (e.detail > 1) return;
|
|
3695
|
+
onPick(screen.id, e);
|
|
3696
|
+
}}
|
|
3697
|
+
onDoubleClick={(e) => {
|
|
3698
|
+
e.preventDefault();
|
|
3699
|
+
e.stopPropagation();
|
|
3700
|
+
onEdit(screen.id, e);
|
|
3701
|
+
}}
|
|
3080
3702
|
onMouseDown={(e) => {
|
|
3081
3703
|
if (e.button !== 0) return;
|
|
3082
3704
|
if (penActive || creationToolActive) return;
|
|
3705
|
+
if (e.altKey) {
|
|
3706
|
+
onStartDuplicateGesture(screen, display, e);
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3083
3709
|
if (e.shiftKey) {
|
|
3084
3710
|
e.stopPropagation();
|
|
3085
|
-
onPick(screen.id, e);
|
|
3086
3711
|
return;
|
|
3087
3712
|
}
|
|
3088
3713
|
onStartFrameDrag(screen.id, e);
|
|
3089
3714
|
}}
|
|
3090
3715
|
>
|
|
3091
|
-
<div
|
|
3716
|
+
<div
|
|
3717
|
+
data-frame-label
|
|
3718
|
+
className="absolute left-1 top-1/2 flex min-w-0 items-center gap-1.5"
|
|
3719
|
+
onMouseEnter={() => updateDirectHover(true)}
|
|
3720
|
+
onMouseLeave={() => updateDirectHover(false)}
|
|
3721
|
+
style={{
|
|
3722
|
+
maxWidth: labelInfoMaxWidth,
|
|
3723
|
+
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
3724
|
+
transformOrigin: "left center",
|
|
3725
|
+
transition: getChromeLabelTransition(chromeSettling),
|
|
3726
|
+
}}
|
|
3727
|
+
>
|
|
3092
3728
|
<span
|
|
3093
3729
|
className={cn(
|
|
3094
3730
|
"h-1.5 w-1.5 shrink-0 rounded-full",
|
|
@@ -3096,26 +3732,45 @@ function Screen({
|
|
|
3096
3732
|
)}
|
|
3097
3733
|
/>
|
|
3098
3734
|
<span
|
|
3735
|
+
data-frame-title
|
|
3099
3736
|
className={cn(
|
|
3100
|
-
"truncate text-[11px] font-medium",
|
|
3101
|
-
emphasized
|
|
3737
|
+
"min-w-0 truncate text-[11px] font-medium",
|
|
3738
|
+
emphasized
|
|
3739
|
+
? "text-[var(--design-editor-accent-color)]"
|
|
3740
|
+
: activeOrEmphasized
|
|
3741
|
+
? "text-foreground"
|
|
3742
|
+
: "text-muted-foreground",
|
|
3102
3743
|
)}
|
|
3103
3744
|
title={screen.filename}
|
|
3104
3745
|
>
|
|
3105
3746
|
{display}
|
|
3106
3747
|
</span>
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3748
|
+
{showDimensions ? (
|
|
3749
|
+
<span
|
|
3750
|
+
data-frame-dimensions
|
|
3751
|
+
className="hidden shrink-0 text-[10px] tabular-nums text-muted-foreground/70 sm:inline"
|
|
3752
|
+
>
|
|
3753
|
+
{previewViewport.displayWidth} x {previewViewport.displayHeight}
|
|
3754
|
+
</span>
|
|
3755
|
+
) : null}
|
|
3110
3756
|
</div>
|
|
3111
3757
|
<button
|
|
3112
3758
|
type="button"
|
|
3759
|
+
data-frame-full-view
|
|
3113
3760
|
className={cn(
|
|
3114
|
-
"
|
|
3761
|
+
"absolute top-1/2 z-40 flex h-5 shrink-0 items-center gap-1 overflow-hidden rounded-md border border-border bg-background/95 px-1.5 text-[10px] font-medium text-foreground opacity-0 shadow-sm transition-opacity",
|
|
3115
3762
|
"hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
3116
|
-
"group-hover/frame:opacity-100 group-focus-within/frame:opacity-100",
|
|
3117
3763
|
emphasized && "opacity-100",
|
|
3764
|
+
fullViewOutsideFrame ? "left-full" : "right-1",
|
|
3118
3765
|
)}
|
|
3766
|
+
style={{
|
|
3767
|
+
maxWidth: fullViewMaxWidth,
|
|
3768
|
+
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
3769
|
+
transformOrigin: fullViewOutsideFrame
|
|
3770
|
+
? "left center"
|
|
3771
|
+
: "right center",
|
|
3772
|
+
transition: getChromeLabelTransition(chromeSettling),
|
|
3773
|
+
}}
|
|
3119
3774
|
aria-label={t("multiScreenCanvas.fullView")}
|
|
3120
3775
|
title={t("multiScreenCanvas.fullView")}
|
|
3121
3776
|
onClick={(event) => onEdit(screen.id, event)}
|
|
@@ -3123,6 +3778,8 @@ function Screen({
|
|
|
3123
3778
|
event.preventDefault();
|
|
3124
3779
|
event.stopPropagation();
|
|
3125
3780
|
}}
|
|
3781
|
+
onMouseEnter={() => updateDirectHover(true)}
|
|
3782
|
+
onMouseLeave={() => updateDirectHover(false)}
|
|
3126
3783
|
>
|
|
3127
3784
|
<IconMaximize className="size-3 shrink-0" />
|
|
3128
3785
|
<span className="truncate">{t("multiScreenCanvas.fullView")}</span>
|
|
@@ -3133,7 +3790,10 @@ function Screen({
|
|
|
3133
3790
|
role="button"
|
|
3134
3791
|
tabIndex={0}
|
|
3135
3792
|
onClick={(e) => {
|
|
3136
|
-
if (isInteractiveScreenContentTarget(e.target))
|
|
3793
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3794
|
+
e.stopPropagation();
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3137
3797
|
e.stopPropagation();
|
|
3138
3798
|
if (suppressNextClick.current) {
|
|
3139
3799
|
suppressNextClick.current = false;
|
|
@@ -3143,13 +3803,19 @@ function Screen({
|
|
|
3143
3803
|
onPick(screen.id, e);
|
|
3144
3804
|
}}
|
|
3145
3805
|
onDoubleClick={(e) => {
|
|
3146
|
-
if (isInteractiveScreenContentTarget(e.target))
|
|
3806
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3807
|
+
e.stopPropagation();
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3147
3810
|
e.preventDefault();
|
|
3148
3811
|
e.stopPropagation();
|
|
3149
3812
|
}}
|
|
3150
3813
|
onMouseDown={(e) => {
|
|
3814
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3815
|
+
e.stopPropagation();
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3151
3818
|
if (creationToolActive) return;
|
|
3152
|
-
if (isInteractiveScreenContentTarget(e.target)) return;
|
|
3153
3819
|
if (e.detail > 1) {
|
|
3154
3820
|
e.stopPropagation();
|
|
3155
3821
|
return;
|
|
@@ -3168,46 +3834,52 @@ function Screen({
|
|
|
3168
3834
|
onStartFrameDrag(screen.id, e);
|
|
3169
3835
|
}
|
|
3170
3836
|
}}
|
|
3837
|
+
onMouseMove={(e) => {
|
|
3838
|
+
updateDirectHover(
|
|
3839
|
+
isDirectScreenHoverTarget(e.target, e.currentTarget),
|
|
3840
|
+
);
|
|
3841
|
+
}}
|
|
3842
|
+
onMouseLeave={() => updateDirectHover(false)}
|
|
3171
3843
|
className={cn(
|
|
3172
3844
|
"group/artboard relative block overflow-visible rounded-lg bg-background text-left outline-none transition-colors",
|
|
3173
3845
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
3174
3846
|
emphasized
|
|
3175
3847
|
? "text-foreground"
|
|
3176
|
-
:
|
|
3848
|
+
: cn(
|
|
3849
|
+
"text-muted-foreground",
|
|
3850
|
+
showHoverChrome && "hover:text-foreground",
|
|
3851
|
+
),
|
|
3177
3852
|
)}
|
|
3178
3853
|
style={{
|
|
3179
3854
|
width: geometry.width,
|
|
3180
3855
|
height: geometry.height,
|
|
3181
|
-
cursor:
|
|
3182
|
-
penActive || creationToolActive
|
|
3183
|
-
? "crosshair"
|
|
3184
|
-
: isSelected
|
|
3185
|
-
? "move"
|
|
3186
|
-
: "pointer",
|
|
3856
|
+
cursor: penActive || creationToolActive ? "crosshair" : "pointer",
|
|
3187
3857
|
touchAction: "none",
|
|
3188
3858
|
}}
|
|
3189
3859
|
>
|
|
3190
3860
|
<span
|
|
3861
|
+
data-screen-hover-outline
|
|
3191
3862
|
className={cn(
|
|
3192
3863
|
"pointer-events-none absolute border transition-opacity",
|
|
3193
|
-
|
|
3864
|
+
showHoverChrome
|
|
3194
3865
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
3195
|
-
:
|
|
3196
|
-
? "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100"
|
|
3197
|
-
: "border-transparent opacity-0",
|
|
3866
|
+
: "border-transparent opacity-0",
|
|
3198
3867
|
)}
|
|
3199
3868
|
style={{
|
|
3200
3869
|
inset: -5 * chromeScale,
|
|
3201
3870
|
borderRadius: 13 * chromeScale,
|
|
3202
3871
|
borderWidth: 1.5 * chromeScale,
|
|
3872
|
+
transition: getChromeBorderTransition(chromeSettling),
|
|
3203
3873
|
}}
|
|
3204
3874
|
/>
|
|
3205
3875
|
<span
|
|
3876
|
+
data-screen-content
|
|
3206
3877
|
className={cn(
|
|
3207
3878
|
"relative block h-full w-full overflow-hidden rounded-lg border bg-white shadow-2xl transition-colors",
|
|
3208
|
-
"border-border
|
|
3879
|
+
"border-border",
|
|
3880
|
+
showHoverChrome && "border-muted-foreground/60",
|
|
3209
3881
|
)}
|
|
3210
|
-
style={{ pointerEvents:
|
|
3882
|
+
style={{ pointerEvents: screenContentInteractive ? "auto" : "none" }}
|
|
3211
3883
|
>
|
|
3212
3884
|
{screenContent ?? (
|
|
3213
3885
|
<iframe
|
|
@@ -3217,12 +3889,24 @@ function Screen({
|
|
|
3217
3889
|
loading="lazy"
|
|
3218
3890
|
className="pointer-events-none border-0"
|
|
3219
3891
|
style={{
|
|
3220
|
-
width:
|
|
3221
|
-
height:
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3892
|
+
width: previewViewport.viewportWidth,
|
|
3893
|
+
height: previewViewport.viewportHeight,
|
|
3894
|
+
// Untouched same-aspect overview thumbnails may scale uniformly.
|
|
3895
|
+
// User-resized frames use their real iframe viewport so
|
|
3896
|
+
// responsive layouts recompute instead of getting stretched.
|
|
3897
|
+
transform:
|
|
3898
|
+
previewViewport.scale === 1
|
|
3899
|
+
? undefined
|
|
3900
|
+
: `scale(${previewViewport.scale})`,
|
|
3225
3901
|
transformOrigin: "top left",
|
|
3902
|
+
backgroundColor: "white",
|
|
3903
|
+
colorScheme: "light",
|
|
3904
|
+
// Prevent the browser from discarding the composited layer at
|
|
3905
|
+
// fractional zoom levels, which causes the iframe to go black.
|
|
3906
|
+
// backface-visibility:hidden forces the browser to keep the
|
|
3907
|
+
// backing store alive even when the effective scale is very small
|
|
3908
|
+
// (e.g. 0.25 iframe scale × 0.5 canvas zoom = 0.125 total).
|
|
3909
|
+
backfaceVisibility: "hidden",
|
|
3226
3910
|
}}
|
|
3227
3911
|
title={screen.filename}
|
|
3228
3912
|
/>
|
|
@@ -3233,50 +3917,132 @@ function Screen({
|
|
|
3233
3917
|
aria-hidden="true"
|
|
3234
3918
|
/>
|
|
3235
3919
|
) : null}
|
|
3920
|
+
{canvasGestureActive && screenContent ? (
|
|
3921
|
+
<span
|
|
3922
|
+
data-screen-interaction-shield
|
|
3923
|
+
className="pointer-events-auto absolute inset-0 z-30"
|
|
3924
|
+
aria-hidden="true"
|
|
3925
|
+
/>
|
|
3926
|
+
) : null}
|
|
3236
3927
|
<span className="pointer-events-none absolute inset-0 rounded-[7px] border border-black/5" />
|
|
3237
3928
|
</span>
|
|
3238
3929
|
<ResizeHandles
|
|
3239
|
-
active={
|
|
3240
|
-
enabled={
|
|
3930
|
+
active={false}
|
|
3931
|
+
enabled={
|
|
3932
|
+
!selectionOutlined &&
|
|
3933
|
+
!penActive &&
|
|
3934
|
+
!creationToolActive &&
|
|
3935
|
+
handlesEnabled
|
|
3936
|
+
}
|
|
3937
|
+
showOnHover={false}
|
|
3241
3938
|
showRotate
|
|
3242
3939
|
chromeScale={chromeScale}
|
|
3940
|
+
chromeSettling={chromeSettling}
|
|
3243
3941
|
onStartResize={(handle, e) => onStartResize(screen.id, handle, e)}
|
|
3244
3942
|
onStartRotate={(e) => onStartRotate(screen.id, e)}
|
|
3245
3943
|
/>
|
|
3246
3944
|
</div>
|
|
3247
3945
|
</div>
|
|
3248
3946
|
);
|
|
3947
|
+
}, areScreenPropsEqual);
|
|
3948
|
+
|
|
3949
|
+
function areScreenPropsEqual(prev: ScreenProps, next: ScreenProps) {
|
|
3950
|
+
return (
|
|
3951
|
+
prev.screen === next.screen &&
|
|
3952
|
+
prev.screenContent === next.screenContent &&
|
|
3953
|
+
sameResolvedMetadata(prev.metadata, next.metadata) &&
|
|
3954
|
+
sameFrameGeometry(prev.geometry, next.geometry) &&
|
|
3955
|
+
prev.isActive === next.isActive &&
|
|
3956
|
+
prev.isSelected === next.isSelected &&
|
|
3957
|
+
prev.isDirectlyHovered === next.isDirectlyHovered &&
|
|
3958
|
+
prev.hasHoveredChild === next.hasHoveredChild &&
|
|
3959
|
+
prev.groupSelected === next.groupSelected &&
|
|
3960
|
+
prev.handlesEnabled === next.handlesEnabled &&
|
|
3961
|
+
prev.penActive === next.penActive &&
|
|
3962
|
+
prev.creationToolActive === next.creationToolActive &&
|
|
3963
|
+
prev.canvasGestureActive === next.canvasGestureActive &&
|
|
3964
|
+
prev.chromeScale === next.chromeScale &&
|
|
3965
|
+
prev.chromeSettling === next.chromeSettling &&
|
|
3966
|
+
prev.onPick === next.onPick &&
|
|
3967
|
+
prev.onEdit === next.onEdit &&
|
|
3968
|
+
prev.onStartFrameDrag === next.onStartFrameDrag &&
|
|
3969
|
+
prev.onStartResize === next.onStartResize &&
|
|
3970
|
+
prev.onStartRotate === next.onStartRotate &&
|
|
3971
|
+
prev.onStartDuplicateGesture === next.onStartDuplicateGesture
|
|
3972
|
+
);
|
|
3249
3973
|
}
|
|
3250
3974
|
|
|
3251
3975
|
function GroupSelectionBox({
|
|
3252
3976
|
bounds,
|
|
3253
3977
|
chromeScale,
|
|
3978
|
+
chromeSettling,
|
|
3254
3979
|
onStartResize,
|
|
3255
3980
|
}: {
|
|
3256
3981
|
bounds: NonNullable<ReturnType<typeof getFrameGroupBounds>>;
|
|
3257
3982
|
chromeScale: number;
|
|
3983
|
+
chromeSettling: boolean;
|
|
3984
|
+
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
3985
|
+
}) {
|
|
3986
|
+
return (
|
|
3987
|
+
<SelectionBox
|
|
3988
|
+
geometry={{
|
|
3989
|
+
x: bounds.left,
|
|
3990
|
+
y: bounds.top,
|
|
3991
|
+
width: bounds.width,
|
|
3992
|
+
height: bounds.height,
|
|
3993
|
+
}}
|
|
3994
|
+
chromeScale={chromeScale}
|
|
3995
|
+
chromeSettling={chromeSettling}
|
|
3996
|
+
showRotate={false}
|
|
3997
|
+
onStartResize={onStartResize}
|
|
3998
|
+
onStartRotate={() => {}}
|
|
3999
|
+
/>
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
function SelectionBox({
|
|
4004
|
+
geometry,
|
|
4005
|
+
chromeScale,
|
|
4006
|
+
chromeSettling,
|
|
4007
|
+
showRotate = true,
|
|
4008
|
+
onStartResize,
|
|
4009
|
+
onStartRotate,
|
|
4010
|
+
}: {
|
|
4011
|
+
geometry: FrameGeometry;
|
|
4012
|
+
chromeScale: number;
|
|
4013
|
+
chromeSettling: boolean;
|
|
4014
|
+
showRotate?: boolean;
|
|
3258
4015
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
4016
|
+
onStartRotate: (e: React.MouseEvent) => void;
|
|
3259
4017
|
}) {
|
|
3260
4018
|
return (
|
|
3261
4019
|
<div
|
|
4020
|
+
data-frame-selection-box
|
|
3262
4021
|
data-frame-shell
|
|
3263
|
-
className="pointer-events-none absolute
|
|
4022
|
+
className="pointer-events-none absolute border border-[var(--design-editor-accent-color)]"
|
|
3264
4023
|
style={{
|
|
3265
|
-
left: SURFACE_PADDING +
|
|
3266
|
-
top: SURFACE_PADDING +
|
|
3267
|
-
width:
|
|
3268
|
-
height:
|
|
4024
|
+
left: SURFACE_PADDING + geometry.x,
|
|
4025
|
+
top: SURFACE_PADDING + geometry.y,
|
|
4026
|
+
width: geometry.width,
|
|
4027
|
+
height: geometry.height,
|
|
3269
4028
|
borderRadius: 13 * chromeScale,
|
|
3270
4029
|
borderWidth: 1.5 * chromeScale,
|
|
4030
|
+
transition: getSelectionBoxTransition(chromeSettling),
|
|
4031
|
+
transform: geometry.rotation
|
|
4032
|
+
? `rotate(${geometry.rotation}deg)`
|
|
4033
|
+
: undefined,
|
|
4034
|
+
transformOrigin: `${geometry.width / 2}px ${geometry.height / 2}px`,
|
|
4035
|
+
zIndex: 1_000_000,
|
|
3271
4036
|
}}
|
|
3272
4037
|
>
|
|
3273
4038
|
<ResizeHandles
|
|
3274
4039
|
active
|
|
3275
4040
|
enabled
|
|
3276
|
-
showRotate={
|
|
4041
|
+
showRotate={showRotate}
|
|
3277
4042
|
chromeScale={chromeScale}
|
|
4043
|
+
chromeSettling={chromeSettling}
|
|
3278
4044
|
onStartResize={onStartResize}
|
|
3279
|
-
onStartRotate={
|
|
4045
|
+
onStartRotate={onStartRotate}
|
|
3280
4046
|
/>
|
|
3281
4047
|
</div>
|
|
3282
4048
|
);
|
|
@@ -3285,15 +4051,19 @@ function GroupSelectionBox({
|
|
|
3285
4051
|
function ResizeHandles({
|
|
3286
4052
|
active,
|
|
3287
4053
|
enabled,
|
|
4054
|
+
showOnHover = true,
|
|
3288
4055
|
showRotate = true,
|
|
3289
4056
|
chromeScale = 1,
|
|
4057
|
+
chromeSettling = false,
|
|
3290
4058
|
onStartResize,
|
|
3291
4059
|
onStartRotate,
|
|
3292
4060
|
}: {
|
|
3293
4061
|
active: boolean;
|
|
3294
4062
|
enabled: boolean;
|
|
4063
|
+
showOnHover?: boolean;
|
|
3295
4064
|
showRotate?: boolean;
|
|
3296
4065
|
chromeScale?: number;
|
|
4066
|
+
chromeSettling?: boolean;
|
|
3297
4067
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
3298
4068
|
onStartRotate: (e: React.MouseEvent) => void;
|
|
3299
4069
|
}) {
|
|
@@ -3303,7 +4073,11 @@ function ResizeHandles({
|
|
|
3303
4073
|
"pointer-events-auto absolute z-20 rounded-[2px] border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-contrast-color)] shadow transition-opacity",
|
|
3304
4074
|
active
|
|
3305
4075
|
? "opacity-100"
|
|
3306
|
-
:
|
|
4076
|
+
: cn(
|
|
4077
|
+
"opacity-0",
|
|
4078
|
+
showOnHover &&
|
|
4079
|
+
"group-hover/artboard:opacity-100 group-focus-visible/artboard:opacity-100",
|
|
4080
|
+
),
|
|
3307
4081
|
);
|
|
3308
4082
|
const edgeHandleClass =
|
|
3309
4083
|
"pointer-events-auto absolute z-10 bg-transparent opacity-0";
|
|
@@ -3313,18 +4087,28 @@ function ResizeHandles({
|
|
|
3313
4087
|
{EDGE_RESIZE_HANDLE_CONFIGS.map((config) => (
|
|
3314
4088
|
<span
|
|
3315
4089
|
key={config.handle}
|
|
3316
|
-
data-resize-handle
|
|
4090
|
+
data-resize-handle={config.handle}
|
|
3317
4091
|
className={edgeHandleClass}
|
|
3318
|
-
style={edgeHandleStyle(
|
|
4092
|
+
style={edgeHandleStyle(
|
|
4093
|
+
config.handle,
|
|
4094
|
+
config.cursor,
|
|
4095
|
+
chromeScale,
|
|
4096
|
+
chromeSettling,
|
|
4097
|
+
)}
|
|
3319
4098
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
3320
4099
|
/>
|
|
3321
4100
|
))}
|
|
3322
4101
|
{CORNER_RESIZE_HANDLE_CONFIGS.map((config) => (
|
|
3323
4102
|
<span
|
|
3324
4103
|
key={config.handle}
|
|
3325
|
-
data-resize-handle
|
|
4104
|
+
data-resize-handle={config.handle}
|
|
3326
4105
|
className={visibleHandleClass}
|
|
3327
|
-
style={cornerHandleStyle(
|
|
4106
|
+
style={cornerHandleStyle(
|
|
4107
|
+
config.handle,
|
|
4108
|
+
config.cursor,
|
|
4109
|
+
chromeScale,
|
|
4110
|
+
chromeSettling,
|
|
4111
|
+
)}
|
|
3328
4112
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
3329
4113
|
/>
|
|
3330
4114
|
))}
|
|
@@ -3337,9 +4121,17 @@ function ResizeHandles({
|
|
|
3337
4121
|
"pointer-events-auto absolute z-10 size-5 rounded-full transition-opacity active:cursor-grabbing",
|
|
3338
4122
|
active
|
|
3339
4123
|
? "opacity-100"
|
|
3340
|
-
:
|
|
4124
|
+
: cn(
|
|
4125
|
+
"opacity-0",
|
|
4126
|
+
showOnHover &&
|
|
4127
|
+
"group-hover/artboard:opacity-100 group-focus-visible/artboard:opacity-100",
|
|
4128
|
+
),
|
|
4129
|
+
)}
|
|
4130
|
+
style={rotateHandleStyle(
|
|
4131
|
+
config.corner,
|
|
4132
|
+
chromeScale,
|
|
4133
|
+
chromeSettling,
|
|
3341
4134
|
)}
|
|
3342
|
-
style={rotateHandleStyle(config.corner, chromeScale)}
|
|
3343
4135
|
onMouseDown={onStartRotate}
|
|
3344
4136
|
/>
|
|
3345
4137
|
))
|
|
@@ -3381,12 +4173,14 @@ function edgeHandleStyle(
|
|
|
3381
4173
|
handle: ResizeHandle,
|
|
3382
4174
|
cursor: string,
|
|
3383
4175
|
chromeScale: number,
|
|
4176
|
+
chromeSettling: boolean,
|
|
3384
4177
|
): CSSProperties {
|
|
3385
4178
|
const size = 14 * chromeScale;
|
|
3386
4179
|
const offset = -size / 2;
|
|
3387
4180
|
if (handle === "n" || handle === "s") {
|
|
3388
4181
|
return {
|
|
3389
4182
|
cursor,
|
|
4183
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3390
4184
|
left: 0,
|
|
3391
4185
|
right: 0,
|
|
3392
4186
|
height: size,
|
|
@@ -3395,6 +4189,7 @@ function edgeHandleStyle(
|
|
|
3395
4189
|
}
|
|
3396
4190
|
return {
|
|
3397
4191
|
cursor,
|
|
4192
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3398
4193
|
top: 0,
|
|
3399
4194
|
bottom: 0,
|
|
3400
4195
|
width: size,
|
|
@@ -3406,11 +4201,13 @@ function cornerHandleStyle(
|
|
|
3406
4201
|
handle: ResizeHandle,
|
|
3407
4202
|
cursor: string,
|
|
3408
4203
|
chromeScale: number,
|
|
4204
|
+
chromeSettling: boolean,
|
|
3409
4205
|
): CSSProperties {
|
|
3410
4206
|
const size = 10 * chromeScale;
|
|
3411
4207
|
const offset = -size / 2;
|
|
3412
4208
|
return {
|
|
3413
4209
|
cursor,
|
|
4210
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3414
4211
|
width: size,
|
|
3415
4212
|
height: size,
|
|
3416
4213
|
borderWidth: Math.max(1, 1.25 * chromeScale),
|
|
@@ -3419,11 +4216,16 @@ function cornerHandleStyle(
|
|
|
3419
4216
|
};
|
|
3420
4217
|
}
|
|
3421
4218
|
|
|
3422
|
-
function rotateHandleStyle(
|
|
4219
|
+
function rotateHandleStyle(
|
|
4220
|
+
corner: string,
|
|
4221
|
+
chromeScale: number,
|
|
4222
|
+
chromeSettling: boolean,
|
|
4223
|
+
): CSSProperties {
|
|
3423
4224
|
const size = 28 * chromeScale;
|
|
3424
4225
|
const offset = -34 * chromeScale;
|
|
3425
4226
|
return {
|
|
3426
4227
|
cursor: "grab",
|
|
4228
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3427
4229
|
width: size,
|
|
3428
4230
|
height: size,
|
|
3429
4231
|
...(corner.includes("n") ? { top: offset } : { bottom: offset }),
|
|
@@ -3443,25 +4245,72 @@ interface BoundsRect {
|
|
|
3443
4245
|
bottom: number;
|
|
3444
4246
|
}
|
|
3445
4247
|
|
|
3446
|
-
|
|
4248
|
+
interface ScreenViewportSize {
|
|
4249
|
+
width: number;
|
|
4250
|
+
height: number;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
function getInitialFrameGeometry(
|
|
4254
|
+
index: number,
|
|
4255
|
+
metadata?: ScreenViewportSize,
|
|
4256
|
+
): FrameGeometry {
|
|
4257
|
+
// Seed default frames with the actual screen dimensions and the 3-column grid
|
|
4258
|
+
// the overview centering math (which uses SCREEN_WIDTH/SCREEN_GAP) expects, so
|
|
4259
|
+
// a design without persisted geometry opens centered. (The larger
|
|
4260
|
+
// assigned-region grid is only for the agent's generation planning, not the
|
|
4261
|
+
// editor's default placement.)
|
|
3447
4262
|
const column = index % 3;
|
|
3448
4263
|
const row = Math.floor(index / 3);
|
|
4264
|
+
const height = getOverviewFrameHeight(SCREEN_WIDTH, metadata);
|
|
3449
4265
|
return {
|
|
3450
4266
|
x: column * (SCREEN_WIDTH + SCREEN_GAP),
|
|
3451
|
-
y: row * (
|
|
4267
|
+
y: row * (height + FRAME_LABEL_HEIGHT + SCREEN_GAP),
|
|
3452
4268
|
width: SCREEN_WIDTH,
|
|
3453
|
-
height
|
|
4269
|
+
height,
|
|
3454
4270
|
};
|
|
3455
4271
|
}
|
|
3456
4272
|
|
|
3457
|
-
function
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
4273
|
+
function getOverviewFrameHeight(width: number, metadata?: ScreenViewportSize) {
|
|
4274
|
+
const sourceWidth =
|
|
4275
|
+
metadata?.width && metadata.width > 0 ? metadata.width : 1280;
|
|
4276
|
+
const sourceHeight =
|
|
4277
|
+
metadata?.height && metadata.height > 0 ? metadata.height : 2560;
|
|
4278
|
+
return Math.max(80, Math.round((width * sourceHeight) / sourceWidth));
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
function getScreenPreviewViewport(
|
|
4282
|
+
metadata: ScreenViewportSize,
|
|
4283
|
+
geometry: ScreenViewportSize,
|
|
4284
|
+
) {
|
|
4285
|
+
const metadataWidth = Math.max(1, Math.round(metadata.width));
|
|
4286
|
+
const metadataHeight = Math.max(1, Math.round(metadata.height));
|
|
4287
|
+
const geometryWidth = Math.max(1, Math.round(geometry.width));
|
|
4288
|
+
const geometryHeight = Math.max(1, Math.round(geometry.height));
|
|
4289
|
+
const metadataAspect = metadataWidth / metadataHeight;
|
|
4290
|
+
const geometryAspect = geometryWidth / geometryHeight;
|
|
4291
|
+
const aspectMatches = Math.abs(metadataAspect - geometryAspect) < 0.005;
|
|
4292
|
+
|
|
4293
|
+
if (aspectMatches) {
|
|
4294
|
+
return {
|
|
4295
|
+
viewportWidth: metadataWidth,
|
|
4296
|
+
viewportHeight: metadataHeight,
|
|
4297
|
+
displayWidth: metadataWidth,
|
|
4298
|
+
displayHeight: metadataHeight,
|
|
4299
|
+
scale:
|
|
4300
|
+
Math.abs(metadataWidth - geometryWidth) < 0.5 &&
|
|
4301
|
+
Math.abs(metadataHeight - geometryHeight) < 0.5
|
|
4302
|
+
? 1
|
|
4303
|
+
: geometryWidth / metadataWidth,
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
return {
|
|
4308
|
+
viewportWidth: geometryWidth,
|
|
4309
|
+
viewportHeight: geometryHeight,
|
|
4310
|
+
displayWidth: geometryWidth,
|
|
4311
|
+
displayHeight: geometryHeight,
|
|
4312
|
+
scale: 1,
|
|
4313
|
+
};
|
|
3465
4314
|
}
|
|
3466
4315
|
|
|
3467
4316
|
function dedupeIds(ids: string[]) {
|
|
@@ -3519,6 +4368,22 @@ function isInteractiveScreenContentTarget(target: EventTarget | null) {
|
|
|
3519
4368
|
);
|
|
3520
4369
|
}
|
|
3521
4370
|
|
|
4371
|
+
function mutePreviewIframePointerEvents(root: HTMLElement | null) {
|
|
4372
|
+
if (!root) return () => {};
|
|
4373
|
+
const previous = new Map<HTMLIFrameElement, string>();
|
|
4374
|
+
root
|
|
4375
|
+
.querySelectorAll<HTMLIFrameElement>("[data-design-preview-iframe]")
|
|
4376
|
+
.forEach((iframe) => {
|
|
4377
|
+
previous.set(iframe, iframe.style.pointerEvents);
|
|
4378
|
+
iframe.style.pointerEvents = "none";
|
|
4379
|
+
});
|
|
4380
|
+
return () => {
|
|
4381
|
+
previous.forEach((pointerEvents, iframe) => {
|
|
4382
|
+
if (iframe.isConnected) iframe.style.pointerEvents = pointerEvents;
|
|
4383
|
+
});
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
|
|
3522
4387
|
function frameBoundsToGeometry(bounds: {
|
|
3523
4388
|
left: number;
|
|
3524
4389
|
top: number;
|
|
@@ -3571,6 +4436,84 @@ function rectContainsPoint(bounds: BoundsRect, point: Point) {
|
|
|
3571
4436
|
);
|
|
3572
4437
|
}
|
|
3573
4438
|
|
|
4439
|
+
/** Rotate `point` into the local (unrotated) space of a frame whose center is
|
|
4440
|
+
* `center` and whose CSS transform is `rotate(degrees deg)`. Passing the
|
|
4441
|
+
* inverse rotation maps world-space coordinates into the frame's local space
|
|
4442
|
+
* so unrotated bounds tests remain correct. */
|
|
4443
|
+
function rotatePointAroundCenter(
|
|
4444
|
+
point: Point,
|
|
4445
|
+
center: Point,
|
|
4446
|
+
degrees: number,
|
|
4447
|
+
): Point {
|
|
4448
|
+
if (!degrees) return point;
|
|
4449
|
+
const rad = (-degrees * Math.PI) / 180; // inverse rotation
|
|
4450
|
+
const dx = point.x - center.x;
|
|
4451
|
+
const dy = point.y - center.y;
|
|
4452
|
+
const cos = Math.cos(rad);
|
|
4453
|
+
const sin = Math.sin(rad);
|
|
4454
|
+
return {
|
|
4455
|
+
x: center.x + dx * cos - dy * sin,
|
|
4456
|
+
y: center.y + dx * sin + dy * cos,
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
/** Test whether a marquee rect (in canvas space) intersects an OBB described by
|
|
4461
|
+
* `bounds` (unrotated AABB) that has been rotated `degrees` around `center`.
|
|
4462
|
+
* We map the four marquee corners into the frame's local space and check
|
|
4463
|
+
* whether any corner is inside the bounds; we also check the reverse (any
|
|
4464
|
+
* frame corner inside the marquee) to handle the case where the marquee is
|
|
4465
|
+
* entirely contained within the rotated frame. */
|
|
4466
|
+
function rotatedRectIntersects(
|
|
4467
|
+
rect: MarqueeRect,
|
|
4468
|
+
bounds: BoundsRect,
|
|
4469
|
+
center: Point,
|
|
4470
|
+
degrees: number,
|
|
4471
|
+
): boolean {
|
|
4472
|
+
if (!degrees) {
|
|
4473
|
+
return rectIntersects(rect, bounds);
|
|
4474
|
+
}
|
|
4475
|
+
// Four corners of the marquee rect in canvas space.
|
|
4476
|
+
const marqueeCorners: Point[] = [
|
|
4477
|
+
{ x: rect.x, y: rect.y },
|
|
4478
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
4479
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
4480
|
+
{ x: rect.x, y: rect.y + rect.height },
|
|
4481
|
+
];
|
|
4482
|
+
// Test: any marquee corner inside the unrotated frame bounds?
|
|
4483
|
+
for (const corner of marqueeCorners) {
|
|
4484
|
+
const local = rotatePointAroundCenter(corner, center, degrees);
|
|
4485
|
+
if (rectContainsPoint(bounds, local)) return true;
|
|
4486
|
+
}
|
|
4487
|
+
// Four corners of the unrotated frame in canvas space (rotate them outward).
|
|
4488
|
+
const frameCorners: Point[] = [
|
|
4489
|
+
{ x: bounds.left, y: bounds.top },
|
|
4490
|
+
{ x: bounds.right, y: bounds.top },
|
|
4491
|
+
{ x: bounds.right, y: bounds.bottom },
|
|
4492
|
+
{ x: bounds.left, y: bounds.bottom },
|
|
4493
|
+
];
|
|
4494
|
+
const rad = (degrees * Math.PI) / 180;
|
|
4495
|
+
const cos = Math.cos(rad);
|
|
4496
|
+
const sin = Math.sin(rad);
|
|
4497
|
+
const marqueeRight = rect.x + rect.width;
|
|
4498
|
+
const marqueeBottom = rect.y + rect.height;
|
|
4499
|
+
// Test: any rotated frame corner inside the marquee rect?
|
|
4500
|
+
for (const fc of frameCorners) {
|
|
4501
|
+
const dx = fc.x - center.x;
|
|
4502
|
+
const dy = fc.y - center.y;
|
|
4503
|
+
const wx = center.x + dx * cos - dy * sin;
|
|
4504
|
+
const wy = center.y + dx * sin + dy * cos;
|
|
4505
|
+
if (
|
|
4506
|
+
wx >= rect.x &&
|
|
4507
|
+
wx <= marqueeRight &&
|
|
4508
|
+
wy >= rect.y &&
|
|
4509
|
+
wy <= marqueeBottom
|
|
4510
|
+
) {
|
|
4511
|
+
return true;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
return false;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
3574
4517
|
function sameFrameGeometry(a: FrameGeometry, b: FrameGeometry) {
|
|
3575
4518
|
return (
|
|
3576
4519
|
a.x === b.x &&
|
|
@@ -3582,6 +4525,20 @@ function sameFrameGeometry(a: FrameGeometry, b: FrameGeometry) {
|
|
|
3582
4525
|
);
|
|
3583
4526
|
}
|
|
3584
4527
|
|
|
4528
|
+
function sameResolvedMetadata(
|
|
4529
|
+
a: ResolvedScreenMetadata,
|
|
4530
|
+
b: ResolvedScreenMetadata,
|
|
4531
|
+
) {
|
|
4532
|
+
return (
|
|
4533
|
+
a.source === b.source &&
|
|
4534
|
+
a.previewState === b.previewState &&
|
|
4535
|
+
a.title === b.title &&
|
|
4536
|
+
a.width === b.width &&
|
|
4537
|
+
a.height === b.height &&
|
|
4538
|
+
a.previewUrl === b.previewUrl
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
|
|
3585
4542
|
function cloneFrameGeometryById(
|
|
3586
4543
|
geometryById: FrameGeometryById,
|
|
3587
4544
|
): FrameGeometryById {
|
|
@@ -4009,6 +4966,7 @@ function resolveScreenMetadata(
|
|
|
4009
4966
|
screen: ScreenFile,
|
|
4010
4967
|
keyedMetadata?: ScreenMetadata,
|
|
4011
4968
|
getterMetadata?: ScreenMetadata,
|
|
4969
|
+
previewDeviceFrame: DeviceFrameType = "none",
|
|
4012
4970
|
): ResolvedScreenMetadata {
|
|
4013
4971
|
const metadata = { ...screen, ...keyedMetadata, ...getterMetadata };
|
|
4014
4972
|
const previewUrl =
|
|
@@ -4016,9 +4974,16 @@ function resolveScreenMetadata(
|
|
|
4016
4974
|
metadata.previewUrl ??
|
|
4017
4975
|
screen.previewUrl ??
|
|
4018
4976
|
getPreviewUrl(screen.content);
|
|
4019
|
-
const
|
|
4977
|
+
const deviceViewport =
|
|
4978
|
+
previewDeviceFrame === "none"
|
|
4979
|
+
? undefined
|
|
4980
|
+
: DEVICE_FRAME_VIEWPORTS[previewDeviceFrame];
|
|
4981
|
+
const width =
|
|
4982
|
+
deviceViewport?.width ??
|
|
4983
|
+
(metadata.width && metadata.width > 0 ? metadata.width : 1280);
|
|
4020
4984
|
const height =
|
|
4021
|
-
|
|
4985
|
+
deviceViewport?.height ??
|
|
4986
|
+
(metadata.height && metadata.height > 0 ? metadata.height : 2560);
|
|
4022
4987
|
return {
|
|
4023
4988
|
source:
|
|
4024
4989
|
normalizeSource(metadata.sourceType ?? metadata.source) ??
|
|
@@ -4116,3 +5081,7 @@ function getUrl(value: string | undefined) {
|
|
|
4116
5081
|
return undefined;
|
|
4117
5082
|
}
|
|
4118
5083
|
}
|
|
5084
|
+
|
|
5085
|
+
function clampNumber(value: number, min: number, max: number) {
|
|
5086
|
+
return Math.min(max, Math.max(min, value));
|
|
5087
|
+
}
|