@agent-native/core 0.80.8 → 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 +12 -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/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/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/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 +20 -12
- 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/export-coding-handoff.ts +12 -4
- package/corpus/templates/design/actions/generate-design.ts +6 -21
- package/corpus/templates/design/actions/generate-screens.ts +43 -2
- 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/view-screen.ts +75 -31
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +329 -69
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +479 -77
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +380 -216
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1050 -196
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
- package/corpus/templates/design/app/components/design/index.ts +0 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +225 -133
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +122 -11
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
- 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 +12 -5
- 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/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 +3 -0
- package/corpus/templates/design/app/i18n-data.ts +45 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2602 -657
- package/corpus/templates/design/app/root.tsx +1 -1
- 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-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-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-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-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-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-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 +3 -3
- package/corpus/templates/design/e2e/global-setup.ts +11 -22
- package/corpus/templates/design/e2e/helpers.ts +122 -11
- package/corpus/templates/design/playwright.config.ts +9 -3
- 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/generation-session.ts +41 -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/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/file-upload/actions/upload-image.d.ts +2 -2
- 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/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- 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/agent-engine-api-key-route.d.ts +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 -342
|
@@ -34,10 +34,12 @@ import {
|
|
|
34
34
|
} from "@shared/pen-path";
|
|
35
35
|
import { IconCopy, IconMaximize } from "@tabler/icons-react";
|
|
36
36
|
import {
|
|
37
|
+
memo,
|
|
37
38
|
useRef,
|
|
38
39
|
useState,
|
|
39
40
|
useCallback,
|
|
40
41
|
useEffect,
|
|
42
|
+
useMemo,
|
|
41
43
|
type CSSProperties,
|
|
42
44
|
type ReactNode,
|
|
43
45
|
} from "react";
|
|
@@ -45,6 +47,8 @@ import {
|
|
|
45
47
|
import { prettyScreenName } from "@/lib/screen-names";
|
|
46
48
|
import { cn } from "@/lib/utils";
|
|
47
49
|
|
|
50
|
+
import { DEVICE_FRAME_VIEWPORTS, type DeviceFrameType } from "./types";
|
|
51
|
+
|
|
48
52
|
interface ScreenFile {
|
|
49
53
|
id: string;
|
|
50
54
|
filename: string;
|
|
@@ -55,6 +59,7 @@ interface ScreenFile {
|
|
|
55
59
|
previewState?: string;
|
|
56
60
|
status?: string;
|
|
57
61
|
title?: string;
|
|
62
|
+
updatedAt?: string;
|
|
58
63
|
width?: number;
|
|
59
64
|
height?: number;
|
|
60
65
|
url?: string;
|
|
@@ -129,6 +134,11 @@ interface MultiScreenCanvasProps {
|
|
|
129
134
|
screens: ScreenFile[];
|
|
130
135
|
zoom: number;
|
|
131
136
|
activeId?: string | null;
|
|
137
|
+
selectedScreenIds?: string[];
|
|
138
|
+
activeScreenHasHoveredChild?: boolean;
|
|
139
|
+
hoveredChildScreenId?: string | null;
|
|
140
|
+
directlyHoveredScreenId?: string | null;
|
|
141
|
+
previewDeviceFrame?: DeviceFrameType;
|
|
132
142
|
activeTool?: MultiScreenCanvasTool;
|
|
133
143
|
toolProps?: CanvasToolProps;
|
|
134
144
|
onActiveToolChange?: (tool: MultiScreenCanvasTool) => void;
|
|
@@ -147,16 +157,16 @@ interface MultiScreenCanvasProps {
|
|
|
147
157
|
screenId: string,
|
|
148
158
|
primitive: CanvasPrimitiveInsert,
|
|
149
159
|
) => boolean | string;
|
|
160
|
+
onPrimitiveCreated?: (screenId: string, nodeId: string) => void;
|
|
150
161
|
onCreateScreenFrame?: (geometry: FrameGeometry) => void;
|
|
151
162
|
onDeleteSelection?: (ids: string[]) => boolean | void;
|
|
152
163
|
onZoomChange?: (zoom: number) => void;
|
|
153
|
-
onZoomToEdit?: (id: string) => void;
|
|
154
|
-
zoomToEditThreshold?: number;
|
|
155
164
|
renderScreenContent?: (
|
|
156
165
|
screen: ScreenFile,
|
|
157
166
|
metadata: ResolvedScreenMetadata,
|
|
158
167
|
geometry: FrameGeometry,
|
|
159
168
|
) => ReactNode;
|
|
169
|
+
onScreenSelectionChange?: (ids: string[]) => void;
|
|
160
170
|
selectAllRequest?: number;
|
|
161
171
|
clearSelectionRequest?: number;
|
|
162
172
|
onSelectionChange?: (selectedIds: string[]) => void;
|
|
@@ -166,7 +176,8 @@ interface MultiScreenCanvasProps {
|
|
|
166
176
|
* design-editor overview canvas. Renders every file in the design as a movable,
|
|
167
177
|
* resizable frame inside an infinite, pannable surface.
|
|
168
178
|
*/
|
|
169
|
-
const
|
|
179
|
+
export const OVERVIEW_FRAME_WIDTH = 320;
|
|
180
|
+
const SCREEN_WIDTH = OVERVIEW_FRAME_WIDTH;
|
|
170
181
|
const SCREEN_HEIGHT = 640;
|
|
171
182
|
const SCREEN_CARD_HEIGHT = SCREEN_HEIGHT + 26;
|
|
172
183
|
const SCREEN_GAP = 56;
|
|
@@ -174,12 +185,70 @@ const SURFACE_PADDING = 240;
|
|
|
174
185
|
const DUPLICATE_DRAG_THRESHOLD = 6;
|
|
175
186
|
const DRAG_THRESHOLD = 3;
|
|
176
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;
|
|
177
199
|
const MIN_ZOOM = 2;
|
|
178
200
|
const MAX_ZOOM = 800;
|
|
179
|
-
const ZOOM_SENSITIVITY = 0.
|
|
180
|
-
const MAX_WHEEL_ZOOM_DELTA =
|
|
201
|
+
const ZOOM_SENSITIVITY = 0.01;
|
|
202
|
+
const MAX_WHEEL_ZOOM_DELTA = 120;
|
|
181
203
|
const MAX_WHEEL_PAN_DELTA = 140;
|
|
182
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
|
+
}
|
|
183
252
|
const DRAFT_FRAME_WIDTH = 320;
|
|
184
253
|
const DRAFT_FRAME_HEIGHT = 640;
|
|
185
254
|
const DRAFT_RECT_WIDTH = 160;
|
|
@@ -394,10 +463,29 @@ type DragState =
|
|
|
394
463
|
| DraftCreateDragState
|
|
395
464
|
| PenNodeDragState;
|
|
396
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
|
+
|
|
397
480
|
export function MultiScreenCanvas({
|
|
398
481
|
screens,
|
|
399
482
|
zoom,
|
|
400
483
|
activeId,
|
|
484
|
+
selectedScreenIds,
|
|
485
|
+
activeScreenHasHoveredChild = false,
|
|
486
|
+
hoveredChildScreenId,
|
|
487
|
+
directlyHoveredScreenId,
|
|
488
|
+
previewDeviceFrame = "none",
|
|
401
489
|
activeTool,
|
|
402
490
|
toolProps,
|
|
403
491
|
onActiveToolChange,
|
|
@@ -410,12 +498,12 @@ export function MultiScreenCanvas({
|
|
|
410
498
|
onGeometryChange,
|
|
411
499
|
onGeometryCommit,
|
|
412
500
|
onCreatePrimitive,
|
|
501
|
+
onPrimitiveCreated,
|
|
413
502
|
onCreateScreenFrame,
|
|
414
503
|
onDeleteSelection,
|
|
415
504
|
onZoomChange,
|
|
416
|
-
onZoomToEdit,
|
|
417
|
-
zoomToEditThreshold,
|
|
418
505
|
renderScreenContent,
|
|
506
|
+
onScreenSelectionChange,
|
|
419
507
|
selectAllRequest,
|
|
420
508
|
clearSelectionRequest,
|
|
421
509
|
onSelectionChange,
|
|
@@ -446,7 +534,7 @@ export function MultiScreenCanvas({
|
|
|
446
534
|
const [localActiveTool, setLocalActiveTool] =
|
|
447
535
|
useState<MultiScreenCanvasTool>("move");
|
|
448
536
|
const [selectedIds, setSelectedIds] = useState<string[]>(
|
|
449
|
-
|
|
537
|
+
selectedScreenIds ?? [],
|
|
450
538
|
);
|
|
451
539
|
const selectedIdsRef = useRef(selectedIds);
|
|
452
540
|
const dragState = useRef<DragState | null>(null);
|
|
@@ -466,6 +554,26 @@ export function MultiScreenCanvas({
|
|
|
466
554
|
const [dragCursor, setDragCursor] = useState<string | null>(null);
|
|
467
555
|
const suppressNextPick = useRef(false);
|
|
468
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
|
+
);
|
|
469
577
|
|
|
470
578
|
useEffect(() => {
|
|
471
579
|
onGeometryChangeRef.current = onGeometryChange;
|
|
@@ -559,6 +667,10 @@ export function MultiScreenCanvas({
|
|
|
559
667
|
onSelectionChangeRef.current?.(selectedIds);
|
|
560
668
|
}, [selectedIds]);
|
|
561
669
|
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
onScreenSelectionChange?.(selectedIds);
|
|
672
|
+
}, [onScreenSelectionChange, selectedIds]);
|
|
673
|
+
|
|
562
674
|
useEffect(() => {
|
|
563
675
|
draftPrimitivesRef.current = draftPrimitives;
|
|
564
676
|
}, [draftPrimitives]);
|
|
@@ -581,8 +693,9 @@ export function MultiScreenCanvas({
|
|
|
581
693
|
screens.forEach((screen, index) => {
|
|
582
694
|
const existing = current[screen.id];
|
|
583
695
|
const persisted = geometryById?.[screen.id];
|
|
696
|
+
const metadata = getResolvedMetadata(screen);
|
|
584
697
|
const resolved = {
|
|
585
|
-
...getInitialFrameGeometry(index),
|
|
698
|
+
...getInitialFrameGeometry(index, metadata),
|
|
586
699
|
...persisted,
|
|
587
700
|
} as FrameGeometry;
|
|
588
701
|
next[screen.id] = persisted ? resolved : (existing ?? resolved);
|
|
@@ -600,14 +713,47 @@ export function MultiScreenCanvas({
|
|
|
600
713
|
const next = current.filter((id) => currentIds.has(id));
|
|
601
714
|
return next.length === current.length ? current : next;
|
|
602
715
|
});
|
|
603
|
-
}, [
|
|
716
|
+
}, [
|
|
717
|
+
geometryById,
|
|
718
|
+
getResolvedMetadata,
|
|
719
|
+
screens,
|
|
720
|
+
updateFrameGeometry,
|
|
721
|
+
updateSelectedIds,
|
|
722
|
+
]);
|
|
604
723
|
|
|
605
724
|
useEffect(() => {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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]);
|
|
752
|
+
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
if (!selectedScreenIds) return;
|
|
755
|
+
updateSelectedIds(() => selectedScreenIds);
|
|
756
|
+
}, [screens, selectedScreenIds, updateSelectedIds]);
|
|
611
757
|
|
|
612
758
|
useEffect(() => {
|
|
613
759
|
if (
|
|
@@ -640,11 +786,18 @@ export function MultiScreenCanvas({
|
|
|
640
786
|
useEffect(() => {
|
|
641
787
|
if (!surfaceRef.current || screens.length === 0) return;
|
|
642
788
|
const rect = surfaceRef.current.getBoundingClientRect();
|
|
643
|
-
const columns = Math.min(screens.length, 3);
|
|
644
|
-
const rows = Math.ceil(screens.length / columns);
|
|
645
789
|
const scale = zoomRef.current / 100;
|
|
646
|
-
const
|
|
647
|
-
|
|
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;
|
|
648
801
|
const visualLeft = Math.max(24, (rect.width - totalWidth * scale) / 2);
|
|
649
802
|
const visualTop = Math.max(24, (rect.height - totalHeight * scale) / 2);
|
|
650
803
|
const nextPan = {
|
|
@@ -653,9 +806,9 @@ export function MultiScreenCanvas({
|
|
|
653
806
|
};
|
|
654
807
|
panRef.current = nextPan;
|
|
655
808
|
setPan(nextPan);
|
|
656
|
-
// Only on mount
|
|
809
|
+
// Only on mount, screen-count changes, or device-preview changes.
|
|
657
810
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
658
|
-
}, [screens.length]);
|
|
811
|
+
}, [previewDeviceFrame, screens.length]);
|
|
659
812
|
|
|
660
813
|
useEffect(() => {
|
|
661
814
|
return () => {
|
|
@@ -664,6 +817,15 @@ export function MultiScreenCanvas({
|
|
|
664
817
|
if (feedbackTimerRef.current !== null) {
|
|
665
818
|
window.clearTimeout(feedbackTimerRef.current);
|
|
666
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
|
+
}
|
|
667
829
|
};
|
|
668
830
|
}, []);
|
|
669
831
|
|
|
@@ -694,8 +856,17 @@ export function MultiScreenCanvas({
|
|
|
694
856
|
}, []);
|
|
695
857
|
|
|
696
858
|
const getCurrentFrameEntries = useCallback(
|
|
697
|
-
() =>
|
|
698
|
-
|
|
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],
|
|
699
870
|
);
|
|
700
871
|
|
|
701
872
|
const getCurrentDraftEntries = useCallback(
|
|
@@ -737,14 +908,6 @@ export function MultiScreenCanvas({
|
|
|
737
908
|
[getCurrentFrameEntries],
|
|
738
909
|
);
|
|
739
910
|
|
|
740
|
-
const getFrameAtClientPoint = useCallback(
|
|
741
|
-
(clientX: number, clientY: number) => {
|
|
742
|
-
const point = canvasPointFromClient(clientX, clientY);
|
|
743
|
-
return getFrameEntryAtPoint(point)?.id;
|
|
744
|
-
},
|
|
745
|
-
[canvasPointFromClient, getFrameEntryAtPoint],
|
|
746
|
-
);
|
|
747
|
-
|
|
748
911
|
const deleteSelectedItems = useCallback(() => {
|
|
749
912
|
const frameIds = selectedIdsRef.current.filter(
|
|
750
913
|
(id) => frameGeometryRef.current[id],
|
|
@@ -797,6 +960,9 @@ export function MultiScreenCanvas({
|
|
|
797
960
|
handleMouseUp: (ev: MouseEvent) => void,
|
|
798
961
|
) => {
|
|
799
962
|
dragCleanup.current?.();
|
|
963
|
+
const restorePreviewPointerEvents = mutePreviewIframePointerEvents(
|
|
964
|
+
surfaceRef.current,
|
|
965
|
+
);
|
|
800
966
|
let lastMouseEvent: MouseEvent | null = null;
|
|
801
967
|
const move = (ev: MouseEvent) => {
|
|
802
968
|
lastMouseEvent = ev;
|
|
@@ -814,6 +980,7 @@ export function MultiScreenCanvas({
|
|
|
814
980
|
window.removeEventListener("mousemove", move);
|
|
815
981
|
window.removeEventListener("mouseup", up);
|
|
816
982
|
window.removeEventListener("blur", cleanupOnBlur);
|
|
983
|
+
restorePreviewPointerEvents();
|
|
817
984
|
dragCleanup.current = null;
|
|
818
985
|
};
|
|
819
986
|
window.addEventListener("mousemove", move);
|
|
@@ -836,10 +1003,34 @@ export function MultiScreenCanvas({
|
|
|
836
1003
|
|
|
837
1004
|
const showTransformFeedback = useCallback(
|
|
838
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;
|
|
839
1030
|
setTransformBadge({
|
|
840
1031
|
text,
|
|
841
|
-
x:
|
|
842
|
-
y:
|
|
1032
|
+
x: clampNumber(preferredX, TRANSFORM_BADGE_EDGE_PADDING, maxX),
|
|
1033
|
+
y: clampNumber(preferredY, TRANSFORM_BADGE_EDGE_PADDING, maxY),
|
|
843
1034
|
});
|
|
844
1035
|
},
|
|
845
1036
|
[],
|
|
@@ -861,6 +1052,74 @@ export function MultiScreenCanvas({
|
|
|
861
1052
|
dragCleanup.current?.();
|
|
862
1053
|
}, []);
|
|
863
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
|
+
|
|
864
1123
|
const beginPan = useCallback(
|
|
865
1124
|
(e: React.MouseEvent) => {
|
|
866
1125
|
e.preventDefault();
|
|
@@ -987,7 +1246,10 @@ export function MultiScreenCanvas({
|
|
|
987
1246
|
: undefined;
|
|
988
1247
|
if (preferred) return preferred;
|
|
989
1248
|
|
|
990
|
-
|
|
1249
|
+
const draftCenter = getFrameCenter(draft.geometry);
|
|
1250
|
+
|
|
1251
|
+
// Primary: find the frame whose bounds contain the draft's center point.
|
|
1252
|
+
const containing = entries
|
|
991
1253
|
.filter(({ geometry }) => {
|
|
992
1254
|
const bounds = {
|
|
993
1255
|
left: geometry.x,
|
|
@@ -995,7 +1257,6 @@ export function MultiScreenCanvas({
|
|
|
995
1257
|
right: geometry.x + geometry.width,
|
|
996
1258
|
bottom: geometry.y + geometry.height,
|
|
997
1259
|
};
|
|
998
|
-
const draftCenter = getFrameCenter(draft.geometry);
|
|
999
1260
|
const local = rotatePointAroundCenter(
|
|
1000
1261
|
draftCenter,
|
|
1001
1262
|
getFrameCenter(geometry),
|
|
@@ -1004,6 +1265,22 @@ export function MultiScreenCanvas({
|
|
|
1004
1265
|
return rectContainsPoint(bounds, local);
|
|
1005
1266
|
})
|
|
1006
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];
|
|
1007
1284
|
},
|
|
1008
1285
|
[getCurrentFrameEntries],
|
|
1009
1286
|
);
|
|
@@ -1057,7 +1334,8 @@ export function MultiScreenCanvas({
|
|
|
1057
1334
|
current.filter((draft) => draft.id !== nextDraft.id),
|
|
1058
1335
|
);
|
|
1059
1336
|
updateSelectedDraftIds(() => []);
|
|
1060
|
-
updateSelectedIds(() => [
|
|
1337
|
+
updateSelectedIds(() => []);
|
|
1338
|
+
onPrimitiveCreated?.(persisted.frameId, persisted.nodeId);
|
|
1061
1339
|
return;
|
|
1062
1340
|
}
|
|
1063
1341
|
|
|
@@ -1067,6 +1345,7 @@ export function MultiScreenCanvas({
|
|
|
1067
1345
|
},
|
|
1068
1346
|
[
|
|
1069
1347
|
persistDraftPrimitive,
|
|
1348
|
+
onPrimitiveCreated,
|
|
1070
1349
|
updateDraftPrimitives,
|
|
1071
1350
|
updateSelectedDraftIds,
|
|
1072
1351
|
updateSelectedIds,
|
|
@@ -1708,9 +1987,11 @@ export function MultiScreenCanvas({
|
|
|
1708
1987
|
const targetIds = currentSelectedIds.includes(id)
|
|
1709
1988
|
? currentSelectedIds
|
|
1710
1989
|
: [id];
|
|
1990
|
+
if (activeId !== id) {
|
|
1991
|
+
onPick(id);
|
|
1992
|
+
}
|
|
1711
1993
|
if (!currentSelectedIds.includes(id)) {
|
|
1712
1994
|
updateSelectedIds(() => [id]);
|
|
1713
|
-
onPick(id);
|
|
1714
1995
|
}
|
|
1715
1996
|
updateSelectedDraftIds(() => []);
|
|
1716
1997
|
|
|
@@ -1805,6 +2086,7 @@ export function MultiScreenCanvas({
|
|
|
1805
2086
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
1806
2087
|
},
|
|
1807
2088
|
[
|
|
2089
|
+
activeId,
|
|
1808
2090
|
finishDrag,
|
|
1809
2091
|
getCurrentFrameEntries,
|
|
1810
2092
|
installDragListeners,
|
|
@@ -1823,6 +2105,10 @@ export function MultiScreenCanvas({
|
|
|
1823
2105
|
e.stopPropagation();
|
|
1824
2106
|
suppressNextPick.current = true;
|
|
1825
2107
|
|
|
2108
|
+
if (activeId !== id) {
|
|
2109
|
+
onPick(id);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1826
2112
|
const currentSelectedIds = selectedIdsRef.current;
|
|
1827
2113
|
const targetIds = currentSelectedIds.includes(id)
|
|
1828
2114
|
? currentSelectedIds
|
|
@@ -1931,9 +2217,11 @@ export function MultiScreenCanvas({
|
|
|
1931
2217
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
1932
2218
|
},
|
|
1933
2219
|
[
|
|
2220
|
+
activeId,
|
|
1934
2221
|
finishDrag,
|
|
1935
2222
|
getCurrentFrameEntries,
|
|
1936
2223
|
installDragListeners,
|
|
2224
|
+
onPick,
|
|
1937
2225
|
showTransformFeedback,
|
|
1938
2226
|
updateFrameGeometry,
|
|
1939
2227
|
updateSelectedIds,
|
|
@@ -1956,6 +2244,10 @@ export function MultiScreenCanvas({
|
|
|
1956
2244
|
e.stopPropagation();
|
|
1957
2245
|
suppressNextPick.current = true;
|
|
1958
2246
|
|
|
2247
|
+
if (activeId !== id) {
|
|
2248
|
+
onPick(id);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
1959
2251
|
const originFrame = getCurrentFrameEntries().find(
|
|
1960
2252
|
(entry) => entry.id === id,
|
|
1961
2253
|
)?.geometry;
|
|
@@ -2027,10 +2319,12 @@ export function MultiScreenCanvas({
|
|
|
2027
2319
|
installDragListeners(handleMouseMove, handleMouseUp);
|
|
2028
2320
|
},
|
|
2029
2321
|
[
|
|
2322
|
+
activeId,
|
|
2030
2323
|
finishDrag,
|
|
2031
2324
|
getCanvasPoint,
|
|
2032
2325
|
getCurrentFrameEntries,
|
|
2033
2326
|
installDragListeners,
|
|
2327
|
+
onPick,
|
|
2034
2328
|
showTransformFeedback,
|
|
2035
2329
|
updateFrameGeometry,
|
|
2036
2330
|
updateSelectedIds,
|
|
@@ -2047,11 +2341,20 @@ export function MultiScreenCanvas({
|
|
|
2047
2341
|
|
|
2048
2342
|
if (e.shiftKey) {
|
|
2049
2343
|
updateSelectedDraftIds(() => []);
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
);
|
|
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
|
+
}
|
|
2055
2358
|
return;
|
|
2056
2359
|
}
|
|
2057
2360
|
|
|
@@ -2059,7 +2362,7 @@ export function MultiScreenCanvas({
|
|
|
2059
2362
|
updateSelectedIds(() => [id]);
|
|
2060
2363
|
onPick(id);
|
|
2061
2364
|
},
|
|
2062
|
-
[onPick, updateSelectedDraftIds, updateSelectedIds],
|
|
2365
|
+
[activeId, onPick, updateSelectedDraftIds, updateSelectedIds],
|
|
2063
2366
|
);
|
|
2064
2367
|
|
|
2065
2368
|
const handleFrameDoubleClick = useCallback(
|
|
@@ -2232,6 +2535,136 @@ export function MultiScreenCanvas({
|
|
|
2232
2535
|
[activeTool, localActiveTool, updatePenPointer],
|
|
2233
2536
|
);
|
|
2234
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
|
+
|
|
2235
2668
|
const handleWheelEvent = useCallback(
|
|
2236
2669
|
(event: WheelEvent) => {
|
|
2237
2670
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
@@ -2241,40 +2674,18 @@ export function MultiScreenCanvas({
|
|
|
2241
2674
|
const delta = getWheelDelta(event);
|
|
2242
2675
|
|
|
2243
2676
|
if (event.ctrlKey || event.metaKey) {
|
|
2244
|
-
const currentZoom = zoomRef.current;
|
|
2245
2677
|
const zoomDeltaY = clamp(
|
|
2246
2678
|
delta.y,
|
|
2247
2679
|
-MAX_WHEEL_ZOOM_DELTA,
|
|
2248
2680
|
MAX_WHEEL_ZOOM_DELTA,
|
|
2249
2681
|
);
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
MAX_ZOOM,
|
|
2254
|
-
);
|
|
2255
|
-
if (nextZoom === currentZoom) return;
|
|
2256
|
-
|
|
2257
|
-
const nextPan = getPanForZoomToCursor({
|
|
2258
|
-
pan: panRef.current,
|
|
2682
|
+
enqueueWheelGesture({
|
|
2683
|
+
mode: "zoom",
|
|
2684
|
+
deltaY: zoomDeltaY,
|
|
2259
2685
|
cursor: { x: event.clientX - rect.left, y: event.clientY - rect.top },
|
|
2260
|
-
|
|
2261
|
-
|
|
2686
|
+
clientX: event.clientX,
|
|
2687
|
+
clientY: event.clientY,
|
|
2262
2688
|
});
|
|
2263
|
-
const zoomEditFrameId =
|
|
2264
|
-
zoomToEditThreshold !== undefined &&
|
|
2265
|
-
currentZoom < zoomToEditThreshold &&
|
|
2266
|
-
nextZoom >= zoomToEditThreshold
|
|
2267
|
-
? getFrameAtClientPoint(event.clientX, event.clientY)
|
|
2268
|
-
: undefined;
|
|
2269
|
-
|
|
2270
|
-
zoomRef.current = nextZoom;
|
|
2271
|
-
panRef.current = nextPan;
|
|
2272
|
-
setCanvasZoom(nextZoom);
|
|
2273
|
-
setPan(nextPan);
|
|
2274
|
-
onZoomChange?.(nextZoom);
|
|
2275
|
-
if (zoomEditFrameId) {
|
|
2276
|
-
onZoomToEdit?.(zoomEditFrameId);
|
|
2277
|
-
}
|
|
2278
2689
|
return;
|
|
2279
2690
|
}
|
|
2280
2691
|
|
|
@@ -2288,14 +2699,9 @@ export function MultiScreenCanvas({
|
|
|
2288
2699
|
-MAX_WHEEL_PAN_DELTA,
|
|
2289
2700
|
MAX_WHEEL_PAN_DELTA,
|
|
2290
2701
|
);
|
|
2291
|
-
|
|
2292
|
-
x: panRef.current.x - deltaX,
|
|
2293
|
-
y: panRef.current.y - deltaY,
|
|
2294
|
-
};
|
|
2295
|
-
panRef.current = nextPan;
|
|
2296
|
-
setPan(nextPan);
|
|
2702
|
+
enqueueWheelGesture({ mode: "pan", deltaX, deltaY });
|
|
2297
2703
|
},
|
|
2298
|
-
[
|
|
2704
|
+
[enqueueWheelGesture],
|
|
2299
2705
|
);
|
|
2300
2706
|
|
|
2301
2707
|
useEffect(() => {
|
|
@@ -2490,12 +2896,59 @@ export function MultiScreenCanvas({
|
|
|
2490
2896
|
if (tool !== "pen") clearActivePenPath();
|
|
2491
2897
|
}, [activeTool, clearActivePenPath, localActiveTool]);
|
|
2492
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
|
+
|
|
2493
2945
|
const scale = canvasZoom / 100;
|
|
2494
2946
|
const chromeScale = scale > 0 ? 1 / scale : 1;
|
|
2495
2947
|
const showPixelGrid = canvasZoom >= PIXEL_GRID_ZOOM;
|
|
2496
2948
|
const effectiveTool = normalizeCanvasTool(activeTool ?? localActiveTool);
|
|
2497
2949
|
const penActive = effectiveTool === "pen";
|
|
2498
2950
|
const creationToolActive = Boolean(getDraftCreationTool(effectiveTool));
|
|
2951
|
+
const canvasGestureActive = isDragging || isPanning;
|
|
2499
2952
|
const displayedPenPath = penGesturePreview
|
|
2500
2953
|
? penGesturePreview
|
|
2501
2954
|
: activePenPath && penPointer && activePenPath.nodes.length > 0
|
|
@@ -2516,10 +2969,29 @@ export function MultiScreenCanvas({
|
|
|
2516
2969
|
: effectiveTool === "hand"
|
|
2517
2970
|
? "grab"
|
|
2518
2971
|
: "default";
|
|
2519
|
-
const canvasFrames = screens.map((screen, index) =>
|
|
2520
|
-
screen
|
|
2521
|
-
|
|
2522
|
-
|
|
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]);
|
|
2523
2995
|
const selectedFrameEntries = canvasFrames
|
|
2524
2996
|
.filter(({ screen }) => selectedIdSet.has(screen.id))
|
|
2525
2997
|
.map(({ screen, geometry }) => ({ id: screen.id, geometry }));
|
|
@@ -2535,16 +3007,25 @@ export function MultiScreenCanvas({
|
|
|
2535
3007
|
selectedDraftEntries.length > 1
|
|
2536
3008
|
? getFrameGroupBounds(selectedDraftEntries)
|
|
2537
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;
|
|
2538
3018
|
return (
|
|
2539
3019
|
<div
|
|
2540
3020
|
ref={surfaceRef}
|
|
2541
|
-
className="relative h-full w-full select-none overflow-hidden
|
|
3021
|
+
className="relative h-full w-full select-none overflow-hidden"
|
|
2542
3022
|
onMouseDown={handleMouseDown}
|
|
2543
3023
|
onMouseMove={handleMouseMove}
|
|
2544
3024
|
style={{ cursor: surfaceCursor, touchAction: "none" }}
|
|
2545
3025
|
>
|
|
2546
3026
|
{showPixelGrid ? (
|
|
2547
3027
|
<div
|
|
3028
|
+
ref={pixelGridRef}
|
|
2548
3029
|
className="pointer-events-none absolute inset-0 opacity-60"
|
|
2549
3030
|
style={{
|
|
2550
3031
|
backgroundImage:
|
|
@@ -2556,34 +3037,44 @@ export function MultiScreenCanvas({
|
|
|
2556
3037
|
) : null}
|
|
2557
3038
|
|
|
2558
3039
|
<div
|
|
3040
|
+
ref={worldRef}
|
|
2559
3041
|
className="pointer-events-none absolute"
|
|
2560
3042
|
style={{
|
|
2561
|
-
left:
|
|
2562
|
-
top:
|
|
2563
|
-
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})`,
|
|
2564
3053
|
transformOrigin: "top left",
|
|
2565
3054
|
}}
|
|
2566
3055
|
>
|
|
2567
|
-
{canvasFrames.map(({ screen, geometry }) => {
|
|
2568
|
-
const metadata = resolveScreenMetadata(
|
|
2569
|
-
screen,
|
|
2570
|
-
metadataById?.[screen.id],
|
|
2571
|
-
getScreenMetadata?.(screen),
|
|
2572
|
-
);
|
|
3056
|
+
{canvasFrames.map(({ screen, metadata, geometry }) => {
|
|
2573
3057
|
return (
|
|
2574
3058
|
<Screen
|
|
2575
3059
|
key={screen.id}
|
|
2576
3060
|
screen={screen}
|
|
2577
3061
|
metadata={metadata}
|
|
2578
3062
|
geometry={geometry}
|
|
2579
|
-
screenContent={
|
|
3063
|
+
screenContent={screenContentById.get(screen.id)}
|
|
2580
3064
|
isActive={screen.id === activeId}
|
|
2581
3065
|
isSelected={selectedIdSet.has(screen.id)}
|
|
3066
|
+
isDirectlyHovered={screen.id === directlyHoveredScreenId}
|
|
3067
|
+
hasHoveredChild={
|
|
3068
|
+
(screen.id === activeId && activeScreenHasHoveredChild) ||
|
|
3069
|
+
screen.id === hoveredChildScreenId
|
|
3070
|
+
}
|
|
2582
3071
|
groupSelected={hasGroupSelection}
|
|
2583
3072
|
handlesEnabled={!hasGroupSelection}
|
|
2584
3073
|
penActive={penActive}
|
|
2585
3074
|
creationToolActive={creationToolActive}
|
|
3075
|
+
canvasGestureActive={canvasGestureActive}
|
|
2586
3076
|
chromeScale={chromeScale}
|
|
3077
|
+
chromeSettling={chromeSettling}
|
|
2587
3078
|
onPick={handleFrameClick}
|
|
2588
3079
|
onEdit={handleFrameDoubleClick}
|
|
2589
3080
|
onStartFrameDrag={beginFrameDrag}
|
|
@@ -2602,6 +3093,7 @@ export function MultiScreenCanvas({
|
|
|
2602
3093
|
groupSelected={Boolean(selectedDraftGroupBounds)}
|
|
2603
3094
|
penActive={penActive}
|
|
2604
3095
|
chromeScale={chromeScale}
|
|
3096
|
+
chromeSettling={chromeSettling}
|
|
2605
3097
|
onClick={handleDraftClick}
|
|
2606
3098
|
onStartDrag={beginDraftDrag}
|
|
2607
3099
|
onStartResize={beginDraftResize}
|
|
@@ -2616,6 +3108,7 @@ export function MultiScreenCanvas({
|
|
|
2616
3108
|
groupSelected={false}
|
|
2617
3109
|
penActive={penActive}
|
|
2618
3110
|
chromeScale={chromeScale}
|
|
3111
|
+
chromeSettling={chromeSettling}
|
|
2619
3112
|
onClick={() => {}}
|
|
2620
3113
|
onStartDrag={() => {}}
|
|
2621
3114
|
onStartResize={() => {}}
|
|
@@ -2626,10 +3119,39 @@ export function MultiScreenCanvas({
|
|
|
2626
3119
|
<PenPathOverlay path={displayedPenPath} closeHover={penCloseHover} />
|
|
2627
3120
|
) : null}
|
|
2628
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
|
+
|
|
2629
3150
|
{selectedGroupBounds ? (
|
|
2630
3151
|
<GroupSelectionBox
|
|
2631
3152
|
bounds={selectedGroupBounds}
|
|
2632
3153
|
chromeScale={chromeScale}
|
|
3154
|
+
chromeSettling={chromeSettling}
|
|
2633
3155
|
onStartResize={beginGroupResize}
|
|
2634
3156
|
/>
|
|
2635
3157
|
) : null}
|
|
@@ -2638,6 +3160,7 @@ export function MultiScreenCanvas({
|
|
|
2638
3160
|
<GroupSelectionBox
|
|
2639
3161
|
bounds={selectedDraftGroupBounds}
|
|
2640
3162
|
chromeScale={chromeScale}
|
|
3163
|
+
chromeSettling={chromeSettling}
|
|
2641
3164
|
onStartResize={beginDraftGroupResize}
|
|
2642
3165
|
/>
|
|
2643
3166
|
) : null}
|
|
@@ -2663,20 +3186,23 @@ export function MultiScreenCanvas({
|
|
|
2663
3186
|
}
|
|
2664
3187
|
/>
|
|
2665
3188
|
))}
|
|
2666
|
-
|
|
2667
|
-
{marquee ? (
|
|
2668
|
-
<span
|
|
2669
|
-
className="pointer-events-none absolute z-40 border border-[var(--design-editor-accent-color)] bg-[var(--design-editor-selection-color)]"
|
|
2670
|
-
style={{
|
|
2671
|
-
left: SURFACE_PADDING + marquee.x,
|
|
2672
|
-
top: SURFACE_PADDING + marquee.y,
|
|
2673
|
-
width: marquee.width,
|
|
2674
|
-
height: marquee.height,
|
|
2675
|
-
}}
|
|
2676
|
-
/>
|
|
2677
|
-
) : null}
|
|
2678
3189
|
</div>
|
|
2679
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
|
+
|
|
2680
3206
|
{duplicatePreview ? (
|
|
2681
3207
|
<div
|
|
2682
3208
|
className={cn(
|
|
@@ -2712,7 +3238,8 @@ export function MultiScreenCanvas({
|
|
|
2712
3238
|
|
|
2713
3239
|
{transformBadge ? (
|
|
2714
3240
|
<div
|
|
2715
|
-
|
|
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"
|
|
2716
3243
|
style={{ left: transformBadge.x, top: transformBadge.y }}
|
|
2717
3244
|
>
|
|
2718
3245
|
{transformBadge.text}
|
|
@@ -2728,6 +3255,7 @@ function DraftPrimitiveLayer({
|
|
|
2728
3255
|
groupSelected,
|
|
2729
3256
|
penActive,
|
|
2730
3257
|
chromeScale,
|
|
3258
|
+
chromeSettling,
|
|
2731
3259
|
preview = false,
|
|
2732
3260
|
onClick,
|
|
2733
3261
|
onStartDrag,
|
|
@@ -2738,6 +3266,7 @@ function DraftPrimitiveLayer({
|
|
|
2738
3266
|
groupSelected: boolean;
|
|
2739
3267
|
penActive: boolean;
|
|
2740
3268
|
chromeScale: number;
|
|
3269
|
+
chromeSettling: boolean;
|
|
2741
3270
|
preview?: boolean;
|
|
2742
3271
|
onClick: (id: string, e: React.MouseEvent) => void;
|
|
2743
3272
|
onStartDrag: (id: string, e: React.MouseEvent) => void;
|
|
@@ -2755,7 +3284,7 @@ function DraftPrimitiveLayer({
|
|
|
2755
3284
|
type="button"
|
|
2756
3285
|
className={cn(
|
|
2757
3286
|
"group/artboard pointer-events-auto absolute block overflow-visible text-left outline-none",
|
|
2758
|
-
preview || penActive ? "cursor-crosshair" : "cursor-
|
|
3287
|
+
preview || penActive ? "cursor-crosshair" : "cursor-pointer",
|
|
2759
3288
|
)}
|
|
2760
3289
|
style={{
|
|
2761
3290
|
left: SURFACE_PADDING + geometry.x,
|
|
@@ -2780,20 +3309,24 @@ function DraftPrimitiveLayer({
|
|
|
2780
3309
|
<span
|
|
2781
3310
|
className={cn(
|
|
2782
3311
|
"pointer-events-none absolute rounded-sm border transition-opacity",
|
|
2783
|
-
|
|
3312
|
+
preview
|
|
2784
3313
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
2785
|
-
:
|
|
3314
|
+
: selected
|
|
3315
|
+
? "border-transparent opacity-0"
|
|
3316
|
+
: "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100",
|
|
2786
3317
|
)}
|
|
2787
3318
|
style={{
|
|
2788
3319
|
inset: -5 * chromeScale,
|
|
2789
3320
|
borderWidth: 1.5 * chromeScale,
|
|
3321
|
+
transition: getChromeBorderTransition(chromeSettling),
|
|
2790
3322
|
}}
|
|
2791
3323
|
/>
|
|
2792
3324
|
<ResizeHandles
|
|
2793
|
-
active={
|
|
2794
|
-
enabled={!penActive &&
|
|
3325
|
+
active={preview}
|
|
3326
|
+
enabled={!penActive && preview}
|
|
2795
3327
|
showRotate={false}
|
|
2796
3328
|
chromeScale={chromeScale}
|
|
3329
|
+
chromeSettling={chromeSettling}
|
|
2797
3330
|
onStartResize={(handle, event) =>
|
|
2798
3331
|
onStartResize(draft.id, handle, event)
|
|
2799
3332
|
}
|
|
@@ -3034,35 +3567,21 @@ function isPoint(point: Point | undefined): point is Point {
|
|
|
3034
3567
|
return !!point;
|
|
3035
3568
|
}
|
|
3036
3569
|
|
|
3037
|
-
|
|
3038
|
-
screen,
|
|
3039
|
-
metadata,
|
|
3040
|
-
geometry,
|
|
3041
|
-
isActive,
|
|
3042
|
-
isSelected,
|
|
3043
|
-
groupSelected,
|
|
3044
|
-
handlesEnabled,
|
|
3045
|
-
penActive,
|
|
3046
|
-
creationToolActive,
|
|
3047
|
-
chromeScale,
|
|
3048
|
-
onPick,
|
|
3049
|
-
onEdit,
|
|
3050
|
-
onStartFrameDrag,
|
|
3051
|
-
onStartResize,
|
|
3052
|
-
onStartRotate,
|
|
3053
|
-
onStartDuplicateGesture,
|
|
3054
|
-
screenContent,
|
|
3055
|
-
}: {
|
|
3570
|
+
interface ScreenProps {
|
|
3056
3571
|
screen: ScreenFile;
|
|
3057
3572
|
metadata: ResolvedScreenMetadata;
|
|
3058
3573
|
geometry: FrameGeometry;
|
|
3059
3574
|
isActive: boolean;
|
|
3060
3575
|
isSelected: boolean;
|
|
3576
|
+
isDirectlyHovered: boolean;
|
|
3577
|
+
hasHoveredChild: boolean;
|
|
3061
3578
|
groupSelected: boolean;
|
|
3062
3579
|
handlesEnabled: boolean;
|
|
3063
3580
|
penActive: boolean;
|
|
3064
3581
|
creationToolActive: boolean;
|
|
3582
|
+
canvasGestureActive: boolean;
|
|
3065
3583
|
chromeScale: number;
|
|
3584
|
+
chromeSettling: boolean;
|
|
3066
3585
|
screenContent?: ReactNode;
|
|
3067
3586
|
onPick: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
3068
3587
|
onEdit: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
@@ -3078,15 +3597,75 @@ function Screen({
|
|
|
3078
3597
|
display: string,
|
|
3079
3598
|
e: React.MouseEvent<HTMLElement>,
|
|
3080
3599
|
) => void;
|
|
3081
|
-
}
|
|
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) {
|
|
3082
3625
|
const t = useT();
|
|
3083
3626
|
const display = metadata.title ?? prettyScreenName(screen.filename);
|
|
3084
3627
|
const previewUrl = metadata.previewUrl ?? getPreviewUrl(screen.content);
|
|
3628
|
+
const previewViewport = getScreenPreviewViewport(metadata, geometry);
|
|
3085
3629
|
const suppressNextClick = useRef(false);
|
|
3086
|
-
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;
|
|
3087
3637
|
const selectionOutlined = isSelected && !groupSelected;
|
|
3088
|
-
const
|
|
3089
|
-
|
|
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));
|
|
3090
3669
|
|
|
3091
3670
|
return (
|
|
3092
3671
|
<div
|
|
@@ -3094,17 +3673,32 @@ function Screen({
|
|
|
3094
3673
|
className="group/frame pointer-events-auto absolute"
|
|
3095
3674
|
style={{
|
|
3096
3675
|
left: SURFACE_PADDING + geometry.x,
|
|
3097
|
-
top: SURFACE_PADDING + geometry.y -
|
|
3676
|
+
top: SURFACE_PADDING + geometry.y - frameLabelHeight,
|
|
3098
3677
|
width: geometry.width,
|
|
3099
3678
|
transform: geometry.rotation
|
|
3100
3679
|
? `rotate(${geometry.rotation}deg)`
|
|
3101
3680
|
: undefined,
|
|
3102
|
-
transformOrigin: `${geometry.width / 2}px ${
|
|
3681
|
+
transformOrigin: `${geometry.width / 2}px ${frameLabelHeight + geometry.height / 2}px`,
|
|
3103
3682
|
zIndex: geometry.z,
|
|
3104
3683
|
}}
|
|
3105
3684
|
>
|
|
3106
3685
|
<div
|
|
3107
|
-
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
|
+
}}
|
|
3108
3702
|
onMouseDown={(e) => {
|
|
3109
3703
|
if (e.button !== 0) return;
|
|
3110
3704
|
if (penActive || creationToolActive) return;
|
|
@@ -3114,13 +3708,23 @@ function Screen({
|
|
|
3114
3708
|
}
|
|
3115
3709
|
if (e.shiftKey) {
|
|
3116
3710
|
e.stopPropagation();
|
|
3117
|
-
onPick(screen.id, e);
|
|
3118
3711
|
return;
|
|
3119
3712
|
}
|
|
3120
3713
|
onStartFrameDrag(screen.id, e);
|
|
3121
3714
|
}}
|
|
3122
3715
|
>
|
|
3123
|
-
<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
|
+
>
|
|
3124
3728
|
<span
|
|
3125
3729
|
className={cn(
|
|
3126
3730
|
"h-1.5 w-1.5 shrink-0 rounded-full",
|
|
@@ -3128,26 +3732,45 @@ function Screen({
|
|
|
3128
3732
|
)}
|
|
3129
3733
|
/>
|
|
3130
3734
|
<span
|
|
3735
|
+
data-frame-title
|
|
3131
3736
|
className={cn(
|
|
3132
|
-
"truncate text-[11px] font-medium",
|
|
3133
|
-
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",
|
|
3134
3743
|
)}
|
|
3135
3744
|
title={screen.filename}
|
|
3136
3745
|
>
|
|
3137
3746
|
{display}
|
|
3138
3747
|
</span>
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
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}
|
|
3142
3756
|
</div>
|
|
3143
3757
|
<button
|
|
3144
3758
|
type="button"
|
|
3759
|
+
data-frame-full-view
|
|
3145
3760
|
className={cn(
|
|
3146
|
-
"
|
|
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",
|
|
3147
3762
|
"hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
3148
|
-
"group-hover/frame:opacity-100 group-focus-within/frame:opacity-100",
|
|
3149
3763
|
emphasized && "opacity-100",
|
|
3764
|
+
fullViewOutsideFrame ? "left-full" : "right-1",
|
|
3150
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
|
+
}}
|
|
3151
3774
|
aria-label={t("multiScreenCanvas.fullView")}
|
|
3152
3775
|
title={t("multiScreenCanvas.fullView")}
|
|
3153
3776
|
onClick={(event) => onEdit(screen.id, event)}
|
|
@@ -3155,6 +3778,8 @@ function Screen({
|
|
|
3155
3778
|
event.preventDefault();
|
|
3156
3779
|
event.stopPropagation();
|
|
3157
3780
|
}}
|
|
3781
|
+
onMouseEnter={() => updateDirectHover(true)}
|
|
3782
|
+
onMouseLeave={() => updateDirectHover(false)}
|
|
3158
3783
|
>
|
|
3159
3784
|
<IconMaximize className="size-3 shrink-0" />
|
|
3160
3785
|
<span className="truncate">{t("multiScreenCanvas.fullView")}</span>
|
|
@@ -3165,7 +3790,10 @@ function Screen({
|
|
|
3165
3790
|
role="button"
|
|
3166
3791
|
tabIndex={0}
|
|
3167
3792
|
onClick={(e) => {
|
|
3168
|
-
if (isInteractiveScreenContentTarget(e.target))
|
|
3793
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3794
|
+
e.stopPropagation();
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3169
3797
|
e.stopPropagation();
|
|
3170
3798
|
if (suppressNextClick.current) {
|
|
3171
3799
|
suppressNextClick.current = false;
|
|
@@ -3175,13 +3803,19 @@ function Screen({
|
|
|
3175
3803
|
onPick(screen.id, e);
|
|
3176
3804
|
}}
|
|
3177
3805
|
onDoubleClick={(e) => {
|
|
3178
|
-
if (isInteractiveScreenContentTarget(e.target))
|
|
3806
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3807
|
+
e.stopPropagation();
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3179
3810
|
e.preventDefault();
|
|
3180
3811
|
e.stopPropagation();
|
|
3181
3812
|
}}
|
|
3182
3813
|
onMouseDown={(e) => {
|
|
3814
|
+
if (isInteractiveScreenContentTarget(e.target)) {
|
|
3815
|
+
e.stopPropagation();
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3183
3818
|
if (creationToolActive) return;
|
|
3184
|
-
if (isInteractiveScreenContentTarget(e.target)) return;
|
|
3185
3819
|
if (e.detail > 1) {
|
|
3186
3820
|
e.stopPropagation();
|
|
3187
3821
|
return;
|
|
@@ -3200,46 +3834,52 @@ function Screen({
|
|
|
3200
3834
|
onStartFrameDrag(screen.id, e);
|
|
3201
3835
|
}
|
|
3202
3836
|
}}
|
|
3837
|
+
onMouseMove={(e) => {
|
|
3838
|
+
updateDirectHover(
|
|
3839
|
+
isDirectScreenHoverTarget(e.target, e.currentTarget),
|
|
3840
|
+
);
|
|
3841
|
+
}}
|
|
3842
|
+
onMouseLeave={() => updateDirectHover(false)}
|
|
3203
3843
|
className={cn(
|
|
3204
3844
|
"group/artboard relative block overflow-visible rounded-lg bg-background text-left outline-none transition-colors",
|
|
3205
3845
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
3206
3846
|
emphasized
|
|
3207
3847
|
? "text-foreground"
|
|
3208
|
-
:
|
|
3848
|
+
: cn(
|
|
3849
|
+
"text-muted-foreground",
|
|
3850
|
+
showHoverChrome && "hover:text-foreground",
|
|
3851
|
+
),
|
|
3209
3852
|
)}
|
|
3210
3853
|
style={{
|
|
3211
3854
|
width: geometry.width,
|
|
3212
3855
|
height: geometry.height,
|
|
3213
|
-
cursor:
|
|
3214
|
-
penActive || creationToolActive
|
|
3215
|
-
? "crosshair"
|
|
3216
|
-
: isSelected
|
|
3217
|
-
? "move"
|
|
3218
|
-
: "pointer",
|
|
3856
|
+
cursor: penActive || creationToolActive ? "crosshair" : "pointer",
|
|
3219
3857
|
touchAction: "none",
|
|
3220
3858
|
}}
|
|
3221
3859
|
>
|
|
3222
3860
|
<span
|
|
3861
|
+
data-screen-hover-outline
|
|
3223
3862
|
className={cn(
|
|
3224
3863
|
"pointer-events-none absolute border transition-opacity",
|
|
3225
|
-
|
|
3864
|
+
showHoverChrome
|
|
3226
3865
|
? "border-[var(--design-editor-accent-color)] opacity-100"
|
|
3227
|
-
:
|
|
3228
|
-
? "border-[var(--design-editor-accent-color)] opacity-0 group-hover/artboard:opacity-100"
|
|
3229
|
-
: "border-transparent opacity-0",
|
|
3866
|
+
: "border-transparent opacity-0",
|
|
3230
3867
|
)}
|
|
3231
3868
|
style={{
|
|
3232
3869
|
inset: -5 * chromeScale,
|
|
3233
3870
|
borderRadius: 13 * chromeScale,
|
|
3234
3871
|
borderWidth: 1.5 * chromeScale,
|
|
3872
|
+
transition: getChromeBorderTransition(chromeSettling),
|
|
3235
3873
|
}}
|
|
3236
3874
|
/>
|
|
3237
3875
|
<span
|
|
3876
|
+
data-screen-content
|
|
3238
3877
|
className={cn(
|
|
3239
3878
|
"relative block h-full w-full overflow-hidden rounded-lg border bg-white shadow-2xl transition-colors",
|
|
3240
|
-
"border-border
|
|
3879
|
+
"border-border",
|
|
3880
|
+
showHoverChrome && "border-muted-foreground/60",
|
|
3241
3881
|
)}
|
|
3242
|
-
style={{ pointerEvents:
|
|
3882
|
+
style={{ pointerEvents: screenContentInteractive ? "auto" : "none" }}
|
|
3243
3883
|
>
|
|
3244
3884
|
{screenContent ?? (
|
|
3245
3885
|
<iframe
|
|
@@ -3249,12 +3889,24 @@ function Screen({
|
|
|
3249
3889
|
loading="lazy"
|
|
3250
3890
|
className="pointer-events-none border-0"
|
|
3251
3891
|
style={{
|
|
3252
|
-
width:
|
|
3253
|
-
height:
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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})`,
|
|
3257
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",
|
|
3258
3910
|
}}
|
|
3259
3911
|
title={screen.filename}
|
|
3260
3912
|
/>
|
|
@@ -3265,50 +3917,132 @@ function Screen({
|
|
|
3265
3917
|
aria-hidden="true"
|
|
3266
3918
|
/>
|
|
3267
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}
|
|
3268
3927
|
<span className="pointer-events-none absolute inset-0 rounded-[7px] border border-black/5" />
|
|
3269
3928
|
</span>
|
|
3270
3929
|
<ResizeHandles
|
|
3271
|
-
active={
|
|
3272
|
-
enabled={
|
|
3930
|
+
active={false}
|
|
3931
|
+
enabled={
|
|
3932
|
+
!selectionOutlined &&
|
|
3933
|
+
!penActive &&
|
|
3934
|
+
!creationToolActive &&
|
|
3935
|
+
handlesEnabled
|
|
3936
|
+
}
|
|
3937
|
+
showOnHover={false}
|
|
3273
3938
|
showRotate
|
|
3274
3939
|
chromeScale={chromeScale}
|
|
3940
|
+
chromeSettling={chromeSettling}
|
|
3275
3941
|
onStartResize={(handle, e) => onStartResize(screen.id, handle, e)}
|
|
3276
3942
|
onStartRotate={(e) => onStartRotate(screen.id, e)}
|
|
3277
3943
|
/>
|
|
3278
3944
|
</div>
|
|
3279
3945
|
</div>
|
|
3280
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
|
+
);
|
|
3281
3973
|
}
|
|
3282
3974
|
|
|
3283
3975
|
function GroupSelectionBox({
|
|
3284
3976
|
bounds,
|
|
3285
3977
|
chromeScale,
|
|
3978
|
+
chromeSettling,
|
|
3286
3979
|
onStartResize,
|
|
3287
3980
|
}: {
|
|
3288
3981
|
bounds: NonNullable<ReturnType<typeof getFrameGroupBounds>>;
|
|
3289
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;
|
|
3290
4015
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
4016
|
+
onStartRotate: (e: React.MouseEvent) => void;
|
|
3291
4017
|
}) {
|
|
3292
4018
|
return (
|
|
3293
4019
|
<div
|
|
4020
|
+
data-frame-selection-box
|
|
3294
4021
|
data-frame-shell
|
|
3295
|
-
className="pointer-events-none absolute
|
|
4022
|
+
className="pointer-events-none absolute border border-[var(--design-editor-accent-color)]"
|
|
3296
4023
|
style={{
|
|
3297
|
-
left: SURFACE_PADDING +
|
|
3298
|
-
top: SURFACE_PADDING +
|
|
3299
|
-
width:
|
|
3300
|
-
height:
|
|
4024
|
+
left: SURFACE_PADDING + geometry.x,
|
|
4025
|
+
top: SURFACE_PADDING + geometry.y,
|
|
4026
|
+
width: geometry.width,
|
|
4027
|
+
height: geometry.height,
|
|
3301
4028
|
borderRadius: 13 * chromeScale,
|
|
3302
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,
|
|
3303
4036
|
}}
|
|
3304
4037
|
>
|
|
3305
4038
|
<ResizeHandles
|
|
3306
4039
|
active
|
|
3307
4040
|
enabled
|
|
3308
|
-
showRotate={
|
|
4041
|
+
showRotate={showRotate}
|
|
3309
4042
|
chromeScale={chromeScale}
|
|
4043
|
+
chromeSettling={chromeSettling}
|
|
3310
4044
|
onStartResize={onStartResize}
|
|
3311
|
-
onStartRotate={
|
|
4045
|
+
onStartRotate={onStartRotate}
|
|
3312
4046
|
/>
|
|
3313
4047
|
</div>
|
|
3314
4048
|
);
|
|
@@ -3317,15 +4051,19 @@ function GroupSelectionBox({
|
|
|
3317
4051
|
function ResizeHandles({
|
|
3318
4052
|
active,
|
|
3319
4053
|
enabled,
|
|
4054
|
+
showOnHover = true,
|
|
3320
4055
|
showRotate = true,
|
|
3321
4056
|
chromeScale = 1,
|
|
4057
|
+
chromeSettling = false,
|
|
3322
4058
|
onStartResize,
|
|
3323
4059
|
onStartRotate,
|
|
3324
4060
|
}: {
|
|
3325
4061
|
active: boolean;
|
|
3326
4062
|
enabled: boolean;
|
|
4063
|
+
showOnHover?: boolean;
|
|
3327
4064
|
showRotate?: boolean;
|
|
3328
4065
|
chromeScale?: number;
|
|
4066
|
+
chromeSettling?: boolean;
|
|
3329
4067
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
3330
4068
|
onStartRotate: (e: React.MouseEvent) => void;
|
|
3331
4069
|
}) {
|
|
@@ -3335,7 +4073,11 @@ function ResizeHandles({
|
|
|
3335
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",
|
|
3336
4074
|
active
|
|
3337
4075
|
? "opacity-100"
|
|
3338
|
-
:
|
|
4076
|
+
: cn(
|
|
4077
|
+
"opacity-0",
|
|
4078
|
+
showOnHover &&
|
|
4079
|
+
"group-hover/artboard:opacity-100 group-focus-visible/artboard:opacity-100",
|
|
4080
|
+
),
|
|
3339
4081
|
);
|
|
3340
4082
|
const edgeHandleClass =
|
|
3341
4083
|
"pointer-events-auto absolute z-10 bg-transparent opacity-0";
|
|
@@ -3345,18 +4087,28 @@ function ResizeHandles({
|
|
|
3345
4087
|
{EDGE_RESIZE_HANDLE_CONFIGS.map((config) => (
|
|
3346
4088
|
<span
|
|
3347
4089
|
key={config.handle}
|
|
3348
|
-
data-resize-handle
|
|
4090
|
+
data-resize-handle={config.handle}
|
|
3349
4091
|
className={edgeHandleClass}
|
|
3350
|
-
style={edgeHandleStyle(
|
|
4092
|
+
style={edgeHandleStyle(
|
|
4093
|
+
config.handle,
|
|
4094
|
+
config.cursor,
|
|
4095
|
+
chromeScale,
|
|
4096
|
+
chromeSettling,
|
|
4097
|
+
)}
|
|
3351
4098
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
3352
4099
|
/>
|
|
3353
4100
|
))}
|
|
3354
4101
|
{CORNER_RESIZE_HANDLE_CONFIGS.map((config) => (
|
|
3355
4102
|
<span
|
|
3356
4103
|
key={config.handle}
|
|
3357
|
-
data-resize-handle
|
|
4104
|
+
data-resize-handle={config.handle}
|
|
3358
4105
|
className={visibleHandleClass}
|
|
3359
|
-
style={cornerHandleStyle(
|
|
4106
|
+
style={cornerHandleStyle(
|
|
4107
|
+
config.handle,
|
|
4108
|
+
config.cursor,
|
|
4109
|
+
chromeScale,
|
|
4110
|
+
chromeSettling,
|
|
4111
|
+
)}
|
|
3360
4112
|
onMouseDown={(e) => onStartResize(config.handle, e)}
|
|
3361
4113
|
/>
|
|
3362
4114
|
))}
|
|
@@ -3369,9 +4121,17 @@ function ResizeHandles({
|
|
|
3369
4121
|
"pointer-events-auto absolute z-10 size-5 rounded-full transition-opacity active:cursor-grabbing",
|
|
3370
4122
|
active
|
|
3371
4123
|
? "opacity-100"
|
|
3372
|
-
:
|
|
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,
|
|
3373
4134
|
)}
|
|
3374
|
-
style={rotateHandleStyle(config.corner, chromeScale)}
|
|
3375
4135
|
onMouseDown={onStartRotate}
|
|
3376
4136
|
/>
|
|
3377
4137
|
))
|
|
@@ -3413,12 +4173,14 @@ function edgeHandleStyle(
|
|
|
3413
4173
|
handle: ResizeHandle,
|
|
3414
4174
|
cursor: string,
|
|
3415
4175
|
chromeScale: number,
|
|
4176
|
+
chromeSettling: boolean,
|
|
3416
4177
|
): CSSProperties {
|
|
3417
4178
|
const size = 14 * chromeScale;
|
|
3418
4179
|
const offset = -size / 2;
|
|
3419
4180
|
if (handle === "n" || handle === "s") {
|
|
3420
4181
|
return {
|
|
3421
4182
|
cursor,
|
|
4183
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3422
4184
|
left: 0,
|
|
3423
4185
|
right: 0,
|
|
3424
4186
|
height: size,
|
|
@@ -3427,6 +4189,7 @@ function edgeHandleStyle(
|
|
|
3427
4189
|
}
|
|
3428
4190
|
return {
|
|
3429
4191
|
cursor,
|
|
4192
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3430
4193
|
top: 0,
|
|
3431
4194
|
bottom: 0,
|
|
3432
4195
|
width: size,
|
|
@@ -3438,11 +4201,13 @@ function cornerHandleStyle(
|
|
|
3438
4201
|
handle: ResizeHandle,
|
|
3439
4202
|
cursor: string,
|
|
3440
4203
|
chromeScale: number,
|
|
4204
|
+
chromeSettling: boolean,
|
|
3441
4205
|
): CSSProperties {
|
|
3442
4206
|
const size = 10 * chromeScale;
|
|
3443
4207
|
const offset = -size / 2;
|
|
3444
4208
|
return {
|
|
3445
4209
|
cursor,
|
|
4210
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3446
4211
|
width: size,
|
|
3447
4212
|
height: size,
|
|
3448
4213
|
borderWidth: Math.max(1, 1.25 * chromeScale),
|
|
@@ -3451,11 +4216,16 @@ function cornerHandleStyle(
|
|
|
3451
4216
|
};
|
|
3452
4217
|
}
|
|
3453
4218
|
|
|
3454
|
-
function rotateHandleStyle(
|
|
4219
|
+
function rotateHandleStyle(
|
|
4220
|
+
corner: string,
|
|
4221
|
+
chromeScale: number,
|
|
4222
|
+
chromeSettling: boolean,
|
|
4223
|
+
): CSSProperties {
|
|
3455
4224
|
const size = 28 * chromeScale;
|
|
3456
4225
|
const offset = -34 * chromeScale;
|
|
3457
4226
|
return {
|
|
3458
4227
|
cursor: "grab",
|
|
4228
|
+
transition: getChromeHandleTransition(chromeSettling),
|
|
3459
4229
|
width: size,
|
|
3460
4230
|
height: size,
|
|
3461
4231
|
...(corner.includes("n") ? { top: offset } : { bottom: offset }),
|
|
@@ -3475,7 +4245,15 @@ interface BoundsRect {
|
|
|
3475
4245
|
bottom: number;
|
|
3476
4246
|
}
|
|
3477
4247
|
|
|
3478
|
-
|
|
4248
|
+
interface ScreenViewportSize {
|
|
4249
|
+
width: number;
|
|
4250
|
+
height: number;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
function getInitialFrameGeometry(
|
|
4254
|
+
index: number,
|
|
4255
|
+
metadata?: ScreenViewportSize,
|
|
4256
|
+
): FrameGeometry {
|
|
3479
4257
|
// Seed default frames with the actual screen dimensions and the 3-column grid
|
|
3480
4258
|
// the overview centering math (which uses SCREEN_WIDTH/SCREEN_GAP) expects, so
|
|
3481
4259
|
// a design without persisted geometry opens centered. (The larger
|
|
@@ -3483,22 +4261,56 @@ function getInitialFrameGeometry(index: number): FrameGeometry {
|
|
|
3483
4261
|
// editor's default placement.)
|
|
3484
4262
|
const column = index % 3;
|
|
3485
4263
|
const row = Math.floor(index / 3);
|
|
4264
|
+
const height = getOverviewFrameHeight(SCREEN_WIDTH, metadata);
|
|
3486
4265
|
return {
|
|
3487
4266
|
x: column * (SCREEN_WIDTH + SCREEN_GAP),
|
|
3488
|
-
y: row * (
|
|
4267
|
+
y: row * (height + FRAME_LABEL_HEIGHT + SCREEN_GAP),
|
|
3489
4268
|
width: SCREEN_WIDTH,
|
|
3490
|
-
height
|
|
4269
|
+
height,
|
|
3491
4270
|
};
|
|
3492
4271
|
}
|
|
3493
4272
|
|
|
3494
|
-
function
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
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
|
+
};
|
|
3502
4314
|
}
|
|
3503
4315
|
|
|
3504
4316
|
function dedupeIds(ids: string[]) {
|
|
@@ -3556,6 +4368,22 @@ function isInteractiveScreenContentTarget(target: EventTarget | null) {
|
|
|
3556
4368
|
);
|
|
3557
4369
|
}
|
|
3558
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
|
+
|
|
3559
4387
|
function frameBoundsToGeometry(bounds: {
|
|
3560
4388
|
left: number;
|
|
3561
4389
|
top: number;
|
|
@@ -3697,6 +4525,20 @@ function sameFrameGeometry(a: FrameGeometry, b: FrameGeometry) {
|
|
|
3697
4525
|
);
|
|
3698
4526
|
}
|
|
3699
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
|
+
|
|
3700
4542
|
function cloneFrameGeometryById(
|
|
3701
4543
|
geometryById: FrameGeometryById,
|
|
3702
4544
|
): FrameGeometryById {
|
|
@@ -4124,6 +4966,7 @@ function resolveScreenMetadata(
|
|
|
4124
4966
|
screen: ScreenFile,
|
|
4125
4967
|
keyedMetadata?: ScreenMetadata,
|
|
4126
4968
|
getterMetadata?: ScreenMetadata,
|
|
4969
|
+
previewDeviceFrame: DeviceFrameType = "none",
|
|
4127
4970
|
): ResolvedScreenMetadata {
|
|
4128
4971
|
const metadata = { ...screen, ...keyedMetadata, ...getterMetadata };
|
|
4129
4972
|
const previewUrl =
|
|
@@ -4131,9 +4974,16 @@ function resolveScreenMetadata(
|
|
|
4131
4974
|
metadata.previewUrl ??
|
|
4132
4975
|
screen.previewUrl ??
|
|
4133
4976
|
getPreviewUrl(screen.content);
|
|
4134
|
-
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);
|
|
4135
4984
|
const height =
|
|
4136
|
-
|
|
4985
|
+
deviceViewport?.height ??
|
|
4986
|
+
(metadata.height && metadata.height > 0 ? metadata.height : 2560);
|
|
4137
4987
|
return {
|
|
4138
4988
|
source:
|
|
4139
4989
|
normalizeSource(metadata.sourceType ?? metadata.source) ??
|
|
@@ -4231,3 +5081,7 @@ function getUrl(value: string | undefined) {
|
|
|
4231
5081
|
return undefined;
|
|
4232
5082
|
}
|
|
4233
5083
|
}
|
|
5084
|
+
|
|
5085
|
+
function clampNumber(value: number, min: number, max: number) {
|
|
5086
|
+
return Math.min(max, Math.max(min, value));
|
|
5087
|
+
}
|