@agent-native/core 0.80.8 → 0.80.10
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 +19 -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/design-connect.ts +28 -2
- package/corpus/core/src/cli/mcp.ts +10 -0
- package/corpus/core/src/cli/skills.ts +103 -36
- 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/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- 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-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-design.ts +1 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/present-design-variants.ts +355 -30
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/actions/view-screen.ts +75 -31
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -1
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
- 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/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- 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/components/visual-editor/DrawOverlay.tsx +1 -1
- 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/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
- package/corpus/templates/design/app/i18n-data.ts +314 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
- package/corpus/templates/design/app/root.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.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-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.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-moves-can-be-undone-and-redone-without-flashing-the-ca.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-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.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-adds-a-studio-layer-with-tokens-respon.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/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.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 +128 -11
- package/corpus/templates/design/playwright.config.ts +9 -3
- package/corpus/templates/design/server/db/schema.ts +123 -0
- 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/plugins/db.ts +90 -0
- package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1141 -93
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/generation-session.ts +41 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -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/CanvasArea.tsx +6 -2
- 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-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- 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/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- 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 +95 -33
- 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/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/observability/routes.d.ts +3 -3
- 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/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/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
|
@@ -15,6 +15,249 @@ import { isTrustedCanvasBridgeMessage } from "./bridge-security";
|
|
|
15
15
|
import { DeviceFrame } from "./DeviceFrame";
|
|
16
16
|
import type { ElementInfo, DeviceFrameType } from "./types";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Allowlist check for Fusion (Builder-hosted) frame origins.
|
|
20
|
+
*
|
|
21
|
+
* Fusion frames are served cross-origin from the Builder-hosted app, so the
|
|
22
|
+
* strict `origin === parentOrigin` bridge check can never match. Before relaxing
|
|
23
|
+
* trust to window-identity only, we must confirm the message origin is actually
|
|
24
|
+
* a Builder host — the exact origin of the `fusionUrl` we were asked to render,
|
|
25
|
+
* or any `*.builder.io` host (plus the bare `builder.io`), over https. This
|
|
26
|
+
* prevents the relaxed-trust path from accepting messages from an arbitrary
|
|
27
|
+
* cross-origin frame that merely shares our iframe's window reference.
|
|
28
|
+
*/
|
|
29
|
+
function isAllowedFusionOrigin(
|
|
30
|
+
origin: string,
|
|
31
|
+
fusionUrl: string | undefined,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (!origin || origin === "null") return false;
|
|
34
|
+
let host: string;
|
|
35
|
+
let protocol: string;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(origin);
|
|
38
|
+
host = parsed.hostname.toLowerCase();
|
|
39
|
+
protocol = parsed.protocol;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// Only allow secure (https) Builder origins.
|
|
44
|
+
if (protocol !== "https:") return false;
|
|
45
|
+
// Exact match against the configured fusion URL's origin.
|
|
46
|
+
if (fusionUrl) {
|
|
47
|
+
try {
|
|
48
|
+
if (new URL(fusionUrl).origin === origin) return true;
|
|
49
|
+
} catch {
|
|
50
|
+
// Malformed fusionUrl — fall through to the host-family allowlist.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Builder host family: builder.io and any subdomain of it.
|
|
54
|
+
return host === "builder.io" || host.endsWith(".builder.io");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wire shape for a single motion track sent via the `motion-load-tracks`
|
|
59
|
+
* postMessage. Matches the serialisable subset of `MotionTrack` from
|
|
60
|
+
* `shared/motion-timeline.ts` without requiring an import at the UI layer.
|
|
61
|
+
*/
|
|
62
|
+
export interface MotionTrackWire {
|
|
63
|
+
targetNodeId: string;
|
|
64
|
+
property: string;
|
|
65
|
+
keyframes: Array<{ t: number; value: string; ease?: string }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Motion-preview bridge. Injected alongside the other bridge scripts so the
|
|
70
|
+
* MotionDock's scrubbing preview works in ALL editor modes without writing
|
|
71
|
+
* anything to the DB, Yjs state, or source files.
|
|
72
|
+
*
|
|
73
|
+
* Protocol (parent → iframe):
|
|
74
|
+
*
|
|
75
|
+
* { type: 'motion-load-tracks', tracks: MotionTrackWire[] }
|
|
76
|
+
* Load (or replace) the track list for this document. Each entry:
|
|
77
|
+
* { targetNodeId, property, keyframes: [{ t, value, ease? }] }
|
|
78
|
+
* where t ∈ [0, 1]. Sent whenever the active timeline changes.
|
|
79
|
+
*
|
|
80
|
+
* { type: 'motion-preview', t, durationMs }
|
|
81
|
+
* Seek all loaded tracks to normalised position t ∈ [0, 1] and apply
|
|
82
|
+
* the interpolated CSS property values as inline styles on the matching
|
|
83
|
+
* [data-agent-native-node-id="…"] elements. Never writes to storage.
|
|
84
|
+
*
|
|
85
|
+
* { type: 'motion-preview-clear' }
|
|
86
|
+
* Remove all motion-preview inline-style overrides and the in-memory
|
|
87
|
+
* track list. Called when the dock is closed or the timeline is
|
|
88
|
+
* discarded.
|
|
89
|
+
*
|
|
90
|
+
* Easing is deliberately simple: linear interpolation between keyframe
|
|
91
|
+
* values (CSS handles easing when the compiled CSS is actually applied;
|
|
92
|
+
* preview is a live visualisation, not a perfect recreation of the final
|
|
93
|
+
* animation).
|
|
94
|
+
*/
|
|
95
|
+
const MOTION_PREVIEW_BRIDGE_SCRIPT = `
|
|
96
|
+
<script data-agent-native-motion-preview-bridge>
|
|
97
|
+
(function() {
|
|
98
|
+
// Track list loaded by 'motion-load-tracks'.
|
|
99
|
+
var loadedTracks = [];
|
|
100
|
+
// Map of nodeId -> [property, ...] we have touched, for cleanup.
|
|
101
|
+
var touchedProps = {};
|
|
102
|
+
|
|
103
|
+
function lerp(a, b, ratio) {
|
|
104
|
+
var numA = parseFloat(a);
|
|
105
|
+
var numB = parseFloat(b);
|
|
106
|
+
if (Number.isFinite(numA) && Number.isFinite(numB)) {
|
|
107
|
+
var suffix = a.replace(/^-?[\\d.]+/, '') || b.replace(/^-?[\\d.]+/, '');
|
|
108
|
+
return (numA + (numB - numA) * ratio).toFixed(4).replace(/\\.?0+$/, '') + suffix;
|
|
109
|
+
}
|
|
110
|
+
// Non-numeric: snap to b after the midpoint.
|
|
111
|
+
return ratio < 0.5 ? a : b;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function interpolate(keyframes, t) {
|
|
115
|
+
if (!keyframes || keyframes.length === 0) return '';
|
|
116
|
+
if (keyframes.length === 1) return keyframes[0].value;
|
|
117
|
+
// Find surrounding keyframes.
|
|
118
|
+
var prev = keyframes[0];
|
|
119
|
+
var next = keyframes[keyframes.length - 1];
|
|
120
|
+
for (var i = 0; i < keyframes.length - 1; i++) {
|
|
121
|
+
if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
|
|
122
|
+
prev = keyframes[i];
|
|
123
|
+
next = keyframes[i + 1];
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var span = next.t - prev.t;
|
|
128
|
+
if (span <= 0) return prev.value;
|
|
129
|
+
var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
|
|
130
|
+
return lerp(prev.value, next.value, ratio);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyPreview(t) {
|
|
134
|
+
for (var i = 0; i < loadedTracks.length; i++) {
|
|
135
|
+
var track = loadedTracks[i];
|
|
136
|
+
var el = document.querySelector('[data-agent-native-node-id="' + track.targetNodeId + '"]');
|
|
137
|
+
if (!el) continue;
|
|
138
|
+
var value = interpolate(track.keyframes, t);
|
|
139
|
+
if (value === '') continue;
|
|
140
|
+
el.style[track.property] = value;
|
|
141
|
+
if (!touchedProps[track.targetNodeId]) touchedProps[track.targetNodeId] = [];
|
|
142
|
+
if (touchedProps[track.targetNodeId].indexOf(track.property) === -1) {
|
|
143
|
+
touchedProps[track.targetNodeId].push(track.property);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clearPreview() {
|
|
149
|
+
var nodeIds = Object.keys(touchedProps);
|
|
150
|
+
for (var i = 0; i < nodeIds.length; i++) {
|
|
151
|
+
var el = document.querySelector('[data-agent-native-node-id="' + nodeIds[i] + '"]');
|
|
152
|
+
if (!el) continue;
|
|
153
|
+
var props = touchedProps[nodeIds[i]];
|
|
154
|
+
for (var j = 0; j < props.length; j++) {
|
|
155
|
+
el.style[props[j]] = '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
touchedProps = {};
|
|
159
|
+
loadedTracks = [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
window.addEventListener('message', function(e) {
|
|
163
|
+
if (e.source !== window.parent) return;
|
|
164
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
165
|
+
if (e.data.type === 'motion-load-tracks') {
|
|
166
|
+
loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
|
|
167
|
+
touchedProps = {};
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (e.data.type === 'motion-preview') {
|
|
171
|
+
var t = Number(e.data.t);
|
|
172
|
+
if (!Number.isFinite(t)) return;
|
|
173
|
+
t = Math.max(0, Math.min(1, t));
|
|
174
|
+
applyPreview(t);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (e.data.type === 'motion-preview-clear') {
|
|
178
|
+
clearPreview();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
})();
|
|
183
|
+
</script>
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Shader-fill preview bridge. ALWAYS injected alongside the other bridge
|
|
188
|
+
* scripts so the parent can apply a CSS gradient approximation of a shader
|
|
189
|
+
* fill to the currently-selected element **without** persisting anything.
|
|
190
|
+
*
|
|
191
|
+
* Protocol (parent → iframe):
|
|
192
|
+
*
|
|
193
|
+
* { type: 'shader-fill-preview', selector, nodeId, css }
|
|
194
|
+
* Apply `css` as the `background` inline style on the first element that
|
|
195
|
+
* matches `selector` (preferred) or `[data-agent-native-node-id="nodeId"]`.
|
|
196
|
+
* When both are absent, targets `document.body`. Stores the previous
|
|
197
|
+
* background value so it can be restored on clear.
|
|
198
|
+
* Preview-only — never writes to DB, Yjs, or source files.
|
|
199
|
+
*
|
|
200
|
+
* { type: 'shader-fill-preview-clear' }
|
|
201
|
+
* Remove the applied background override and restore the previous value.
|
|
202
|
+
* Called when the user discards the preview or switches selections.
|
|
203
|
+
*/
|
|
204
|
+
const SHADER_FILL_PREVIEW_BRIDGE_SCRIPT = `
|
|
205
|
+
<script data-agent-native-shader-fill-preview-bridge>
|
|
206
|
+
(function() {
|
|
207
|
+
// Track the element we patched and its original background so we can undo.
|
|
208
|
+
var patchedEl = null;
|
|
209
|
+
var originalBackground = '';
|
|
210
|
+
|
|
211
|
+
function resolveTarget(selector, nodeId) {
|
|
212
|
+
if (selector) {
|
|
213
|
+
try {
|
|
214
|
+
var hit = document.querySelector(selector);
|
|
215
|
+
if (hit) return hit;
|
|
216
|
+
} catch (_err) {}
|
|
217
|
+
}
|
|
218
|
+
if (nodeId) {
|
|
219
|
+
var byId = document.querySelector('[data-agent-native-node-id="' + nodeId.replace(/"/g, '\\\\"') + '"]');
|
|
220
|
+
if (byId) return byId;
|
|
221
|
+
}
|
|
222
|
+
return document.body;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyPreview(selector, nodeId, css) {
|
|
226
|
+
// Clear any prior patch first so we don't stack patches.
|
|
227
|
+
clearPreview();
|
|
228
|
+
var el = resolveTarget(selector, nodeId);
|
|
229
|
+
if (!el) return;
|
|
230
|
+
originalBackground = el.style.background || '';
|
|
231
|
+
el.style.background = css || '';
|
|
232
|
+
patchedEl = el;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function clearPreview() {
|
|
236
|
+
if (!patchedEl) return;
|
|
237
|
+
patchedEl.style.background = originalBackground;
|
|
238
|
+
patchedEl = null;
|
|
239
|
+
originalBackground = '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
window.addEventListener('message', function(e) {
|
|
243
|
+
if (e.source !== window.parent) return;
|
|
244
|
+
if (!e.data || typeof e.data.type !== 'string') return;
|
|
245
|
+
if (e.data.type === 'shader-fill-preview') {
|
|
246
|
+
var selector = typeof e.data.selector === 'string' ? e.data.selector : '';
|
|
247
|
+
var nodeId = typeof e.data.nodeId === 'string' ? e.data.nodeId : '';
|
|
248
|
+
var css = typeof e.data.css === 'string' ? e.data.css : '';
|
|
249
|
+
applyPreview(selector, nodeId, css);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (e.data.type === 'shader-fill-preview-clear') {
|
|
253
|
+
clearPreview();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
})();
|
|
258
|
+
</script>
|
|
259
|
+
`;
|
|
260
|
+
|
|
18
261
|
/**
|
|
19
262
|
* Tweak-bridge script. ALWAYS injected so the parent's postMessage
|
|
20
263
|
* (`tweak-values`) can update CSS custom properties on the iframe's :root
|
|
@@ -247,8 +490,44 @@ function createEditorBridgeThemeScript(vars: Record<string, string>) {
|
|
|
247
490
|
const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
248
491
|
<script data-agent-native-editor-chrome-bridge>
|
|
249
492
|
(function() {
|
|
250
|
-
var
|
|
493
|
+
var readOnly = __READ_ONLY__;
|
|
494
|
+
var textEditingEnabled = !readOnly && __TEXT_EDITING_ENABLED__;
|
|
251
495
|
var scaleToolEnabled = false;
|
|
496
|
+
var editorChromeScaleX = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_X__) || 1);
|
|
497
|
+
var editorChromeScaleY = Math.max(0.05, Number(__EDITOR_CHROME_SCALE_Y__) || editorChromeScaleX);
|
|
498
|
+
|
|
499
|
+
// Ease the constant-size selection chrome to its new size when overview zoom
|
|
500
|
+
// settles (parent posts set-editor-chrome-scale), matching the canvas chrome.
|
|
501
|
+
// Only chrome-scale-driven props animate; the overlay's live position is excluded.
|
|
502
|
+
(function () {
|
|
503
|
+
var chromeTransitionStyle = document.createElement('style');
|
|
504
|
+
chromeTransitionStyle.textContent =
|
|
505
|
+
'[data-agent-native-edit-overlay="selection"]{transition:border-width 150ms ease-out}' +
|
|
506
|
+
'[data-agent-native-edge-handle],[data-agent-native-edit-handle],[data-agent-native-rotate-handle]{transition:width 150ms ease-out,height 150ms ease-out,border-width 150ms ease-out,top 150ms ease-out,bottom 150ms ease-out,left 150ms ease-out,right 150ms ease-out}' +
|
|
507
|
+
'[data-agent-native-spacing-line]{position:absolute;display:none;pointer-events:none;border-radius:999px}' +
|
|
508
|
+
'[data-agent-native-spacing-region]{position:absolute;display:none;box-sizing:border-box;pointer-events:auto;background-size:6px 6px}' +
|
|
509
|
+
'[data-agent-native-spacing-region][data-orientation="vertical"]{cursor:ew-resize}' +
|
|
510
|
+
'[data-agent-native-spacing-region][data-orientation="horizontal"]{cursor:ns-resize}';
|
|
511
|
+
(document.head || document.documentElement).appendChild(chromeTransitionStyle);
|
|
512
|
+
})();
|
|
513
|
+
|
|
514
|
+
function chromeScaleX() {
|
|
515
|
+
return 1 / Math.max(0.05, editorChromeScaleX);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function chromeScaleY() {
|
|
519
|
+
return 1 / Math.max(0.05, editorChromeScaleY);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function chromeLineScale() {
|
|
523
|
+
return 1 / Math.max(0.05, Math.max(editorChromeScaleX, editorChromeScaleY));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function syncEditorChromeScaleVars() {
|
|
527
|
+
document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-x', String(chromeScaleX()));
|
|
528
|
+
document.documentElement.style.setProperty('--agent-native-editor-chrome-scale-y', String(chromeScaleY()));
|
|
529
|
+
document.documentElement.style.setProperty('--agent-native-editor-chrome-line-scale', String(chromeLineScale()));
|
|
530
|
+
}
|
|
252
531
|
|
|
253
532
|
function escapeIdent(value) {
|
|
254
533
|
if (window.CSS && typeof window.CSS.escape === 'function') {
|
|
@@ -321,6 +600,32 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
321
600
|
);
|
|
322
601
|
}
|
|
323
602
|
|
|
603
|
+
function isDocumentRootElement(el) {
|
|
604
|
+
return el === document.body || el === document.documentElement;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function closestStableSourceElement(el) {
|
|
608
|
+
if (!el || !el.closest) return null;
|
|
609
|
+
var stable = el.closest('[data-agent-native-node-id],[data-code-layer-id],[data-layer-id],[data-builder-id],[data-loc]');
|
|
610
|
+
if (!stable || isDocumentRootElement(stable)) return null;
|
|
611
|
+
return stable;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function hasStableOwnSource(el) {
|
|
615
|
+
return !!(
|
|
616
|
+
el &&
|
|
617
|
+
!isDocumentRootElement(el) &&
|
|
618
|
+
getSourceId(el)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function selectionTargetForHit(hit) {
|
|
623
|
+
if (!hit || isDocumentRootElement(hit)) return hit;
|
|
624
|
+
if (selectedEl && hit !== selectedEl && selectedEl.contains(hit)) return hit;
|
|
625
|
+
if (hasStableOwnSource(hit)) return hit;
|
|
626
|
+
return closestStableSourceElement(hit) || hit;
|
|
627
|
+
}
|
|
628
|
+
|
|
324
629
|
function freshRuntimeNodeId(prefix) {
|
|
325
630
|
var random = '';
|
|
326
631
|
try {
|
|
@@ -356,7 +661,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
356
661
|
if (stableOwnSelector) return stableOwnSelector;
|
|
357
662
|
|
|
358
663
|
if (el.id) return '#' + escapeIdent(el.id);
|
|
359
|
-
var stableAncestor = el
|
|
664
|
+
var stableAncestor = closestStableSourceElement(el);
|
|
360
665
|
if (stableAncestor && stableAncestor !== el) {
|
|
361
666
|
var stableAncestorSelector = selectorPart(stableAncestor);
|
|
362
667
|
if (stableAncestorSelector) {
|
|
@@ -379,7 +684,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
379
684
|
? window.getComputedStyle(el.parentElement)
|
|
380
685
|
: null;
|
|
381
686
|
var parentDisplay = parentStyles ? parentStyles.display : undefined;
|
|
382
|
-
var
|
|
687
|
+
var sourceBacked = hasStableOwnSource(el) || !!closestStableSourceElement(el);
|
|
688
|
+
var sourceId = sourceBacked ? (getSourceId(el) || getSelector(el)) : '';
|
|
383
689
|
var parentLayout = parentStyles
|
|
384
690
|
? {
|
|
385
691
|
display: parentStyles.display,
|
|
@@ -392,15 +698,24 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
392
698
|
position: parentStyles.position,
|
|
393
699
|
}
|
|
394
700
|
: undefined;
|
|
395
|
-
var capabilities =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
701
|
+
var capabilities = sourceBacked
|
|
702
|
+
? [
|
|
703
|
+
{
|
|
704
|
+
kind: 'deterministic-style-edit',
|
|
705
|
+
label: 'deterministic-style-edit',
|
|
706
|
+
confidence: 0.92,
|
|
707
|
+
reason: 'Inline style can be patched and replayed through HMR/collab.',
|
|
708
|
+
},
|
|
709
|
+
]
|
|
710
|
+
: [
|
|
711
|
+
{
|
|
712
|
+
kind: 'unsupported',
|
|
713
|
+
label: 'runtime-only-element',
|
|
714
|
+
confidence: 0.3,
|
|
715
|
+
reason: 'This runtime node is not anchored to a source code layer.',
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
if (sourceBacked && el.classList && el.classList.length > 0) {
|
|
404
719
|
capabilities.push({
|
|
405
720
|
kind: 'deterministic-class-edit',
|
|
406
721
|
label: 'deterministic-class-edit',
|
|
@@ -408,7 +723,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
408
723
|
reason: 'Class tokens are visible on the selected element.',
|
|
409
724
|
});
|
|
410
725
|
}
|
|
411
|
-
if (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid') {
|
|
726
|
+
if (sourceBacked && (parentDisplay === 'flex' || parentDisplay === 'inline-flex' || parentDisplay === 'grid' || parentDisplay === 'inline-grid')) {
|
|
412
727
|
capabilities.push({
|
|
413
728
|
kind: 'agent-structural-edit',
|
|
414
729
|
label: 'agent-structural-edit',
|
|
@@ -416,6 +731,38 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
416
731
|
reason: 'Parent layout context decides whether movement means gap, order, alignment, or wrapper structure.',
|
|
417
732
|
});
|
|
418
733
|
}
|
|
734
|
+
// --- provenance: read source-location attributes when present ---
|
|
735
|
+
// These are emitted by connected apps via @vitejs/plugin-react jsxDEV or a
|
|
736
|
+
// Babel source plugin. Cross-origin localhost iframes cannot be read (CSP /
|
|
737
|
+
// same-origin policy), so this will be undefined in that case — expected.
|
|
738
|
+
var provenance = undefined;
|
|
739
|
+
var dataSourceFile = el.getAttribute('data-source-file');
|
|
740
|
+
var dataSourceLine = el.getAttribute('data-source-line');
|
|
741
|
+
var dataSourceColumn = el.getAttribute('data-source-column');
|
|
742
|
+
var dataComponentName = el.getAttribute('data-component-name');
|
|
743
|
+
var dataLoc = el.getAttribute('data-loc');
|
|
744
|
+
// data-loc may encode "file:line:col" (Babel source plugin convention).
|
|
745
|
+
// Only parse it when data-source-file is absent, to avoid double-reads.
|
|
746
|
+
if (!dataSourceFile && dataLoc) {
|
|
747
|
+
var lastColonIndex = dataLoc.lastIndexOf(':');
|
|
748
|
+
var lastPart = lastColonIndex >= 0 ? dataLoc.slice(lastColonIndex + 1) : '';
|
|
749
|
+
if (lastColonIndex >= 0 && /^\\d+$/.test(lastPart)) {
|
|
750
|
+
var beforeLastPart = dataLoc.slice(0, lastColonIndex);
|
|
751
|
+
var previousColonIndex = beforeLastPart.lastIndexOf(':');
|
|
752
|
+
var previousPart = previousColonIndex >= 0 ? beforeLastPart.slice(previousColonIndex + 1) : '';
|
|
753
|
+
var hasColumn = /^\\d+$/.test(previousPart);
|
|
754
|
+
dataSourceFile = hasColumn ? beforeLastPart.slice(0, previousColonIndex) : beforeLastPart;
|
|
755
|
+
dataSourceLine = hasColumn ? previousPart : lastPart;
|
|
756
|
+
if (hasColumn) dataSourceColumn = lastPart;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (dataSourceFile || dataSourceLine || dataSourceColumn || dataComponentName) {
|
|
760
|
+
provenance = {};
|
|
761
|
+
if (dataSourceFile) provenance.sourceFile = dataSourceFile;
|
|
762
|
+
if (dataSourceLine) { var ln = parseInt(dataSourceLine, 10); if (!isNaN(ln)) provenance.line = ln; }
|
|
763
|
+
if (dataSourceColumn) { var col = parseInt(dataSourceColumn, 10); if (!isNaN(col)) provenance.column = col; }
|
|
764
|
+
if (dataComponentName) provenance.component = dataComponentName;
|
|
765
|
+
}
|
|
419
766
|
return {
|
|
420
767
|
tagName: el.tagName.toLowerCase(),
|
|
421
768
|
id: el.id || undefined,
|
|
@@ -433,9 +780,9 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
433
780
|
lineHeight: cs.lineHeight,
|
|
434
781
|
letterSpacing: cs.letterSpacing,
|
|
435
782
|
textAlign: cs.textAlign,
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
783
|
+
display: cs.display,
|
|
784
|
+
overflow: cs.overflow,
|
|
785
|
+
flexDirection: cs.flexDirection,
|
|
439
786
|
justifyContent: cs.justifyContent,
|
|
440
787
|
alignItems: cs.alignItems,
|
|
441
788
|
alignSelf: cs.alignSelf,
|
|
@@ -491,6 +838,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
491
838
|
confidence: capabilities.reduce(function(best, item) {
|
|
492
839
|
return Math.max(best, item.confidence || 0);
|
|
493
840
|
}, 0),
|
|
841
|
+
provenance: provenance,
|
|
494
842
|
};
|
|
495
843
|
}
|
|
496
844
|
|
|
@@ -501,12 +849,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
501
849
|
|
|
502
850
|
var highlightOverlay = document.createElement('div');
|
|
503
851
|
highlightOverlay.setAttribute('data-agent-native-edit-overlay', 'highlight');
|
|
504
|
-
highlightOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:
|
|
852
|
+
highlightOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:99997;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;';
|
|
505
853
|
document.body.appendChild(highlightOverlay);
|
|
506
854
|
|
|
507
855
|
var selectionOverlay = document.createElement('div');
|
|
508
856
|
selectionOverlay.setAttribute('data-agent-native-edit-overlay', 'selection');
|
|
509
|
-
selectionOverlay.style.cssText = 'position:fixed;pointer-events:
|
|
857
|
+
selectionOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:99998;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;cursor:default;';
|
|
510
858
|
['n','e','s','w'].forEach(function(pos) {
|
|
511
859
|
var edge = document.createElement('span');
|
|
512
860
|
edge.setAttribute('data-agent-native-edge-handle', pos);
|
|
@@ -542,7 +890,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
542
890
|
var handle = document.createElement('span');
|
|
543
891
|
handle.setAttribute('data-agent-native-edit-handle', pos);
|
|
544
892
|
var cursor = pos === 'n' || pos === 's' ? 'ns-resize' : pos === 'e' || pos === 'w' ? 'ew-resize' : pos === 'nw' || pos === 'se' ? 'nwse-resize' : 'nesw-resize';
|
|
545
|
-
handle.style.cssText = 'position:absolute;width:7px;height:7px;border:1px solid var(--design-editor-accent-color);background:var(--design-editor-accent-contrast-color);box-sizing:border-box;border-radius:1px;pointer-events:auto;cursor:' + cursor + ';';
|
|
893
|
+
handle.style.cssText = 'position:absolute;z-index:1;width:7px;height:7px;border:1px solid var(--design-editor-accent-color);background:var(--design-editor-accent-contrast-color);box-sizing:border-box;border-radius:1px;pointer-events:auto;cursor:' + cursor + ';';
|
|
546
894
|
if (pos.indexOf('n') !== -1) handle.style.top = '-4px';
|
|
547
895
|
if (pos.indexOf('s') !== -1) handle.style.bottom = '-4px';
|
|
548
896
|
if (pos.indexOf('w') !== -1) handle.style.left = '-4px';
|
|
@@ -567,10 +915,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
567
915
|
if (pos.indexOf('e') !== -1) rotate.style.right = '-26px';
|
|
568
916
|
selectionOverlay.appendChild(rotate);
|
|
569
917
|
});
|
|
570
|
-
var
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
selectionOverlay.appendChild(
|
|
918
|
+
var spacingOverlay = document.createElement('div');
|
|
919
|
+
spacingOverlay.setAttribute('data-agent-native-spacing-overlay', '');
|
|
920
|
+
spacingOverlay.style.cssText = 'position:absolute;inset:0;display:none;pointer-events:none;';
|
|
921
|
+
selectionOverlay.appendChild(spacingOverlay);
|
|
574
922
|
document.body.appendChild(selectionOverlay);
|
|
575
923
|
|
|
576
924
|
var transformBadge = document.createElement('div');
|
|
@@ -578,6 +926,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
578
926
|
transformBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--background) / 0.96);color:hsl(var(--foreground));font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;padding:3px 5px;box-shadow:0 8px 20px color-mix(in srgb, hsl(var(--foreground)) 16%, transparent);';
|
|
579
927
|
document.body.appendChild(transformBadge);
|
|
580
928
|
|
|
929
|
+
var spacingBadge = document.createElement('div');
|
|
930
|
+
spacingBadge.setAttribute('data-agent-native-spacing-badge', '');
|
|
931
|
+
spacingBadge.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;color:white;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:700;padding:2px 4px;box-shadow:0 4px 14px rgba(0,0,0,0.18);';
|
|
932
|
+
document.body.appendChild(spacingBadge);
|
|
933
|
+
|
|
581
934
|
var insertionGuide = document.createElement('div');
|
|
582
935
|
insertionGuide.setAttribute('data-agent-native-insertion-guide', '');
|
|
583
936
|
insertionGuide.style.cssText = 'position:fixed;z-index:100000;display:none;pointer-events:none;background:var(--design-editor-accent-color);border-radius:999px;box-shadow:0 0 0 1px var(--design-editor-accent-color);';
|
|
@@ -588,14 +941,118 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
588
941
|
measurementOverlay.style.cssText = 'position:fixed;inset:0;z-index:100001;display:none;pointer-events:none;color:var(--design-editor-measure-color);font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;';
|
|
589
942
|
document.body.appendChild(measurementOverlay);
|
|
590
943
|
|
|
944
|
+
// Component-instance tag: a small pill that floats above the selection
|
|
945
|
+
// outline whenever the selected element carries a data-agent-native-component
|
|
946
|
+
// attribute. Clicking it sends a 'component-source-jump' message to the
|
|
947
|
+
// parent so the editor can invoke open-component-source.
|
|
948
|
+
var componentTagOverlay = document.createElement('div');
|
|
949
|
+
componentTagOverlay.setAttribute('data-agent-native-edit-overlay', 'component-tag');
|
|
950
|
+
componentTagOverlay.style.cssText = [
|
|
951
|
+
'position:fixed',
|
|
952
|
+
'z-index:100002',
|
|
953
|
+
'display:none',
|
|
954
|
+
'pointer-events:auto',
|
|
955
|
+
'cursor:pointer',
|
|
956
|
+
'padding:2px 6px',
|
|
957
|
+
'border-radius:4px',
|
|
958
|
+
'font:11px/1.6 ui-sans-serif,system-ui,sans-serif',
|
|
959
|
+
'white-space:nowrap',
|
|
960
|
+
'user-select:none',
|
|
961
|
+
'-webkit-user-select:none',
|
|
962
|
+
'background:var(--design-editor-accent-color)',
|
|
963
|
+
'color:var(--design-editor-accent-contrast-color)',
|
|
964
|
+
'box-shadow:0 1px 4px color-mix(in srgb,var(--design-editor-accent-color) 40%,transparent)',
|
|
965
|
+
'border:1px solid color-mix(in srgb,var(--design-editor-accent-strong-color) 60%,transparent)',
|
|
966
|
+
'outline:2px solid transparent',
|
|
967
|
+
'transition:opacity 0.1s',
|
|
968
|
+
].join(';') + ';';
|
|
969
|
+
document.body.appendChild(componentTagOverlay);
|
|
970
|
+
|
|
971
|
+
componentTagOverlay.addEventListener('click', function(e) {
|
|
972
|
+
e.stopPropagation();
|
|
973
|
+
e.preventDefault();
|
|
974
|
+
var nodeId = componentTagOverlay.getAttribute('data-component-node-id') || '';
|
|
975
|
+
var componentName = componentTagOverlay.getAttribute('data-component-name') || '';
|
|
976
|
+
if (!nodeId || !componentName) return;
|
|
977
|
+
try {
|
|
978
|
+
window.parent.postMessage({
|
|
979
|
+
type: 'component-source-jump',
|
|
980
|
+
nodeId: nodeId,
|
|
981
|
+
componentName: componentName,
|
|
982
|
+
}, '*');
|
|
983
|
+
} catch (_err) {}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
function updateComponentTag(el) {
|
|
987
|
+
if (!el) {
|
|
988
|
+
clearComponentTag();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
var compName = el.getAttribute && el.getAttribute('data-agent-native-component');
|
|
992
|
+
if (!compName) {
|
|
993
|
+
clearComponentTag();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
var nodeId = (
|
|
997
|
+
el.getAttribute('data-agent-native-node-id') ||
|
|
998
|
+
el.getAttribute('data-code-layer-id') ||
|
|
999
|
+
el.getAttribute('data-layer-id') ||
|
|
1000
|
+
el.id ||
|
|
1001
|
+
''
|
|
1002
|
+
);
|
|
1003
|
+
componentTagOverlay.textContent = compName + ' →';
|
|
1004
|
+
componentTagOverlay.setAttribute('data-component-node-id', nodeId);
|
|
1005
|
+
componentTagOverlay.setAttribute('data-component-name', compName);
|
|
1006
|
+
|
|
1007
|
+
var rect = el.getBoundingClientRect();
|
|
1008
|
+
var tagHeight = 22;
|
|
1009
|
+
var tagTop = rect.top - tagHeight - 4;
|
|
1010
|
+
if (tagTop < 4) tagTop = rect.top + 4;
|
|
1011
|
+
componentTagOverlay.style.display = 'block';
|
|
1012
|
+
componentTagOverlay.style.left = rect.left + 'px';
|
|
1013
|
+
componentTagOverlay.style.top = tagTop + 'px';
|
|
1014
|
+
// Accent outline on the selection overlay to distinguish component roots.
|
|
1015
|
+
selectionOverlay.style.outline = '2px solid var(--design-editor-accent-strong-color)';
|
|
1016
|
+
selectionOverlay.style.outlineOffset = '2px';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function clearComponentTag() {
|
|
1020
|
+
componentTagOverlay.style.display = 'none';
|
|
1021
|
+
componentTagOverlay.removeAttribute('data-component-node-id');
|
|
1022
|
+
componentTagOverlay.removeAttribute('data-component-name');
|
|
1023
|
+
selectionOverlay.style.outline = '';
|
|
1024
|
+
selectionOverlay.style.outlineOffset = '';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
591
1027
|
var selectedEl = null;
|
|
592
1028
|
var hoveredEl = null;
|
|
593
1029
|
var activeTextEditEl = null;
|
|
594
1030
|
var textEditPointerState = null;
|
|
595
1031
|
var pendingStructureMove = null;
|
|
1032
|
+
var pendingShieldDrag = null;
|
|
1033
|
+
var suppressNextShieldClick = false;
|
|
1034
|
+
var suppressNextShieldClickTimer = null;
|
|
1035
|
+
var selectedSpacingHovered = false;
|
|
1036
|
+
var hoveredSpacingHandleKey = '';
|
|
1037
|
+
var spacingHandleStateByKey = {};
|
|
1038
|
+
var spacingHandleNodesByKey = {};
|
|
1039
|
+
var spacingDrag = null;
|
|
596
1040
|
var lockedSelectors = [];
|
|
597
1041
|
var hiddenSelectors = [];
|
|
598
1042
|
|
|
1043
|
+
function clearRuntimeSelection() {
|
|
1044
|
+
selectedEl = null;
|
|
1045
|
+
hoveredEl = null;
|
|
1046
|
+
selectedSpacingHovered = false;
|
|
1047
|
+
hoveredSpacingHandleKey = '';
|
|
1048
|
+
spacingDrag = null;
|
|
1049
|
+
selectionOverlay.style.display = 'none';
|
|
1050
|
+
highlightOverlay.style.display = 'none';
|
|
1051
|
+
hideSpacingOverlay();
|
|
1052
|
+
hideMeasurements();
|
|
1053
|
+
clearComponentTag();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
599
1056
|
function matchesSelectorList(el, selectors) {
|
|
600
1057
|
if (!el || !selectors || selectors.length === 0) return false;
|
|
601
1058
|
for (var i = 0; i < selectors.length; i += 1) {
|
|
@@ -752,7 +1209,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
752
1209
|
for (var i = 0; i < activeCandidates.length && !selectedEl; i += 1) {
|
|
753
1210
|
try {
|
|
754
1211
|
var match = document.querySelector(activeCandidates[i]);
|
|
755
|
-
|
|
1212
|
+
// Skip the editor's own injected overlay chrome and re-anchor to a
|
|
1213
|
+
// source-backed element. A stale positional candidate like
|
|
1214
|
+
// body > div:nth-of-type(6) can otherwise re-match an overlay div
|
|
1215
|
+
// (the only direct div children of body at runtime), which then has
|
|
1216
|
+
// no code-layer node and fails every edit.
|
|
1217
|
+
if (match && !isLayerInteractionBlocked(match) && !isOverlayElement(match)) {
|
|
1218
|
+
selectedEl = selectionTargetForHit(match) || match;
|
|
1219
|
+
}
|
|
756
1220
|
} catch (_err) {}
|
|
757
1221
|
}
|
|
758
1222
|
if (selectedEl) {
|
|
@@ -766,50 +1230,441 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
766
1230
|
refreshOverlays();
|
|
767
1231
|
}
|
|
768
1232
|
|
|
769
|
-
function
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1233
|
+
function hideSpacingOverlay() {
|
|
1234
|
+
spacingOverlay.style.display = 'none';
|
|
1235
|
+
spacingOverlay.innerHTML = '';
|
|
1236
|
+
spacingHandleStateByKey = {};
|
|
1237
|
+
spacingHandleNodesByKey = {};
|
|
1238
|
+
if (!spacingDrag) spacingBadge.style.display = 'none';
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function visibleLayoutChildren(el) {
|
|
1242
|
+
if (!el || !el.children) return [];
|
|
1243
|
+
return Array.prototype.slice.call(el.children).filter(function(child) {
|
|
1244
|
+
if (!child || child.nodeType !== 1 || isOverlayElement(child) || isLayerInteractionBlocked(child)) return false;
|
|
1245
|
+
var cs = window.getComputedStyle(child);
|
|
1246
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed') return false;
|
|
1247
|
+
var rect = child.getBoundingClientRect();
|
|
1248
|
+
return rect.width > 0 && rect.height > 0;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function spacingColor(kind) {
|
|
1253
|
+
return kind === 'gap' ? '#ff4fd8' : 'var(--design-editor-accent-color)';
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function spacingFill(kind, orientation) {
|
|
1257
|
+
var tint = kind === 'gap' ? 'rgba(255, 79, 216, 0.28)' : 'rgba(46, 168, 255, 0.24)';
|
|
1258
|
+
var stripe = kind === 'gap' ? 'rgba(255, 79, 216, 0.58)' : 'rgba(46, 168, 255, 0.52)';
|
|
1259
|
+
var angle = orientation === 'vertical' ? '135deg' : '45deg';
|
|
1260
|
+
return 'repeating-linear-gradient(' + angle + ', ' + stripe + ' 0 1px, ' + tint + ' 1px 4px, transparent 4px 7px)';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function clampSpacingValue(value) {
|
|
1264
|
+
var rounded = Math.round(value);
|
|
1265
|
+
if (!Number.isFinite(rounded)) return 0;
|
|
1266
|
+
return Math.max(0, Math.min(999, rounded));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function makeSpacingHandle(config) {
|
|
1270
|
+
var region = config.region;
|
|
1271
|
+
if (!region || region.width <= 0 || region.height <= 0) return null;
|
|
1272
|
+
return {
|
|
1273
|
+
key: config.key,
|
|
1274
|
+
groupKey: config.groupKey || config.key,
|
|
1275
|
+
kind: config.kind,
|
|
1276
|
+
property: config.property,
|
|
1277
|
+
oppositeProperty: config.oppositeProperty || '',
|
|
1278
|
+
side: config.side || '',
|
|
1279
|
+
orientation: config.orientation,
|
|
1280
|
+
value: clampSpacingValue(config.value),
|
|
1281
|
+
region: {
|
|
1282
|
+
x: Math.round(region.x),
|
|
1283
|
+
y: Math.round(region.y),
|
|
1284
|
+
width: Math.max(1, Math.round(region.width)),
|
|
1285
|
+
height: Math.max(1, Math.round(region.height)),
|
|
1286
|
+
},
|
|
1287
|
+
line: config.line,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function childLocalRect(child, containerRect) {
|
|
1292
|
+
var rect = child.getBoundingClientRect();
|
|
1293
|
+
return {
|
|
1294
|
+
left: rect.left - containerRect.left,
|
|
1295
|
+
top: rect.top - containerRect.top,
|
|
1296
|
+
right: rect.right - containerRect.left,
|
|
1297
|
+
bottom: rect.bottom - containerRect.top,
|
|
1298
|
+
width: rect.width,
|
|
1299
|
+
height: rect.height,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function childRectsOverlap(a, b, axis) {
|
|
1304
|
+
if (axis === 'x') {
|
|
1305
|
+
return Math.max(a.top, b.top) < Math.min(a.bottom, b.bottom);
|
|
1306
|
+
}
|
|
1307
|
+
return Math.max(a.left, b.left) < Math.min(a.right, b.right);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function buildPaddingSpacingHandles(el, rect, cs) {
|
|
1311
|
+
var handles = [];
|
|
1312
|
+
var borderTop = readPx(cs.borderTopWidth);
|
|
1313
|
+
var borderRight = readPx(cs.borderRightWidth);
|
|
1314
|
+
var borderBottom = readPx(cs.borderBottomWidth);
|
|
1315
|
+
var borderLeft = readPx(cs.borderLeftWidth);
|
|
1316
|
+
var paddingTop = readPx(cs.paddingTop);
|
|
1317
|
+
var paddingRight = readPx(cs.paddingRight);
|
|
1318
|
+
var paddingBottom = readPx(cs.paddingBottom);
|
|
1319
|
+
var paddingLeft = readPx(cs.paddingLeft);
|
|
1320
|
+
var sx = chromeScaleX();
|
|
1321
|
+
var sy = chromeScaleY();
|
|
1322
|
+
var line = Math.max(1, chromeLineScale());
|
|
1323
|
+
var hLineWidth = Math.max(8, Math.min(24, rect.width * 0.18)) * sx;
|
|
1324
|
+
var vLineHeight = Math.max(8, Math.min(24, rect.height * 0.18)) * sy;
|
|
1325
|
+
var innerLeft = borderLeft;
|
|
1326
|
+
var innerTop = borderTop;
|
|
1327
|
+
var innerWidth = Math.max(1, rect.width - borderLeft - borderRight);
|
|
1328
|
+
var innerHeight = Math.max(1, rect.height - borderTop - borderBottom);
|
|
1329
|
+
if (paddingTop > 0) {
|
|
1330
|
+
handles.push(makeSpacingHandle({
|
|
1331
|
+
key: 'padding:top',
|
|
1332
|
+
kind: 'padding',
|
|
1333
|
+
property: 'paddingTop',
|
|
1334
|
+
oppositeProperty: 'paddingBottom',
|
|
1335
|
+
side: 'top',
|
|
1336
|
+
orientation: 'horizontal',
|
|
1337
|
+
value: paddingTop,
|
|
1338
|
+
region: { x: innerLeft, y: innerTop, width: innerWidth, height: paddingTop },
|
|
1339
|
+
line: {
|
|
1340
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1341
|
+
y: innerTop + paddingTop / 2 - line / 2,
|
|
1342
|
+
width: hLineWidth,
|
|
1343
|
+
height: line,
|
|
1344
|
+
},
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
if (paddingBottom > 0) {
|
|
1348
|
+
handles.push(makeSpacingHandle({
|
|
1349
|
+
key: 'padding:bottom',
|
|
1350
|
+
kind: 'padding',
|
|
1351
|
+
property: 'paddingBottom',
|
|
1352
|
+
oppositeProperty: 'paddingTop',
|
|
1353
|
+
side: 'bottom',
|
|
1354
|
+
orientation: 'horizontal',
|
|
1355
|
+
value: paddingBottom,
|
|
1356
|
+
region: { x: innerLeft, y: rect.height - borderBottom - paddingBottom, width: innerWidth, height: paddingBottom },
|
|
1357
|
+
line: {
|
|
1358
|
+
x: rect.width / 2 - hLineWidth / 2,
|
|
1359
|
+
y: rect.height - borderBottom - paddingBottom / 2 - line / 2,
|
|
1360
|
+
width: hLineWidth,
|
|
1361
|
+
height: line,
|
|
1362
|
+
},
|
|
1363
|
+
}));
|
|
1364
|
+
}
|
|
1365
|
+
if (paddingLeft > 0) {
|
|
1366
|
+
handles.push(makeSpacingHandle({
|
|
1367
|
+
key: 'padding:left',
|
|
1368
|
+
kind: 'padding',
|
|
1369
|
+
property: 'paddingLeft',
|
|
1370
|
+
oppositeProperty: 'paddingRight',
|
|
1371
|
+
side: 'left',
|
|
1372
|
+
orientation: 'vertical',
|
|
1373
|
+
value: paddingLeft,
|
|
1374
|
+
region: { x: innerLeft, y: innerTop, width: paddingLeft, height: innerHeight },
|
|
1375
|
+
line: {
|
|
1376
|
+
x: innerLeft + paddingLeft / 2 - line / 2,
|
|
1377
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1378
|
+
width: line,
|
|
1379
|
+
height: vLineHeight,
|
|
1380
|
+
},
|
|
1381
|
+
}));
|
|
1382
|
+
}
|
|
1383
|
+
if (paddingRight > 0) {
|
|
1384
|
+
handles.push(makeSpacingHandle({
|
|
1385
|
+
key: 'padding:right',
|
|
1386
|
+
kind: 'padding',
|
|
1387
|
+
property: 'paddingRight',
|
|
1388
|
+
oppositeProperty: 'paddingLeft',
|
|
1389
|
+
side: 'right',
|
|
1390
|
+
orientation: 'vertical',
|
|
1391
|
+
value: paddingRight,
|
|
1392
|
+
region: { x: rect.width - borderRight - paddingRight, y: innerTop, width: paddingRight, height: innerHeight },
|
|
1393
|
+
line: {
|
|
1394
|
+
x: rect.width - borderRight - paddingRight / 2 - line / 2,
|
|
1395
|
+
y: rect.height / 2 - vLineHeight / 2,
|
|
1396
|
+
width: line,
|
|
1397
|
+
height: vLineHeight,
|
|
1398
|
+
},
|
|
1399
|
+
}));
|
|
1400
|
+
}
|
|
1401
|
+
return handles.filter(Boolean);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function buildGapSpacingHandles(el, rect, cs) {
|
|
1405
|
+
var children = visibleLayoutChildren(el);
|
|
1406
|
+
if (children.length < 2) return [];
|
|
1407
|
+
var handles = [];
|
|
1408
|
+
var sx = chromeScaleX();
|
|
1409
|
+
var sy = chromeScaleY();
|
|
1410
|
+
var line = Math.max(1, chromeLineScale());
|
|
1411
|
+
var hLineWidth = 10 * sx;
|
|
1412
|
+
var vLineHeight = 10 * sy;
|
|
1413
|
+
var isFlex = cs.display === 'flex' || cs.display === 'inline-flex';
|
|
1414
|
+
var isGrid = cs.display === 'grid' || cs.display === 'inline-grid';
|
|
1415
|
+
if (!isFlex && !isGrid) return handles;
|
|
1416
|
+
var primaryAxis = isFlex && cs.flexDirection && cs.flexDirection.indexOf('column') === 0 ? 'y' : 'x';
|
|
1417
|
+
var childRects = children.map(function(child) {
|
|
1418
|
+
return childLocalRect(child, rect);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
function addAxisGaps(axis, property, groupKey) {
|
|
1422
|
+
var cssGap = readPx(cs[property]);
|
|
1423
|
+
if (cssGap <= 0) return;
|
|
1424
|
+
var sorted = childRects.slice().sort(function(a, b) {
|
|
1425
|
+
return axis === 'x' ? a.left - b.left : a.top - b.top;
|
|
1426
|
+
});
|
|
1427
|
+
var count = 0;
|
|
1428
|
+
for (var i = 0; i < sorted.length - 1; i += 1) {
|
|
1429
|
+
var a = sorted[i];
|
|
1430
|
+
var b = sorted[i + 1];
|
|
1431
|
+
if (!childRectsOverlap(a, b, axis)) continue;
|
|
1432
|
+
var gap = axis === 'x' ? b.left - a.right : b.top - a.bottom;
|
|
1433
|
+
if (gap <= 1) continue;
|
|
1434
|
+
if (axis === 'x') {
|
|
1435
|
+
var top = Math.max(a.top, b.top);
|
|
1436
|
+
var bottom = Math.min(a.bottom, b.bottom);
|
|
1437
|
+
var height = Math.max(1, bottom - top);
|
|
1438
|
+
handles.push(makeSpacingHandle({
|
|
1439
|
+
key: groupKey + ':' + count,
|
|
1440
|
+
groupKey: groupKey,
|
|
1441
|
+
kind: 'gap',
|
|
1442
|
+
property: property,
|
|
1443
|
+
orientation: 'vertical',
|
|
1444
|
+
value: cssGap,
|
|
1445
|
+
region: { x: a.right, y: top, width: gap, height: height },
|
|
1446
|
+
line: {
|
|
1447
|
+
x: a.right + gap / 2 - line / 2,
|
|
1448
|
+
y: top + height / 2 - vLineHeight / 2,
|
|
1449
|
+
width: line,
|
|
1450
|
+
height: vLineHeight,
|
|
1451
|
+
},
|
|
1452
|
+
}));
|
|
1453
|
+
} else {
|
|
1454
|
+
var left = Math.max(a.left, b.left);
|
|
1455
|
+
var right = Math.min(a.right, b.right);
|
|
1456
|
+
var width = Math.max(1, right - left);
|
|
1457
|
+
handles.push(makeSpacingHandle({
|
|
1458
|
+
key: groupKey + ':' + count,
|
|
1459
|
+
groupKey: groupKey,
|
|
1460
|
+
kind: 'gap',
|
|
1461
|
+
property: property,
|
|
1462
|
+
orientation: 'horizontal',
|
|
1463
|
+
value: cssGap,
|
|
1464
|
+
region: { x: left, y: a.bottom, width: width, height: gap },
|
|
1465
|
+
line: {
|
|
1466
|
+
x: left + width / 2 - hLineWidth / 2,
|
|
1467
|
+
y: a.bottom + gap / 2 - line / 2,
|
|
1468
|
+
width: hLineWidth,
|
|
1469
|
+
height: line,
|
|
1470
|
+
},
|
|
1471
|
+
}));
|
|
1472
|
+
}
|
|
1473
|
+
count += 1;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (primaryAxis === 'x') {
|
|
1478
|
+
addAxisGaps('x', 'columnGap', 'gap:column');
|
|
1479
|
+
if (isGrid) addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1480
|
+
} else {
|
|
1481
|
+
addAxisGaps('y', 'rowGap', 'gap:row');
|
|
1482
|
+
if (isGrid) addAxisGaps('x', 'columnGap', 'gap:column');
|
|
773
1483
|
}
|
|
1484
|
+
return handles.filter(Boolean);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function buildSpacingHandles(el) {
|
|
1488
|
+
if (!el || !document.documentElement.contains(el)) return [];
|
|
1489
|
+
if (Math.abs(currentRotation(el)) > 0.01) return [];
|
|
1490
|
+
var children = visibleLayoutChildren(el);
|
|
1491
|
+
if (children.length === 0) return [];
|
|
1492
|
+
var rect = el.getBoundingClientRect();
|
|
1493
|
+
if (rect.width <= 0 || rect.height <= 0) return [];
|
|
774
1494
|
var cs = window.getComputedStyle(el);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1495
|
+
return buildPaddingSpacingHandles(el, rect, cs).concat(buildGapSpacingHandles(el, rect, cs));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function showSpacingBadgeForHandle(handle, value) {
|
|
1499
|
+
if (!selectedEl || !handle) {
|
|
1500
|
+
spacingBadge.style.display = 'none';
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
var rect = selectedEl.getBoundingClientRect();
|
|
1504
|
+
var x = rect.left + handle.region.x + handle.region.width / 2;
|
|
1505
|
+
var y = rect.top + handle.region.y + handle.region.height / 2;
|
|
1506
|
+
spacingBadge.textContent = String(clampSpacingValue(value));
|
|
1507
|
+
spacingBadge.style.display = 'block';
|
|
1508
|
+
spacingBadge.style.background = spacingColor(handle.kind);
|
|
1509
|
+
spacingBadge.style.left = x + 'px';
|
|
1510
|
+
spacingBadge.style.top = y + 'px';
|
|
1511
|
+
spacingBadge.style.transform = 'translate(-50%, -50%)';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function renderSpacingHandle(handle, activeGroupKey) {
|
|
1515
|
+
if (!handle) return;
|
|
1516
|
+
spacingHandleStateByKey[handle.key] = handle;
|
|
1517
|
+
var highlighted = Boolean(activeGroupKey && handle.groupKey === activeGroupKey);
|
|
1518
|
+
var lineNode = document.createElement('span');
|
|
1519
|
+
lineNode.setAttribute('data-agent-native-spacing-line', handle.kind);
|
|
1520
|
+
lineNode.style.display = 'block';
|
|
1521
|
+
lineNode.style.left = handle.line.x + 'px';
|
|
1522
|
+
lineNode.style.top = handle.line.y + 'px';
|
|
1523
|
+
lineNode.style.width = Math.max(1, handle.line.width) + 'px';
|
|
1524
|
+
lineNode.style.height = Math.max(1, handle.line.height) + 'px';
|
|
1525
|
+
lineNode.style.background = spacingColor(handle.kind);
|
|
1526
|
+
spacingOverlay.appendChild(lineNode);
|
|
1527
|
+
|
|
1528
|
+
var regionNode = document.createElement('span');
|
|
1529
|
+
regionNode.setAttribute('data-agent-native-spacing-region', handle.kind);
|
|
1530
|
+
regionNode.setAttribute('data-orientation', handle.orientation);
|
|
1531
|
+
regionNode.setAttribute('data-spacing-key', handle.key);
|
|
1532
|
+
regionNode.style.display = 'block';
|
|
1533
|
+
regionNode.style.left = handle.region.x + 'px';
|
|
1534
|
+
regionNode.style.top = handle.region.y + 'px';
|
|
1535
|
+
regionNode.style.width = handle.region.width + 'px';
|
|
1536
|
+
regionNode.style.height = handle.region.height + 'px';
|
|
1537
|
+
regionNode.style.background = highlighted ? spacingFill(handle.kind, handle.orientation) : 'transparent';
|
|
1538
|
+
regionNode.style.outline = highlighted ? '1px solid ' + spacingColor(handle.kind) : '0';
|
|
1539
|
+
regionNode.style.outlineOffset = '-1px';
|
|
1540
|
+
regionNode.addEventListener('mouseenter', function() {
|
|
1541
|
+
hoveredSpacingHandleKey = handle.key;
|
|
1542
|
+
selectedSpacingHovered = true;
|
|
1543
|
+
updateSpacingOverlay(selectedEl);
|
|
1544
|
+
});
|
|
1545
|
+
regionNode.addEventListener('mouseleave', function() {
|
|
1546
|
+
if (spacingDrag) return;
|
|
1547
|
+
hoveredSpacingHandleKey = '';
|
|
1548
|
+
updateSpacingOverlay(selectedEl);
|
|
1549
|
+
});
|
|
1550
|
+
regionNode.addEventListener('mousedown', function(event) {
|
|
1551
|
+
startSpacingDrag(handle.key, event);
|
|
1552
|
+
}, true);
|
|
1553
|
+
spacingHandleNodesByKey[handle.key] = regionNode;
|
|
1554
|
+
spacingOverlay.appendChild(regionNode);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function updateSpacingOverlay(el) {
|
|
1558
|
+
if (el && el !== selectedEl) {
|
|
1559
|
+
hideSpacingOverlay();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (!selectedEl || !document.documentElement.contains(selectedEl)) {
|
|
1563
|
+
hideSpacingOverlay();
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (!selectedSpacingHovered && !hoveredSpacingHandleKey && !spacingDrag) {
|
|
1567
|
+
hideSpacingOverlay();
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
var handles = buildSpacingHandles(selectedEl);
|
|
1571
|
+
if (handles.length === 0) {
|
|
1572
|
+
hideSpacingOverlay();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
spacingOverlay.style.display = 'block';
|
|
1576
|
+
spacingOverlay.innerHTML = '';
|
|
1577
|
+
spacingHandleStateByKey = {};
|
|
1578
|
+
spacingHandleNodesByKey = {};
|
|
1579
|
+
var activeHandle =
|
|
1580
|
+
(spacingDrag && spacingDrag.handle) ||
|
|
1581
|
+
handles.find(function(handle) { return handle.key === hoveredSpacingHandleKey; }) ||
|
|
1582
|
+
null;
|
|
1583
|
+
var activeGroupKey = activeHandle ? activeHandle.groupKey : '';
|
|
1584
|
+
handles.forEach(function(handle) {
|
|
1585
|
+
renderSpacingHandle(handle, activeGroupKey);
|
|
1586
|
+
});
|
|
1587
|
+
if (activeHandle) {
|
|
1588
|
+
showSpacingBadgeForHandle(activeHandle, spacingDrag ? spacingDrag.currentValue : activeHandle.value);
|
|
1589
|
+
} else {
|
|
1590
|
+
spacingBadge.style.display = 'none';
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function applyEditorChromeScale() {
|
|
1595
|
+
syncEditorChromeScaleVars();
|
|
1596
|
+
var sx = chromeScaleX();
|
|
1597
|
+
var sy = chromeScaleY();
|
|
1598
|
+
var line = chromeLineScale();
|
|
1599
|
+
highlightOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
1600
|
+
selectionOverlay.style.borderWidth = (1.5 * line) + 'px';
|
|
1601
|
+
if (selectedEl) updateSpacingOverlay(selectedEl);
|
|
1602
|
+
|
|
1603
|
+
selectionOverlay.querySelectorAll('[data-agent-native-edge-handle]').forEach(function(edge) {
|
|
1604
|
+
var pos = edge.getAttribute('data-agent-native-edge-handle');
|
|
1605
|
+
if (pos === 'n' || pos === 's') {
|
|
1606
|
+
edge.style.height = (10 * sy) + 'px';
|
|
1607
|
+
edge.style[pos === 'n' ? 'top' : 'bottom'] = (-5 * sy) + 'px';
|
|
1608
|
+
}
|
|
1609
|
+
if (pos === 'e' || pos === 'w') {
|
|
1610
|
+
edge.style.width = (10 * sx) + 'px';
|
|
1611
|
+
edge.style[pos === 'w' ? 'left' : 'right'] = (-5 * sx) + 'px';
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
selectionOverlay.querySelectorAll('[data-agent-native-edit-handle]').forEach(function(handle) {
|
|
1616
|
+
var pos = handle.getAttribute('data-agent-native-edit-handle') || '';
|
|
1617
|
+
handle.style.width = (7 * sx) + 'px';
|
|
1618
|
+
handle.style.height = (7 * sy) + 'px';
|
|
1619
|
+
handle.style.borderWidth = Math.max(1, 1 * line) + 'px';
|
|
1620
|
+
if (pos.indexOf('n') !== -1) handle.style.top = (-4 * sy) + 'px';
|
|
1621
|
+
if (pos.indexOf('s') !== -1) handle.style.bottom = (-4 * sy) + 'px';
|
|
1622
|
+
if (pos.indexOf('w') !== -1) handle.style.left = (-4 * sx) + 'px';
|
|
1623
|
+
if (pos.indexOf('e') !== -1) handle.style.right = (-4 * sx) + 'px';
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
selectionOverlay.querySelectorAll('[data-agent-native-rotate-handle]').forEach(function(handle) {
|
|
1627
|
+
var pos = handle.getAttribute('data-agent-native-rotate-handle') || '';
|
|
1628
|
+
handle.style.width = (18 * sx) + 'px';
|
|
1629
|
+
handle.style.height = (18 * sy) + 'px';
|
|
1630
|
+
if (pos.indexOf('n') !== -1) handle.style.top = (-26 * sy) + 'px';
|
|
1631
|
+
if (pos.indexOf('s') !== -1) handle.style.bottom = (-26 * sy) + 'px';
|
|
1632
|
+
if (pos.indexOf('w') !== -1) handle.style.left = (-26 * sx) + 'px';
|
|
1633
|
+
if (pos.indexOf('e') !== -1) handle.style.right = (-26 * sx) + 'px';
|
|
1634
|
+
});
|
|
785
1635
|
}
|
|
786
1636
|
|
|
787
1637
|
function positionOverlay(overlay, el) {
|
|
788
1638
|
if (!el || !document.documentElement.contains(el)) {
|
|
789
1639
|
overlay.style.display = 'none';
|
|
1640
|
+
if (overlay === selectionOverlay) clearComponentTag();
|
|
790
1641
|
return;
|
|
791
1642
|
}
|
|
792
1643
|
// For the selection overlay, prefer the CSS box + rotation transform so
|
|
793
1644
|
// the handles hug the rotated element rather than its inflated AABB.
|
|
794
1645
|
if (overlay === selectionOverlay) {
|
|
795
1646
|
var elCs = window.getComputedStyle(el);
|
|
796
|
-
var elLeft =
|
|
797
|
-
var elTop =
|
|
798
|
-
var elW =
|
|
799
|
-
var elH =
|
|
1647
|
+
var elLeft = readFinitePx(el.style.left || elCs.left);
|
|
1648
|
+
var elTop = readFinitePx(el.style.top || elCs.top);
|
|
1649
|
+
var elW = readFinitePx(el.style.width || elCs.width);
|
|
1650
|
+
var elH = readFinitePx(el.style.height || elCs.height);
|
|
800
1651
|
var elRot = currentRotation(el);
|
|
1652
|
+
var canUseLocalBox = Math.abs(elRot) > 0.01 && elLeft !== null && elTop !== null && elW !== null && elH !== null;
|
|
801
1653
|
// Convert element-local left/top to viewport coords by walking to the
|
|
802
1654
|
// nearest positioned ancestor (same reference frame as getBoundingClientRect).
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1655
|
+
if (canUseLocalBox) {
|
|
1656
|
+
var parentRect = (el.offsetParent || document.documentElement).getBoundingClientRect();
|
|
1657
|
+
overlay.style.display = 'block';
|
|
1658
|
+
overlay.style.left = (parentRect.left + elLeft) + 'px';
|
|
1659
|
+
overlay.style.top = (parentRect.top + elTop) + 'px';
|
|
1660
|
+
overlay.style.width = elW + 'px';
|
|
1661
|
+
overlay.style.height = elH + 'px';
|
|
1662
|
+
overlay.style.transform = 'rotate(' + elRot + 'deg)';
|
|
1663
|
+
overlay.style.transformOrigin = '0 0';
|
|
1664
|
+
updateSpacingOverlay(el);
|
|
1665
|
+
updateComponentTag(el);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
813
1668
|
}
|
|
814
1669
|
var rect = el.getBoundingClientRect();
|
|
815
1670
|
overlay.style.display = 'block';
|
|
@@ -817,10 +1672,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
817
1672
|
overlay.style.left = rect.left + 'px';
|
|
818
1673
|
overlay.style.width = rect.width + 'px';
|
|
819
1674
|
overlay.style.height = rect.height + 'px';
|
|
1675
|
+
overlay.style.transform = '';
|
|
1676
|
+
if (overlay === selectionOverlay) {
|
|
1677
|
+
updateSpacingOverlay(el);
|
|
1678
|
+
updateComponentTag(el);
|
|
1679
|
+
}
|
|
820
1680
|
}
|
|
821
1681
|
|
|
822
1682
|
function refreshOverlays() {
|
|
823
|
-
if (hoveredEl
|
|
1683
|
+
if (hoveredEl && hoveredEl !== selectedEl) {
|
|
1684
|
+
positionOverlay(highlightOverlay, hoveredEl);
|
|
1685
|
+
} else {
|
|
1686
|
+
highlightOverlay.style.display = 'none';
|
|
1687
|
+
}
|
|
824
1688
|
if (selectedEl) positionOverlay(selectionOverlay, selectedEl);
|
|
825
1689
|
}
|
|
826
1690
|
|
|
@@ -891,6 +1755,13 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
891
1755
|
);
|
|
892
1756
|
}
|
|
893
1757
|
|
|
1758
|
+
function dragEventNames(e) {
|
|
1759
|
+
var pointerGesture = e && e.type && e.type.indexOf('pointer') === 0;
|
|
1760
|
+
return pointerGesture
|
|
1761
|
+
? { move: 'pointermove', up: 'pointerup' }
|
|
1762
|
+
: { move: 'mousemove', up: 'mouseup' };
|
|
1763
|
+
}
|
|
1764
|
+
|
|
894
1765
|
function elementFromEditorPoint(clientX, clientY) {
|
|
895
1766
|
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
|
|
896
1767
|
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
|
|
@@ -1055,6 +1926,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1055
1926
|
function selectElementAtEvent(e) {
|
|
1056
1927
|
stopNativeInteraction(e);
|
|
1057
1928
|
blurActiveTextEditor();
|
|
1929
|
+
if (suppressNextShieldClick) {
|
|
1930
|
+
suppressNextShieldClick = false;
|
|
1931
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1932
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1933
|
+
suppressNextShieldClickTimer = null;
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1058
1937
|
// Suppress the first click in a double-click sequence — the dblclick
|
|
1059
1938
|
// handler (beginTextEditingFromEvent) will fire immediately after and a
|
|
1060
1939
|
// spurious element-select would cause inspector flicker.
|
|
@@ -1062,26 +1941,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1062
1941
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1063
1942
|
if (!target || target === document.body || target === document.documentElement) {
|
|
1064
1943
|
// Click on empty canvas: clear the current selection (matches Figma).
|
|
1065
|
-
|
|
1066
|
-
selectionOverlay.style.display = 'none';
|
|
1944
|
+
clearRuntimeSelection();
|
|
1067
1945
|
window.parent.postMessage({ type: 'clear-selection' }, '*');
|
|
1068
1946
|
return;
|
|
1069
1947
|
}
|
|
1070
|
-
|
|
1948
|
+
selectedSpacingHovered = false;
|
|
1949
|
+
hoveredSpacingHandleKey = '';
|
|
1950
|
+
selectedEl = selectionTargetForHit(target);
|
|
1071
1951
|
var info = getElementInfo(selectedEl);
|
|
1072
1952
|
positionOverlay(selectionOverlay, selectedEl);
|
|
1073
1953
|
window.parent.postMessage({ type: 'element-select', payload: info }, '*');
|
|
1074
1954
|
}
|
|
1075
1955
|
|
|
1956
|
+
function suppressNextShieldClickBriefly() {
|
|
1957
|
+
suppressNextShieldClick = true;
|
|
1958
|
+
if (suppressNextShieldClickTimer !== null) {
|
|
1959
|
+
clearTimeout(suppressNextShieldClickTimer);
|
|
1960
|
+
}
|
|
1961
|
+
suppressNextShieldClickTimer = setTimeout(function() {
|
|
1962
|
+
suppressNextShieldClick = false;
|
|
1963
|
+
suppressNextShieldClickTimer = null;
|
|
1964
|
+
}, 250);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1076
1967
|
function openContextMenuAtEvent(e) {
|
|
1077
1968
|
stopNativeInteraction(e);
|
|
1078
1969
|
blurActiveTextEditor();
|
|
1079
1970
|
var target = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1080
1971
|
var info = null;
|
|
1081
1972
|
if (target) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1973
|
+
selectedSpacingHovered = false;
|
|
1974
|
+
hoveredSpacingHandleKey = '';
|
|
1975
|
+
selectedEl = selectionTargetForHit(target);
|
|
1976
|
+
info = getElementInfo(selectedEl);
|
|
1977
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
1085
1978
|
window.parent.postMessage({ type: 'element-select', payload: info }, '*');
|
|
1086
1979
|
}
|
|
1087
1980
|
window.parent.postMessage({
|
|
@@ -1138,6 +2031,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1138
2031
|
return Number.isFinite(num) ? num : 0;
|
|
1139
2032
|
}
|
|
1140
2033
|
|
|
2034
|
+
function readFinitePx(value) {
|
|
2035
|
+
if (!value || value === 'auto') return null;
|
|
2036
|
+
var num = parseFloat(value);
|
|
2037
|
+
return Number.isFinite(num) ? num : null;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1141
2040
|
function currentRotation(el) {
|
|
1142
2041
|
var transform = el.style.transform || window.getComputedStyle(el).transform || '';
|
|
1143
2042
|
var match = transform.match(/rotate\\((-?\\d+(?:\\.\\d+)?)deg\\)/);
|
|
@@ -1178,6 +2077,81 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1178
2077
|
}, '*');
|
|
1179
2078
|
}
|
|
1180
2079
|
|
|
2080
|
+
function spacingValueFromPointer(handle, originValue, startX, startY, clientX, clientY) {
|
|
2081
|
+
var delta =
|
|
2082
|
+
handle.orientation === 'vertical' ? clientX - startX : clientY - startY;
|
|
2083
|
+
if (handle.kind === 'padding' && (handle.side === 'right' || handle.side === 'bottom')) {
|
|
2084
|
+
delta = -delta;
|
|
2085
|
+
}
|
|
2086
|
+
return clampSpacingValue(originValue + delta);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
function applySpacingDragValue(target, handle, value, mirrorOpposite) {
|
|
2090
|
+
if (!target || !handle) return;
|
|
2091
|
+
target.style[handle.property] = value + 'px';
|
|
2092
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2093
|
+
target.style[handle.oppositeProperty] = value + 'px';
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function startSpacingDrag(key, e) {
|
|
2098
|
+
var handle = spacingHandleStateByKey[key];
|
|
2099
|
+
if (!selectedEl || !handle || isLayerInteractionBlocked(selectedEl)) return;
|
|
2100
|
+
e.preventDefault();
|
|
2101
|
+
e.stopPropagation();
|
|
2102
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2103
|
+
var events = dragEventNames(e);
|
|
2104
|
+
var dragEl = selectedEl;
|
|
2105
|
+
var originValue = handle.value;
|
|
2106
|
+
var startX = e.clientX;
|
|
2107
|
+
var startY = e.clientY;
|
|
2108
|
+
hoveredSpacingHandleKey = key;
|
|
2109
|
+
selectedSpacingHovered = true;
|
|
2110
|
+
spacingDrag = {
|
|
2111
|
+
handle: handle,
|
|
2112
|
+
currentValue: originValue,
|
|
2113
|
+
mirrorOpposite: !!e.altKey,
|
|
2114
|
+
};
|
|
2115
|
+
showSpacingBadgeForHandle(handle, originValue);
|
|
2116
|
+
|
|
2117
|
+
function onMove(ev) {
|
|
2118
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) return;
|
|
2119
|
+
var nextValue = spacingValueFromPointer(handle, originValue, startX, startY, ev.clientX, ev.clientY);
|
|
2120
|
+
spacingDrag = {
|
|
2121
|
+
handle: handle,
|
|
2122
|
+
currentValue: nextValue,
|
|
2123
|
+
mirrorOpposite: !!ev.altKey,
|
|
2124
|
+
};
|
|
2125
|
+
applySpacingDragValue(dragEl, handle, nextValue, !!ev.altKey);
|
|
2126
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2127
|
+
showSpacingBadgeForHandle(handle, nextValue);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function onUp(ev) {
|
|
2131
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2132
|
+
document.removeEventListener(events.up, onUp, true);
|
|
2133
|
+
if (!dragEl || !document.documentElement.contains(dragEl)) {
|
|
2134
|
+
spacingDrag = null;
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
var finalValue = spacingDrag ? spacingDrag.currentValue : originValue;
|
|
2138
|
+
var mirrorOpposite = spacingDrag ? spacingDrag.mirrorOpposite : !!ev.altKey;
|
|
2139
|
+
applySpacingDragValue(dragEl, handle, finalValue, mirrorOpposite);
|
|
2140
|
+
selectedEl = dragEl;
|
|
2141
|
+
spacingDrag = null;
|
|
2142
|
+
var styles = {};
|
|
2143
|
+
styles[handle.property] = finalValue + 'px';
|
|
2144
|
+
if (handle.kind === 'padding' && mirrorOpposite && handle.oppositeProperty) {
|
|
2145
|
+
styles[handle.oppositeProperty] = finalValue + 'px';
|
|
2146
|
+
}
|
|
2147
|
+
postVisualStyleChange(styles);
|
|
2148
|
+
positionOverlay(selectionOverlay, dragEl);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
document.addEventListener(events.move, onMove, true);
|
|
2152
|
+
document.addEventListener(events.up, onUp, true);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1181
2155
|
function postTextContentChange(el, value, html) {
|
|
1182
2156
|
window.parent.postMessage({
|
|
1183
2157
|
type: 'text-content-change',
|
|
@@ -1263,7 +2237,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1263
2237
|
}
|
|
1264
2238
|
|
|
1265
2239
|
function isOverlayElement(el) {
|
|
1266
|
-
|
|
2240
|
+
// Use closest() so that children of overlay elements (e.g. spacing-region
|
|
2241
|
+
// spans inside selectionOverlay that have pointer-events:auto via a CSS rule
|
|
2242
|
+
// and can therefore still be returned by elementFromPoint even when the
|
|
2243
|
+
// parent overlay has pointer-events:none set inline) are also treated as
|
|
2244
|
+
// overlay elements and never used as drag-drop anchor targets.
|
|
2245
|
+
return Boolean(
|
|
2246
|
+
el && el.closest && el.closest('[data-agent-native-edit-overlay]')
|
|
2247
|
+
);
|
|
1267
2248
|
}
|
|
1268
2249
|
|
|
1269
2250
|
function draggableElementChildren(parent) {
|
|
@@ -1277,9 +2258,40 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1277
2258
|
if (el === document.body || el === document.documentElement) return false;
|
|
1278
2259
|
var cs = window.getComputedStyle(el);
|
|
1279
2260
|
if (cs.position === 'absolute' || cs.position === 'fixed') return false;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// keep in sync with EditPanel.tsx CONTAINER_TAGS/LEAF_TAGS (~line 1679)
|
|
2265
|
+
var BRIDGE_CONTAINER_TAGS = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'form', 'ul', 'ol', 'figure', 'fieldset', 'details', 'dialog', 'blockquote', 'table', 'tbody', 'thead', 'tr'];
|
|
2266
|
+
var BRIDGE_LEAF_TAGS = ['img', 'video', 'picture', 'audio', 'canvas', 'svg', 'path', 'input', 'textarea', 'select', 'br', 'hr', 'iframe'];
|
|
2267
|
+
var BRIDGE_TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'strong', 'em', 'label', 'li'];
|
|
2268
|
+
|
|
2269
|
+
function isContainerDropTarget(el) {
|
|
2270
|
+
if (!el || el === document.documentElement) return false;
|
|
2271
|
+
if (isOverlayElement(el) || isLayerInteractionBlocked(el)) return false;
|
|
2272
|
+
if (el === document.body) return true;
|
|
2273
|
+
var tag = (el.tagName || '').toLowerCase();
|
|
2274
|
+
// Reject leaf/text tags — they cannot accept children
|
|
2275
|
+
if (BRIDGE_LEAF_TAGS.indexOf(tag) !== -1 || BRIDGE_TEXT_TAGS.indexOf(tag) !== -1) return false;
|
|
2276
|
+
var cs = window.getComputedStyle(el);
|
|
2277
|
+
if (
|
|
2278
|
+
cs.display === 'flex' ||
|
|
2279
|
+
cs.display === 'inline-flex' ||
|
|
2280
|
+
cs.display === 'grid' ||
|
|
2281
|
+
cs.display === 'inline-grid'
|
|
2282
|
+
) {
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2285
|
+
return BRIDGE_CONTAINER_TAGS.indexOf(tag) !== -1;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function edgePlacementForRect(rect, axis, clientX, clientY) {
|
|
2289
|
+
var size = axis === 'x' ? rect.width : rect.height;
|
|
2290
|
+
if (!size) return null;
|
|
2291
|
+
var offset = axis === 'x' ? clientX - rect.left : clientY - rect.top;
|
|
2292
|
+
if (offset < size * 0.22) return 'before';
|
|
2293
|
+
if (offset > size * 0.78) return 'after';
|
|
2294
|
+
return null;
|
|
1283
2295
|
}
|
|
1284
2296
|
|
|
1285
2297
|
function parentFlowAxis(parent) {
|
|
@@ -1304,18 +2316,19 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1304
2316
|
var hit = elementFromEditorPoint(clientX, clientY);
|
|
1305
2317
|
if (
|
|
1306
2318
|
hit &&
|
|
1307
|
-
hit !== document.body &&
|
|
1308
2319
|
hit !== document.documentElement &&
|
|
1309
2320
|
hit !== el &&
|
|
1310
2321
|
!el.contains(hit) &&
|
|
1311
|
-
!hit.contains(el) &&
|
|
1312
2322
|
!isOverlayElement(hit)
|
|
1313
2323
|
) {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2324
|
+
if (isContainerDropTarget(hit)) {
|
|
2325
|
+
var containerRect = hit.getBoundingClientRect();
|
|
2326
|
+
var edgeAxis = hit.parentElement ? parentFlowAxis(hit.parentElement) : parentFlowAxis(hit);
|
|
2327
|
+
var edgePlacement = edgePlacementForRect(containerRect, edgeAxis, clientX, clientY);
|
|
2328
|
+
if (!edgePlacement) {
|
|
2329
|
+
return { anchor: hit, placement: 'inside', axis: parentFlowAxis(hit) };
|
|
2330
|
+
}
|
|
2331
|
+
return { anchor: hit, placement: edgePlacement, axis: edgeAxis };
|
|
1319
2332
|
}
|
|
1320
2333
|
var hitParent = hit.parentElement;
|
|
1321
2334
|
if (hitParent) {
|
|
@@ -1362,6 +2375,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1362
2375
|
insertionGuide.style.display = 'block';
|
|
1363
2376
|
insertionGuide.style.background = 'var(--design-editor-accent-color)';
|
|
1364
2377
|
insertionGuide.style.border = '0';
|
|
2378
|
+
insertionGuide.style.borderRadius = '999px';
|
|
1365
2379
|
insertionGuide.style.boxShadow = '0 0 0 1px var(--design-editor-accent-color)';
|
|
1366
2380
|
if (target.placement === 'inside') {
|
|
1367
2381
|
insertionGuide.style.left = rect.left + 'px';
|
|
@@ -1370,6 +2384,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1370
2384
|
insertionGuide.style.height = rect.height + 'px';
|
|
1371
2385
|
insertionGuide.style.background = 'color-mix(in srgb, var(--design-editor-accent-color) 14%, transparent)';
|
|
1372
2386
|
insertionGuide.style.border = '2px solid var(--design-editor-accent-color)';
|
|
2387
|
+
insertionGuide.style.borderRadius = '4px';
|
|
1373
2388
|
insertionGuide.style.boxShadow = 'none';
|
|
1374
2389
|
return;
|
|
1375
2390
|
}
|
|
@@ -1437,6 +2452,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1437
2452
|
if (isLayerInteractionBlocked(selectedEl)) return;
|
|
1438
2453
|
e.preventDefault();
|
|
1439
2454
|
e.stopPropagation();
|
|
2455
|
+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
2456
|
+
var events = dragEventNames(e);
|
|
1440
2457
|
var originalSelectedEl = selectedEl;
|
|
1441
2458
|
var duplicatedForDrag = false;
|
|
1442
2459
|
if (e.altKey && selectedEl !== document.body && selectedEl !== document.documentElement) {
|
|
@@ -1454,16 +2471,96 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1454
2471
|
var reorderEl = selectedEl;
|
|
1455
2472
|
var currentTarget = reorderTargetForPoint(reorderEl, e.clientX, e.clientY);
|
|
1456
2473
|
showInsertionGuideFor(currentTarget);
|
|
2474
|
+
// Cross-screen drag state: true when the pointer is outside this iframe's
|
|
2475
|
+
// viewport bounds. The host frame renders the ghost + highlight and owns
|
|
2476
|
+
// the drop when this is true; the bridge suppresses its in-iframe reorder.
|
|
2477
|
+
var pointerOutsideIframe = false;
|
|
2478
|
+
var reorderSelector = getSelector(reorderEl);
|
|
2479
|
+
var reorderSourceId = getSourceId(reorderEl);
|
|
1457
2480
|
function onReorderMove(ev) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
2481
|
+
var vw = window.innerWidth;
|
|
2482
|
+
var vh = window.innerHeight;
|
|
2483
|
+
var cx = ev.clientX;
|
|
2484
|
+
var cy = ev.clientY;
|
|
2485
|
+
var outside = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2486
|
+
pointerOutsideIframe = outside;
|
|
2487
|
+
// Always notify the host frame so it can track the cursor position,
|
|
2488
|
+
// render the ghost, and highlight the target screen.
|
|
2489
|
+
window.parent.postMessage({
|
|
2490
|
+
type: 'agent-native:cross-screen-drag',
|
|
2491
|
+
phase: 'move',
|
|
2492
|
+
selector: reorderSelector,
|
|
2493
|
+
sourceId: reorderSourceId,
|
|
2494
|
+
iframeX: cx,
|
|
2495
|
+
iframeY: cy,
|
|
2496
|
+
viewportW: vw,
|
|
2497
|
+
viewportH: vh,
|
|
2498
|
+
}, '*');
|
|
2499
|
+
if (outside) {
|
|
2500
|
+
// Cursor left this iframe — hide the in-iframe insertion guide so
|
|
2501
|
+
// it does not render while the host shows a cross-screen drop target.
|
|
2502
|
+
hideInsertionGuide();
|
|
2503
|
+
showTransformBadge('Move layer', cx, cy);
|
|
2504
|
+
} else {
|
|
2505
|
+
// Cursor is inside this iframe — use existing in-iframe behavior.
|
|
2506
|
+
currentTarget = reorderTargetForPoint(reorderEl, cx, cy);
|
|
2507
|
+
showInsertionGuideFor(currentTarget);
|
|
2508
|
+
showTransformBadge(currentTarget ? 'Move layer' : 'Move', cx, cy);
|
|
2509
|
+
}
|
|
1461
2510
|
}
|
|
1462
|
-
function
|
|
1463
|
-
document.removeEventListener(
|
|
1464
|
-
document.removeEventListener(
|
|
2511
|
+
function onReorderEscape() {
|
|
2512
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2513
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2514
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
2515
|
+
hideTransformBadge();
|
|
2516
|
+
hideInsertionGuide();
|
|
2517
|
+
window.parent.postMessage({ type: 'agent-native:cross-screen-drag', phase: 'cancel' }, '*');
|
|
2518
|
+
// Revert any clone that was inserted for alt-drag.
|
|
2519
|
+
if (duplicatedForDrag && reorderEl && reorderEl !== originalSelectedEl) {
|
|
2520
|
+
if (reorderEl.parentElement) reorderEl.parentElement.removeChild(reorderEl);
|
|
2521
|
+
selectedEl = originalSelectedEl;
|
|
2522
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2523
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function onReorderKeyDown(ev) {
|
|
2527
|
+
if (ev.key === 'Escape') {
|
|
2528
|
+
ev.preventDefault();
|
|
2529
|
+
ev.stopPropagation();
|
|
2530
|
+
if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
|
|
2531
|
+
onReorderEscape();
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
function onReorderUp(ev) {
|
|
2535
|
+
document.removeEventListener(events.move, onReorderMove, true);
|
|
2536
|
+
document.removeEventListener(events.up, onReorderUp, true);
|
|
2537
|
+
document.removeEventListener('keydown', onReorderKeyDown, true);
|
|
1465
2538
|
hideTransformBadge();
|
|
1466
2539
|
hideInsertionGuide();
|
|
2540
|
+
var vw = window.innerWidth;
|
|
2541
|
+
var vh = window.innerHeight;
|
|
2542
|
+
var cx = ev ? ev.clientX : 0;
|
|
2543
|
+
var cy = ev ? ev.clientY : 0;
|
|
2544
|
+
var outsideOnDrop = cx < 0 || cy < 0 || cx > vw || cy > vh;
|
|
2545
|
+
// Post the end message so the host can finalize a cross-screen drop.
|
|
2546
|
+
window.parent.postMessage({
|
|
2547
|
+
type: 'agent-native:cross-screen-drag',
|
|
2548
|
+
phase: 'end',
|
|
2549
|
+
selector: reorderSelector,
|
|
2550
|
+
sourceId: reorderSourceId,
|
|
2551
|
+
iframeX: cx,
|
|
2552
|
+
iframeY: cy,
|
|
2553
|
+
viewportW: vw,
|
|
2554
|
+
viewportH: vh,
|
|
2555
|
+
}, '*');
|
|
2556
|
+
// When the pointer is outside this iframe at release, the host owns the
|
|
2557
|
+
// move (cross-screen drop). Do NOT apply the in-iframe reorder so we
|
|
2558
|
+
// avoid a ghost element left in screen A's DOM.
|
|
2559
|
+
// Use outsideOnDrop only — pointerOutsideIframe is stale when the user
|
|
2560
|
+
// briefly exits the iframe and re-enters before releasing. The host
|
|
2561
|
+
// already clears cross-screen state on re-entry so checking the
|
|
2562
|
+
// momentary excursion flag here would wrongly drop the element nowhere.
|
|
2563
|
+
if (outsideOnDrop) return;
|
|
1467
2564
|
if (!currentTarget) {
|
|
1468
2565
|
// No valid drop target — clean up the clone if one was inserted so
|
|
1469
2566
|
// no ghost element is left in the DOM.
|
|
@@ -1489,9 +2586,10 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1489
2586
|
applyRuntimeReorder(reorderEl, currentTarget);
|
|
1490
2587
|
postVisualStructureChange(reorderEl, currentTarget, { prevParent: prevParent, prevNextSibling: prevNextSibling });
|
|
1491
2588
|
}
|
|
1492
|
-
|
|
1493
|
-
document.addEventListener(
|
|
1494
|
-
document.addEventListener(
|
|
2589
|
+
}
|
|
2590
|
+
document.addEventListener(events.move, onReorderMove, true);
|
|
2591
|
+
document.addEventListener(events.up, onReorderUp, true);
|
|
2592
|
+
document.addEventListener('keydown', onReorderKeyDown, true);
|
|
1495
2593
|
return;
|
|
1496
2594
|
}
|
|
1497
2595
|
ensurePositionable(selectedEl);
|
|
@@ -1518,8 +2616,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1518
2616
|
refreshOverlays();
|
|
1519
2617
|
}
|
|
1520
2618
|
function onUp() {
|
|
1521
|
-
document.removeEventListener(
|
|
1522
|
-
document.removeEventListener(
|
|
2619
|
+
document.removeEventListener(events.move, onMove, true);
|
|
2620
|
+
document.removeEventListener(events.up, onUp, true);
|
|
1523
2621
|
hideTransformBadge();
|
|
1524
2622
|
if (!dragEl) return;
|
|
1525
2623
|
if (duplicatedForDrag && !moved) {
|
|
@@ -1545,8 +2643,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1545
2643
|
}, '*');
|
|
1546
2644
|
}
|
|
1547
2645
|
}
|
|
1548
|
-
document.addEventListener(
|
|
1549
|
-
document.addEventListener(
|
|
2646
|
+
document.addEventListener(events.move, onMove, true);
|
|
2647
|
+
document.addEventListener(events.up, onUp, true);
|
|
1550
2648
|
}
|
|
1551
2649
|
|
|
1552
2650
|
function startResize(handle, e) {
|
|
@@ -1602,9 +2700,16 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1602
2700
|
if (handle.indexOf('s') !== -1) height = origin.height + dy;
|
|
1603
2701
|
// Apply Shift / scaleToolEnabled aspect-ratio lock BEFORE the min-size
|
|
1604
2702
|
// clamp so the ratio is computed from unclamped values (bug fix).
|
|
1605
|
-
if (ev.shiftKey
|
|
1606
|
-
|
|
1607
|
-
|
|
2703
|
+
if (ev.shiftKey) {
|
|
2704
|
+
// Shift locks aspect ratio for ALL 8 handles (corners and edges).
|
|
2705
|
+
if (handle === 'e' || handle === 'w') {
|
|
2706
|
+
height = width / origin.ratio;
|
|
2707
|
+
} else if (handle === 'n' || handle === 's') {
|
|
2708
|
+
width = height * origin.ratio;
|
|
2709
|
+
} else if (handle.length === 2) {
|
|
2710
|
+
if (Math.abs(dx) > Math.abs(dy)) height = width / origin.ratio;
|
|
2711
|
+
else width = height * origin.ratio;
|
|
2712
|
+
}
|
|
1608
2713
|
}
|
|
1609
2714
|
if (scaleToolEnabled) {
|
|
1610
2715
|
// Scale tool: enforce aspect ratio on all 8 handles, not just corners.
|
|
@@ -1617,9 +2722,22 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1617
2722
|
else width = height * origin.ratio;
|
|
1618
2723
|
}
|
|
1619
2724
|
}
|
|
1620
|
-
// Clamp
|
|
1621
|
-
|
|
1622
|
-
|
|
2725
|
+
// Clamp to minimum size.
|
|
2726
|
+
var clampedW = Math.max(8, width);
|
|
2727
|
+
var clampedH = Math.max(8, height);
|
|
2728
|
+
// After clamping, re-apply the ratio if Shift or scale tool is active so
|
|
2729
|
+
// the clamped dimension doesn't silently break the locked aspect ratio.
|
|
2730
|
+
if (ev.shiftKey || scaleToolEnabled) {
|
|
2731
|
+
if (clampedW !== width) {
|
|
2732
|
+
// Width was clamped; re-derive height from the clamped width.
|
|
2733
|
+
clampedH = Math.max(8, clampedW / origin.ratio);
|
|
2734
|
+
} else if (clampedH !== height) {
|
|
2735
|
+
// Height was clamped; re-derive width from the clamped height.
|
|
2736
|
+
clampedW = Math.max(8, clampedH * origin.ratio);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
width = clampedW;
|
|
2740
|
+
height = clampedH;
|
|
1623
2741
|
// Re-anchor the pinned edge for w/n handles after aspect-ratio lock and
|
|
1624
2742
|
// clamping so the opposite (e/s) edge stays fixed regardless of whether
|
|
1625
2743
|
// the dimension change was driven by raw dx/dy or by the ratio lock.
|
|
@@ -1703,7 +2821,68 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1703
2821
|
document.addEventListener('mouseup', onUp, true);
|
|
1704
2822
|
}
|
|
1705
2823
|
|
|
2824
|
+
function clearPendingShieldDrag() {
|
|
2825
|
+
if (!pendingShieldDrag) return;
|
|
2826
|
+
document.removeEventListener(pendingShieldDrag.move, pendingShieldDrag.onMove, true);
|
|
2827
|
+
document.removeEventListener(pendingShieldDrag.up, pendingShieldDrag.onUp, true);
|
|
2828
|
+
pendingShieldDrag = null;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function beginPotentialShieldDrag(e) {
|
|
2832
|
+
stopNativeInteraction(e);
|
|
2833
|
+
if (e.button !== 0 || activeTextEditEl) return;
|
|
2834
|
+
var events = dragEventNames(e);
|
|
2835
|
+
var hit = elementFromEditorPoint(e.clientX, e.clientY);
|
|
2836
|
+
if (!hit || hit === document.body || hit === document.documentElement) return;
|
|
2837
|
+
var dragTarget =
|
|
2838
|
+
selectedEl &&
|
|
2839
|
+
document.documentElement.contains(selectedEl) &&
|
|
2840
|
+
selectedEl.contains(hit)
|
|
2841
|
+
? selectedEl
|
|
2842
|
+
: selectionTargetForHit(hit);
|
|
2843
|
+
if (
|
|
2844
|
+
!dragTarget ||
|
|
2845
|
+
dragTarget === document.body ||
|
|
2846
|
+
dragTarget === document.documentElement ||
|
|
2847
|
+
isLayerInteractionBlocked(dragTarget)
|
|
2848
|
+
) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
var startX = e.clientX;
|
|
2852
|
+
var startY = e.clientY;
|
|
2853
|
+
var didStartDrag = false;
|
|
2854
|
+
function selectDragTarget() {
|
|
2855
|
+
selectedEl = dragTarget;
|
|
2856
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
2857
|
+
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(selectedEl) }, '*');
|
|
2858
|
+
}
|
|
2859
|
+
function onMove(ev) {
|
|
2860
|
+
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
|
|
2861
|
+
clearPendingShieldDrag();
|
|
2862
|
+
didStartDrag = true;
|
|
2863
|
+
selectDragTarget();
|
|
2864
|
+
suppressNextShieldClickBriefly();
|
|
2865
|
+
startMove(ev);
|
|
2866
|
+
}
|
|
2867
|
+
function onUp(ev) {
|
|
2868
|
+
clearPendingShieldDrag();
|
|
2869
|
+
if (didStartDrag) return;
|
|
2870
|
+
if (ev) stopNativeInteraction(ev);
|
|
2871
|
+
selectDragTarget();
|
|
2872
|
+
suppressNextShieldClickBriefly();
|
|
2873
|
+
}
|
|
2874
|
+
clearPendingShieldDrag();
|
|
2875
|
+
pendingShieldDrag = { move: events.move, up: events.up, onMove: onMove, onUp: onUp };
|
|
2876
|
+
document.addEventListener(events.move, onMove, true);
|
|
2877
|
+
document.addEventListener(events.up, onUp, true);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
1706
2880
|
selectionOverlay.addEventListener('mousedown', function(e) {
|
|
2881
|
+
var spacingKey = e.target && e.target.getAttribute && e.target.getAttribute('data-spacing-key');
|
|
2882
|
+
if (spacingKey) {
|
|
2883
|
+
startSpacingDrag(spacingKey, e);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
1707
2886
|
var resizeHandle = e.target && e.target.getAttribute && e.target.getAttribute('data-agent-native-edit-handle');
|
|
1708
2887
|
if (!resizeHandle && e.target && e.target.getAttribute) {
|
|
1709
2888
|
resizeHandle = e.target.getAttribute('data-agent-native-edge-handle');
|
|
@@ -1720,6 +2899,8 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1720
2899
|
startMove(e);
|
|
1721
2900
|
}, true);
|
|
1722
2901
|
|
|
2902
|
+
shieldOverlay.addEventListener('pointerdown', beginPotentialShieldDrag, true);
|
|
2903
|
+
|
|
1723
2904
|
['pointerdown','pointerup','mousedown','mouseup','auxclick'].forEach(function(type) {
|
|
1724
2905
|
shieldOverlay.addEventListener(type, stopNativeInteraction, true);
|
|
1725
2906
|
});
|
|
@@ -1735,6 +2916,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1735
2916
|
document.addEventListener('keydown', function(e) {
|
|
1736
2917
|
if (!shouldForwardDesignHotkey(e)) return;
|
|
1737
2918
|
stopNativeInteraction(e);
|
|
2919
|
+
if (e.key === 'Escape') clearRuntimeSelection();
|
|
1738
2920
|
window.parent.postMessage({
|
|
1739
2921
|
type: 'design-hotkey',
|
|
1740
2922
|
key: e.key,
|
|
@@ -1788,7 +2970,12 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1788
2970
|
stopNativeInteraction(e);
|
|
1789
2971
|
var target = findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY));
|
|
1790
2972
|
if (!target || target.nodeType !== 1) return;
|
|
1791
|
-
|
|
2973
|
+
// Anchor the selection identity to the nearest source-backed element. Text
|
|
2974
|
+
// editing still operates on the actual target text node, but a later
|
|
2975
|
+
// style edit posts from selectedEl, so it must point at a patchable
|
|
2976
|
+
// code-layer node rather than a runtime-only descendant (which would emit a
|
|
2977
|
+
// brittle body > div:nth-of-type(...) selector that never resolves).
|
|
2978
|
+
selectedEl = selectionTargetForHit(target) || target;
|
|
1792
2979
|
var originalText = target.textContent || '';
|
|
1793
2980
|
var originalHtml = target.innerHTML || '';
|
|
1794
2981
|
var committed = false;
|
|
@@ -1801,6 +2988,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1801
2988
|
setTextEditingPointerPassthrough(true);
|
|
1802
2989
|
positionOverlay(selectionOverlay, target);
|
|
1803
2990
|
window.parent.postMessage({ type: 'element-select', payload: getElementInfo(target) }, '*');
|
|
2991
|
+
window.parent.postMessage({ type: 'element-dblclick-text', payload: getElementInfo(target) }, '*');
|
|
1804
2992
|
postTextEditingState(target, true);
|
|
1805
2993
|
|
|
1806
2994
|
function finish(commit) {
|
|
@@ -1882,11 +3070,30 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1882
3070
|
hoveredEl = elementFromEditorPoint(e.clientX, e.clientY);
|
|
1883
3071
|
if (!hoveredEl) {
|
|
1884
3072
|
highlightOverlay.style.display = 'none';
|
|
3073
|
+
if (!spacingDrag) {
|
|
3074
|
+
selectedSpacingHovered = false;
|
|
3075
|
+
hoveredSpacingHandleKey = '';
|
|
3076
|
+
updateSpacingOverlay(selectedEl);
|
|
3077
|
+
}
|
|
1885
3078
|
hideMeasurements();
|
|
1886
3079
|
return;
|
|
1887
3080
|
}
|
|
1888
3081
|
if (hoveredEl && hoveredEl.closest('[data-agent-native-text-editing]')) return;
|
|
1889
|
-
|
|
3082
|
+
if (!spacingDrag) {
|
|
3083
|
+
selectedSpacingHovered = Boolean(
|
|
3084
|
+
selectedEl &&
|
|
3085
|
+
hoveredEl &&
|
|
3086
|
+
(hoveredEl === selectedEl ||
|
|
3087
|
+
(selectedEl.contains && selectedEl.contains(hoveredEl))),
|
|
3088
|
+
);
|
|
3089
|
+
if (!selectedSpacingHovered) hoveredSpacingHandleKey = '';
|
|
3090
|
+
updateSpacingOverlay(selectedEl);
|
|
3091
|
+
}
|
|
3092
|
+
if (hoveredEl === selectedEl) {
|
|
3093
|
+
highlightOverlay.style.display = 'none';
|
|
3094
|
+
} else {
|
|
3095
|
+
positionOverlay(highlightOverlay, hoveredEl);
|
|
3096
|
+
}
|
|
1890
3097
|
if (e.altKey && selectedEl && hoveredEl && selectedEl !== hoveredEl) {
|
|
1891
3098
|
showMeasurements(selectedEl, hoveredEl);
|
|
1892
3099
|
} else {
|
|
@@ -1899,8 +3106,14 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1899
3106
|
shieldOverlay.addEventListener('pointerleave', function(e) {
|
|
1900
3107
|
stopNativeInteraction(e);
|
|
1901
3108
|
hoveredEl = null;
|
|
3109
|
+
if (!spacingDrag) {
|
|
3110
|
+
selectedSpacingHovered = false;
|
|
3111
|
+
hoveredSpacingHandleKey = '';
|
|
3112
|
+
updateSpacingOverlay(selectedEl);
|
|
3113
|
+
}
|
|
1902
3114
|
highlightOverlay.style.display = 'none';
|
|
1903
3115
|
hideMeasurements();
|
|
3116
|
+
window.parent.postMessage({ type: 'element-hover', payload: null }, '*');
|
|
1904
3117
|
}, true);
|
|
1905
3118
|
|
|
1906
3119
|
window.addEventListener('keyup', function(e) {
|
|
@@ -1910,16 +3123,21 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1910
3123
|
window.addEventListener('message', function(e) {
|
|
1911
3124
|
if (e.source !== window.parent) return;
|
|
1912
3125
|
if (!e.data) return;
|
|
3126
|
+
if (e.data.type === 'set-editor-chrome-scale') {
|
|
3127
|
+
// Live-update the constant-size chrome scale WITHOUT rebuilding srcdoc.
|
|
3128
|
+
// Rebuilding srcdoc reloads the iframe and flashes the content white.
|
|
3129
|
+
editorChromeScaleX = Math.max(0.05, Number(e.data.scaleX) || 1);
|
|
3130
|
+
editorChromeScaleY = Math.max(0.05, Number(e.data.scaleY) || editorChromeScaleX);
|
|
3131
|
+
applyEditorChromeScale();
|
|
3132
|
+
if (selectedEl || hoveredEl) refreshOverlays();
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
1913
3135
|
if (e.data.type === 'scale-tool-mode') {
|
|
1914
3136
|
scaleToolEnabled = !!e.data.enabled;
|
|
1915
3137
|
return;
|
|
1916
3138
|
}
|
|
1917
3139
|
if (e.data.type === 'clear-selection') {
|
|
1918
|
-
|
|
1919
|
-
hoveredEl = null;
|
|
1920
|
-
selectionOverlay.style.display = 'none';
|
|
1921
|
-
highlightOverlay.style.display = 'none';
|
|
1922
|
-
hideMeasurements();
|
|
3140
|
+
clearRuntimeSelection();
|
|
1923
3141
|
return;
|
|
1924
3142
|
}
|
|
1925
3143
|
if (e.data.type === 'select-element') {
|
|
@@ -1952,10 +3170,11 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1952
3170
|
} catch (_err) {}
|
|
1953
3171
|
}
|
|
1954
3172
|
if (!target) return;
|
|
3173
|
+
selectedSpacingHovered = false;
|
|
3174
|
+
hoveredSpacingHandleKey = '';
|
|
1955
3175
|
selectedEl = target;
|
|
1956
|
-
var selectedInfo = getElementInfo(target);
|
|
1957
3176
|
positionOverlay(selectionOverlay, target);
|
|
1958
|
-
|
|
3177
|
+
if (hoveredEl === selectedEl) highlightOverlay.style.display = 'none';
|
|
1959
3178
|
return;
|
|
1960
3179
|
}
|
|
1961
3180
|
if (e.data.type === 'hover-element') {
|
|
@@ -1978,7 +3197,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
1978
3197
|
}
|
|
1979
3198
|
var hoverTarget = findRuntimeTarget(String(e.data.selector || ''), hoverCandidates);
|
|
1980
3199
|
hoveredEl = hoverTarget;
|
|
1981
|
-
if (hoveredEl && !isLayerInteractionBlocked(hoveredEl)) {
|
|
3200
|
+
if (hoveredEl && !isLayerInteractionBlocked(hoveredEl) && hoveredEl !== selectedEl) {
|
|
1982
3201
|
positionOverlay(highlightOverlay, hoveredEl);
|
|
1983
3202
|
} else {
|
|
1984
3203
|
highlightOverlay.style.display = 'none';
|
|
@@ -2045,6 +3264,7 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2045
3264
|
|
|
2046
3265
|
window.addEventListener('scroll', refreshOverlays, true);
|
|
2047
3266
|
window.addEventListener('resize', refreshOverlays);
|
|
3267
|
+
applyEditorChromeScale();
|
|
2048
3268
|
})();
|
|
2049
3269
|
</script>
|
|
2050
3270
|
`;
|
|
@@ -2052,6 +3272,41 @@ const EDITOR_CHROME_BRIDGE_SCRIPT = `
|
|
|
2052
3272
|
interface DesignCanvasProps {
|
|
2053
3273
|
content: string;
|
|
2054
3274
|
contentKey?: string;
|
|
3275
|
+
/**
|
|
3276
|
+
* The runtime source tier for this canvas.
|
|
3277
|
+
*
|
|
3278
|
+
* - `"inline"` (default) — HTML/Alpine `srcdoc` iframe; same-origin null
|
|
3279
|
+
* origin; all bridge scripts injected by DesignCanvas.
|
|
3280
|
+
* - `"localhost"` — `src=devServerUrl`; dev server is same-origin in most
|
|
3281
|
+
* setups; bridge trust: origin must match parent or be "null".
|
|
3282
|
+
* - `"fusion"` — `src=builderHostedUrl`; cross-origin Builder-hosted app;
|
|
3283
|
+
* bridge trust is relaxed to window-identity only (no origin check) so
|
|
3284
|
+
* the Builder-hosted iframe can communicate with the editor. The sandbox
|
|
3285
|
+
* grants `allow-same-origin` so the Builder app can reach its own resources.
|
|
3286
|
+
*
|
|
3287
|
+
* When omitted, DesignCanvas infers the tier from the content value:
|
|
3288
|
+
* a value that passes `getExternalPreviewUrl` is treated as `"localhost"`;
|
|
3289
|
+
* otherwise `"inline"`. Pass `sourceType="fusion"` explicitly when the
|
|
3290
|
+
* content URL is a Builder-hosted (cross-origin) app so the bridge security
|
|
3291
|
+
* model uses window-identity trust instead of same-origin trust.
|
|
3292
|
+
*/
|
|
3293
|
+
sourceType?: "inline" | "localhost" | "fusion";
|
|
3294
|
+
/**
|
|
3295
|
+
* Explicit Builder-hosted app URL for fusion source rendering.
|
|
3296
|
+
*
|
|
3297
|
+
* When `sourceType === "fusion"` and this prop is provided, the iframe uses
|
|
3298
|
+
* this URL as `src` regardless of what `content` contains. This lets the
|
|
3299
|
+
* caller hold the original inline HTML in `content` (for collab/history
|
|
3300
|
+
* purposes) while pointing the canvas at the migrated Builder-hosted app.
|
|
3301
|
+
*
|
|
3302
|
+
* When absent and `sourceType === "fusion"`, the component falls back to
|
|
3303
|
+
* the existing external-URL detection on `content` (i.e. if `content` is
|
|
3304
|
+
* itself a URL it is used as-is, which is the pattern when the branch URL
|
|
3305
|
+
* has been written into the design file content).
|
|
3306
|
+
*
|
|
3307
|
+
* For `"inline"` and `"localhost"` sources this prop is ignored.
|
|
3308
|
+
*/
|
|
3309
|
+
fusionUrl?: string;
|
|
2055
3310
|
zoom: number;
|
|
2056
3311
|
onZoomChange?: (zoom: number) => void;
|
|
2057
3312
|
deviceFrame: DeviceFrameType;
|
|
@@ -2060,12 +3315,17 @@ interface DesignCanvasProps {
|
|
|
2060
3315
|
viewportHeight: number;
|
|
2061
3316
|
displayWidth: number;
|
|
2062
3317
|
displayHeight: number;
|
|
3318
|
+
fluid?: boolean;
|
|
2063
3319
|
};
|
|
3320
|
+
editorChromeScaleX?: number;
|
|
3321
|
+
editorChromeScaleY?: number;
|
|
2064
3322
|
editMode: boolean;
|
|
2065
3323
|
interactMode: boolean;
|
|
3324
|
+
readOnly?: boolean;
|
|
2066
3325
|
scaleMode?: boolean;
|
|
2067
3326
|
onElementSelect: (info: ElementInfo) => void;
|
|
2068
|
-
onElementHover: (info: ElementInfo) => void;
|
|
3327
|
+
onElementHover: (info: ElementInfo | null) => void;
|
|
3328
|
+
onClearSelection?: () => void;
|
|
2069
3329
|
onVisualStyleChange?: (
|
|
2070
3330
|
selector: string,
|
|
2071
3331
|
styles: Record<string, string>,
|
|
@@ -2082,6 +3342,7 @@ interface DesignCanvasProps {
|
|
|
2082
3342
|
selector?: string;
|
|
2083
3343
|
hasRange?: boolean;
|
|
2084
3344
|
}) => void;
|
|
3345
|
+
onElementDblClickText?: (info: ElementInfo) => void;
|
|
2085
3346
|
onIframeHotkey?: (event: IframeHotkeyPayload) => void;
|
|
2086
3347
|
onIframeContextMenu?: (event: IframeContextMenuPayload) => void;
|
|
2087
3348
|
onVisualStructureChange?: (
|
|
@@ -2120,6 +3381,7 @@ interface DesignCanvasProps {
|
|
|
2120
3381
|
lockedSelectors?: string[];
|
|
2121
3382
|
hiddenSelectors?: string[];
|
|
2122
3383
|
clearSelectionRequest?: number;
|
|
3384
|
+
registerRuntimeBridge?: boolean;
|
|
2123
3385
|
/** Called when the user exits pin mode. */
|
|
2124
3386
|
onExitPinMode?: () => void;
|
|
2125
3387
|
/** Stable id of the open design (used for pin scoping + agent prompt). */
|
|
@@ -2137,6 +3399,55 @@ interface DesignCanvasProps {
|
|
|
2137
3399
|
* opened in a new tab by the iframe itself and never reach this callback.
|
|
2138
3400
|
*/
|
|
2139
3401
|
onPrototypeNavigate?: (screen: string, href: string) => void;
|
|
3402
|
+
/**
|
|
3403
|
+
* Motion tracks to load into the iframe's motion-preview bridge. Sent via
|
|
3404
|
+
* `motion-load-tracks` whenever this prop changes. When cleared
|
|
3405
|
+
* (`undefined` or `[]`) a `motion-preview-clear` message is sent to remove
|
|
3406
|
+
* any applied preview overrides.
|
|
3407
|
+
*
|
|
3408
|
+
* The MotionDock sends scrub ticks as `{ type: 'motion-preview', t,
|
|
3409
|
+
* durationMs }` directly from its `canvasIframeRef`. DesignCanvas only
|
|
3410
|
+
* needs the tracks so the bridge can interpolate values at each tick.
|
|
3411
|
+
*/
|
|
3412
|
+
motionTracks?: MotionTrackWire[];
|
|
3413
|
+
/**
|
|
3414
|
+
* Explicit iframe width in pixels. When provided it overrides the width
|
|
3415
|
+
* derived from `deviceFrame`, enabling per-breakpoint preview (e.g. Mobile
|
|
3416
|
+
* 390 / Tablet 768 / Desktop 1280 side-by-side frames in the overview).
|
|
3417
|
+
* The height still comes from `deviceFrame`; `deviceFrame="none"` keeps
|
|
3418
|
+
* 100% height.
|
|
3419
|
+
*/
|
|
3420
|
+
previewWidthPx?: number;
|
|
3421
|
+
/**
|
|
3422
|
+
* Shader-fill CSS preview to apply to a selected element inside the iframe.
|
|
3423
|
+
*
|
|
3424
|
+
* When set, the canvas sends a `shader-fill-preview` bridge message that
|
|
3425
|
+
* applies the CSS `background` value on the target element **without
|
|
3426
|
+
* persisting anything**. When cleared (`null` / `undefined`) a
|
|
3427
|
+
* `shader-fill-preview-clear` message is sent to restore the original
|
|
3428
|
+
* background.
|
|
3429
|
+
*
|
|
3430
|
+
* Preview-only — never writes to DB, Yjs, or source. Part of the §6.7
|
|
3431
|
+
* shader-fill PREVIEW path; the apply path remains gated until runtime
|
|
3432
|
+
* rendering + source-write + diff proof are all in place.
|
|
3433
|
+
*/
|
|
3434
|
+
shaderFillPreview?: {
|
|
3435
|
+
/** CSS selector for the target element (preferred over nodeId). */
|
|
3436
|
+
selector?: string;
|
|
3437
|
+
/** data-agent-native-node-id value for the target element. */
|
|
3438
|
+
nodeId?: string;
|
|
3439
|
+
/** The CSS `background` value returned by preview-shader-fill. */
|
|
3440
|
+
css: string;
|
|
3441
|
+
} | null;
|
|
3442
|
+
/**
|
|
3443
|
+
* Called when the user clicks the component-instance source tag (the
|
|
3444
|
+
* "ComponentName →" pill that floats above a selected component root).
|
|
3445
|
+
* The parent should invoke `open-component-source` with these params.
|
|
3446
|
+
*/
|
|
3447
|
+
onComponentSourceJump?: (params: {
|
|
3448
|
+
nodeId: string;
|
|
3449
|
+
componentName: string;
|
|
3450
|
+
}) => void;
|
|
2140
3451
|
}
|
|
2141
3452
|
|
|
2142
3453
|
function getExternalPreviewUrl(content: string): string | null {
|
|
@@ -2172,19 +3483,26 @@ export interface IframeContextMenuPayload {
|
|
|
2172
3483
|
export function DesignCanvas({
|
|
2173
3484
|
content,
|
|
2174
3485
|
contentKey,
|
|
3486
|
+
sourceType,
|
|
3487
|
+
fusionUrl,
|
|
2175
3488
|
zoom,
|
|
2176
3489
|
onZoomChange,
|
|
2177
3490
|
deviceFrame,
|
|
2178
3491
|
embeddedFrame,
|
|
3492
|
+
editorChromeScaleX = 1,
|
|
3493
|
+
editorChromeScaleY = editorChromeScaleX,
|
|
2179
3494
|
editMode,
|
|
2180
3495
|
interactMode,
|
|
3496
|
+
readOnly = false,
|
|
2181
3497
|
scaleMode = false,
|
|
2182
3498
|
clearSelectionRequest,
|
|
2183
3499
|
onElementSelect,
|
|
2184
3500
|
onElementHover,
|
|
3501
|
+
onClearSelection,
|
|
2185
3502
|
onVisualStyleChange,
|
|
2186
3503
|
onTextContentChange,
|
|
2187
3504
|
onTextEditingStateChange,
|
|
3505
|
+
onElementDblClickText,
|
|
2188
3506
|
onIframeHotkey,
|
|
2189
3507
|
onIframeContextMenu,
|
|
2190
3508
|
onVisualStructureChange,
|
|
@@ -2200,11 +3518,16 @@ export function DesignCanvas({
|
|
|
2200
3518
|
lockedSelectors = [],
|
|
2201
3519
|
hiddenSelectors = [],
|
|
2202
3520
|
onExitPinMode,
|
|
3521
|
+
registerRuntimeBridge = true,
|
|
2203
3522
|
designId,
|
|
2204
3523
|
designTitle,
|
|
2205
3524
|
commentContextId,
|
|
2206
3525
|
commentContextLabel,
|
|
2207
3526
|
onPrototypeNavigate,
|
|
3527
|
+
motionTracks,
|
|
3528
|
+
previewWidthPx,
|
|
3529
|
+
onComponentSourceJump,
|
|
3530
|
+
shaderFillPreview,
|
|
2208
3531
|
}: DesignCanvasProps) {
|
|
2209
3532
|
const t = useT();
|
|
2210
3533
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
@@ -2215,10 +3538,25 @@ export function DesignCanvas({
|
|
|
2215
3538
|
const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
|
|
2216
3539
|
const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
|
|
2217
3540
|
const isEmbeddedFrame = Boolean(embeddedFrame);
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
)
|
|
3541
|
+
// Resolve the URL to render in the iframe:
|
|
3542
|
+
// 1. When sourceType === "fusion" and fusionUrl is set, prefer the explicit
|
|
3543
|
+
// Builder-hosted URL over whatever is in `content` (which may still be the
|
|
3544
|
+
// original inline HTML).
|
|
3545
|
+
// 2. Otherwise fall back to the content-based URL detection (handles the case
|
|
3546
|
+
// where the branch URL has been written into the design file content, or
|
|
3547
|
+
// where the localhost URL is the file content).
|
|
3548
|
+
const externalPreviewUrl = useMemo(() => {
|
|
3549
|
+
if (sourceType === "fusion" && fusionUrl) {
|
|
3550
|
+
try {
|
|
3551
|
+
const url = new URL(fusionUrl);
|
|
3552
|
+
url.hash = "";
|
|
3553
|
+
return url.toString();
|
|
3554
|
+
} catch {
|
|
3555
|
+
// fall through to content detection below
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return getExternalPreviewUrl(renderedContent);
|
|
3559
|
+
}, [fusionUrl, renderedContent, sourceType]);
|
|
2222
3560
|
zoomRef.current = zoom;
|
|
2223
3561
|
|
|
2224
3562
|
const queuedAnnotationPins = useMemo(
|
|
@@ -2255,18 +3593,24 @@ export function DesignCanvas({
|
|
|
2255
3593
|
// outside Edit mode. The editor chrome bridge is omitted only for Interact.
|
|
2256
3594
|
const srcdoc = useMemo(() => {
|
|
2257
3595
|
if (externalPreviewUrl) return undefined;
|
|
2258
|
-
const editorChromeBridge =
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
3596
|
+
const editorChromeBridge =
|
|
3597
|
+
interactMode || readOnly
|
|
3598
|
+
? ""
|
|
3599
|
+
: createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
|
|
3600
|
+
EDITOR_CHROME_BRIDGE_SCRIPT.replace(
|
|
3601
|
+
"__READ_ONLY__",
|
|
3602
|
+
readOnly ? "true" : "false",
|
|
3603
|
+
)
|
|
3604
|
+
.replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
|
|
3605
|
+
.replace("__EDITOR_CHROME_SCALE_X__", String(editorChromeScaleX))
|
|
3606
|
+
.replace("__EDITOR_CHROME_SCALE_Y__", String(editorChromeScaleY));
|
|
2265
3607
|
const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
|
|
2266
3608
|
"__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
|
|
2267
3609
|
isEmbeddedFrame ? "true" : "false",
|
|
2268
3610
|
);
|
|
2269
3611
|
const bridgeToInject =
|
|
3612
|
+
MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
3613
|
+
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
2270
3614
|
TWEAK_BRIDGE_SCRIPT +
|
|
2271
3615
|
ZOOM_BRIDGE_SCRIPT +
|
|
2272
3616
|
NAV_BRIDGE_SCRIPT +
|
|
@@ -2280,28 +3624,51 @@ export function DesignCanvas({
|
|
|
2280
3624
|
}
|
|
2281
3625
|
// No body/html tags — wrap it
|
|
2282
3626
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderedContent}${bridgeToInject}</body></html>`;
|
|
3627
|
+
// editorChromeScaleX/Y are intentionally NOT deps: they only seed the initial
|
|
3628
|
+
// baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
|
|
3629
|
+
// postMessage above. Including them here rebuilds srcdoc on every zoom commit,
|
|
3630
|
+
// which reloads the iframe and flashes the screen content white.
|
|
3631
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2283
3632
|
}, [
|
|
2284
3633
|
editMode,
|
|
2285
3634
|
externalPreviewUrl,
|
|
2286
3635
|
interactMode,
|
|
2287
3636
|
isEmbeddedFrame,
|
|
3637
|
+
readOnly,
|
|
2288
3638
|
renderedContent,
|
|
2289
3639
|
]);
|
|
2290
3640
|
|
|
2291
3641
|
// Listen for messages from the iframe
|
|
2292
3642
|
useEffect(() => {
|
|
2293
3643
|
function handleMessage(e: MessageEvent) {
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
3644
|
+
const iframeWindow = iframeRef.current?.contentWindow;
|
|
3645
|
+
// For fusion sources the Builder-hosted app is cross-origin, so the strict
|
|
3646
|
+
// `origin === parentOrigin` check can never match. We still require window
|
|
3647
|
+
// identity (the message must come from our own iframe window, not any
|
|
3648
|
+
// arbitrary cross-origin frame), AND we validate the message origin
|
|
3649
|
+
// against a Builder-host allowlist (the configured fusionUrl origin or the
|
|
3650
|
+
// *.builder.io family) before relaxing the origin check. If the origin is
|
|
3651
|
+
// not on the allowlist we keep the strict check so a hostile frame that
|
|
3652
|
+
// somehow shares our window reference still can't be trusted.
|
|
3653
|
+
const trusted =
|
|
3654
|
+
sourceType === "fusion"
|
|
3655
|
+
? iframeWindow != null &&
|
|
3656
|
+
e.source === iframeWindow &&
|
|
3657
|
+
isAllowedFusionOrigin(e.origin, fusionUrl)
|
|
3658
|
+
: isTrustedCanvasBridgeMessage({
|
|
3659
|
+
source: e.source,
|
|
3660
|
+
origin: e.origin,
|
|
3661
|
+
iframeWindow,
|
|
3662
|
+
parentOrigin: window.location.origin,
|
|
3663
|
+
});
|
|
3664
|
+
if (!trusted) {
|
|
2302
3665
|
return;
|
|
2303
3666
|
}
|
|
2304
3667
|
if (!e.data || !e.data.type) return;
|
|
3668
|
+
if (e.data.type === "clear-selection") {
|
|
3669
|
+
onClearSelection?.();
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
2305
3672
|
if (e.data.type === "element-select") {
|
|
2306
3673
|
onElementSelect(e.data.payload);
|
|
2307
3674
|
}
|
|
@@ -2335,9 +3702,15 @@ export function DesignCanvas({
|
|
|
2335
3702
|
const placement = String(e.data.placement || "after");
|
|
2336
3703
|
const requestId =
|
|
2337
3704
|
typeof e.data.requestId === "string" ? e.data.requestId : undefined;
|
|
3705
|
+
const sourceId =
|
|
3706
|
+
typeof e.data.sourceId === "string" ? e.data.sourceId : undefined;
|
|
3707
|
+
const anchorSourceId =
|
|
3708
|
+
typeof e.data.anchorSourceId === "string"
|
|
3709
|
+
? e.data.anchorSourceId
|
|
3710
|
+
: undefined;
|
|
2338
3711
|
if (
|
|
2339
|
-
selector &&
|
|
2340
|
-
anchorSelector &&
|
|
3712
|
+
(selector || sourceId) &&
|
|
3713
|
+
(anchorSelector || anchorSourceId) &&
|
|
2341
3714
|
(placement === "before" ||
|
|
2342
3715
|
placement === "after" ||
|
|
2343
3716
|
placement === "inside")
|
|
@@ -2349,14 +3722,8 @@ export function DesignCanvas({
|
|
|
2349
3722
|
e.data.payload,
|
|
2350
3723
|
{
|
|
2351
3724
|
requestId,
|
|
2352
|
-
sourceId
|
|
2353
|
-
|
|
2354
|
-
? e.data.sourceId
|
|
2355
|
-
: undefined,
|
|
2356
|
-
anchorSourceId:
|
|
2357
|
-
typeof e.data.anchorSourceId === "string"
|
|
2358
|
-
? e.data.anchorSourceId
|
|
2359
|
-
: undefined,
|
|
3725
|
+
sourceId,
|
|
3726
|
+
anchorSourceId,
|
|
2360
3727
|
},
|
|
2361
3728
|
);
|
|
2362
3729
|
if (requestId) {
|
|
@@ -2409,6 +3776,10 @@ export function DesignCanvas({
|
|
|
2409
3776
|
});
|
|
2410
3777
|
return;
|
|
2411
3778
|
}
|
|
3779
|
+
if (e.data.type === "element-dblclick-text") {
|
|
3780
|
+
onElementDblClickText?.(e.data.payload);
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
2412
3783
|
if (e.data.type === "design-hotkey") {
|
|
2413
3784
|
onIframeHotkey?.({
|
|
2414
3785
|
key: String(e.data.key || ""),
|
|
@@ -2454,6 +3825,16 @@ export function DesignCanvas({
|
|
|
2454
3825
|
);
|
|
2455
3826
|
return;
|
|
2456
3827
|
}
|
|
3828
|
+
if (e.data.type === "component-source-jump") {
|
|
3829
|
+
// The user clicked the component-instance tag ("ComponentName →").
|
|
3830
|
+
// Relay to the parent so it can invoke open-component-source.
|
|
3831
|
+
const nodeId = String(e.data.nodeId || "");
|
|
3832
|
+
const componentName = String(e.data.componentName || "");
|
|
3833
|
+
if (nodeId && componentName) {
|
|
3834
|
+
onComponentSourceJump?.({ nodeId, componentName });
|
|
3835
|
+
}
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
2457
3838
|
if (e.data.type === "embedded-canvas-wheel") {
|
|
2458
3839
|
if (!isEmbeddedFrame) return;
|
|
2459
3840
|
const iframe = iframeRef.current;
|
|
@@ -2527,9 +3908,11 @@ export function DesignCanvas({
|
|
|
2527
3908
|
}, [
|
|
2528
3909
|
onElementSelect,
|
|
2529
3910
|
onElementHover,
|
|
3911
|
+
onClearSelection,
|
|
2530
3912
|
onVisualStyleChange,
|
|
2531
3913
|
onTextContentChange,
|
|
2532
3914
|
onTextEditingStateChange,
|
|
3915
|
+
onElementDblClickText,
|
|
2533
3916
|
onIframeHotkey,
|
|
2534
3917
|
onIframeContextMenu,
|
|
2535
3918
|
onVisualStructureChange,
|
|
@@ -2537,7 +3920,10 @@ export function DesignCanvas({
|
|
|
2537
3920
|
onZoomChange,
|
|
2538
3921
|
deviceFrame,
|
|
2539
3922
|
onPrototypeNavigate,
|
|
3923
|
+
onComponentSourceJump,
|
|
2540
3924
|
isEmbeddedFrame,
|
|
3925
|
+
sourceType,
|
|
3926
|
+
fusionUrl,
|
|
2541
3927
|
]);
|
|
2542
3928
|
|
|
2543
3929
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -2575,14 +3961,43 @@ export function DesignCanvas({
|
|
|
2575
3961
|
: { type: "hover-element", selector: "", selectorCandidates: [] },
|
|
2576
3962
|
"*",
|
|
2577
3963
|
);
|
|
3964
|
+
// Re-send motion tracks so the preview bridge is ready after a reload.
|
|
3965
|
+
if (motionTracks && motionTracks.length > 0) {
|
|
3966
|
+
iframe.contentWindow?.postMessage(
|
|
3967
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
3968
|
+
"*",
|
|
3969
|
+
);
|
|
3970
|
+
} else {
|
|
3971
|
+
iframe.contentWindow?.postMessage({ type: "motion-preview-clear" }, "*");
|
|
3972
|
+
}
|
|
3973
|
+
// Re-apply the shader-fill preview after a reload so the preview survives
|
|
3974
|
+
// screen switches. Preview-only — never writes to DB, Yjs, or source.
|
|
3975
|
+
if (shaderFillPreview) {
|
|
3976
|
+
iframe.contentWindow?.postMessage(
|
|
3977
|
+
{
|
|
3978
|
+
type: "shader-fill-preview",
|
|
3979
|
+
selector: shaderFillPreview.selector ?? "",
|
|
3980
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
3981
|
+
css: shaderFillPreview.css,
|
|
3982
|
+
},
|
|
3983
|
+
"*",
|
|
3984
|
+
);
|
|
3985
|
+
} else {
|
|
3986
|
+
iframe.contentWindow?.postMessage(
|
|
3987
|
+
{ type: "shader-fill-preview-clear" },
|
|
3988
|
+
"*",
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
2578
3991
|
}, [
|
|
2579
3992
|
hoveredSelector,
|
|
2580
3993
|
hoveredSelectorCandidates,
|
|
2581
3994
|
hiddenSelectors,
|
|
2582
3995
|
lockedSelectors,
|
|
3996
|
+
motionTracks,
|
|
2583
3997
|
scaleMode,
|
|
2584
3998
|
selectedSelector,
|
|
2585
3999
|
selectedSelectorCandidates,
|
|
4000
|
+
shaderFillPreview,
|
|
2586
4001
|
tweakValues,
|
|
2587
4002
|
]);
|
|
2588
4003
|
|
|
@@ -2605,6 +4020,59 @@ export function DesignCanvas({
|
|
|
2605
4020
|
);
|
|
2606
4021
|
}, [clearSelectionRequest]);
|
|
2607
4022
|
|
|
4023
|
+
// Sync motion tracks to the iframe bridge whenever they change.
|
|
4024
|
+
// When motionTracks is empty/undefined, clear any preview overrides so the
|
|
4025
|
+
// design returns to its authored state (no stale inline styles).
|
|
4026
|
+
useEffect(() => {
|
|
4027
|
+
const win = iframeRef.current?.contentWindow;
|
|
4028
|
+
if (!win) return;
|
|
4029
|
+
if (!motionTracks || motionTracks.length === 0) {
|
|
4030
|
+
win.postMessage({ type: "motion-preview-clear" }, "*");
|
|
4031
|
+
} else {
|
|
4032
|
+
win.postMessage(
|
|
4033
|
+
{ type: "motion-load-tracks", tracks: motionTracks },
|
|
4034
|
+
"*",
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
}, [motionTracks]);
|
|
4038
|
+
|
|
4039
|
+
// Sync shader-fill preview to the iframe whenever the prop changes.
|
|
4040
|
+
// When cleared (null / undefined) send a clear message so the bridge
|
|
4041
|
+
// restores the original background on the previously-patched element.
|
|
4042
|
+
// Preview-only — never writes to DB, Yjs, or source.
|
|
4043
|
+
useEffect(() => {
|
|
4044
|
+
const win = iframeRef.current?.contentWindow;
|
|
4045
|
+
if (!win) return;
|
|
4046
|
+
if (!shaderFillPreview) {
|
|
4047
|
+
win.postMessage({ type: "shader-fill-preview-clear" }, "*");
|
|
4048
|
+
} else {
|
|
4049
|
+
win.postMessage(
|
|
4050
|
+
{
|
|
4051
|
+
type: "shader-fill-preview",
|
|
4052
|
+
selector: shaderFillPreview.selector ?? "",
|
|
4053
|
+
nodeId: shaderFillPreview.nodeId ?? "",
|
|
4054
|
+
css: shaderFillPreview.css,
|
|
4055
|
+
},
|
|
4056
|
+
"*",
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
}, [shaderFillPreview]);
|
|
4060
|
+
|
|
4061
|
+
// Push the constant-size chrome scale into the iframe LIVE (CSS vars only) when
|
|
4062
|
+
// overview zoom settles. This is intentionally separate from the srcdoc build so
|
|
4063
|
+
// a scale change never rebuilds srcdoc / reloads the iframe (which flashes the
|
|
4064
|
+
// content white). The baked __EDITOR_CHROME_SCALE__ values cover first paint.
|
|
4065
|
+
useEffect(() => {
|
|
4066
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4067
|
+
{
|
|
4068
|
+
type: "set-editor-chrome-scale",
|
|
4069
|
+
scaleX: editorChromeScaleX,
|
|
4070
|
+
scaleY: editorChromeScaleY,
|
|
4071
|
+
},
|
|
4072
|
+
"*",
|
|
4073
|
+
);
|
|
4074
|
+
}, [editorChromeScaleX, editorChromeScaleY]);
|
|
4075
|
+
|
|
2608
4076
|
const sendStyleChange = useCallback(
|
|
2609
4077
|
(selector: string, property: string, value: string) => {
|
|
2610
4078
|
const iframe = iframeRef.current;
|
|
@@ -2617,6 +4085,61 @@ export function DesignCanvas({
|
|
|
2617
4085
|
[],
|
|
2618
4086
|
);
|
|
2619
4087
|
|
|
4088
|
+
/**
|
|
4089
|
+
* Send a motion-preview scrub tick to the iframe. `t` is the normalised
|
|
4090
|
+
* playhead position in [0, 1]. Tracks must have been loaded first via the
|
|
4091
|
+
* `motionTracks` prop (or an explicit `motion-load-tracks` message).
|
|
4092
|
+
* Preview-only — never writes to DB/Yjs/source.
|
|
4093
|
+
*/
|
|
4094
|
+
const sendMotionPreview = useCallback((t: number, durationMs?: number) => {
|
|
4095
|
+
const iframe = iframeRef.current;
|
|
4096
|
+
if (!iframe?.contentWindow) return;
|
|
4097
|
+
iframe.contentWindow.postMessage(
|
|
4098
|
+
{ type: "motion-preview", t: Math.max(0, Math.min(1, t)), durationMs },
|
|
4099
|
+
"*",
|
|
4100
|
+
);
|
|
4101
|
+
}, []);
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* Clear all motion-preview inline-style overrides in the iframe and remove
|
|
4105
|
+
* the in-memory track list. Call when the Motion dock is closed.
|
|
4106
|
+
*/
|
|
4107
|
+
const clearMotionPreview = useCallback(() => {
|
|
4108
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4109
|
+
{ type: "motion-preview-clear" },
|
|
4110
|
+
"*",
|
|
4111
|
+
);
|
|
4112
|
+
}, []);
|
|
4113
|
+
|
|
4114
|
+
/**
|
|
4115
|
+
* Send a shader-fill CSS preview to the iframe. Targets the element
|
|
4116
|
+
* identified by `selector` (preferred) or `nodeId`. Preview-only — the
|
|
4117
|
+
* bridge script restores the original background on clear.
|
|
4118
|
+
*/
|
|
4119
|
+
const sendShaderFillPreview = useCallback(
|
|
4120
|
+
(selector: string, nodeId: string, css: string) => {
|
|
4121
|
+
const win = iframeRef.current?.contentWindow;
|
|
4122
|
+
if (!win) return;
|
|
4123
|
+
win.postMessage(
|
|
4124
|
+
{ type: "shader-fill-preview", selector, nodeId, css },
|
|
4125
|
+
"*",
|
|
4126
|
+
);
|
|
4127
|
+
},
|
|
4128
|
+
[],
|
|
4129
|
+
);
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Clear the shader-fill preview in the iframe, restoring the original
|
|
4133
|
+
* background on the patched element. Call when the preview is dismissed
|
|
4134
|
+
* or when the selection changes to a different element.
|
|
4135
|
+
*/
|
|
4136
|
+
const clearShaderFillPreview = useCallback(() => {
|
|
4137
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
4138
|
+
{ type: "shader-fill-preview-clear" },
|
|
4139
|
+
"*",
|
|
4140
|
+
);
|
|
4141
|
+
}, []);
|
|
4142
|
+
|
|
2620
4143
|
const replacePreviewContent = useCallback(
|
|
2621
4144
|
(nextContent: string, selector?: string | null, candidates?: string[]) => {
|
|
2622
4145
|
const iframe = iframeRef.current;
|
|
@@ -2654,15 +4177,65 @@ export function DesignCanvas({
|
|
|
2654
4177
|
|
|
2655
4178
|
// Expose iframe runtime mutations for the editor orchestrator.
|
|
2656
4179
|
useEffect(() => {
|
|
4180
|
+
if (!registerRuntimeBridge) return;
|
|
2657
4181
|
(window as any).__designCanvasSendStyle = sendStyleChange;
|
|
2658
4182
|
(window as any).__designCanvasReplaceContent = replacePreviewContent;
|
|
2659
4183
|
(window as any).__designCanvasDeleteElement = deleteRuntimeElement;
|
|
4184
|
+
(window as any).__designCanvasSendMotionPreview = sendMotionPreview;
|
|
4185
|
+
(window as any).__designCanvasClearMotionPreview = clearMotionPreview;
|
|
4186
|
+
// Shader-fill preview helpers (preview-only, §6.7 gating applies to apply).
|
|
4187
|
+
(window as any).__designCanvasSendShaderFillPreview = sendShaderFillPreview;
|
|
4188
|
+
(window as any).__designCanvasClearShaderFillPreview =
|
|
4189
|
+
clearShaderFillPreview;
|
|
2660
4190
|
return () => {
|
|
2661
|
-
delete
|
|
2662
|
-
|
|
2663
|
-
|
|
4191
|
+
// Identity-guard each delete so a stale unmounting instance never clobbers
|
|
4192
|
+
// a freshly mounted instance's bridge during a remount race.
|
|
4193
|
+
if ((window as any).__designCanvasSendStyle === sendStyleChange) {
|
|
4194
|
+
delete (window as any).__designCanvasSendStyle;
|
|
4195
|
+
}
|
|
4196
|
+
if (
|
|
4197
|
+
(window as any).__designCanvasReplaceContent === replacePreviewContent
|
|
4198
|
+
) {
|
|
4199
|
+
delete (window as any).__designCanvasReplaceContent;
|
|
4200
|
+
}
|
|
4201
|
+
if (
|
|
4202
|
+
(window as any).__designCanvasDeleteElement === deleteRuntimeElement
|
|
4203
|
+
) {
|
|
4204
|
+
delete (window as any).__designCanvasDeleteElement;
|
|
4205
|
+
}
|
|
4206
|
+
if (
|
|
4207
|
+
(window as any).__designCanvasSendMotionPreview === sendMotionPreview
|
|
4208
|
+
) {
|
|
4209
|
+
delete (window as any).__designCanvasSendMotionPreview;
|
|
4210
|
+
}
|
|
4211
|
+
if (
|
|
4212
|
+
(window as any).__designCanvasClearMotionPreview === clearMotionPreview
|
|
4213
|
+
) {
|
|
4214
|
+
delete (window as any).__designCanvasClearMotionPreview;
|
|
4215
|
+
}
|
|
4216
|
+
if (
|
|
4217
|
+
(window as any).__designCanvasSendShaderFillPreview ===
|
|
4218
|
+
sendShaderFillPreview
|
|
4219
|
+
) {
|
|
4220
|
+
delete (window as any).__designCanvasSendShaderFillPreview;
|
|
4221
|
+
}
|
|
4222
|
+
if (
|
|
4223
|
+
(window as any).__designCanvasClearShaderFillPreview ===
|
|
4224
|
+
clearShaderFillPreview
|
|
4225
|
+
) {
|
|
4226
|
+
delete (window as any).__designCanvasClearShaderFillPreview;
|
|
4227
|
+
}
|
|
2664
4228
|
};
|
|
2665
|
-
}, [
|
|
4229
|
+
}, [
|
|
4230
|
+
deleteRuntimeElement,
|
|
4231
|
+
registerRuntimeBridge,
|
|
4232
|
+
replacePreviewContent,
|
|
4233
|
+
sendStyleChange,
|
|
4234
|
+
sendMotionPreview,
|
|
4235
|
+
clearMotionPreview,
|
|
4236
|
+
sendShaderFillPreview,
|
|
4237
|
+
clearShaderFillPreview,
|
|
4238
|
+
]);
|
|
2666
4239
|
|
|
2667
4240
|
// Device dimensions match real-world devices. iframes are replaced elements
|
|
2668
4241
|
// with an intrinsic 300×150 size, so `aspect-ratio` + `height: auto` doesn't
|
|
@@ -2679,6 +4252,13 @@ export function DesignCanvas({
|
|
|
2679
4252
|
|
|
2680
4253
|
const { width: iframeWidth, height: iframeHeight } =
|
|
2681
4254
|
deviceDimensions[deviceFrame];
|
|
4255
|
+
const embeddedFrameFluid = embeddedFrame?.fluid === true;
|
|
4256
|
+
|
|
4257
|
+
// Per-breakpoint override: when previewWidthPx is set it takes priority over
|
|
4258
|
+
// the deviceFrame width so the caller can render the same source at an
|
|
4259
|
+
// explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
|
|
4260
|
+
const resolvedWidth =
|
|
4261
|
+
previewWidthPx != null ? `${previewWidthPx}px` : iframeWidth;
|
|
2682
4262
|
|
|
2683
4263
|
// Wrap the iframe in a positioned container so DrawOverlay /
|
|
2684
4264
|
// CanvasCommentPins can absolutely-position themselves on top of the
|
|
@@ -2691,9 +4271,15 @@ export function DesignCanvas({
|
|
|
2691
4271
|
<div
|
|
2692
4272
|
className="design-canvas-iframe-wrapper relative inline-block ring-1 ring-border/60 shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(0,0,0,0.45)]"
|
|
2693
4273
|
style={{
|
|
2694
|
-
width: embeddedFrame
|
|
4274
|
+
width: embeddedFrame
|
|
4275
|
+
? embeddedFrameFluid
|
|
4276
|
+
? "100%"
|
|
4277
|
+
: embeddedFrame.viewportWidth
|
|
4278
|
+
: resolvedWidth,
|
|
2695
4279
|
height: embeddedFrame
|
|
2696
|
-
?
|
|
4280
|
+
? embeddedFrameFluid
|
|
4281
|
+
? "100%"
|
|
4282
|
+
: embeddedFrame.viewportHeight
|
|
2697
4283
|
: deviceFrame === "none"
|
|
2698
4284
|
? "100%"
|
|
2699
4285
|
: (iframeHeight ?? undefined),
|
|
@@ -2709,6 +4295,12 @@ export function DesignCanvas({
|
|
|
2709
4295
|
: "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
2710
4296
|
}
|
|
2711
4297
|
data-design-preview-iframe
|
|
4298
|
+
data-design-source-type={
|
|
4299
|
+
sourceType ??
|
|
4300
|
+
(externalPreviewUrl
|
|
4301
|
+
? "localhost" // inferred — content is a URL
|
|
4302
|
+
: "inline")
|
|
4303
|
+
}
|
|
2712
4304
|
className="block h-full w-full border-0 bg-transparent"
|
|
2713
4305
|
title={t("designEditor.designPreview")}
|
|
2714
4306
|
/>
|
|
@@ -2771,6 +4363,17 @@ export function DesignCanvas({
|
|
|
2771
4363
|
);
|
|
2772
4364
|
|
|
2773
4365
|
if (embeddedFrame) {
|
|
4366
|
+
if (embeddedFrameFluid) {
|
|
4367
|
+
return (
|
|
4368
|
+
<div
|
|
4369
|
+
ref={scrollContainerRef}
|
|
4370
|
+
className="relative h-full w-full overflow-hidden"
|
|
4371
|
+
>
|
|
4372
|
+
{iframeElement}
|
|
4373
|
+
</div>
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
|
|
2774
4377
|
const scaleX =
|
|
2775
4378
|
embeddedFrame.displayWidth / Math.max(1, embeddedFrame.viewportWidth);
|
|
2776
4379
|
const scaleY =
|