@agent-native/core 0.169.1 → 0.171.0
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/agent-native.eject.json +4 -1
- package/bin/agent-native.js +23 -5
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +28 -0
- package/corpus/templates/analytics/app/pages/Agents.tsx +25 -1
- package/corpus/templates/analytics/app/routes/$.tsx +10 -0
- package/corpus/templates/analytics/app/routes.ts +3 -1
- package/corpus/templates/analytics/changelog/2026-08-22-fixed-the-agent-native-templates-first-party-dashboard-panel.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-22-org-admin-panels-no-longer-report-a-database-error-as-a-per.md +9 -0
- package/corpus/templates/analytics/changelog/2026-08-22-slack-requests-report-invalid-inputs.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-22-unknown-analytics-links-now-return-a-real-not-found-status.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +2 -1
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +2 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +29 -7
- package/corpus/templates/analytics/server/handlers/slack.ts +59 -5
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +11 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +132 -2
- package/corpus/templates/analytics/server/lib/slack.ts +4 -2
- package/corpus/templates/analytics/vitest.config.ts +6 -1
- package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/assets/app/routes/library.tsx +5 -3
- package/corpus/templates/assets/app/routes.ts +3 -1
- package/corpus/templates/assets/changelog/2026-08-22-library-page-loads-again-instead-of-crashing-on-open.md +6 -0
- package/corpus/templates/assets/vitest.config.ts +6 -1
- package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/brain/app/components/ui/pointer-lock.ts +1 -0
- package/corpus/templates/brain/app/routes/sources.tsx +8 -3
- package/corpus/templates/brain/changelog/2026-08-22-tuning-or-adding-a-source-no-longer-leaves-the-sources-page.md +6 -0
- package/corpus/templates/brain/vitest.config.ts +1 -0
- package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/calendar/actions/list-booking-links.ts +83 -4
- package/corpus/templates/calendar/actions/update-booking-link.ts +4 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +11 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +1 -1
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +103 -44
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +28 -24
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +9 -2
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +47 -58
- package/corpus/templates/calendar/app/hooks/use-booking-links.ts +5 -1
- package/corpus/templates/calendar/app/lib/event-layout.ts +52 -26
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +126 -98
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +1 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +88 -81
- package/corpus/templates/calendar/app/routes.ts +3 -1
- package/corpus/templates/calendar/changelog/2026-08-22-booking-link-visibility-respects-shared-access.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-clicking-an-out-of-office-event-s-marker-no-longer-creates-a.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-event-detail-views-now-show-your-calendar-timezone.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-google-calendar-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-overlapping-events-now-show-each-others-boundaries.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-public-booking-pages-now-offer-time-slots-from-the-host-s-we.md +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +5 -7
- package/corpus/templates/calendar/server/handlers/google-auth.ts +10 -2
- package/corpus/templates/calendar/server/lib/google-calendar.ts +139 -14
- package/corpus/templates/calendar/server/onboarding.ts +85 -0
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +9 -1
- package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/chat/app/routes.ts +3 -1
- package/corpus/templates/chat/vitest.config.ts +8 -0
- package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/clips/actions/get-meeting.ts +9 -4
- package/corpus/templates/clips/actions/list-recordings.ts +16 -3
- package/corpus/templates/clips/actions/search-meetings.ts +1 -0
- package/corpus/templates/clips/actions/view-screen.ts +8 -1
- package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +32 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +29 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +33 -4
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -0
- package/corpus/templates/clips/app/routes/access-request.approve.tsx +16 -9
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +17 -7
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -7
- package/corpus/templates/clips/changelog/2026-08-21-camera-bubble-black-circle.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-22-screen-recordings-respect-the-microphone-toggle.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-22-the-recordings-library-no-longer-fails-right-after-server-start.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-23-the-agent-can-see-your-library-right-after-server-start.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +1 -2
- package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +39 -7
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +21 -25
- package/corpus/templates/clips/desktop/src/app.tsx +53 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-playback.ts +68 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +72 -14
- package/corpus/templates/clips/desktop/src/lib/updater.ts +8 -3
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +162 -20
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -6
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +150 -13
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +12 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +28 -3
- package/corpus/templates/clips/server/lib/emails.ts +17 -33
- package/corpus/templates/content/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +140 -15
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +12 -14
- package/corpus/templates/content/app/components/ui/pointer-lock.ts +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +18 -2
- package/corpus/templates/content/app/hooks/use-local-storage.ts +38 -22
- package/corpus/templates/content/app/i18n-data.ts +25 -0
- package/corpus/templates/content/app/routes.ts +3 -1
- package/corpus/templates/content/changelog/2026-08-22-code-blocks-inserted-from-the-slash-menu-no-longer-get-stuck.md +6 -0
- package/corpus/templates/content/changelog/2026-08-22-the-sidebar-page-tree-no-longer-flashes-empty-after-creating-a-page.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-08-21-content-mermaid-rendering-shape.md +233 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +9 -2
- package/corpus/templates/content/vitest.config.ts +7 -1
- package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/design/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1383 -88
- package/corpus/templates/design/actions/generate-design.ts +98 -47
- package/corpus/templates/design/actions/get-design-system.ts +6 -1
- package/corpus/templates/design/actions/refresh-design-system-with-builder.ts +146 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +70 -26
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +284 -136
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2057 -141
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +6 -2
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +4 -4
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +10 -1
- package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +19 -2
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +9 -2
- package/corpus/templates/design/app/components/layout/Header.tsx +15 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +7 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -4
- package/corpus/templates/design/app/i18n-data.ts +1 -0
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +12 -0
- package/corpus/templates/design/app/lib/design-system-data.ts +68 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +77 -35
- package/corpus/templates/design/app/pages/DesignSystems.tsx +136 -42
- package/corpus/templates/design/app/pages/Index.tsx +2 -0
- package/corpus/templates/design/app/pages/Templates.tsx +0 -10
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +10 -0
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +50 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +10 -8
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +6 -6
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +18 -7
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +4 -90
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +2 -1
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +4 -3
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +12 -2
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +9 -2
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +11 -1
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +11 -19
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +6 -6
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +12 -15
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -59
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/types.ts +3 -3
- package/corpus/templates/design/app/routes.ts +3 -1
- package/corpus/templates/design/changelog/2026-08-20-canvas-edits-patch-the-frame-instead-of-rebuilding-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-builder-design-system-imports-reconcile-after-indexing.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-draw-mode-needs-shift-y.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-eyedropper-shortcut.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-figma-style-alignment-guides-and-snapping.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-group-transforms-keep-the-selection.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-inspector-fields-keep-focus-on-enter.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-pen-alt-keeps-the-incoming-handle.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-pen-on-the-board-creates-one-vector.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-scale-a-multi-selection-from-the-group-handles.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-scale-resizes-text-with-the-object.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-selecting-another-object-keeps-the-scale-tool-armed.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-the-shape-tool-remembers-the-shape-you-picked.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-vectors-drawn-in-a-frame-land-where-you-drew-them.md +6 -0
- package/corpus/templates/design/changelog/2026-08-22-multi-screen-generation-no-longer-loses-a-saved-screens-layo.md +6 -0
- package/corpus/templates/design/changelog/2026-08-22-rename-and-search-fields-are-labeled-for-screen-readers.md +6 -0
- package/corpus/templates/design/package.json +2 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +311 -0
- package/corpus/templates/design/shared/canvas-math.ts +612 -206
- package/corpus/templates/design/shared/pen-path.ts +37 -0
- package/corpus/templates/design/vitest.config.ts +7 -1
- package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/dispatch/actions/list-workspace-connections.ts +34 -3
- package/corpus/templates/dispatch/app/routes/integrations.tsx +55 -21
- package/corpus/templates/dispatch/app/routes.ts +3 -1
- package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/factory/.env.example +2 -7
- package/corpus/templates/factory/AGENTS.md +3 -0
- package/corpus/templates/factory/README.md +5 -3
- package/corpus/templates/factory/actions/create-factory.ts +16 -3
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +2 -0
- package/corpus/templates/factory/actions/get-triage-item.ts +2 -0
- package/corpus/templates/factory/actions/list-factory-audit.ts +116 -36
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
- package/corpus/templates/factory/actions/list-triage-items.ts +22 -19
- package/corpus/templates/factory/actions/run-factory-automation.ts +7 -3
- package/corpus/templates/factory/actions/save-factory-automation.ts +5 -2
- package/corpus/templates/factory/actions/save-triage-config.ts +2 -8
- package/corpus/templates/factory/actions/start-builder-for-item.ts +21 -0
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +229 -297
- package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +397 -247
- package/corpus/templates/factory/app/components/factory/FactorySourceSettingsGroup.tsx +33 -0
- package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +202 -162
- package/corpus/templates/factory/app/i18n/en-US.ts +32 -4
- package/corpus/templates/factory/app/routes/factory.tsx +84 -11
- package/corpus/templates/factory/app/routes.ts +3 -1
- package/corpus/templates/factory/changelog/2026-08-21-activity-runs-use-automation-display-names.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-create-and-settings-source-cards.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-keep-automations-visible-while-running.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-nested-factory-automations-can-poll.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-settings-sticky-unsaved-bar.md +6 -0
- package/corpus/templates/factory/server/connectors/credentials.ts +10 -5
- package/corpus/templates/factory/server/lib/factory-audit-report.ts +412 -0
- package/corpus/templates/factory/server/lib/factory-config-reconcile.ts +321 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +25 -2
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +13 -4
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +148 -4
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +4 -6
- package/corpus/templates/factory/vitest.config.ts +17 -2
- package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/forms/app/routes/_app.settings.tsx +4 -1
- package/corpus/templates/forms/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +11 -1
- package/corpus/templates/forms/vitest.config.ts +1 -0
- package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/macros/app/routes.ts +3 -1
- package/corpus/templates/macros/vitest.config.ts +6 -1
- package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/mail/actions/manage-gmail-filters.ts +10 -2
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -37
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +14 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +66 -36
- package/corpus/templates/mail/app/hooks/use-google-auth.ts +2 -0
- package/corpus/templates/mail/app/lib/google-oauth-setup.ts +17 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +69 -9
- package/corpus/templates/mail/app/routes/$.tsx +9 -0
- package/corpus/templates/mail/app/routes/$view.$threadId.tsx +23 -1
- package/corpus/templates/mail/app/routes/$view.tsx +47 -1
- package/corpus/templates/mail/app/routes.ts +3 -1
- package/corpus/templates/mail/changelog/2026-08-22-gmail-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-22-gmail-filters-now-shows-the-real-error.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-22-unknown-mail-urls-now-show-the-404-page.md +6 -0
- package/corpus/templates/mail/server/handlers/google-auth.ts +11 -2
- package/corpus/templates/mail/server/lib/google-auth.ts +92 -12
- package/corpus/templates/mail/server/onboarding.ts +66 -18
- package/corpus/templates/mail/vitest.config.ts +6 -1
- package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/plan/app/routes/settings.tsx +4 -1
- package/corpus/templates/plan/app/routes.ts +3 -1
- package/corpus/templates/plan/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/plan/netlify.toml +1 -1
- package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/slides/actions/patch-deck.ts +79 -10
- package/corpus/templates/slides/actions/update-slide.ts +11 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +14 -4
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -52
- package/corpus/templates/slides/app/components/editor/GoogleDocImportHint.tsx +8 -53
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +9 -75
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +8 -60
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +10 -2
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +19 -7
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +49 -36
- package/corpus/templates/slides/app/routes/access-request.approve.tsx +16 -9
- package/corpus/templates/slides/app/routes.ts +3 -1
- package/corpus/templates/slides/changelog/2026-08-22-already-uploaded-chat-images-are-no-longer-dropped.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-google-drive-and-slides-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-multi-slide-repairs-now-report-slides-that-still-overflow.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-update-slide-now-reports-which-batched-edit-was-skipped.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +52 -1
- package/corpus/templates/slides/server/lib/chat-attachments.ts +29 -3
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +66 -16
- package/corpus/templates/slides/server/lib/slide-content-patch.ts +13 -2
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -2
- package/corpus/templates/slides/tsconfig.json +5 -0
- package/corpus/templates/slides/vitest.config.ts +6 -1
- package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/tasks/app/routes/settings.tsx +4 -1
- package/corpus/templates/tasks/app/routes.ts +3 -1
- package/corpus/templates/tasks/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/tasks/vitest.config.ts +7 -1
- package/dist/action.d.ts +91 -0
- package/dist/action.js +50 -0
- package/dist/agent/abort-reasons.d.ts +38 -0
- package/dist/agent/abort-reasons.js +49 -0
- package/dist/agent/engine/ai-sdk-engine.js +8 -5
- package/dist/agent/engine/anthropic-engine.js +7 -5
- package/dist/agent/engine/first-event-timeout.d.ts +42 -17
- package/dist/agent/engine/first-event-timeout.js +67 -25
- package/dist/agent/production-agent.d.ts +23 -0
- package/dist/agent/production-agent.js +107 -102
- package/dist/agent/run-loop-with-resume.d.ts +2 -28
- package/dist/agent/run-loop-with-resume.js +25 -40
- package/dist/agent/run-manager.js +5 -4
- package/dist/agent/run-store.d.ts +55 -0
- package/dist/agent/run-store.js +124 -1
- package/dist/agent/tool-search.js +2 -1
- package/dist/agent/types.d.ts +9 -4
- package/dist/app-config/auth.d.ts +1 -0
- package/dist/app-config/auth.js +7 -0
- package/dist/app-config/configuration-error.d.ts +11 -0
- package/dist/app-config/configuration-error.js +14 -0
- package/dist/app-config/index.d.ts +1 -0
- package/dist/app-config/index.js +1 -0
- package/dist/app-config/integrations.d.ts +1 -0
- package/dist/app-config/integrations.js +10 -0
- package/dist/app-config/plugins.d.ts +34 -0
- package/dist/app-config/plugins.js +39 -0
- package/dist/app-config/run-lifecycle-invariants.d.ts +18 -9
- package/dist/app-config/run-lifecycle-invariants.js +18 -25
- package/dist/app-config/schema.d.ts +17 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/chat-threads/schema-migrations.js +20 -0
- package/dist/chat-threads/store.js +21 -4
- package/dist/cli/native-dependencies.d.ts +4 -0
- package/dist/cli/native-dependencies.js +15 -0
- package/dist/client/EnvironmentBadge.d.ts +1 -1
- package/dist/client/EnvironmentBadge.js +40 -5
- package/dist/client/MultiTabAssistantChat.d.ts +1 -1
- package/dist/client/MultiTabAssistantChat.js +35 -14
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat/index.js +1 -1
- package/dist/client/agent-page/use-jobs.d.ts +2 -1
- package/dist/client/agent-page/use-jobs.js +3 -5
- package/dist/client/agent-sidebar-state.js +3 -1
- package/dist/client/blocks/library/code-tabs.js +5 -0
- package/dist/client/blocks/library/code.js +1 -0
- package/dist/client/chat-first/apps-rail.js +2 -2
- package/dist/client/chat-model-groups.d.ts +12 -0
- package/dist/client/chat-model-groups.js +3 -2
- package/dist/client/integrations/GoogleProductLogo.d.ts +5 -0
- package/dist/client/integrations/GoogleProductLogo.js +45 -0
- package/dist/client/integrations/index.d.ts +2 -0
- package/dist/client/integrations/index.js +2 -0
- package/dist/client/integrations/workspace-provider-oauth.d.ts +8 -0
- package/dist/client/integrations/workspace-provider-oauth.js +13 -0
- package/dist/client/resources/mcp-integration-catalog.js +0 -29
- package/dist/client/settings/SettingsTabsPage.js +10 -6
- package/dist/client/sharing/ShareButton.js +11 -2
- package/dist/client/use-chat-models.d.ts +6 -0
- package/dist/client/use-chat-models.js +10 -0
- package/dist/client/use-db-sync.js +27 -9
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +111 -9
- package/dist/connections/catalog.js +86 -30
- package/dist/db/create-get-db.js +18 -0
- package/dist/db/ddl-guard.d.ts +0 -4
- package/dist/db/ddl-guard.js +46 -0
- package/dist/deploy/build.d.ts +19 -0
- package/dist/deploy/build.js +190 -16
- package/dist/deploy/function-bundle.d.ts +45 -0
- package/dist/deploy/function-bundle.js +337 -0
- package/dist/deploy/workspace-deploy.js +19 -1
- package/dist/eject/workspace-connections.js +4 -0
- package/dist/email-catalog/registry.d.ts +4 -0
- package/dist/email-catalog/registry.js +79 -14
- package/dist/extensions/content-patch.js +14 -2
- package/dist/integrations/plugin.d.ts +10 -0
- package/dist/integrations/plugin.js +40 -6
- package/dist/jobs/actions/run-automation-now.d.ts +2 -1
- package/dist/jobs/actions/run-automation-now.js +11 -5
- package/dist/jobs/frontmatter.js +53 -2
- package/dist/jobs/run-now.d.ts +11 -1
- package/dist/jobs/run-now.js +42 -7
- package/dist/jobs/scheduler.d.ts +1 -0
- package/dist/jobs/scheduler.js +2 -1
- package/dist/localization/core-messages/ar-SA.js +3 -0
- package/dist/localization/core-messages/de-DE.js +3 -0
- package/dist/localization/core-messages/en-US.d.ts +3 -0
- package/dist/localization/core-messages/en-US.js +3 -0
- package/dist/localization/core-messages/es-ES.js +3 -0
- package/dist/localization/core-messages/fr-FR.js +3 -0
- package/dist/localization/core-messages/hi-IN.js +3 -0
- package/dist/localization/core-messages/ja-JP.js +3 -0
- package/dist/localization/core-messages/ko-KR.js +3 -0
- package/dist/localization/core-messages/pt-BR.js +3 -0
- package/dist/localization/core-messages/zh-CN.js +3 -0
- package/dist/localization/core-messages/zh-TW.js +3 -0
- package/dist/localization/core-messages.d.ts +3 -0
- package/dist/localization/default-messages.d.ts +2 -5
- package/dist/localization/default-messages.js +2 -5
- package/dist/mcp/build-server.d.ts +9 -0
- package/dist/mcp/build-server.js +37 -8
- package/dist/mcp-client/oauth-client.d.ts +7 -0
- package/dist/mcp-client/oauth-client.js +12 -1
- package/dist/oauth-tokens/index.d.ts +1 -1
- package/dist/oauth-tokens/index.js +1 -1
- package/dist/oauth-tokens/lifecycle.d.ts +11 -0
- package/dist/oauth-tokens/lifecycle.js +17 -0
- package/dist/oauth-tokens/store.js +7 -0
- package/dist/observability/posthog-ai.d.ts +15 -1
- package/dist/observability/posthog-ai.js +24 -12
- package/dist/observability/traces.js +158 -28
- package/dist/onboarding/app-profile.js +2 -2
- package/dist/onboarding/plugin.js +10 -2
- package/dist/onboarding/types.d.ts +2 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/provider-api/index.js +2 -0
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/resources/store.js +13 -4
- package/dist/scripts/agent-engines/list-agent-engines.js +29 -3
- package/dist/secrets/register-framework-secrets.js +1 -1
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +27 -3
- package/dist/server/agent-chat/action-filters-a2a.js +48 -9
- package/dist/server/agent-chat-plugin.js +36 -9
- package/dist/server/agent-discovery.js +43 -20
- package/dist/server/auth.js +18 -16
- package/dist/server/beta-opt-out-html.js +45 -4
- package/dist/server/better-auth-instance.d.ts +2 -1
- package/dist/server/better-auth-instance.js +37 -7
- package/dist/server/builder-design-systems.d.ts +4 -1
- package/dist/server/builder-design-systems.js +87 -6
- package/dist/server/builder-oauth.d.ts +1 -7
- package/dist/server/builder-oauth.js +80 -163
- package/dist/server/core-routes-plugin.d.ts +29 -0
- package/dist/server/core-routes-plugin.js +100 -20
- package/dist/server/default-plugins.d.ts +18 -0
- package/dist/server/default-plugins.js +22 -0
- package/dist/server/edge.d.ts +1 -0
- package/dist/server/edge.js +1 -0
- package/dist/server/framework-request-handler.js +31 -3
- package/dist/server/google-oauth.d.ts +3 -0
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/onboarding-html.js +37 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +6 -1
- package/dist/server/workspace-provider-oauth.js +72 -20
- package/dist/shared/agent-sidebar-url.d.ts +1 -0
- package/dist/shared/agent-sidebar-url.js +1 -0
- package/dist/shared/environment-lanes.d.ts +2 -0
- package/dist/shared/environment-lanes.js +3 -1
- package/dist/shared/index.d.ts +3 -1
- package/dist/shared/index.js +3 -1
- package/dist/shared/surface-visibility.d.ts +31 -0
- package/dist/shared/surface-visibility.js +57 -0
- package/dist/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/chat/app/routes.ts +3 -1
- package/dist/templates/chat/vitest.config.ts +8 -0
- package/dist/templates/default/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/default/app/routes.ts +3 -1
- package/dist/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/tracking/registry.d.ts +11 -0
- package/dist/tracking/registry.js +5 -2
- package/dist/triggers/actions.js +10 -3
- package/dist/usage/store.js +23 -3
- package/dist/user-profile/actions/get-auth-methods.js +18 -5
- package/dist/vite/client.js +33 -7
- package/docs/content/actions-access-control.mdx +43 -4
- package/docs/content/actions-defining.mdx +16 -0
- package/docs/content/authentication.mdx +40 -21
- package/docs/content/environment-variables.mdx +1 -0
- package/docs/content/template-clips-capture-everywhere.mdx +29 -30
- package/docs/content/template-clips-developers.mdx +4 -4
- package/docs/content/template-clips.mdx +3 -1
- package/package.json +2 -2
- package/src/eject/workspace-connections.ts +4 -0
- package/src/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/chat/app/routes.ts +3 -1
- package/src/templates/chat/vitest.config.ts +8 -0
- package/src/templates/default/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/default/app/routes.ts +3 -1
- package/src/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
|
@@ -40,6 +40,11 @@ declare var __DESIGN_CANVAS_CONTENT_OFFSET_Y__: number;
|
|
|
40
40
|
declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
|
|
41
41
|
declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
42
42
|
declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
43
|
+
/** Head inner HTML of the document this srcdoc was built from. The live head
|
|
44
|
+
* cannot supply it: a blocking script src (the Tailwind runtime) has already
|
|
45
|
+
* injected into it by the time this bridge runs, and adopting that as the
|
|
46
|
+
* source baseline makes the first diff delete the compiled stylesheet. */
|
|
47
|
+
declare var __INITIAL_SOURCE_HEAD__: string;
|
|
43
48
|
|
|
44
49
|
(function () {
|
|
45
50
|
// Idempotency guard: replace-document-content / srcdoc rebuilds can end up
|
|
@@ -323,7 +328,10 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
323
328
|
* it, and the re-inserted `<script src>` cannot rebuild it because innerHTML
|
|
324
329
|
* never executes scripts. The screen then renders unstyled for good.
|
|
325
330
|
*/
|
|
326
|
-
var lastSourceHeadHtml: string | null =
|
|
331
|
+
var lastSourceHeadHtml: string | null =
|
|
332
|
+
typeof __INITIAL_SOURCE_HEAD__ === "string"
|
|
333
|
+
? __INITIAL_SOURCE_HEAD__ || null
|
|
334
|
+
: null;
|
|
327
335
|
|
|
328
336
|
/**
|
|
329
337
|
* Swaps only the nodes the previous source head contributed. Assigning
|
|
@@ -332,6 +340,45 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
332
340
|
* `<script src>` re-inserted in its place can never rebuild it, because
|
|
333
341
|
* innerHTML does not execute scripts.
|
|
334
342
|
*/
|
|
343
|
+
/**
|
|
344
|
+
* Stable identity for the head nodes a source document owns and can change
|
|
345
|
+
* in place. Deliberately narrow: anything without a signature is only ever
|
|
346
|
+
* matched by exact `outerHTML`, so a runtime-injected node is never a
|
|
347
|
+
* replacement target.
|
|
348
|
+
*/
|
|
349
|
+
function headNodeSignature(node: Element): string {
|
|
350
|
+
var tag = node.tagName.toLowerCase();
|
|
351
|
+
if (tag === "title" || tag === "base") return tag;
|
|
352
|
+
if (tag === "style") {
|
|
353
|
+
var attrs = node.attributes;
|
|
354
|
+
for (var i = 0; i < attrs.length; i += 1) {
|
|
355
|
+
var name = attrs[i]!.name;
|
|
356
|
+
if (name.indexOf("data-agent-native-") === 0) return "style:" + name;
|
|
357
|
+
}
|
|
358
|
+
return "";
|
|
359
|
+
}
|
|
360
|
+
if (tag === "meta") {
|
|
361
|
+
var meta = ["name", "property", "http-equiv", "charset"];
|
|
362
|
+
for (var m = 0; m < meta.length; m += 1) {
|
|
363
|
+
if (node.hasAttribute(meta[m]!)) {
|
|
364
|
+
return "meta:" + meta[m] + "=" + node.getAttribute(meta[m]!);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findHeadNodeBySignature(signature: string): Element | null {
|
|
373
|
+
var children = document.head ? document.head.children : null;
|
|
374
|
+
if (!children) return null;
|
|
375
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
376
|
+
var candidate = children[i]!;
|
|
377
|
+
if (headNodeSignature(candidate) === signature) return candidate;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
335
382
|
function replaceSourceHeadNodes(
|
|
336
383
|
previousSourceHtml: string | null,
|
|
337
384
|
nextSourceHtml: string,
|
|
@@ -344,6 +391,15 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
344
391
|
var key = node.outerHTML;
|
|
345
392
|
staleCounts[key] = (staleCounts[key] || 0) + 1;
|
|
346
393
|
});
|
|
394
|
+
// A node the next head still carries is not stale. Removing and
|
|
395
|
+
// re-inserting it cancels an async script that has not finished loading,
|
|
396
|
+
// and the replacement is inert — innerHTML-built scripts never execute.
|
|
397
|
+
var retained = document.createElement("head");
|
|
398
|
+
retained.innerHTML = nextSourceHtml || "";
|
|
399
|
+
Array.prototype.forEach.call(retained.children, function (node: Element) {
|
|
400
|
+
var key = node.outerHTML;
|
|
401
|
+
if (staleCounts[key]) staleCounts[key] -= 1;
|
|
402
|
+
});
|
|
347
403
|
Array.prototype.slice.call(document.head.children).forEach(function (
|
|
348
404
|
node: Element,
|
|
349
405
|
) {
|
|
@@ -354,8 +410,39 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
354
410
|
});
|
|
355
411
|
var next = document.createElement("head");
|
|
356
412
|
next.innerHTML = nextSourceHtml || "";
|
|
413
|
+
var present: Record<string, number> = {};
|
|
414
|
+
Array.prototype.forEach.call(
|
|
415
|
+
document.head.children,
|
|
416
|
+
function (node: Element) {
|
|
417
|
+
var key = node.outerHTML;
|
|
418
|
+
present[key] = (present[key] || 0) + 1;
|
|
419
|
+
},
|
|
420
|
+
);
|
|
357
421
|
var anchor = document.head.firstChild;
|
|
358
422
|
Array.prototype.slice.call(next.children).forEach(function (node: Element) {
|
|
423
|
+
// The seed pass below passes a null previous head, so every node it
|
|
424
|
+
// carries would otherwise be inserted on top of the identical one the
|
|
425
|
+
// srcdoc already rendered — two copies of the managed stylesheet.
|
|
426
|
+
var key = node.outerHTML;
|
|
427
|
+
if (present[key]) {
|
|
428
|
+
present[key] -= 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// Still the seed pass: with no previous head to diff against, an
|
|
432
|
+
// existing node whose CONTENT changed cannot be matched by outerHTML.
|
|
433
|
+
// Left alone it survives alongside its replacement, and because new
|
|
434
|
+
// nodes are prepended the stale one wins the cascade. Match it by
|
|
435
|
+
// identity instead and swap in place. Unmatched live nodes stay put —
|
|
436
|
+
// they are what the page's own runtime injected.
|
|
437
|
+
var signature =
|
|
438
|
+
previousSourceHtml === null ? headNodeSignature(node) : "";
|
|
439
|
+
if (signature) {
|
|
440
|
+
var existing = findHeadNodeBySignature(signature);
|
|
441
|
+
if (existing) {
|
|
442
|
+
document.head.replaceChild(document.importNode(node, true), existing);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
359
446
|
document.head.insertBefore(document.importNode(node, true), anchor);
|
|
360
447
|
});
|
|
361
448
|
}
|
|
@@ -2484,6 +2571,25 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
2484
2571
|
"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);";
|
|
2485
2572
|
document.body.appendChild(spacingBadge);
|
|
2486
2573
|
|
|
2574
|
+
// Figma's constraint indicator: dashed lines running from the dragged
|
|
2575
|
+
// element to the frame edges it is pinned to. Distinct from the snap
|
|
2576
|
+
// guides above — this shows how the element will REFLOW when its parent
|
|
2577
|
+
// resizes, not what it is currently aligned with.
|
|
2578
|
+
var constraintGuideLayer = document.createElement("div");
|
|
2579
|
+
constraintGuideLayer.setAttribute(
|
|
2580
|
+
"data-agent-native-edit-overlay",
|
|
2581
|
+
"constraint-guide",
|
|
2582
|
+
);
|
|
2583
|
+
constraintGuideLayer.style.cssText =
|
|
2584
|
+
"position:fixed;inset:0;z-index:99999;display:none;pointer-events:none;";
|
|
2585
|
+
document.body.appendChild(constraintGuideLayer);
|
|
2586
|
+
|
|
2587
|
+
var sizeBadge = document.createElement("div");
|
|
2588
|
+
sizeBadge.setAttribute("data-agent-native-edit-overlay", "size-badge");
|
|
2589
|
+
sizeBadge.style.cssText =
|
|
2590
|
+
"position:fixed;z-index:100000;display:none;pointer-events:none;border-radius:3px;background:var(--design-editor-accent-color);color:var(--design-editor-accent-contrast-color);font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;padding:2px 4px;white-space:nowrap;";
|
|
2591
|
+
document.body.appendChild(sizeBadge);
|
|
2592
|
+
|
|
2487
2593
|
var insertionGuide = document.createElement("div");
|
|
2488
2594
|
insertionGuide.setAttribute("data-agent-native-insertion-guide", "");
|
|
2489
2595
|
insertionGuide.setAttribute(
|
|
@@ -2494,24 +2600,18 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
2494
2600
|
"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);";
|
|
2495
2601
|
document.body.appendChild(insertionGuide);
|
|
2496
2602
|
|
|
2497
|
-
// Alignment
|
|
2498
|
-
// element inside the iframe — Figma-style snap-to-sibling guides.
|
|
2499
|
-
//
|
|
2500
|
-
//
|
|
2603
|
+
// Alignment and spacing guides shown while dragging (and resizing) an
|
|
2604
|
+
// element inside the iframe — Figma-style snap-to-sibling guides. One
|
|
2605
|
+
// container whose children are rebuilt per drag tick, since a snapped
|
|
2606
|
+
// position can sit on any number of guide lines at once. Tagged as an
|
|
2501
2607
|
// edit-overlay so elementFromEditorPoint/isOverlayElement never treat a
|
|
2502
2608
|
// guide line as a hit-test or drop target. Color matches the overview
|
|
2503
|
-
// canvas's alignment guides
|
|
2504
|
-
var
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
"position:fixed;z-index:100000;display:none;pointer-events:none;
|
|
2508
|
-
document.body.appendChild(
|
|
2509
|
-
|
|
2510
|
-
var snapGuideH = document.createElement("div");
|
|
2511
|
-
snapGuideH.setAttribute("data-agent-native-edit-overlay", "snap-guide");
|
|
2512
|
-
snapGuideH.style.cssText =
|
|
2513
|
-
"position:fixed;z-index:100000;display:none;pointer-events:none;height:1px;background:hsl(var(--destructive) / 0.9);";
|
|
2514
|
-
document.body.appendChild(snapGuideH);
|
|
2609
|
+
// canvas's alignment guides, in the forwarded measure colour.
|
|
2610
|
+
var snapGuideLayer = document.createElement("div");
|
|
2611
|
+
snapGuideLayer.setAttribute("data-agent-native-edit-overlay", "snap-guide");
|
|
2612
|
+
snapGuideLayer.style.cssText =
|
|
2613
|
+
"position:fixed;inset:0;z-index:100000;display:none;pointer-events:none;";
|
|
2614
|
+
document.body.appendChild(snapGuideLayer);
|
|
2515
2615
|
|
|
2516
2616
|
var measurementOverlay = document.createElement("div");
|
|
2517
2617
|
measurementOverlay.setAttribute("data-agent-native-measurement-overlay", "");
|
|
@@ -2705,6 +2805,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
2705
2805
|
|
|
2706
2806
|
function hideSelectionOverlay(): void {
|
|
2707
2807
|
selectionOverlay.style.display = "none";
|
|
2808
|
+
hideSizeBadge();
|
|
2708
2809
|
hideSpacingOverlay();
|
|
2709
2810
|
hideParentAutoLayoutOverlay();
|
|
2710
2811
|
clearComponentTag();
|
|
@@ -3024,7 +3125,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
3024
3125
|
);
|
|
3025
3126
|
handle.setAttribute("data-corner", pos);
|
|
3026
3127
|
handle.style.cssText =
|
|
3027
|
-
"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:
|
|
3128
|
+
"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:" +
|
|
3129
|
+
(pos === "nw" || pos === "se" ? "nwse-resize" : "nesw-resize") +
|
|
3130
|
+
";";
|
|
3028
3131
|
if (pos.indexOf("n") !== -1) handle.style.top = "-4px";
|
|
3029
3132
|
if (pos.indexOf("s") !== -1) handle.style.bottom = "-4px";
|
|
3030
3133
|
if (pos.indexOf("w") !== -1) handle.style.left = "-4px";
|
|
@@ -3061,6 +3164,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
3061
3164
|
)
|
|
3062
3165
|
.forEach(function (handle) {
|
|
3063
3166
|
var pos = handle.getAttribute("data-corner") || "";
|
|
3167
|
+
// A viewer must not get four dead 7px squares over their content.
|
|
3168
|
+
handle.style.pointerEvents = readOnly ? "none" : "auto";
|
|
3064
3169
|
handle.style.width = 7 * sx + "px";
|
|
3065
3170
|
handle.style.height = 7 * sy + "px";
|
|
3066
3171
|
handle.style.borderWidth = 1 * line + "px";
|
|
@@ -3200,6 +3305,513 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
3200
3305
|
});
|
|
3201
3306
|
}
|
|
3202
3307
|
|
|
3308
|
+
interface MorphContext {
|
|
3309
|
+
keyed: Map<string, Element>;
|
|
3310
|
+
nextKeys: Set<string>;
|
|
3311
|
+
/** Keys whose live element cannot be reused because its tag changed, so
|
|
3312
|
+
* the unkeyed probe must treat it as doomed even though the key lives on
|
|
3313
|
+
* in the next document. */
|
|
3314
|
+
obsolete: Set<string>;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* What the SOURCE document declared for one element, as of the last time the
|
|
3319
|
+
* source was applied to it.
|
|
3320
|
+
*
|
|
3321
|
+
* The morph needs this because the live DOM is authored by two writers: the
|
|
3322
|
+
* design source, and whatever runtime the prototype loads (Alpine above all).
|
|
3323
|
+
* Only the source's own contributions may be reconciled away — an x-show
|
|
3324
|
+
* `display:none`, an `:class` token or an x-for clone has no counterpart in
|
|
3325
|
+
* source and must survive an unrelated edit. Inferring that from directive
|
|
3326
|
+
* names cannot work: it misses plugins, and it cannot tell an authored class
|
|
3327
|
+
* the user just deleted from a class Alpine computed.
|
|
3328
|
+
*/
|
|
3329
|
+
interface SourceMeta {
|
|
3330
|
+
attrs: string[];
|
|
3331
|
+
className: string;
|
|
3332
|
+
style: string;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
function sourceMetaFor(element: Element): SourceMeta | undefined {
|
|
3336
|
+
return (element as Element & { __anSourceMeta?: SourceMeta })
|
|
3337
|
+
.__anSourceMeta;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
function isSourceOwned(node: Node): boolean {
|
|
3341
|
+
return (node as Node & { __anSource?: boolean }).__anSource === true;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function recordSourceOwnership(node: Node): void {
|
|
3345
|
+
(node as Node & { __anSource?: boolean }).__anSource = true;
|
|
3346
|
+
if (node.nodeType !== 1) return;
|
|
3347
|
+
var element = node as Element;
|
|
3348
|
+
var names: string[] = [];
|
|
3349
|
+
for (var i = 0; i < element.attributes.length; i += 1) {
|
|
3350
|
+
names.push(element.attributes[i]!.name);
|
|
3351
|
+
}
|
|
3352
|
+
(element as Element & { __anSourceMeta?: SourceMeta }).__anSourceMeta = {
|
|
3353
|
+
attrs: names,
|
|
3354
|
+
className: element.getAttribute("class") ?? "",
|
|
3355
|
+
style: element.getAttribute("style") ?? "",
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function recordSourceSubtree(root: Node): void {
|
|
3360
|
+
if (
|
|
3361
|
+
root.nodeType === 1 &&
|
|
3362
|
+
(root as Element).hasAttribute("data-agent-native-edit-overlay")
|
|
3363
|
+
) {
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
recordSourceOwnership(root);
|
|
3367
|
+
if (root.nodeType !== 1) return;
|
|
3368
|
+
var template = templateContentOf(root as Element);
|
|
3369
|
+
if (template) {
|
|
3370
|
+
var held = template.childNodes;
|
|
3371
|
+
for (var t = 0; t < held.length; t += 1) recordSourceSubtree(held[t]!);
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
var children = (root as Element).childNodes;
|
|
3375
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
3376
|
+
recordSourceSubtree(children[i]!);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
/** A template's authored children live in `.content`, not as child nodes, so
|
|
3381
|
+
* a walk over childNodes silently ignores every edit inside an x-if/x-for
|
|
3382
|
+
* template until Alpine instantiates the stale markup. */
|
|
3383
|
+
function templateContentOf(element: Element): DocumentFragment | null {
|
|
3384
|
+
if (element.nodeName !== "TEMPLATE") return null;
|
|
3385
|
+
return (element as HTMLTemplateElement).content ?? null;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function scopedMorphContext(
|
|
3389
|
+
liveRoot: ParentNode,
|
|
3390
|
+
nextRoot: ParentNode,
|
|
3391
|
+
): MorphContext {
|
|
3392
|
+
var keyed = new Map<string, Element>();
|
|
3393
|
+
liveRoot.querySelectorAll("[data-agent-native-node-id]").forEach(function (
|
|
3394
|
+
element: Element,
|
|
3395
|
+
) {
|
|
3396
|
+
if (!isSourceOwned(element)) return;
|
|
3397
|
+
var key = element.getAttribute("data-agent-native-node-id");
|
|
3398
|
+
if (key && !keyed.has(key)) keyed.set(key, element);
|
|
3399
|
+
});
|
|
3400
|
+
var nextKeys = new Set<string>();
|
|
3401
|
+
nextRoot.querySelectorAll("[data-agent-native-node-id]").forEach(function (
|
|
3402
|
+
element: Element,
|
|
3403
|
+
) {
|
|
3404
|
+
var key = element.getAttribute("data-agent-native-node-id");
|
|
3405
|
+
if (key) nextKeys.add(key);
|
|
3406
|
+
});
|
|
3407
|
+
return { keyed: keyed, nextKeys: nextKeys, obsolete: new Set<string>() };
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
/**
|
|
3411
|
+
* Seeds ownership from the document the srcdoc was built from: this runs
|
|
3412
|
+
* inline at the end of body during parsing, before any deferred script, so
|
|
3413
|
+
* Alpine and every other deferred runtime has yet to render.
|
|
3414
|
+
*
|
|
3415
|
+
* Two things it cannot see. The head is already contaminated — a blocking
|
|
3416
|
+
* script src such as the Tailwind runtime has injected there — which is why
|
|
3417
|
+
* the head baseline is baked in at build time instead. And DOM appended by
|
|
3418
|
+
* the design's OWN synchronous body scripts, which ran before this point, is
|
|
3419
|
+
* indistinguishable from authored markup and will be treated as source. Our
|
|
3420
|
+
* injected bridges are exempt: everything they add carries
|
|
3421
|
+
* data-agent-native-edit-overlay, which recordSourceSubtree skips.
|
|
3422
|
+
*/
|
|
3423
|
+
function captureInitialSourceOwnership(): void {
|
|
3424
|
+
if (document.body) recordSourceSubtree(document.body);
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
function classTokens(value: string): string[] {
|
|
3428
|
+
return value.split(/\s+/).filter(function (token) {
|
|
3429
|
+
return token.length > 0;
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
/** Keeps class tokens the runtime added while applying the source's edit. */
|
|
3434
|
+
function applyClassAttribute(
|
|
3435
|
+
live: Element,
|
|
3436
|
+
previousSource: string,
|
|
3437
|
+
nextSource: string,
|
|
3438
|
+
): void {
|
|
3439
|
+
var previous = classTokens(previousSource);
|
|
3440
|
+
var current = classTokens(live.getAttribute("class") ?? "");
|
|
3441
|
+
var result = classTokens(nextSource);
|
|
3442
|
+
for (var i = 0; i < current.length; i += 1) {
|
|
3443
|
+
var token = current[i]!;
|
|
3444
|
+
if (previous.indexOf(token) !== -1) continue;
|
|
3445
|
+
if (result.indexOf(token) === -1) result.push(token);
|
|
3446
|
+
}
|
|
3447
|
+
var value = result.join(" ");
|
|
3448
|
+
if ((live.getAttribute("class") ?? "") === value) return;
|
|
3449
|
+
if (value) live.setAttribute("class", value);
|
|
3450
|
+
else live.removeAttribute("class");
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
/** Parsed through a real CSSStyleDeclaration so quoted values and
|
|
3454
|
+
* `!important` survive a round trip that a split on ";" would corrupt. */
|
|
3455
|
+
var styleProbe: HTMLElement | null = null;
|
|
3456
|
+
|
|
3457
|
+
function styleDeclarations(value: string): Array<[string, string, string]> {
|
|
3458
|
+
var probe = styleProbe || (styleProbe = document.createElement("div"));
|
|
3459
|
+
probe.style.cssText = value || "";
|
|
3460
|
+
var out: Array<[string, string, string]> = [];
|
|
3461
|
+
for (var i = 0; i < probe.style.length; i += 1) {
|
|
3462
|
+
var property = probe.style.item(i);
|
|
3463
|
+
out.push([
|
|
3464
|
+
property,
|
|
3465
|
+
probe.style.getPropertyValue(property),
|
|
3466
|
+
probe.style.getPropertyPriority(property),
|
|
3467
|
+
]);
|
|
3468
|
+
}
|
|
3469
|
+
return out;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
/** Property-level counterpart to applyClassAttribute: a property the source
|
|
3473
|
+
* never declared belongs to the runtime (x-show writes display). */
|
|
3474
|
+
function applyStyleAttribute(
|
|
3475
|
+
live: Element,
|
|
3476
|
+
previousSource: string,
|
|
3477
|
+
nextSource: string,
|
|
3478
|
+
): void {
|
|
3479
|
+
var previousOwned: Record<string, string> = {};
|
|
3480
|
+
styleDeclarations(previousSource).forEach(function (entry) {
|
|
3481
|
+
previousOwned[entry[0]] = entry[1];
|
|
3482
|
+
});
|
|
3483
|
+
var nextOwned: Record<string, true> = {};
|
|
3484
|
+
styleDeclarations(nextSource).forEach(function (entry) {
|
|
3485
|
+
nextOwned[entry[0]] = true;
|
|
3486
|
+
});
|
|
3487
|
+
var target = document.createElement("div");
|
|
3488
|
+
target.style.cssText = nextSource || "";
|
|
3489
|
+
styleDeclarations(live.getAttribute("style") ?? "").forEach(
|
|
3490
|
+
function (entry) {
|
|
3491
|
+
if (nextOwned[entry[0]]) return;
|
|
3492
|
+
var wasSource = Object.prototype.hasOwnProperty.call(
|
|
3493
|
+
previousOwned,
|
|
3494
|
+
entry[0],
|
|
3495
|
+
);
|
|
3496
|
+
// Source declared this property and the live value still matches, so
|
|
3497
|
+
// it is the source's to drop. A live value that has diverged is the
|
|
3498
|
+
// runtime's — x-show writing display over an authored one — and
|
|
3499
|
+
// dropping it un-hides the element.
|
|
3500
|
+
if (wasSource && previousOwned[entry[0]] === entry[1]) return;
|
|
3501
|
+
target.style.setProperty(entry[0], entry[1], entry[2]);
|
|
3502
|
+
},
|
|
3503
|
+
);
|
|
3504
|
+
var value = target.style.cssText;
|
|
3505
|
+
if ((live.getAttribute("style") ?? "") === value) return;
|
|
3506
|
+
if (value) live.setAttribute("style", value);
|
|
3507
|
+
else live.removeAttribute("style");
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
/** Form controls carry live state in properties the attributes do not
|
|
3511
|
+
* mirror. Gated on the DEFAULT changing, so a value the user typed is never
|
|
3512
|
+
* overwritten by an unrelated edit. */
|
|
3513
|
+
function morphFormState(live: Element, next: Element): void {
|
|
3514
|
+
if (live.nodeName === "INPUT") {
|
|
3515
|
+
var input = live as HTMLInputElement;
|
|
3516
|
+
var nextChecked = next.hasAttribute("checked");
|
|
3517
|
+
if (input.defaultChecked !== nextChecked) {
|
|
3518
|
+
input.defaultChecked = nextChecked;
|
|
3519
|
+
input.checked = nextChecked;
|
|
3520
|
+
}
|
|
3521
|
+
var nextValue = next.getAttribute("value");
|
|
3522
|
+
if (nextValue !== null && input.defaultValue !== nextValue) {
|
|
3523
|
+
input.defaultValue = nextValue;
|
|
3524
|
+
input.value = nextValue;
|
|
3525
|
+
}
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
if (live.nodeName === "TEXTAREA") {
|
|
3529
|
+
var area = live as HTMLTextAreaElement;
|
|
3530
|
+
var nextText = next.textContent ?? "";
|
|
3531
|
+
if (area.defaultValue !== nextText) {
|
|
3532
|
+
area.defaultValue = nextText;
|
|
3533
|
+
area.value = nextText;
|
|
3534
|
+
}
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
if (live.nodeName === "OPTION") {
|
|
3538
|
+
var option = live as HTMLOptionElement;
|
|
3539
|
+
var nextSelected = next.hasAttribute("selected");
|
|
3540
|
+
if (option.defaultSelected !== nextSelected) {
|
|
3541
|
+
option.defaultSelected = nextSelected;
|
|
3542
|
+
option.selected = nextSelected;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
/** x-text and x-html hand their entire child list to the runtime; whatever
|
|
3548
|
+
* the source still carries inside them is pre-hydration fallback, not
|
|
3549
|
+
* content to reconcile. */
|
|
3550
|
+
function declaresRuntimeChildren(element: Element): boolean {
|
|
3551
|
+
return (
|
|
3552
|
+
element.hasAttribute("x-text") ||
|
|
3553
|
+
element.hasAttribute("x-html") ||
|
|
3554
|
+
element.hasAttribute("v-text") ||
|
|
3555
|
+
element.hasAttribute("v-html")
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function scopeDirectiveChanged(live: Element, next: Element): boolean {
|
|
3560
|
+
return (
|
|
3561
|
+
(live.getAttribute("x-data") ?? "") !==
|
|
3562
|
+
(next.getAttribute("x-data") ?? "")
|
|
3563
|
+
);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function morphNodeKey(node: Node): string | null {
|
|
3567
|
+
if (node.nodeType !== 1) return null;
|
|
3568
|
+
return (node as Element).getAttribute("data-agent-native-node-id");
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
function morphAttributes(live: Element, next: Element): void {
|
|
3572
|
+
var meta = sourceMetaFor(live);
|
|
3573
|
+
var previousAttrs = meta ? meta.attrs : [];
|
|
3574
|
+
var previousClass = meta ? meta.className : "";
|
|
3575
|
+
var previousStyle = meta ? meta.style : "";
|
|
3576
|
+
var nextNames: string[] = [];
|
|
3577
|
+
|
|
3578
|
+
var nextAttrs = next.attributes;
|
|
3579
|
+
for (var i = 0; i < nextAttrs.length; i += 1) {
|
|
3580
|
+
var attr = nextAttrs[i]!;
|
|
3581
|
+
nextNames.push(attr.name);
|
|
3582
|
+
if (attr.name === "class") {
|
|
3583
|
+
applyClassAttribute(live, previousClass, attr.value);
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3586
|
+
if (attr.name === "style") {
|
|
3587
|
+
applyStyleAttribute(live, previousStyle, attr.value);
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3590
|
+
// Alpine strips x-cloak the moment it initialises a tree. Source still
|
|
3591
|
+
// carries it, and putting it back re-hides an element that is running.
|
|
3592
|
+
if (attr.name === "x-cloak" && !live.hasAttribute("x-cloak")) continue;
|
|
3593
|
+
if (live.getAttribute(attr.name) === attr.value) continue;
|
|
3594
|
+
// A namespaced attribute (xlink:href inside an SVG) set through the
|
|
3595
|
+
// plain setter becomes an inert same-named attribute the renderer
|
|
3596
|
+
// ignores.
|
|
3597
|
+
if (attr.namespaceURI) {
|
|
3598
|
+
live.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
|
|
3599
|
+
} else {
|
|
3600
|
+
live.setAttribute(attr.name, attr.value);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
for (var j = 0; j < previousAttrs.length; j += 1) {
|
|
3605
|
+
var name = previousAttrs[j]!;
|
|
3606
|
+
if (nextNames.indexOf(name) !== -1) continue;
|
|
3607
|
+
if (name === "class") {
|
|
3608
|
+
applyClassAttribute(live, previousClass, "");
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
if (name === "style") {
|
|
3612
|
+
applyStyleAttribute(live, previousStyle, "");
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
live.removeAttribute(name);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
(live as Element & { __anSourceMeta?: SourceMeta }).__anSourceMeta = {
|
|
3619
|
+
attrs: nextNames,
|
|
3620
|
+
className: next.getAttribute("class") ?? "",
|
|
3621
|
+
style: next.getAttribute("style") ?? "",
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
function morphChildren(
|
|
3626
|
+
live: Element | DocumentFragment,
|
|
3627
|
+
next: Element | DocumentFragment,
|
|
3628
|
+
context: MorphContext,
|
|
3629
|
+
): void {
|
|
3630
|
+
var cursor = live.firstChild;
|
|
3631
|
+
var nextChild = next.firstChild;
|
|
3632
|
+
while (nextChild) {
|
|
3633
|
+
var key = morphNodeKey(nextChild);
|
|
3634
|
+
var reuse: Node | null = null;
|
|
3635
|
+
if (key) {
|
|
3636
|
+
var candidate = context.keyed.get(key) ?? null;
|
|
3637
|
+
// A keyed node that already contains this parent cannot be moved
|
|
3638
|
+
// inside itself; recreate it instead of building a cycle. A candidate
|
|
3639
|
+
// whose tag changed cannot be morphed into the new one either — the
|
|
3640
|
+
// iframe would keep a div where source now says button.
|
|
3641
|
+
if (
|
|
3642
|
+
candidate &&
|
|
3643
|
+
!candidate.contains(live) &&
|
|
3644
|
+
candidate.nodeName === nextChild.nodeName &&
|
|
3645
|
+
candidate.namespaceURI === (nextChild as Element).namespaceURI
|
|
3646
|
+
) {
|
|
3647
|
+
reuse = candidate;
|
|
3648
|
+
// Claim it: authored markup can repeat an id, and reusing one live
|
|
3649
|
+
// element for both would move the same node twice and drop one.
|
|
3650
|
+
context.keyed.delete(key);
|
|
3651
|
+
} else if (candidate) {
|
|
3652
|
+
context.obsolete.add(key);
|
|
3653
|
+
}
|
|
3654
|
+
} else {
|
|
3655
|
+
// Skip live siblings that cannot be this source child: runtime output,
|
|
3656
|
+
// and keyed nodes already destined to disappear. Stopping at one would
|
|
3657
|
+
// import a fresh copy of the unchanged node behind it and destroy the
|
|
3658
|
+
// original's listeners and state.
|
|
3659
|
+
var probe: Node | null = cursor;
|
|
3660
|
+
while (probe) {
|
|
3661
|
+
var probeKey = morphNodeKey(probe);
|
|
3662
|
+
if (probeKey) {
|
|
3663
|
+
if (
|
|
3664
|
+
context.nextKeys.has(probeKey) &&
|
|
3665
|
+
!context.obsolete.has(probeKey)
|
|
3666
|
+
) {
|
|
3667
|
+
break;
|
|
3668
|
+
}
|
|
3669
|
+
probe = probe.nextSibling;
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
if (!isSourceOwned(probe)) {
|
|
3673
|
+
probe = probe.nextSibling;
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
if (
|
|
3677
|
+
probe.nodeType === nextChild.nodeType &&
|
|
3678
|
+
probe.nodeName === nextChild.nodeName
|
|
3679
|
+
) {
|
|
3680
|
+
reuse = probe;
|
|
3681
|
+
}
|
|
3682
|
+
break;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
if (
|
|
3686
|
+
reuse &&
|
|
3687
|
+
reuse.nodeType === 1 &&
|
|
3688
|
+
scopeDirectiveChanged(reuse as Element, nextChild as Element)
|
|
3689
|
+
) {
|
|
3690
|
+
// Rebuild just this component rather than the frame: Alpine evaluates
|
|
3691
|
+
// x-data once at init, so patching it in place leaves every binding
|
|
3692
|
+
// underneath reading the previous scope. Alpine's own observer
|
|
3693
|
+
// initialises the replacement.
|
|
3694
|
+
var rebuilt = document.importNode(nextChild as Element, true);
|
|
3695
|
+
// Anchor on `cursor`, not on `reuse`: a keyed candidate can live
|
|
3696
|
+
// anywhere in the document, so when this parent is itself newly
|
|
3697
|
+
// inserted the old node is not its child and insertBefore throws.
|
|
3698
|
+
live.insertBefore(rebuilt, cursor);
|
|
3699
|
+
if (reuse.parentNode) reuse.parentNode.removeChild(reuse);
|
|
3700
|
+
recordSourceSubtree(rebuilt);
|
|
3701
|
+
cursor = rebuilt.nextSibling;
|
|
3702
|
+
nextChild = nextChild.nextSibling;
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
if (reuse) {
|
|
3706
|
+
if (reuse !== cursor) live.insertBefore(reuse, cursor);
|
|
3707
|
+
if (reuse.nodeType === 1) {
|
|
3708
|
+
morphElement(reuse as Element, nextChild as Element, context);
|
|
3709
|
+
} else if (reuse.nodeValue !== nextChild.nodeValue) {
|
|
3710
|
+
reuse.nodeValue = nextChild.nodeValue;
|
|
3711
|
+
}
|
|
3712
|
+
cursor = reuse.nextSibling;
|
|
3713
|
+
} else if (nextChild.nodeType === 1) {
|
|
3714
|
+
// Shell first, then reconcile: a deep import would clone keyed
|
|
3715
|
+
// descendants that already exist live, and the originals would be
|
|
3716
|
+
// swept as stale right after. Wrapping a component in a new parent
|
|
3717
|
+
// (the Group action) has to move the existing child, not rebuild it.
|
|
3718
|
+
var shell = document.importNode(nextChild as Element, false) as Element;
|
|
3719
|
+
live.insertBefore(shell, cursor);
|
|
3720
|
+
recordSourceOwnership(shell);
|
|
3721
|
+
morphElement(shell, nextChild as Element, context);
|
|
3722
|
+
// That reconcile can pull `cursor` itself into the shell, leaving the
|
|
3723
|
+
// outer walk holding a node this parent no longer owns.
|
|
3724
|
+
cursor = shell.nextSibling;
|
|
3725
|
+
} else {
|
|
3726
|
+
var imported = document.importNode(nextChild, true);
|
|
3727
|
+
live.insertBefore(imported, cursor);
|
|
3728
|
+
recordSourceSubtree(imported);
|
|
3729
|
+
}
|
|
3730
|
+
nextChild = nextChild.nextSibling;
|
|
3731
|
+
}
|
|
3732
|
+
while (cursor) {
|
|
3733
|
+
var stale = cursor;
|
|
3734
|
+
cursor = cursor.nextSibling;
|
|
3735
|
+
// A keyed node can have been moved into a subtree inserted earlier in
|
|
3736
|
+
// this same pass; it is no longer this parent's to remove.
|
|
3737
|
+
if (stale.parentNode !== live) continue;
|
|
3738
|
+
// Only the source's own children are the source's to delete. What is
|
|
3739
|
+
// left is runtime output — an x-for clone, x-text's text node — and the
|
|
3740
|
+
// runtime holds the same parent element, so it never re-renders it.
|
|
3741
|
+
if (!isSourceOwned(stale)) continue;
|
|
3742
|
+
live.removeChild(stale);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
function morphElement(
|
|
3747
|
+
live: Element,
|
|
3748
|
+
next: Element,
|
|
3749
|
+
context: MorphContext,
|
|
3750
|
+
): void {
|
|
3751
|
+
// Before morphAttributes: writing the `value` attribute moves
|
|
3752
|
+
// defaultValue, and the guard inside morphFormState reads defaultValue to
|
|
3753
|
+
// decide whether the SOURCE default changed. Run it after and a dirty
|
|
3754
|
+
// input never sees an explicit source edit.
|
|
3755
|
+
morphFormState(live, next);
|
|
3756
|
+
morphAttributes(live, next);
|
|
3757
|
+
if (declaresRuntimeChildren(next) || declaresRuntimeChildren(live)) return;
|
|
3758
|
+
var liveTemplate = templateContentOf(live);
|
|
3759
|
+
var nextTemplate = templateContentOf(next);
|
|
3760
|
+
if (liveTemplate && nextTemplate) {
|
|
3761
|
+
// Its own key scope: a node id can legitimately appear both inside a
|
|
3762
|
+
// template and in the instantiated body, and the outer map must not
|
|
3763
|
+
// hand the live one over to the template.
|
|
3764
|
+
morphChildren(
|
|
3765
|
+
liveTemplate,
|
|
3766
|
+
nextTemplate,
|
|
3767
|
+
scopedMorphContext(liveTemplate, nextTemplate),
|
|
3768
|
+
);
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
morphChildren(live, next, context);
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
/**
|
|
3775
|
+
* Reconcile the live body against the parsed next document, reusing every
|
|
3776
|
+
* node whose `data-agent-native-node-id` is unchanged.
|
|
3777
|
+
*
|
|
3778
|
+
* Never `body.innerHTML = next`. That rebuilds every node in the screen, so
|
|
3779
|
+
* editing one element restarts Alpine components, CSS transitions and media,
|
|
3780
|
+
* drops focus and inner scroll positions, and re-decodes every image — the
|
|
3781
|
+
* "the frame refreshed" report. This walk touches only what differs.
|
|
3782
|
+
*
|
|
3783
|
+
* Like innerHTML it does not execute an inserted script, so a changed script
|
|
3784
|
+
* is still the caller's cue to rebuild the document — see
|
|
3785
|
+
* `runtimeDocumentNeedsReload` in DesignCanvas.tsx.
|
|
3786
|
+
*/
|
|
3787
|
+
function morphRuntimeBody(nextBody: Element): void {
|
|
3788
|
+
var keyed = new Map<string, Element>();
|
|
3789
|
+
document.querySelectorAll("[data-agent-native-node-id]").forEach(function (
|
|
3790
|
+
element: Element,
|
|
3791
|
+
) {
|
|
3792
|
+
// An x-for clone copies the template's markup, node id and all. Indexing
|
|
3793
|
+
// one would let the morph adopt a runtime clone as the source node and
|
|
3794
|
+
// move it out of the list it belongs to.
|
|
3795
|
+
if (!isSourceOwned(element)) return;
|
|
3796
|
+
var key = element.getAttribute("data-agent-native-node-id");
|
|
3797
|
+
// First occurrence wins: a duplicated id in authored markup must not
|
|
3798
|
+
// let a later node steal an earlier node's identity.
|
|
3799
|
+
if (key && !keyed.has(key)) keyed.set(key, element);
|
|
3800
|
+
});
|
|
3801
|
+
var nextKeys = new Set<string>();
|
|
3802
|
+
nextBody.querySelectorAll("[data-agent-native-node-id]").forEach(function (
|
|
3803
|
+
element: Element,
|
|
3804
|
+
) {
|
|
3805
|
+
var key = element.getAttribute("data-agent-native-node-id");
|
|
3806
|
+
if (key) nextKeys.add(key);
|
|
3807
|
+
});
|
|
3808
|
+
morphElement(document.body, nextBody, {
|
|
3809
|
+
keyed: keyed,
|
|
3810
|
+
nextKeys: nextKeys,
|
|
3811
|
+
obsolete: new Set<string>(),
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3203
3815
|
function replaceRuntimeDocument(
|
|
3204
3816
|
html: string,
|
|
3205
3817
|
preferredSelector: string,
|
|
@@ -3283,9 +3895,15 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
3283
3895
|
var nextHeadHtml = nextDoc.head ? nextDoc.head.innerHTML : "";
|
|
3284
3896
|
ensureEditorChromeStyle();
|
|
3285
3897
|
if (lastSourceHeadHtml === null) {
|
|
3286
|
-
// First patch after a srcdoc build
|
|
3287
|
-
//
|
|
3288
|
-
// the
|
|
3898
|
+
// First patch after a srcdoc build. The document already carries the
|
|
3899
|
+
// head it was built from, so this seeds the baseline — but it cannot
|
|
3900
|
+
// just adopt: when the first patch is itself a head edit (a breakpoint,
|
|
3901
|
+
// motion or token write, none of which reload the frame any more),
|
|
3902
|
+
// adopting means that stylesheet never reaches the live document and
|
|
3903
|
+
// every later diff is measured against a head that was never applied.
|
|
3904
|
+
// Insert only what is genuinely new; replaceSourceHeadNodes skips nodes
|
|
3905
|
+
// already present.
|
|
3906
|
+
replaceSourceHeadNodes(null, nextHeadHtml);
|
|
3289
3907
|
lastSourceHeadHtml = nextHeadHtml;
|
|
3290
3908
|
}
|
|
3291
3909
|
var currentHeadHtml = lastSourceHeadHtml;
|
|
@@ -3376,17 +3994,12 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
3376
3994
|
ensureEditorChromeStyle();
|
|
3377
3995
|
lastSourceHeadHtml = nextHeadHtml;
|
|
3378
3996
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
) {
|
|
3382
|
-
|
|
3383
|
-
});
|
|
3384
|
-
Array.prototype.slice.call(nextDoc.body.attributes).forEach(function (
|
|
3385
|
-
attribute: Attr,
|
|
3386
|
-
) {
|
|
3387
|
-
document.body.setAttribute(attribute.name, attribute.value);
|
|
3997
|
+
// Detached first so the keyed walk below never sees editor chrome as a
|
|
3998
|
+
// stale child of the source document and removes it.
|
|
3999
|
+
persistentNodes.forEach(function (node) {
|
|
4000
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
3388
4001
|
});
|
|
3389
|
-
|
|
4002
|
+
morphRuntimeBody(nextDoc.body);
|
|
3390
4003
|
persistentNodes.forEach(function (node) {
|
|
3391
4004
|
document.body.appendChild(node);
|
|
3392
4005
|
});
|
|
@@ -4551,6 +5164,19 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
4551
5164
|
overlay.style.cssText =
|
|
4552
5165
|
"position:fixed;pointer-events:none;z-index:99996;border:1.5px solid var(--design-editor-accent-color);background:transparent;display:none;box-sizing:border-box;";
|
|
4553
5166
|
appendPassiveSelectionHandles(overlay);
|
|
5167
|
+
overlay.addEventListener(
|
|
5168
|
+
"mousedown",
|
|
5169
|
+
function (e) {
|
|
5170
|
+
if (readOnly) return;
|
|
5171
|
+
var corner =
|
|
5172
|
+
e.target &&
|
|
5173
|
+
(e.target as Element).getAttribute &&
|
|
5174
|
+
(e.target as Element).getAttribute("data-corner");
|
|
5175
|
+
if (!corner) return;
|
|
5176
|
+
startGroupResize(corner, e);
|
|
5177
|
+
},
|
|
5178
|
+
true,
|
|
5179
|
+
);
|
|
4554
5180
|
document.body.appendChild(overlay);
|
|
4555
5181
|
multiSelectionBoundsOverlay = overlay;
|
|
4556
5182
|
return overlay;
|
|
@@ -4564,6 +5190,11 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
4564
5190
|
passiveSelectionEls.forEach(function (el) {
|
|
4565
5191
|
if (el && document.documentElement.contains(el)) members.push(el);
|
|
4566
5192
|
});
|
|
5193
|
+
// One set of handles per multi-selection: the primary member's own sit
|
|
5194
|
+
// over the group's at a shared corner and win the hit test.
|
|
5195
|
+
setSelectionOverlayResizeChromeVisible(
|
|
5196
|
+
!readOnly && !activeTextEditEl && members.length < 2,
|
|
5197
|
+
);
|
|
4567
5198
|
if (members.length < 2 || selectionChromeHidden) {
|
|
4568
5199
|
if (multiSelectionBoundsOverlay) {
|
|
4569
5200
|
multiSelectionBoundsOverlay.style.display = "none";
|
|
@@ -4634,6 +5265,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
4634
5265
|
// falls back to its own read in that case.
|
|
4635
5266
|
updateComponentTag(el, rect);
|
|
4636
5267
|
updateParentAutoLayoutOverlay(el);
|
|
5268
|
+
showSizeBadge(el);
|
|
4637
5269
|
} else {
|
|
4638
5270
|
applyElementOverlayChrome(overlay, el);
|
|
4639
5271
|
}
|
|
@@ -4674,6 +5306,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
4674
5306
|
var overlay = passiveSelectionOverlays[index];
|
|
4675
5307
|
if (overlay) positionOverlay(overlay, el);
|
|
4676
5308
|
});
|
|
5309
|
+
// A multi-selection has no single size to report; positionOverlay owns
|
|
5310
|
+
// the badge for the single-selection case.
|
|
5311
|
+
if (passiveSelectionEls.length > 0) hideSizeBadge();
|
|
4677
5312
|
positionMultiSelectionBounds();
|
|
4678
5313
|
positionGradientOverlay();
|
|
4679
5314
|
syncOverlayObservers();
|
|
@@ -7260,17 +7895,10 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
7260
7895
|
return true;
|
|
7261
7896
|
}
|
|
7262
7897
|
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
// flow-insert lands the members consecutively in their existing visual
|
|
7268
|
-
// order) when gestureEl belongs to a 2+ selection, or just [gestureEl]
|
|
7269
|
-
// otherwise. Members nested inside another member are dropped: moving the
|
|
7270
|
-
// ancestor already moves them, and double-applying the delta would fling
|
|
7271
|
-
// them.
|
|
7272
|
-
function collectMoveGroupMembers(gestureEl: Element): Element[] {
|
|
7273
|
-
if (!gestureEl) return [];
|
|
7898
|
+
/** The current multi-selection's own elements: the primary plus the passive
|
|
7899
|
+
* shift-click/marquee set, minus blocked layers and anything contained by
|
|
7900
|
+
* another member. */
|
|
7901
|
+
function collectSelectionMembers(): Element[] {
|
|
7274
7902
|
var raw: Element[] = [];
|
|
7275
7903
|
if (selectedEl) raw.push(selectedEl);
|
|
7276
7904
|
for (var i = 0; i < passiveSelectionEls.length; i += 1) {
|
|
@@ -7291,12 +7919,25 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
7291
7919
|
}
|
|
7292
7920
|
members.push(candidate);
|
|
7293
7921
|
}
|
|
7294
|
-
|
|
7295
|
-
members = members.filter(function (member) {
|
|
7922
|
+
return members.filter(function (member) {
|
|
7296
7923
|
return !members.some(function (other) {
|
|
7297
7924
|
return other !== member && other.contains(member);
|
|
7298
7925
|
});
|
|
7299
7926
|
});
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
// Multi-select group move: when the user drags an element that is a member
|
|
7930
|
+
// of the current multi-selection (primary selectedEl + the passive
|
|
7931
|
+
// shift-click/marquee set), the whole group moves together — Figma
|
|
7932
|
+
// behavior. Returns the full member list in DOCUMENT ORDER (so a group
|
|
7933
|
+
// flow-insert lands the members consecutively in their existing visual
|
|
7934
|
+
// order) when gestureEl belongs to a 2+ selection, or just [gestureEl]
|
|
7935
|
+
// otherwise. Members nested inside another member are dropped: moving the
|
|
7936
|
+
// ancestor already moves them, and double-applying the delta would fling
|
|
7937
|
+
// them.
|
|
7938
|
+
function collectMoveGroupMembers(gestureEl: Element): Element[] {
|
|
7939
|
+
if (!gestureEl) return [];
|
|
7940
|
+
var members = collectSelectionMembers();
|
|
7300
7941
|
var gestureMember: Element | null = null;
|
|
7301
7942
|
for (var k = 0; k < members.length; k += 1) {
|
|
7302
7943
|
if (
|
|
@@ -9039,110 +9680,896 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
9039
9680
|
return rects;
|
|
9040
9681
|
}
|
|
9041
9682
|
|
|
9042
|
-
//
|
|
9043
|
-
//
|
|
9044
|
-
//
|
|
9045
|
-
//
|
|
9046
|
-
//
|
|
9047
|
-
|
|
9048
|
-
//
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9683
|
+
// Hand-port of shared/canvas-math.ts computeDragSnap and its helpers:
|
|
9684
|
+
// alignment offset per axis, then every guide line the snapped position
|
|
9685
|
+
// actually sits on, then a spacing snap on whichever axes alignment left
|
|
9686
|
+
// free. Keep the two in sync — canvas-math is the reference implementation
|
|
9687
|
+
// and editor-chrome-bridge.snap.test.ts proves this copy against it.
|
|
9688
|
+
var SNAP_ALIGN_EPSILON = 1e-6;
|
|
9689
|
+
// Guides are drawn at this tight tolerance, not the snap pull: on an axis
|
|
9690
|
+
// locked by an alignment guide the element never moved, so a near-equal
|
|
9691
|
+
// pair would otherwise get labelled as an equal gap it does not have.
|
|
9692
|
+
var SPACING_MATCH_EPSILON = 0.5;
|
|
9693
|
+
// Screen px: beyond this a neighbour is not what the user is positioning
|
|
9694
|
+
// against, and its measurement is a line stretched over empty space.
|
|
9695
|
+
var PROXIMITY_RANGE_PX = 160;
|
|
9696
|
+
|
|
9697
|
+
function axisSnapValues(bounds, axis) {
|
|
9698
|
+
return axis === "x"
|
|
9699
|
+
? [bounds.left, bounds.centerX, bounds.right]
|
|
9700
|
+
: [bounds.top, bounds.centerY, bounds.bottom];
|
|
9701
|
+
}
|
|
9702
|
+
|
|
9703
|
+
function axisStart(bounds, axis) {
|
|
9704
|
+
return axis === "x" ? bounds.left : bounds.top;
|
|
9705
|
+
}
|
|
9706
|
+
|
|
9707
|
+
function axisEnd(bounds, axis) {
|
|
9708
|
+
return axis === "x" ? bounds.right : bounds.bottom;
|
|
9709
|
+
}
|
|
9710
|
+
|
|
9711
|
+
function crossStart(bounds, axis) {
|
|
9712
|
+
return axis === "x" ? bounds.top : bounds.left;
|
|
9713
|
+
}
|
|
9714
|
+
|
|
9715
|
+
function crossEnd(bounds, axis) {
|
|
9716
|
+
return axis === "x" ? bounds.bottom : bounds.right;
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9719
|
+
function crossAxisOverlaps(axis, a, b) {
|
|
9720
|
+
return (
|
|
9721
|
+
crossStart(a, axis) < crossEnd(b, axis) &&
|
|
9722
|
+
crossEnd(a, axis) > crossStart(b, axis)
|
|
9723
|
+
);
|
|
9724
|
+
}
|
|
9725
|
+
|
|
9726
|
+
function translateRectBounds(bounds, dx, dy) {
|
|
9727
|
+
return {
|
|
9728
|
+
left: bounds.left + dx,
|
|
9729
|
+
right: bounds.right + dx,
|
|
9730
|
+
centerX: bounds.centerX + dx,
|
|
9731
|
+
top: bounds.top + dy,
|
|
9732
|
+
bottom: bounds.bottom + dy,
|
|
9733
|
+
centerY: bounds.centerY + dy,
|
|
9734
|
+
};
|
|
9735
|
+
}
|
|
9736
|
+
|
|
9737
|
+
function findAxisSnapOffset(axis, moving, candidates, threshold) {
|
|
9738
|
+
var offset = null;
|
|
9739
|
+
var bestDistance = Infinity;
|
|
9740
|
+
var movingValues = axisSnapValues(moving, axis);
|
|
9053
9741
|
for (var i = 0; i < candidates.length; i += 1) {
|
|
9054
|
-
var
|
|
9055
|
-
var
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
if (!bestX || distanceX < bestX.distance) {
|
|
9063
|
-
bestX = {
|
|
9064
|
-
distance: distanceX,
|
|
9065
|
-
offset: offsetX,
|
|
9066
|
-
guide: {
|
|
9067
|
-
position: xTargets[xj],
|
|
9068
|
-
start: Math.min(moving.top, candidate.top),
|
|
9069
|
-
end: Math.max(moving.bottom, candidate.bottom),
|
|
9070
|
-
},
|
|
9071
|
-
};
|
|
9072
|
-
}
|
|
9742
|
+
var targets = axisSnapValues(candidates[i], axis);
|
|
9743
|
+
for (var mi = 0; mi < movingValues.length; mi += 1) {
|
|
9744
|
+
for (var ti = 0; ti < targets.length; ti += 1) {
|
|
9745
|
+
var candidate = targets[ti] - movingValues[mi];
|
|
9746
|
+
var distance = Math.abs(candidate);
|
|
9747
|
+
if (distance > threshold || distance >= bestDistance) continue;
|
|
9748
|
+
bestDistance = distance;
|
|
9749
|
+
offset = candidate;
|
|
9073
9750
|
}
|
|
9074
9751
|
}
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9752
|
+
}
|
|
9753
|
+
return offset;
|
|
9754
|
+
}
|
|
9755
|
+
|
|
9756
|
+
function buildAxisGuides(axis, moving, candidates) {
|
|
9757
|
+
var positions: number[] = [];
|
|
9758
|
+
var spans: { start: number; end: number }[] = [];
|
|
9759
|
+
var movingValues = axisSnapValues(moving, axis);
|
|
9760
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
9761
|
+
var candidate = candidates[i];
|
|
9762
|
+
var targets = axisSnapValues(candidate, axis);
|
|
9763
|
+
for (var mi = 0; mi < movingValues.length; mi += 1) {
|
|
9764
|
+
for (var ti = 0; ti < targets.length; ti += 1) {
|
|
9765
|
+
if (Math.abs(targets[ti] - movingValues[mi]) > SNAP_ALIGN_EPSILON) {
|
|
9766
|
+
continue;
|
|
9767
|
+
}
|
|
9768
|
+
var slot = positions.indexOf(targets[ti]);
|
|
9769
|
+
if (slot === -1) {
|
|
9770
|
+
positions.push(targets[ti]);
|
|
9771
|
+
spans.push({
|
|
9772
|
+
start: Math.min(
|
|
9773
|
+
crossStart(moving, axis),
|
|
9774
|
+
crossStart(candidate, axis),
|
|
9775
|
+
),
|
|
9776
|
+
end: Math.max(crossEnd(moving, axis), crossEnd(candidate, axis)),
|
|
9777
|
+
});
|
|
9778
|
+
} else {
|
|
9779
|
+
spans[slot].start = Math.min(
|
|
9780
|
+
spans[slot].start,
|
|
9781
|
+
crossStart(candidate, axis),
|
|
9782
|
+
);
|
|
9783
|
+
spans[slot].end = Math.max(
|
|
9784
|
+
spans[slot].end,
|
|
9785
|
+
crossEnd(candidate, axis),
|
|
9786
|
+
);
|
|
9092
9787
|
}
|
|
9093
9788
|
}
|
|
9094
9789
|
}
|
|
9095
9790
|
}
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
};
|
|
9791
|
+
var guides: {
|
|
9792
|
+
orientation: string;
|
|
9793
|
+
position: number;
|
|
9794
|
+
start: number;
|
|
9795
|
+
end: number;
|
|
9796
|
+
}[] = [];
|
|
9797
|
+
for (var g = 0; g < positions.length; g += 1) {
|
|
9798
|
+
guides.push({
|
|
9799
|
+
orientation: axis === "x" ? "vertical" : "horizontal",
|
|
9800
|
+
position: positions[g],
|
|
9801
|
+
start: spans[g].start,
|
|
9802
|
+
end: spans[g].end,
|
|
9803
|
+
});
|
|
9804
|
+
}
|
|
9805
|
+
return guides;
|
|
9102
9806
|
}
|
|
9103
9807
|
|
|
9104
|
-
function
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9808
|
+
function collectAxisGapCandidates(axis, moving, candidates) {
|
|
9809
|
+
var gaps: {
|
|
9810
|
+
side: string;
|
|
9811
|
+
gap: number;
|
|
9812
|
+
gapStart: number;
|
|
9813
|
+
gapEnd: number;
|
|
9814
|
+
crossStart: number;
|
|
9815
|
+
crossEnd: number;
|
|
9816
|
+
}[] = [];
|
|
9817
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
9818
|
+
var bounds = candidates[i];
|
|
9819
|
+
if (!crossAxisOverlaps(axis, bounds, moving)) continue;
|
|
9820
|
+
var band = {
|
|
9821
|
+
crossStart: Math.max(
|
|
9822
|
+
crossStart(bounds, axis),
|
|
9823
|
+
crossStart(moving, axis),
|
|
9824
|
+
),
|
|
9825
|
+
crossEnd: Math.min(crossEnd(bounds, axis), crossEnd(moving, axis)),
|
|
9826
|
+
};
|
|
9827
|
+
if (axisEnd(bounds, axis) <= axisStart(moving, axis)) {
|
|
9828
|
+
gaps.push({
|
|
9829
|
+
side: "before",
|
|
9830
|
+
gap: axisStart(moving, axis) - axisEnd(bounds, axis),
|
|
9831
|
+
gapStart: axisEnd(bounds, axis),
|
|
9832
|
+
gapEnd: axisStart(moving, axis),
|
|
9833
|
+
crossStart: band.crossStart,
|
|
9834
|
+
crossEnd: band.crossEnd,
|
|
9835
|
+
});
|
|
9836
|
+
} else if (axisStart(bounds, axis) >= axisEnd(moving, axis)) {
|
|
9837
|
+
gaps.push({
|
|
9838
|
+
side: "after",
|
|
9839
|
+
gap: axisStart(bounds, axis) - axisEnd(moving, axis),
|
|
9840
|
+
gapStart: axisEnd(moving, axis),
|
|
9841
|
+
gapEnd: axisStart(bounds, axis),
|
|
9842
|
+
crossStart: band.crossStart,
|
|
9843
|
+
crossEnd: band.crossEnd,
|
|
9844
|
+
});
|
|
9845
|
+
}
|
|
9127
9846
|
}
|
|
9847
|
+
return gaps;
|
|
9128
9848
|
}
|
|
9129
9849
|
|
|
9130
|
-
function
|
|
9131
|
-
|
|
9132
|
-
|
|
9850
|
+
function closestGapCandidate(gaps, side) {
|
|
9851
|
+
var best = null;
|
|
9852
|
+
for (var i = 0; i < gaps.length; i += 1) {
|
|
9853
|
+
if (gaps[i].side !== side) continue;
|
|
9854
|
+
if (!best || gaps[i].gap < best.gap) best = gaps[i];
|
|
9855
|
+
}
|
|
9856
|
+
return best;
|
|
9133
9857
|
}
|
|
9134
9858
|
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9859
|
+
function collectRhythmGaps(axis, moving, candidates) {
|
|
9860
|
+
var row: any[] = [];
|
|
9861
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
9862
|
+
var entry = candidates[i];
|
|
9863
|
+
if (!crossAxisOverlaps(axis, entry, moving)) continue;
|
|
9864
|
+
// A candidate that spans the whole moving element — the parent content
|
|
9865
|
+
// box — is a wrapper, not a neighbour in the rhythm, and its span would
|
|
9866
|
+
// swallow every real gap in the row.
|
|
9867
|
+
if (
|
|
9868
|
+
axisStart(entry, axis) <= axisStart(moving, axis) &&
|
|
9869
|
+
axisEnd(entry, axis) >= axisEnd(moving, axis)
|
|
9870
|
+
) {
|
|
9871
|
+
continue;
|
|
9872
|
+
}
|
|
9873
|
+
row.push(entry);
|
|
9874
|
+
}
|
|
9875
|
+
row.sort(function (a, b) {
|
|
9876
|
+
return axisStart(a, axis) - axisStart(b, axis);
|
|
9877
|
+
});
|
|
9878
|
+
var gaps: { gap: number; band: any }[] = [];
|
|
9879
|
+
var previous: any = null;
|
|
9880
|
+
for (var r = 0; r < row.length; r += 1) {
|
|
9881
|
+
var bounds = row[r];
|
|
9882
|
+
var gap = previous
|
|
9883
|
+
? axisStart(bounds, axis) - axisEnd(previous, axis)
|
|
9884
|
+
: 0;
|
|
9885
|
+
if (previous && gap > 0 && crossAxisOverlaps(axis, previous, bounds)) {
|
|
9886
|
+
gaps.push({
|
|
9887
|
+
gap: gap,
|
|
9888
|
+
band: {
|
|
9889
|
+
gapStart: axisEnd(previous, axis),
|
|
9890
|
+
gapEnd: axisStart(bounds, axis),
|
|
9891
|
+
crossStart: Math.max(
|
|
9892
|
+
crossStart(previous, axis),
|
|
9893
|
+
crossStart(bounds, axis),
|
|
9894
|
+
),
|
|
9895
|
+
crossEnd: Math.min(
|
|
9896
|
+
crossEnd(previous, axis),
|
|
9897
|
+
crossEnd(bounds, axis),
|
|
9898
|
+
),
|
|
9899
|
+
},
|
|
9900
|
+
});
|
|
9901
|
+
}
|
|
9902
|
+
if (!previous || axisEnd(bounds, axis) > axisEnd(previous, axis)) {
|
|
9903
|
+
previous = bounds;
|
|
9904
|
+
}
|
|
9905
|
+
}
|
|
9906
|
+
return gaps;
|
|
9907
|
+
}
|
|
9908
|
+
|
|
9909
|
+
// Returns the matched side with the offset: picking a side independently
|
|
9910
|
+
// builds chrome for a gap the snap never used.
|
|
9911
|
+
function findSpacingSnapOffset(axis, moving, candidates, tolerance) {
|
|
9912
|
+
var gaps = collectAxisGapCandidates(axis, moving, candidates);
|
|
9913
|
+
var before = closestGapCandidate(gaps, "before");
|
|
9914
|
+
var after = closestGapCandidate(gaps, "after");
|
|
9915
|
+
if (!before && !after) return { offset: 0, side: null };
|
|
9916
|
+
|
|
9917
|
+
var offset = 0;
|
|
9918
|
+
var side = null;
|
|
9919
|
+
var bestDistance = Infinity;
|
|
9920
|
+
function consider(value, matched) {
|
|
9921
|
+
var distance = Math.abs(value);
|
|
9922
|
+
if (distance > tolerance || distance >= bestDistance) return;
|
|
9923
|
+
bestDistance = distance;
|
|
9924
|
+
offset = value;
|
|
9925
|
+
side = matched;
|
|
9926
|
+
}
|
|
9927
|
+
if (before && after) consider((after.gap - before.gap) / 2, "both");
|
|
9928
|
+
var rhythms = collectRhythmGaps(axis, moving, candidates);
|
|
9929
|
+
for (var i = 0; i < rhythms.length; i += 1) {
|
|
9930
|
+
if (before) consider(rhythms[i].gap - before.gap, "before");
|
|
9931
|
+
if (after) consider(after.gap - rhythms[i].gap, "after");
|
|
9932
|
+
}
|
|
9933
|
+
return { offset: offset, side: side };
|
|
9934
|
+
}
|
|
9935
|
+
|
|
9936
|
+
function gapCandidateBand(candidate) {
|
|
9937
|
+
return {
|
|
9938
|
+
gapStart: candidate.gapStart,
|
|
9939
|
+
gapEnd: candidate.gapEnd,
|
|
9940
|
+
crossStart: candidate.crossStart,
|
|
9941
|
+
crossEnd: candidate.crossEnd,
|
|
9942
|
+
};
|
|
9943
|
+
}
|
|
9944
|
+
|
|
9945
|
+
// Every gap already in the row/column that matches: Figma and tldraw both
|
|
9946
|
+
// light the whole run, not only the pair the snap landed on.
|
|
9947
|
+
function matchingRhythmBands(rhythms, gap, tolerance) {
|
|
9948
|
+
var bands: any[] = [];
|
|
9949
|
+
for (var i = 0; i < rhythms.length; i += 1) {
|
|
9950
|
+
if (Math.abs(rhythms[i].gap - gap) <= tolerance)
|
|
9951
|
+
bands.push(rhythms[i].band);
|
|
9952
|
+
}
|
|
9953
|
+
return bands;
|
|
9954
|
+
}
|
|
9955
|
+
|
|
9956
|
+
function buildSpacingGuides(
|
|
9957
|
+
axis,
|
|
9958
|
+
moving,
|
|
9959
|
+
candidates,
|
|
9960
|
+
tolerance,
|
|
9961
|
+
gaps,
|
|
9962
|
+
matchedSide,
|
|
9963
|
+
) {
|
|
9964
|
+
gaps = gaps || collectAxisGapCandidates(axis, moving, candidates);
|
|
9965
|
+
var before = closestGapCandidate(gaps, "before");
|
|
9966
|
+
var after = closestGapCandidate(gaps, "after");
|
|
9967
|
+
var orientation = axis === "x" ? "vertical" : "horizontal";
|
|
9968
|
+
var rhythms = collectRhythmGaps(axis, moving, candidates);
|
|
9969
|
+
if (before && after && Math.abs(before.gap - after.gap) <= tolerance) {
|
|
9970
|
+
var pairGap = (before.gap + after.gap) / 2;
|
|
9971
|
+
return [
|
|
9972
|
+
{
|
|
9973
|
+
orientation: orientation,
|
|
9974
|
+
gap: pairGap,
|
|
9975
|
+
bands: [gapCandidateBand(before), gapCandidateBand(after)].concat(
|
|
9976
|
+
matchingRhythmBands(rhythms, pairGap, tolerance),
|
|
9977
|
+
),
|
|
9978
|
+
},
|
|
9979
|
+
];
|
|
9980
|
+
}
|
|
9981
|
+
var neighbor =
|
|
9982
|
+
matchedSide === "after"
|
|
9983
|
+
? after
|
|
9984
|
+
: matchedSide === "before"
|
|
9985
|
+
? before
|
|
9986
|
+
: before || after;
|
|
9987
|
+
if (!neighbor) return [];
|
|
9988
|
+
var matched = matchingRhythmBands(rhythms, neighbor.gap, tolerance);
|
|
9989
|
+
if (!matched.length) return [];
|
|
9990
|
+
return [
|
|
9991
|
+
{
|
|
9992
|
+
orientation: orientation,
|
|
9993
|
+
gap: neighbor.gap,
|
|
9994
|
+
bands: [gapCandidateBand(neighbor)].concat(matched),
|
|
9995
|
+
},
|
|
9996
|
+
];
|
|
9997
|
+
}
|
|
9998
|
+
|
|
9999
|
+
// Nearest neighbour per axis, within range. Stock Figma only prints a gap
|
|
10000
|
+
// that matches an existing one; showing the nearest unconditionally is a
|
|
10001
|
+
// deliberate divergence (see canvas-math computeProximityMeasurements).
|
|
10002
|
+
function computeProximityMeasurements(moving, candidates, range, gapsByAxis) {
|
|
10003
|
+
var measurements: any[] = [];
|
|
10004
|
+
var axes = ["x", "y"];
|
|
10005
|
+
for (var a = 0; a < axes.length; a += 1) {
|
|
10006
|
+
var axis = axes[a];
|
|
10007
|
+
var gaps =
|
|
10008
|
+
(gapsByAxis && gapsByAxis[axis]) ||
|
|
10009
|
+
collectAxisGapCandidates(axis, moving, candidates);
|
|
10010
|
+
var nearest = null;
|
|
10011
|
+
for (var i = 0; i < gaps.length; i += 1) {
|
|
10012
|
+
if (!nearest || gaps[i].gap < nearest.gap) nearest = gaps[i];
|
|
10013
|
+
}
|
|
10014
|
+
if (!nearest || nearest.gap > range) continue;
|
|
10015
|
+
measurements.push({
|
|
10016
|
+
orientation: axis === "x" ? "vertical" : "horizontal",
|
|
10017
|
+
gap: nearest.gap,
|
|
10018
|
+
band: gapCandidateBand(nearest),
|
|
10019
|
+
});
|
|
10020
|
+
}
|
|
10021
|
+
return measurements;
|
|
10022
|
+
}
|
|
10023
|
+
|
|
10024
|
+
function computeMoveSnapOffset(
|
|
10025
|
+
movingRect,
|
|
10026
|
+
candidates,
|
|
10027
|
+
threshold,
|
|
10028
|
+
isGroup,
|
|
10029
|
+
locked,
|
|
10030
|
+
) {
|
|
10031
|
+
var moving = rectBounds(movingRect);
|
|
10032
|
+
locked = locked || {};
|
|
10033
|
+
var dx = locked.x
|
|
10034
|
+
? null
|
|
10035
|
+
: findAxisSnapOffset("x", moving, candidates, threshold);
|
|
10036
|
+
var dy = locked.y
|
|
10037
|
+
? null
|
|
10038
|
+
: findAxisSnapOffset("y", moving, candidates, threshold);
|
|
10039
|
+
var snapped = translateRectBounds(moving, dx || 0, dy || 0);
|
|
10040
|
+
var guides = (
|
|
10041
|
+
locked.x ? [] : buildAxisGuides("x", snapped, candidates)
|
|
10042
|
+
).concat(locked.y ? [] : buildAxisGuides("y", snapped, candidates));
|
|
10043
|
+
|
|
10044
|
+
// Figma drops spacing guides for a multi-select drag, and so does the
|
|
10045
|
+
// overview canvas; a grouped iframe drag must not diverge from either.
|
|
10046
|
+
if (isGroup) {
|
|
10047
|
+
return {
|
|
10048
|
+
dx: dx || 0,
|
|
10049
|
+
dy: dy || 0,
|
|
10050
|
+
guides: guides,
|
|
10051
|
+
spacingGuides: [],
|
|
10052
|
+
measurements: [],
|
|
10053
|
+
};
|
|
10054
|
+
}
|
|
10055
|
+
|
|
10056
|
+
// Spacing only gets the axes alignment left free — moving a claimed axis
|
|
10057
|
+
// would pull the element off the guide line the user can already see.
|
|
10058
|
+
var idle = { offset: 0, side: null };
|
|
10059
|
+
var spacingXResult = guides.some(function (guide) {
|
|
10060
|
+
return guide.orientation === "vertical";
|
|
10061
|
+
})
|
|
10062
|
+
? idle
|
|
10063
|
+
: locked.x
|
|
10064
|
+
? idle
|
|
10065
|
+
: findSpacingSnapOffset("x", snapped, candidates, threshold);
|
|
10066
|
+
var spacingYResult = guides.some(function (guide) {
|
|
10067
|
+
return guide.orientation === "horizontal";
|
|
10068
|
+
})
|
|
10069
|
+
? idle
|
|
10070
|
+
: locked.y
|
|
10071
|
+
? idle
|
|
10072
|
+
: findSpacingSnapOffset("y", snapped, candidates, threshold);
|
|
10073
|
+
var spacingX = spacingXResult.offset;
|
|
10074
|
+
var spacingY = spacingYResult.offset;
|
|
10075
|
+
var spaced = translateRectBounds(snapped, spacingX, spacingY);
|
|
10076
|
+
// Spacing and proximity ask the same question of the same bounds; one
|
|
10077
|
+
// scan per axis feeds both instead of four scans per drag tick.
|
|
10078
|
+
var settledGaps = {
|
|
10079
|
+
x: collectAxisGapCandidates("x", spaced, candidates),
|
|
10080
|
+
y: collectAxisGapCandidates("y", spaced, candidates),
|
|
10081
|
+
};
|
|
10082
|
+
var spacingGuides = buildSpacingGuides(
|
|
10083
|
+
"x",
|
|
10084
|
+
spaced,
|
|
10085
|
+
candidates,
|
|
10086
|
+
SPACING_MATCH_EPSILON,
|
|
10087
|
+
settledGaps.x,
|
|
10088
|
+
spacingXResult.side,
|
|
10089
|
+
).concat(
|
|
10090
|
+
buildSpacingGuides(
|
|
10091
|
+
"y",
|
|
10092
|
+
spaced,
|
|
10093
|
+
candidates,
|
|
10094
|
+
SPACING_MATCH_EPSILON,
|
|
10095
|
+
settledGaps.y,
|
|
10096
|
+
spacingYResult.side,
|
|
10097
|
+
),
|
|
10098
|
+
);
|
|
10099
|
+
|
|
10100
|
+
var measurements = computeProximityMeasurements(
|
|
10101
|
+
spaced,
|
|
10102
|
+
candidates,
|
|
10103
|
+
(PROXIMITY_RANGE_PX * threshold) / SNAP_THRESHOLD_PX,
|
|
10104
|
+
settledGaps,
|
|
10105
|
+
).filter(function (measurement) {
|
|
10106
|
+
return !spacingGuides.some(function (guide) {
|
|
10107
|
+
return guide.orientation === measurement.orientation;
|
|
10108
|
+
});
|
|
10109
|
+
});
|
|
10110
|
+
|
|
10111
|
+
return {
|
|
10112
|
+
dx: (dx || 0) + spacingX,
|
|
10113
|
+
dy: (dy || 0) + spacingY,
|
|
10114
|
+
guides: guides,
|
|
10115
|
+
spacingGuides: spacingGuides,
|
|
10116
|
+
measurements: measurements,
|
|
10117
|
+
};
|
|
10118
|
+
}
|
|
10119
|
+
|
|
10120
|
+
// Pooled, not rebuilt: this runs on every rAF of a drag, and tearing the
|
|
10121
|
+
// layer down with innerHTML each frame costs a full style recalc for nodes
|
|
10122
|
+
// that are almost always identical in count from one frame to the next.
|
|
10123
|
+
var snapGuideNodeCount = 0;
|
|
10124
|
+
|
|
10125
|
+
function beginSnapGuideNodes() {
|
|
10126
|
+
snapGuideNodeCount = 0;
|
|
10127
|
+
}
|
|
10128
|
+
|
|
10129
|
+
function appendSnapGuideNode(cssText) {
|
|
10130
|
+
var node = snapGuideLayer.children[snapGuideNodeCount] as
|
|
10131
|
+
| HTMLElement
|
|
10132
|
+
| undefined;
|
|
10133
|
+
if (!node) {
|
|
10134
|
+
node = document.createElement("div");
|
|
10135
|
+
snapGuideLayer.appendChild(node);
|
|
10136
|
+
}
|
|
10137
|
+
node.style.cssText = "position:fixed;" + cssText;
|
|
10138
|
+
node.textContent = "";
|
|
10139
|
+
snapGuideNodeCount += 1;
|
|
10140
|
+
return node;
|
|
10141
|
+
}
|
|
10142
|
+
|
|
10143
|
+
function endSnapGuideNodes() {
|
|
10144
|
+
while (snapGuideLayer.children.length > snapGuideNodeCount) {
|
|
10145
|
+
snapGuideLayer.removeChild(snapGuideLayer.lastChild!);
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
|
|
10149
|
+
// Constant-screen-size chrome: guide THICKNESS and the spacing serifs
|
|
10150
|
+
// compensate for the host's iframe scale (chromeLineScale) so they stay
|
|
10151
|
+
// crisp at any zoom; positions and spans stay in content coordinates.
|
|
10152
|
+
function showSnapGuides(guides, spacingGuides, measurements) {
|
|
10153
|
+
measurements = measurements || [];
|
|
10154
|
+
if (!guides.length && !spacingGuides.length && !measurements.length) {
|
|
10155
|
+
hideSnapGuides();
|
|
10156
|
+
return;
|
|
10157
|
+
}
|
|
10158
|
+
beginSnapGuideNodes();
|
|
10159
|
+
if (snapGuideLayer.style.display !== "block") {
|
|
10160
|
+
snapGuideLayer.style.display = "block";
|
|
10161
|
+
}
|
|
10162
|
+
var scale = chromeLineScale();
|
|
10163
|
+
var line = 1 * scale;
|
|
10164
|
+
// Must be a forwarded var (see EDITOR_BRIDGE_VAR_NAMES in DesignCanvas):
|
|
10165
|
+
// an unknown custom property paints transparent, not red, and a guide
|
|
10166
|
+
// with correct geometry and no colour looks exactly like no guide.
|
|
10167
|
+
var fill = "background:var(--design-editor-measure-color);";
|
|
10168
|
+
|
|
10169
|
+
for (var i = 0; i < guides.length; i += 1) {
|
|
10170
|
+
var guide = guides[i];
|
|
10171
|
+
var span = Math.max(1, guide.end - guide.start);
|
|
10172
|
+
appendSnapGuideNode(
|
|
10173
|
+
guide.orientation === "vertical"
|
|
10174
|
+
? "left:" +
|
|
10175
|
+
guide.position +
|
|
10176
|
+
"px;top:" +
|
|
10177
|
+
guide.start +
|
|
10178
|
+
"px;width:" +
|
|
10179
|
+
line +
|
|
10180
|
+
"px;height:" +
|
|
10181
|
+
span +
|
|
10182
|
+
"px;" +
|
|
10183
|
+
fill
|
|
10184
|
+
: "top:" +
|
|
10185
|
+
guide.position +
|
|
10186
|
+
"px;left:" +
|
|
10187
|
+
guide.start +
|
|
10188
|
+
"px;height:" +
|
|
10189
|
+
line +
|
|
10190
|
+
"px;width:" +
|
|
10191
|
+
span +
|
|
10192
|
+
"px;" +
|
|
10193
|
+
fill,
|
|
10194
|
+
);
|
|
10195
|
+
}
|
|
10196
|
+
|
|
10197
|
+
for (var s = 0; s < spacingGuides.length; s += 1) {
|
|
10198
|
+
var spacing = spacingGuides[s];
|
|
10199
|
+
for (var b = 0; b < spacing.bands.length; b += 1) {
|
|
10200
|
+
appendSnapGuideNode(
|
|
10201
|
+
spacingBandCss(spacing.orientation, spacing.bands[b], line, fill),
|
|
10202
|
+
);
|
|
10203
|
+
appendSnapGuideNode(
|
|
10204
|
+
spacingSerifCss(
|
|
10205
|
+
spacing.orientation,
|
|
10206
|
+
spacing.bands[b],
|
|
10207
|
+
line,
|
|
10208
|
+
5 * scale,
|
|
10209
|
+
fill,
|
|
10210
|
+
true,
|
|
10211
|
+
),
|
|
10212
|
+
);
|
|
10213
|
+
appendSnapGuideNode(
|
|
10214
|
+
spacingSerifCss(
|
|
10215
|
+
spacing.orientation,
|
|
10216
|
+
spacing.bands[b],
|
|
10217
|
+
line,
|
|
10218
|
+
5 * scale,
|
|
10219
|
+
fill,
|
|
10220
|
+
false,
|
|
10221
|
+
),
|
|
10222
|
+
);
|
|
10223
|
+
}
|
|
10224
|
+
var label = appendSnapGuideNode(
|
|
10225
|
+
spacingLabelCss(spacing.orientation, spacing.bands[0], scale),
|
|
10226
|
+
);
|
|
10227
|
+
label.textContent = String(Math.round(spacing.gap));
|
|
10228
|
+
}
|
|
10229
|
+
|
|
10230
|
+
for (var m = 0; m < measurements.length; m += 1) {
|
|
10231
|
+
var measurement = measurements[m];
|
|
10232
|
+
appendSnapGuideNode(
|
|
10233
|
+
spacingBandCss(measurement.orientation, measurement.band, line, fill),
|
|
10234
|
+
);
|
|
10235
|
+
appendSnapGuideNode(
|
|
10236
|
+
spacingSerifCss(
|
|
10237
|
+
measurement.orientation,
|
|
10238
|
+
measurement.band,
|
|
10239
|
+
line,
|
|
10240
|
+
5 * scale,
|
|
10241
|
+
fill,
|
|
10242
|
+
true,
|
|
10243
|
+
),
|
|
10244
|
+
);
|
|
10245
|
+
appendSnapGuideNode(
|
|
10246
|
+
spacingSerifCss(
|
|
10247
|
+
measurement.orientation,
|
|
10248
|
+
measurement.band,
|
|
10249
|
+
line,
|
|
10250
|
+
5 * scale,
|
|
10251
|
+
fill,
|
|
10252
|
+
false,
|
|
10253
|
+
),
|
|
10254
|
+
);
|
|
10255
|
+
var measureLabel = appendSnapGuideNode(
|
|
10256
|
+
spacingLabelCss(measurement.orientation, measurement.band, scale),
|
|
10257
|
+
);
|
|
10258
|
+
measureLabel.textContent = String(Math.round(measurement.gap));
|
|
10259
|
+
}
|
|
10260
|
+
endSnapGuideNodes();
|
|
10261
|
+
}
|
|
10262
|
+
|
|
10263
|
+
function spacingBandCss(orientation, band, line, fill) {
|
|
10264
|
+
var crossMid = (band.crossStart + band.crossEnd) / 2;
|
|
10265
|
+
var length = Math.max(0, band.gapEnd - band.gapStart);
|
|
10266
|
+
return orientation === "vertical"
|
|
10267
|
+
? "left:" +
|
|
10268
|
+
band.gapStart +
|
|
10269
|
+
"px;top:" +
|
|
10270
|
+
(crossMid - line / 2) +
|
|
10271
|
+
"px;width:" +
|
|
10272
|
+
length +
|
|
10273
|
+
"px;height:" +
|
|
10274
|
+
line +
|
|
10275
|
+
"px;" +
|
|
10276
|
+
fill
|
|
10277
|
+
: "top:" +
|
|
10278
|
+
band.gapStart +
|
|
10279
|
+
"px;left:" +
|
|
10280
|
+
(crossMid - line / 2) +
|
|
10281
|
+
"px;height:" +
|
|
10282
|
+
length +
|
|
10283
|
+
"px;width:" +
|
|
10284
|
+
line +
|
|
10285
|
+
"px;" +
|
|
10286
|
+
fill;
|
|
10287
|
+
}
|
|
10288
|
+
|
|
10289
|
+
function spacingSerifCss(orientation, band, line, serif, fill, atStart) {
|
|
10290
|
+
var crossMid = (band.crossStart + band.crossEnd) / 2;
|
|
10291
|
+
var along = atStart ? band.gapStart : band.gapEnd;
|
|
10292
|
+
return orientation === "vertical"
|
|
10293
|
+
? "left:" +
|
|
10294
|
+
(along - line / 2) +
|
|
10295
|
+
"px;top:" +
|
|
10296
|
+
(crossMid - serif) +
|
|
10297
|
+
"px;width:" +
|
|
10298
|
+
line +
|
|
10299
|
+
"px;height:" +
|
|
10300
|
+
serif * 2 +
|
|
10301
|
+
"px;" +
|
|
10302
|
+
fill
|
|
10303
|
+
: "top:" +
|
|
10304
|
+
(along - line / 2) +
|
|
10305
|
+
"px;left:" +
|
|
10306
|
+
(crossMid - serif) +
|
|
10307
|
+
"px;height:" +
|
|
10308
|
+
line +
|
|
10309
|
+
"px;width:" +
|
|
10310
|
+
serif * 2 +
|
|
10311
|
+
"px;" +
|
|
10312
|
+
fill;
|
|
10313
|
+
}
|
|
10314
|
+
|
|
10315
|
+
function spacingLabelCss(orientation, band, scale) {
|
|
10316
|
+
var crossMid = (band.crossStart + band.crossEnd) / 2;
|
|
10317
|
+
var alongMid = (band.gapStart + band.gapEnd) / 2;
|
|
10318
|
+
return (
|
|
10319
|
+
(orientation === "vertical"
|
|
10320
|
+
? "left:" + alongMid + "px;top:" + crossMid + "px;"
|
|
10321
|
+
: "left:" + crossMid + "px;top:" + alongMid + "px;") +
|
|
10322
|
+
"transform:translate(-50%,-50%);border-radius:" +
|
|
10323
|
+
3 * scale +
|
|
10324
|
+
"px;padding:" +
|
|
10325
|
+
1 * scale +
|
|
10326
|
+
"px " +
|
|
10327
|
+
4 * scale +
|
|
10328
|
+
"px;font:" +
|
|
10329
|
+
10 * scale +
|
|
10330
|
+
"px/1 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;" +
|
|
10331
|
+
"background:var(--design-editor-measure-color);color:white;"
|
|
10332
|
+
);
|
|
10333
|
+
}
|
|
10334
|
+
|
|
10335
|
+
// Hand-port of deriveConstraintsValue in edit-panel/position-layout-
|
|
10336
|
+
// properties.tsx — the bridge cannot import. Reads AUTHORED inline offsets
|
|
10337
|
+
// only: an absolutely positioned left-only element still has a computed
|
|
10338
|
+
// `right`, and treating that as a pin would draw a line that lies.
|
|
10339
|
+
function authoredOffset(value) {
|
|
10340
|
+
if (!value || value === "auto") return "";
|
|
10341
|
+
return value;
|
|
10342
|
+
}
|
|
10343
|
+
|
|
10344
|
+
function elementConstraints(el) {
|
|
10345
|
+
var style = (el as HTMLElement).style;
|
|
10346
|
+
var left = authoredOffset(style.left);
|
|
10347
|
+
var right = authoredOffset(style.right);
|
|
10348
|
+
var top = authoredOffset(style.top);
|
|
10349
|
+
var bottom = authoredOffset(style.bottom);
|
|
10350
|
+
var transform = style.transform || "";
|
|
10351
|
+
return {
|
|
10352
|
+
horizontal:
|
|
10353
|
+
style.width === "100%"
|
|
10354
|
+
? "scale"
|
|
10355
|
+
: left && right
|
|
10356
|
+
? "left-right"
|
|
10357
|
+
: right && !left
|
|
10358
|
+
? "right"
|
|
10359
|
+
: transform.indexOf("translateX(-50%)") !== -1
|
|
10360
|
+
? "center"
|
|
10361
|
+
: "left",
|
|
10362
|
+
vertical:
|
|
10363
|
+
style.height === "100%"
|
|
10364
|
+
? "scale"
|
|
10365
|
+
: top && bottom
|
|
10366
|
+
? "top-bottom"
|
|
10367
|
+
: bottom && !top
|
|
10368
|
+
? "bottom"
|
|
10369
|
+
: transform.indexOf("translateY(-50%)") !== -1
|
|
10370
|
+
? "center"
|
|
10371
|
+
: "top",
|
|
10372
|
+
};
|
|
10373
|
+
}
|
|
10374
|
+
|
|
10375
|
+
// Pooled for the same reason as the snap guides: this runs every rAF of a
|
|
10376
|
+
// drag, and rebuilding the layer each frame costs a needless style recalc.
|
|
10377
|
+
var constraintNodeCount = 0;
|
|
10378
|
+
|
|
10379
|
+
function appendConstraintLine(
|
|
10380
|
+
from,
|
|
10381
|
+
to,
|
|
10382
|
+
crossPosition,
|
|
10383
|
+
horizontal,
|
|
10384
|
+
thickness,
|
|
10385
|
+
) {
|
|
10386
|
+
var start = Math.min(from, to);
|
|
10387
|
+
var length = Math.max(1, Math.abs(to - from));
|
|
10388
|
+
var node = constraintGuideLayer.children[constraintNodeCount] as
|
|
10389
|
+
| HTMLElement
|
|
10390
|
+
| undefined;
|
|
10391
|
+
if (!node) {
|
|
10392
|
+
node = document.createElement("div");
|
|
10393
|
+
constraintGuideLayer.appendChild(node);
|
|
10394
|
+
}
|
|
10395
|
+
constraintNodeCount += 1;
|
|
10396
|
+
node.style.cssText =
|
|
10397
|
+
"position:fixed;" +
|
|
10398
|
+
(horizontal
|
|
10399
|
+
? "left:" +
|
|
10400
|
+
start +
|
|
10401
|
+
"px;top:" +
|
|
10402
|
+
(crossPosition - thickness / 2) +
|
|
10403
|
+
"px;width:" +
|
|
10404
|
+
length +
|
|
10405
|
+
"px;height:0;border-top:"
|
|
10406
|
+
: "top:" +
|
|
10407
|
+
start +
|
|
10408
|
+
"px;left:" +
|
|
10409
|
+
(crossPosition - thickness / 2) +
|
|
10410
|
+
"px;height:" +
|
|
10411
|
+
length +
|
|
10412
|
+
"px;width:0;border-left:") +
|
|
10413
|
+
thickness +
|
|
10414
|
+
"px dashed var(--design-editor-accent-color);";
|
|
10415
|
+
}
|
|
10416
|
+
|
|
10417
|
+
function showConstraintGuides(el) {
|
|
10418
|
+
if (!el || dragChromeSuppressed) return hideConstraintGuides();
|
|
10419
|
+
// body counts here: it is the screen root, which is exactly the frame an
|
|
10420
|
+
// element's offsets are resolved against. (Auto-layout nesting excludes
|
|
10421
|
+
// body for the opposite reason — it is not a drop target.)
|
|
10422
|
+
var parent =
|
|
10423
|
+
(el as HTMLElement).offsetParent || (el as HTMLElement).parentElement;
|
|
10424
|
+
if (!parent) return hideConstraintGuides();
|
|
10425
|
+
var rect = el.getBoundingClientRect();
|
|
10426
|
+
var frame = parent.getBoundingClientRect();
|
|
10427
|
+
if (rect.width <= 0 || rect.height <= 0) return hideConstraintGuides();
|
|
10428
|
+
if (!constraintGuideLayer.isConnected) {
|
|
10429
|
+
document.body.appendChild(constraintGuideLayer);
|
|
10430
|
+
}
|
|
10431
|
+
constraintNodeCount = 0;
|
|
10432
|
+
if (constraintGuideLayer.style.display !== "block") {
|
|
10433
|
+
constraintGuideLayer.style.display = "block";
|
|
10434
|
+
}
|
|
10435
|
+
|
|
10436
|
+
var thickness = chromeLineScale();
|
|
10437
|
+
var midY = rect.top + rect.height / 2;
|
|
10438
|
+
var midX = rect.left + rect.width / 2;
|
|
10439
|
+
var constraints = elementConstraints(el);
|
|
10440
|
+
|
|
10441
|
+
if (constraints.horizontal === "scale") {
|
|
10442
|
+
appendConstraintLine(frame.left, frame.right, midY, true, thickness);
|
|
10443
|
+
} else {
|
|
10444
|
+
if (
|
|
10445
|
+
constraints.horizontal !== "right" &&
|
|
10446
|
+
constraints.horizontal !== "center"
|
|
10447
|
+
) {
|
|
10448
|
+
appendConstraintLine(frame.left, rect.left, midY, true, thickness);
|
|
10449
|
+
}
|
|
10450
|
+
if (
|
|
10451
|
+
constraints.horizontal === "right" ||
|
|
10452
|
+
constraints.horizontal === "left-right"
|
|
10453
|
+
) {
|
|
10454
|
+
appendConstraintLine(rect.right, frame.right, midY, true, thickness);
|
|
10455
|
+
}
|
|
10456
|
+
if (constraints.horizontal === "center") {
|
|
10457
|
+
appendConstraintLine(
|
|
10458
|
+
frame.left + frame.width / 2,
|
|
10459
|
+
midX,
|
|
10460
|
+
midY,
|
|
10461
|
+
true,
|
|
10462
|
+
thickness,
|
|
10463
|
+
);
|
|
10464
|
+
}
|
|
10465
|
+
}
|
|
10466
|
+
|
|
10467
|
+
if (constraints.vertical === "scale") {
|
|
10468
|
+
appendConstraintLine(frame.top, frame.bottom, midX, false, thickness);
|
|
10469
|
+
} else {
|
|
10470
|
+
if (
|
|
10471
|
+
constraints.vertical !== "bottom" &&
|
|
10472
|
+
constraints.vertical !== "center"
|
|
10473
|
+
) {
|
|
10474
|
+
appendConstraintLine(frame.top, rect.top, midX, false, thickness);
|
|
10475
|
+
}
|
|
10476
|
+
if (
|
|
10477
|
+
constraints.vertical === "bottom" ||
|
|
10478
|
+
constraints.vertical === "top-bottom"
|
|
10479
|
+
) {
|
|
10480
|
+
appendConstraintLine(rect.bottom, frame.bottom, midX, false, thickness);
|
|
10481
|
+
}
|
|
10482
|
+
if (constraints.vertical === "center") {
|
|
10483
|
+
appendConstraintLine(
|
|
10484
|
+
frame.top + frame.height / 2,
|
|
10485
|
+
midY,
|
|
10486
|
+
midX,
|
|
10487
|
+
false,
|
|
10488
|
+
thickness,
|
|
10489
|
+
);
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10492
|
+
trimConstraintNodes();
|
|
10493
|
+
}
|
|
10494
|
+
|
|
10495
|
+
function trimConstraintNodes() {
|
|
10496
|
+
while (constraintGuideLayer.children.length > constraintNodeCount) {
|
|
10497
|
+
constraintGuideLayer.removeChild(constraintGuideLayer.lastChild!);
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
|
|
10501
|
+
function hideConstraintGuides() {
|
|
10502
|
+
if (constraintGuideLayer.style.display === "none") return;
|
|
10503
|
+
constraintGuideLayer.style.display = "none";
|
|
10504
|
+
constraintGuideLayer.innerHTML = "";
|
|
10505
|
+
constraintNodeCount = 0;
|
|
10506
|
+
}
|
|
10507
|
+
|
|
10508
|
+
// Figma pins the dragged object's dimensions just under it, in canvas
|
|
10509
|
+
// space — unlike the transform badge, which tracks the cursor.
|
|
10510
|
+
var sizeBadgeKey = "";
|
|
10511
|
+
// Set while a drag is in a state that suppresses free-placement chrome (an
|
|
10512
|
+
// auto-layout insert, or the pointer outside the iframe). refreshOverlays
|
|
10513
|
+
// runs on the same pointer event and would otherwise re-show the badge and
|
|
10514
|
+
// constraint lines the drag just hid.
|
|
10515
|
+
var dragChromeSuppressed = false;
|
|
10516
|
+
|
|
10517
|
+
function showSizeBadge(el) {
|
|
10518
|
+
if (!el || dragChromeSuppressed) return hideSizeBadge();
|
|
10519
|
+
var rect = el.getBoundingClientRect();
|
|
10520
|
+
if (rect.width <= 0 || rect.height <= 0) return hideSizeBadge();
|
|
10521
|
+
var scale = chromeLineScale();
|
|
10522
|
+
// refreshOverlays fires on every ResizeObserver/MutationObserver tick, so
|
|
10523
|
+
// re-writing identical styles here would invalidate layout for nothing.
|
|
10524
|
+
var key =
|
|
10525
|
+
rect.left +
|
|
10526
|
+
"|" +
|
|
10527
|
+
rect.bottom +
|
|
10528
|
+
"|" +
|
|
10529
|
+
rect.width +
|
|
10530
|
+
"|" +
|
|
10531
|
+
rect.height +
|
|
10532
|
+
"|" +
|
|
10533
|
+
scale;
|
|
10534
|
+
if (key === sizeBadgeKey && sizeBadge.style.display === "block") return;
|
|
10535
|
+
sizeBadgeKey = key;
|
|
10536
|
+
if (!sizeBadge.isConnected) document.body.appendChild(sizeBadge);
|
|
10537
|
+
sizeBadge.textContent =
|
|
10538
|
+
Math.round(rect.width) + " × " + Math.round(rect.height);
|
|
10539
|
+
sizeBadge.style.display = "block";
|
|
10540
|
+
sizeBadge.style.borderRadius = 3 * scale + "px";
|
|
10541
|
+
sizeBadge.style.padding = 2 * scale + "px " + 4 * scale + "px";
|
|
10542
|
+
sizeBadge.style.fontSize = 10 * scale + "px";
|
|
10543
|
+
sizeBadge.style.left = rect.left + rect.width / 2 + "px";
|
|
10544
|
+
sizeBadge.style.top = rect.bottom + 6 * scale + "px";
|
|
10545
|
+
sizeBadge.style.transform = "translateX(-50%)";
|
|
10546
|
+
}
|
|
10547
|
+
|
|
10548
|
+
function hideSizeBadge() {
|
|
10549
|
+
if (sizeBadge.style.display === "none") return;
|
|
10550
|
+
sizeBadge.style.display = "none";
|
|
10551
|
+
sizeBadgeKey = "";
|
|
10552
|
+
}
|
|
10553
|
+
|
|
10554
|
+
function hideSnapGuides() {
|
|
10555
|
+
dragChromeSuppressed = false;
|
|
10556
|
+
if (snapGuideLayer.style.display === "none") return;
|
|
10557
|
+
snapGuideLayer.style.display = "none";
|
|
10558
|
+
snapGuideLayer.innerHTML = "";
|
|
10559
|
+
snapGuideNodeCount = 0;
|
|
10560
|
+
}
|
|
10561
|
+
|
|
10562
|
+
// `gestureElParam` (optional): the specific multi-selection member the
|
|
10563
|
+
// pointer went down on when this drag preserves a 2+ selection instead of
|
|
10564
|
+
// collapsing to one element (see beginPotentialShieldDrag's group branch).
|
|
10565
|
+
// Defaults to selectedEl — the selection-overlay drag path.
|
|
10566
|
+
function startMove(
|
|
10567
|
+
e,
|
|
10568
|
+
gestureElParam?: Element,
|
|
10569
|
+
pointerStartParam?: { clientX: number; clientY: number },
|
|
10570
|
+
) {
|
|
10571
|
+
if (readOnly) return;
|
|
10572
|
+
var gestureEl = gestureElParam || selectedEl;
|
|
9146
10573
|
if (!gestureEl) return;
|
|
9147
10574
|
if (isLayerInteractionBlocked(gestureEl)) return;
|
|
9148
10575
|
e.preventDefault();
|
|
@@ -10026,6 +11453,28 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10026
11453
|
var dragElStartRect = (dragEl as HTMLElement).getBoundingClientRect();
|
|
10027
11454
|
var dragElStartWidth = dragElStartRect.width;
|
|
10028
11455
|
var dragElStartHeight = dragElStartRect.height;
|
|
11456
|
+
// Client px per CSS px for this element. 1 unless an ancestor between it
|
|
11457
|
+
// and the viewport is CSS-scaled; offsetWidth is the untransformed box.
|
|
11458
|
+
// Client px per CSS px contributed by ANCESTORS. Measured on the offset
|
|
11459
|
+
// parent, never on dragEl: its own rect already carries its own
|
|
11460
|
+
// transform, so a rotated or scaled layer would report its local
|
|
11461
|
+
// transform as if the parent were scaled. 1 means "no mapping known",
|
|
11462
|
+
// which is the identity, not a measurement.
|
|
11463
|
+
function ancestorScale(el, axis) {
|
|
11464
|
+
var host = el && (el as HTMLElement).offsetParent;
|
|
11465
|
+
if (!host) return 1;
|
|
11466
|
+
var rect = (host as HTMLElement).getBoundingClientRect();
|
|
11467
|
+
var layout =
|
|
11468
|
+
axis === "x"
|
|
11469
|
+
? (host as HTMLElement).offsetWidth
|
|
11470
|
+
: (host as HTMLElement).offsetHeight;
|
|
11471
|
+
var client = axis === "x" ? rect.width : rect.height;
|
|
11472
|
+
if (!(layout > 0)) return 1;
|
|
11473
|
+
var scale = client / layout;
|
|
11474
|
+
return scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
11475
|
+
}
|
|
11476
|
+
var dragElOffsetScaleX = ancestorScale(dragEl, "x");
|
|
11477
|
+
var dragElOffsetScaleY = ancestorScale(dragEl, "y");
|
|
10029
11478
|
if (!duplicatedForDrag && !isGroupDrag) {
|
|
10030
11479
|
postCrossScreenDrag("start", dragEl, e);
|
|
10031
11480
|
}
|
|
@@ -10084,18 +11533,45 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10084
11533
|
!snapBypass && !duplicatedForDrag
|
|
10085
11534
|
? computeMoveSnapOffset(
|
|
10086
11535
|
{
|
|
10087
|
-
|
|
10088
|
-
|
|
11536
|
+
// snapCandidateRects are client space; nextLeft/nextTop are
|
|
11537
|
+
// offset-parent CSS space. Convert, or nothing ever matches.
|
|
11538
|
+
left:
|
|
11539
|
+
dragElStartRect.left +
|
|
11540
|
+
(nextLeft - originLeft) * dragElOffsetScaleX,
|
|
11541
|
+
top:
|
|
11542
|
+
dragElStartRect.top +
|
|
11543
|
+
(nextTop - originTop) * dragElOffsetScaleY,
|
|
10089
11544
|
width: dragElStartWidth,
|
|
10090
11545
|
height: dragElStartHeight,
|
|
10091
11546
|
},
|
|
10092
11547
|
snapCandidateRects,
|
|
10093
11548
|
// Convert the screen-space base to content px (1/zoom).
|
|
10094
11549
|
SNAP_THRESHOLD_PX * chromeLineScale(),
|
|
11550
|
+
isGroupDrag,
|
|
11551
|
+
ev.shiftKey ? { x: rawDx === 0, y: rawDy === 0 } : null,
|
|
10095
11552
|
)
|
|
10096
|
-
: { dx: 0, dy: 0,
|
|
10097
|
-
|
|
10098
|
-
|
|
11553
|
+
: { dx: 0, dy: 0, guides: [], spacingGuides: [], measurements: [] };
|
|
11554
|
+
if ((window as any).__DND_DEBUG)
|
|
11555
|
+
dndLog("snap:tick", {
|
|
11556
|
+
// Ordered so the fields that decide whether snapping ran at all come
|
|
11557
|
+
// first: the console collapses long objects behind an ellipsis.
|
|
11558
|
+
bypass: snapBypass,
|
|
11559
|
+
duplicated: duplicatedForDrag,
|
|
11560
|
+
mods:
|
|
11561
|
+
(ev.metaKey ? "M" : "") +
|
|
11562
|
+
(ev.ctrlKey ? "C" : "") +
|
|
11563
|
+
(ev.altKey ? "A" : "") +
|
|
11564
|
+
(ev.shiftKey ? "S" : "") || "none",
|
|
11565
|
+
guides: snapResult.guides.length,
|
|
11566
|
+
measurements: snapResult.measurements.length,
|
|
11567
|
+
candidates: snapCandidateRects.length,
|
|
11568
|
+
dropMode: currentAutoLayoutTarget
|
|
11569
|
+
? currentAutoLayoutTarget.dropMode || "(none)"
|
|
11570
|
+
: "no-target",
|
|
11571
|
+
});
|
|
11572
|
+
// Back to CSS space before it is written to style.left/top.
|
|
11573
|
+
nextLeft += snapResult.dx / dragElOffsetScaleX;
|
|
11574
|
+
nextTop += snapResult.dy / dragElOffsetScaleY;
|
|
10099
11575
|
// Apply the SAME delta to every member (one entry for single drags)
|
|
10100
11576
|
// so relative offsets within a multi-selection are preserved. For the
|
|
10101
11577
|
// gesture member this reduces exactly to the previous
|
|
@@ -10144,13 +11620,31 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10144
11620
|
// about to be reflowed into a flex/grid slot, not placed at an x/y
|
|
10145
11621
|
// coordinate), and never while the pointer has left the iframe (the
|
|
10146
11622
|
// host owns a cross-screen drop at that point).
|
|
11623
|
+
//
|
|
11624
|
+
// An "absolute-container" target is a free placement: the element keeps
|
|
11625
|
+
// its x/y inside the frame it lands in, so it is precisely the case
|
|
11626
|
+
// guides are for. Hiding them there left every board drag — where the
|
|
11627
|
+
// primitives live inside an absolutely positioned frame — with no
|
|
11628
|
+
// guides at all.
|
|
11629
|
+
var flowInsertPending =
|
|
11630
|
+
!!currentAutoLayoutTarget &&
|
|
11631
|
+
currentAutoLayoutTarget.dropMode !== "absolute-container";
|
|
10147
11632
|
if (
|
|
10148
|
-
|
|
11633
|
+
flowInsertPending ||
|
|
10149
11634
|
(!duplicatedForDrag && isOutsideIframeViewport(ev.clientX, ev.clientY))
|
|
10150
11635
|
) {
|
|
10151
11636
|
hideSnapGuides();
|
|
11637
|
+
dragChromeSuppressed = true;
|
|
11638
|
+
hideSizeBadge();
|
|
11639
|
+
hideConstraintGuides();
|
|
10152
11640
|
} else {
|
|
10153
|
-
|
|
11641
|
+
dragChromeSuppressed = false;
|
|
11642
|
+
showSnapGuides(
|
|
11643
|
+
snapResult.guides,
|
|
11644
|
+
snapResult.spacingGuides,
|
|
11645
|
+
snapResult.measurements,
|
|
11646
|
+
);
|
|
11647
|
+
showConstraintGuides(dragEl);
|
|
10154
11648
|
}
|
|
10155
11649
|
showTransformBadge(
|
|
10156
11650
|
Math.round(nextLeft) + ", " + Math.round(nextTop),
|
|
@@ -10185,6 +11679,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10185
11679
|
hideTransformBadge();
|
|
10186
11680
|
hideInsertionGuide();
|
|
10187
11681
|
hideSnapGuides();
|
|
11682
|
+
hideSizeBadge();
|
|
11683
|
+
hideConstraintGuides();
|
|
10188
11684
|
currentAutoLayoutTarget = null;
|
|
10189
11685
|
if (duplicatedForDrag) {
|
|
10190
11686
|
if (dragEl && dragEl.parentElement) {
|
|
@@ -10215,12 +11711,16 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10215
11711
|
hideTransformBadge();
|
|
10216
11712
|
hideInsertionGuide();
|
|
10217
11713
|
hideSnapGuides();
|
|
11714
|
+
hideSizeBadge();
|
|
11715
|
+
hideConstraintGuides();
|
|
10218
11716
|
return;
|
|
10219
11717
|
}
|
|
10220
11718
|
cleanupMoveDrag();
|
|
10221
11719
|
hideTransformBadge();
|
|
10222
11720
|
hideInsertionGuide();
|
|
10223
11721
|
hideSnapGuides();
|
|
11722
|
+
hideSizeBadge();
|
|
11723
|
+
hideConstraintGuides();
|
|
10224
11724
|
if (!dragEl) return;
|
|
10225
11725
|
var outsideOnDrop = ev
|
|
10226
11726
|
? isOutsideIframeViewport(ev.clientX, ev.clientY)
|
|
@@ -10356,6 +11856,47 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10356
11856
|
setActiveDragCancel(cancelMoveDrag);
|
|
10357
11857
|
}
|
|
10358
11858
|
|
|
11859
|
+
/**
|
|
11860
|
+
* K-scale descendant text (Figma scales the type inside a frame too). Only
|
|
11861
|
+
* elements carrying their OWN size are listed: an inherited size is already
|
|
11862
|
+
* covered by the root's font-size write and a relative inline unit tracks
|
|
11863
|
+
* its parent, so writing either here would scale it twice.
|
|
11864
|
+
*/
|
|
11865
|
+
function collectScaleFontTargets(root: Element) {
|
|
11866
|
+
var targets: Array<{
|
|
11867
|
+
el: HTMLElement;
|
|
11868
|
+
originFontSize: number;
|
|
11869
|
+
originalInlineFontSize: string;
|
|
11870
|
+
}> = [];
|
|
11871
|
+
var nodes = root.querySelectorAll("*");
|
|
11872
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
11873
|
+
var el = nodes[i] as HTMLElement;
|
|
11874
|
+
if (isOverlayElement(el)) continue;
|
|
11875
|
+
var inlineFontSize = el.style.fontSize || "";
|
|
11876
|
+
if (inlineFontSize && !/px\s*$/i.test(inlineFontSize)) continue;
|
|
11877
|
+
var parent = el.parentElement;
|
|
11878
|
+
var cs = window.getComputedStyle(el);
|
|
11879
|
+
if (
|
|
11880
|
+
!inlineFontSize &&
|
|
11881
|
+
parent &&
|
|
11882
|
+
window.getComputedStyle(parent).fontSize === cs.fontSize
|
|
11883
|
+
) {
|
|
11884
|
+
continue;
|
|
11885
|
+
}
|
|
11886
|
+
var originFontSize = readPx(inlineFontSize || cs.fontSize);
|
|
11887
|
+
if (!(originFontSize > 0)) continue;
|
|
11888
|
+
// The revert baseline is recorded on first sight, and the commit is too
|
|
11889
|
+
// late: by then the preview has already written the scaled size.
|
|
11890
|
+
rememberLiveVisualEditOriginalStyles(el);
|
|
11891
|
+
targets.push({
|
|
11892
|
+
el: el,
|
|
11893
|
+
originFontSize: originFontSize,
|
|
11894
|
+
originalInlineFontSize: el.style.fontSize,
|
|
11895
|
+
});
|
|
11896
|
+
}
|
|
11897
|
+
return targets;
|
|
11898
|
+
}
|
|
11899
|
+
|
|
10359
11900
|
function startResize(handle, e) {
|
|
10360
11901
|
if (readOnly) return;
|
|
10361
11902
|
if (!selectedEl) return;
|
|
@@ -10473,6 +12014,17 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10473
12014
|
// in onUp.
|
|
10474
12015
|
var widthTouched = false;
|
|
10475
12016
|
var heightTouched = false;
|
|
12017
|
+
// Captured on the first K-scale tick, not at drag start: the host can arm
|
|
12018
|
+
// scale-tool-mode mid-gesture.
|
|
12019
|
+
var scaledTextTargetsCache: ReturnType<
|
|
12020
|
+
typeof collectScaleFontTargets
|
|
12021
|
+
> | null = null;
|
|
12022
|
+
function scaledTextTargets() {
|
|
12023
|
+
if (!scaledTextTargetsCache) {
|
|
12024
|
+
scaledTextTargetsCache = collectScaleFontTargets(resizeEl);
|
|
12025
|
+
}
|
|
12026
|
+
return scaledTextTargetsCache;
|
|
12027
|
+
}
|
|
10476
12028
|
function nextRect(ev) {
|
|
10477
12029
|
var screenDx = ev.clientX - startX;
|
|
10478
12030
|
var screenDy = ev.clientY - startY;
|
|
@@ -10608,6 +12160,13 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10608
12160
|
Math.max(1, Math.round(originFontSize * kScaleFactor * 100) / 100) +
|
|
10609
12161
|
"px";
|
|
10610
12162
|
}
|
|
12163
|
+
scaledTextTargets().forEach(function (target) {
|
|
12164
|
+
target.el.style.fontSize =
|
|
12165
|
+
Math.max(
|
|
12166
|
+
1,
|
|
12167
|
+
Math.round(target.originFontSize * kScaleFactor * 100) / 100,
|
|
12168
|
+
) + "px";
|
|
12169
|
+
});
|
|
10611
12170
|
}
|
|
10612
12171
|
showTransformBadge(
|
|
10613
12172
|
Math.round(rect.width) + " x " + Math.round(rect.height),
|
|
@@ -10634,6 +12193,9 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10634
12193
|
resizeEl.style.height = originalInlineHeight;
|
|
10635
12194
|
resizeEl.style.borderWidth = originalInlineBorderWidth;
|
|
10636
12195
|
resizeEl.style.fontSize = originalInlineFontSize;
|
|
12196
|
+
(scaledTextTargetsCache || []).forEach(function (target) {
|
|
12197
|
+
target.el.style.fontSize = target.originalInlineFontSize;
|
|
12198
|
+
});
|
|
10637
12199
|
selectedEl = resizeEl;
|
|
10638
12200
|
positionOverlay(selectionOverlay, selectedEl);
|
|
10639
12201
|
}
|
|
@@ -10690,6 +12252,27 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10690
12252
|
},
|
|
10691
12253
|
"*",
|
|
10692
12254
|
);
|
|
12255
|
+
if (scaleToolEnabled) {
|
|
12256
|
+
(scaledTextTargetsCache || []).forEach(function (target) {
|
|
12257
|
+
var textStyles: Record<string, string> = {
|
|
12258
|
+
fontSize: target.el.style.fontSize,
|
|
12259
|
+
};
|
|
12260
|
+
(window.parent as Window).postMessage(
|
|
12261
|
+
{
|
|
12262
|
+
type: "visual-style-change",
|
|
12263
|
+
selector: getSelector(target.el),
|
|
12264
|
+
styles: textStyles,
|
|
12265
|
+
originalStyles: originalInlineStylesForPatch(
|
|
12266
|
+
target.el,
|
|
12267
|
+
textStyles,
|
|
12268
|
+
),
|
|
12269
|
+
payload: getElementInfo(target.el),
|
|
12270
|
+
preserveSelection: true,
|
|
12271
|
+
},
|
|
12272
|
+
"*",
|
|
12273
|
+
);
|
|
12274
|
+
});
|
|
12275
|
+
}
|
|
10693
12276
|
}
|
|
10694
12277
|
document.addEventListener(events.move, onMove, true);
|
|
10695
12278
|
document.addEventListener(events.up, onUp, true);
|
|
@@ -10697,6 +12280,324 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10697
12280
|
setActiveDragCancel(cancelResizeDrag);
|
|
10698
12281
|
}
|
|
10699
12282
|
|
|
12283
|
+
/**
|
|
12284
|
+
* Scales a multi-selection as one box: the drag resizes the group's bounds
|
|
12285
|
+
* and every member keeps its position and size relative to them. Separate
|
|
12286
|
+
* from startResize, whose single-element invariants (Alt-from-center,
|
|
12287
|
+
* per-axis touch tracking) have no group analogue.
|
|
12288
|
+
*/
|
|
12289
|
+
function startGroupResize(handle, e) {
|
|
12290
|
+
if (readOnly) return;
|
|
12291
|
+
var members = collectSelectionMembers();
|
|
12292
|
+
if (members.length < 2) return;
|
|
12293
|
+
e.preventDefault();
|
|
12294
|
+
e.stopPropagation();
|
|
12295
|
+
var events = dragEventNames(e);
|
|
12296
|
+
var groupLeft = Infinity;
|
|
12297
|
+
var groupTop = Infinity;
|
|
12298
|
+
var groupRight = -Infinity;
|
|
12299
|
+
var groupBottom = -Infinity;
|
|
12300
|
+
var memberStates = members.map(function (member) {
|
|
12301
|
+
var el = member as HTMLElement;
|
|
12302
|
+
// Snapshot before ensurePositionable, or Escape restores the position
|
|
12303
|
+
// it just wrote onto a static member instead of the authored one.
|
|
12304
|
+
var snapshot = {
|
|
12305
|
+
el: el,
|
|
12306
|
+
originalInlinePosition: el.style.position,
|
|
12307
|
+
originalInlineLeft: el.style.left,
|
|
12308
|
+
originalInlineTop: el.style.top,
|
|
12309
|
+
originalInlineWidth: el.style.width,
|
|
12310
|
+
originalInlineHeight: el.style.height,
|
|
12311
|
+
originalInlineBorderWidth: el.style.borderWidth,
|
|
12312
|
+
originalInlineFontSize: el.style.fontSize,
|
|
12313
|
+
originLeft: 0,
|
|
12314
|
+
originTop: 0,
|
|
12315
|
+
originWidth: 0,
|
|
12316
|
+
originHeight: 0,
|
|
12317
|
+
// The center is the one point rotation leaves alone: a rotated
|
|
12318
|
+
// member's client rect is its inflated axis-aligned box, so corners
|
|
12319
|
+
// would scale it to the wrong place.
|
|
12320
|
+
originCenterX: 0,
|
|
12321
|
+
originCenterY: 0,
|
|
12322
|
+
originBorderWidth: 0,
|
|
12323
|
+
originFontSize: 0,
|
|
12324
|
+
textTargets: null as ReturnType<typeof collectScaleFontTargets> | null,
|
|
12325
|
+
};
|
|
12326
|
+
rememberLiveVisualEditOriginalStyles(el);
|
|
12327
|
+
ensurePositionable(el);
|
|
12328
|
+
var cs = window.getComputedStyle(el);
|
|
12329
|
+
var rect = el.getBoundingClientRect();
|
|
12330
|
+
groupLeft = Math.min(groupLeft, rect.left);
|
|
12331
|
+
groupTop = Math.min(groupTop, rect.top);
|
|
12332
|
+
groupRight = Math.max(groupRight, rect.right);
|
|
12333
|
+
groupBottom = Math.max(groupBottom, rect.bottom);
|
|
12334
|
+
snapshot.originLeft = readPx(el.style.left || cs.left);
|
|
12335
|
+
snapshot.originTop = readPx(el.style.top || cs.top);
|
|
12336
|
+
snapshot.originWidth = readPx(cs.width);
|
|
12337
|
+
snapshot.originHeight = readPx(cs.height);
|
|
12338
|
+
snapshot.originCenterX = rect.left + rect.width / 2;
|
|
12339
|
+
snapshot.originCenterY = rect.top + rect.height / 2;
|
|
12340
|
+
snapshot.originBorderWidth = readPx(
|
|
12341
|
+
el.style.borderWidth || cs.borderWidth,
|
|
12342
|
+
);
|
|
12343
|
+
snapshot.originFontSize = readPx(el.style.fontSize || cs.fontSize);
|
|
12344
|
+
return snapshot;
|
|
12345
|
+
});
|
|
12346
|
+
var groupWidth = Math.max(1, groupRight - groupLeft);
|
|
12347
|
+
var groupHeight = Math.max(1, groupBottom - groupTop);
|
|
12348
|
+
// The group's fixed corner for this handle — every member scales away
|
|
12349
|
+
// from it, so the opposite side of the selection stays put.
|
|
12350
|
+
var anchorX = handle.indexOf("w") !== -1 ? groupRight : groupLeft;
|
|
12351
|
+
var anchorY = handle.indexOf("n") !== -1 ? groupBottom : groupTop;
|
|
12352
|
+
// Clamp by the SMALLEST member, not by the group box: a factor that keeps
|
|
12353
|
+
// the group above 8px can still collapse (or mirror) a small member.
|
|
12354
|
+
var minMemberWidth = Math.max(
|
|
12355
|
+
1,
|
|
12356
|
+
Math.min.apply(
|
|
12357
|
+
null,
|
|
12358
|
+
memberStates.map(function (state) {
|
|
12359
|
+
return Math.max(1, state.originWidth);
|
|
12360
|
+
}),
|
|
12361
|
+
),
|
|
12362
|
+
);
|
|
12363
|
+
var minMemberHeight = Math.max(
|
|
12364
|
+
1,
|
|
12365
|
+
Math.min.apply(
|
|
12366
|
+
null,
|
|
12367
|
+
memberStates.map(function (state) {
|
|
12368
|
+
return Math.max(1, state.originHeight);
|
|
12369
|
+
}),
|
|
12370
|
+
),
|
|
12371
|
+
);
|
|
12372
|
+
var minFactorX = 8 / minMemberWidth;
|
|
12373
|
+
var minFactorY = 8 / minMemberHeight;
|
|
12374
|
+
var startX = e.clientX;
|
|
12375
|
+
var startY = e.clientY;
|
|
12376
|
+
var groupGestureViewport = bridgeGestureViewport();
|
|
12377
|
+
var bridgeGroupResizeController = createCanvasGestureController({
|
|
12378
|
+
capabilities: { move: true, resize: true },
|
|
12379
|
+
drag: { threshold: 3, duplicateModifier: "alt" },
|
|
12380
|
+
minSize: 8,
|
|
12381
|
+
adapter: {
|
|
12382
|
+
preview: function () {
|
|
12383
|
+
return { handled: true };
|
|
12384
|
+
},
|
|
12385
|
+
commit: function () {
|
|
12386
|
+
return { handled: true };
|
|
12387
|
+
},
|
|
12388
|
+
cancel: function () {
|
|
12389
|
+
return { handled: true };
|
|
12390
|
+
},
|
|
12391
|
+
},
|
|
12392
|
+
});
|
|
12393
|
+
bridgeGroupResizeController.pointerDown({
|
|
12394
|
+
kind: "resize",
|
|
12395
|
+
objectIds: memberStates.map(function (state) {
|
|
12396
|
+
return getSelector(state.el);
|
|
12397
|
+
}),
|
|
12398
|
+
pointer: bridgeGesturePointer(e),
|
|
12399
|
+
viewport: groupGestureViewport,
|
|
12400
|
+
canvas: {
|
|
12401
|
+
width: groupGestureViewport.width,
|
|
12402
|
+
height: groupGestureViewport.height,
|
|
12403
|
+
},
|
|
12404
|
+
handle: handle,
|
|
12405
|
+
rect: {
|
|
12406
|
+
x: groupLeft,
|
|
12407
|
+
y: groupTop,
|
|
12408
|
+
width: groupWidth,
|
|
12409
|
+
height: groupHeight,
|
|
12410
|
+
},
|
|
12411
|
+
});
|
|
12412
|
+
|
|
12413
|
+
function groupFactors(ev) {
|
|
12414
|
+
var dx = ev.clientX - startX;
|
|
12415
|
+
var dy = ev.clientY - startY;
|
|
12416
|
+
var nextWidth =
|
|
12417
|
+
handle.indexOf("e") !== -1 ? groupWidth + dx : groupWidth - dx;
|
|
12418
|
+
var nextHeight =
|
|
12419
|
+
handle.indexOf("s") !== -1 ? groupHeight + dy : groupHeight - dy;
|
|
12420
|
+
var factorX = nextWidth / groupWidth;
|
|
12421
|
+
var factorY = nextHeight / groupHeight;
|
|
12422
|
+
if (ev.shiftKey || scaleToolEnabled) {
|
|
12423
|
+
// Clamping the axes separately against their own minimums would
|
|
12424
|
+
// break the very lock this branch applies.
|
|
12425
|
+
var uniform = Math.max(
|
|
12426
|
+
Math.max(minFactorX, minFactorY),
|
|
12427
|
+
Math.abs(dx) > Math.abs(dy) ? factorX : factorY,
|
|
12428
|
+
);
|
|
12429
|
+
return { x: uniform, y: uniform };
|
|
12430
|
+
}
|
|
12431
|
+
return {
|
|
12432
|
+
x: Math.max(minFactorX, factorX),
|
|
12433
|
+
y: Math.max(minFactorY, factorY),
|
|
12434
|
+
};
|
|
12435
|
+
}
|
|
12436
|
+
|
|
12437
|
+
function memberTextTargets(state) {
|
|
12438
|
+
if (!state.textTargets) {
|
|
12439
|
+
state.textTargets = collectScaleFontTargets(state.el);
|
|
12440
|
+
}
|
|
12441
|
+
return state.textTargets;
|
|
12442
|
+
}
|
|
12443
|
+
|
|
12444
|
+
function onMove(ev) {
|
|
12445
|
+
var controllerMove = bridgeGroupResizeController.pointerMove(
|
|
12446
|
+
bridgeGesturePointer(ev),
|
|
12447
|
+
);
|
|
12448
|
+
if (controllerMove.phase !== "active") return;
|
|
12449
|
+
var factor = groupFactors(ev);
|
|
12450
|
+
memberStates.forEach(function (state) {
|
|
12451
|
+
var nextCenterX = anchorX + (state.originCenterX - anchorX) * factor.x;
|
|
12452
|
+
var nextCenterY = anchorY + (state.originCenterY - anchorY) * factor.y;
|
|
12453
|
+
var nextWidth = state.originWidth * factor.x;
|
|
12454
|
+
var nextHeight = state.originHeight * factor.y;
|
|
12455
|
+
state.el.style.left =
|
|
12456
|
+
Math.round(
|
|
12457
|
+
state.originLeft +
|
|
12458
|
+
(nextCenterX - state.originCenterX) -
|
|
12459
|
+
(nextWidth - state.originWidth) / 2,
|
|
12460
|
+
) + "px";
|
|
12461
|
+
state.el.style.top =
|
|
12462
|
+
Math.round(
|
|
12463
|
+
state.originTop +
|
|
12464
|
+
(nextCenterY - state.originCenterY) -
|
|
12465
|
+
(nextHeight - state.originHeight) / 2,
|
|
12466
|
+
) + "px";
|
|
12467
|
+
state.el.style.width = Math.round(nextWidth) + "px";
|
|
12468
|
+
state.el.style.height = Math.round(nextHeight) + "px";
|
|
12469
|
+
if (!scaleToolEnabled) return;
|
|
12470
|
+
if (state.originBorderWidth > 0) {
|
|
12471
|
+
state.el.style.borderWidth =
|
|
12472
|
+
Math.max(
|
|
12473
|
+
0,
|
|
12474
|
+
Math.round(state.originBorderWidth * factor.x * 100) / 100,
|
|
12475
|
+
) + "px";
|
|
12476
|
+
}
|
|
12477
|
+
if (state.originFontSize > 0) {
|
|
12478
|
+
state.el.style.fontSize =
|
|
12479
|
+
Math.max(
|
|
12480
|
+
1,
|
|
12481
|
+
Math.round(state.originFontSize * factor.x * 100) / 100,
|
|
12482
|
+
) + "px";
|
|
12483
|
+
}
|
|
12484
|
+
memberTextTargets(state).forEach(function (target) {
|
|
12485
|
+
target.el.style.fontSize =
|
|
12486
|
+
Math.max(
|
|
12487
|
+
1,
|
|
12488
|
+
Math.round(target.originFontSize * factor.x * 100) / 100,
|
|
12489
|
+
) + "px";
|
|
12490
|
+
});
|
|
12491
|
+
});
|
|
12492
|
+
showTransformBadge(
|
|
12493
|
+
Math.round(groupWidth * factor.x) +
|
|
12494
|
+
" x " +
|
|
12495
|
+
Math.round(groupHeight * factor.y),
|
|
12496
|
+
ev.clientX,
|
|
12497
|
+
ev.clientY,
|
|
12498
|
+
);
|
|
12499
|
+
refreshOverlays();
|
|
12500
|
+
}
|
|
12501
|
+
|
|
12502
|
+
function cleanupGroupResizeDrag() {
|
|
12503
|
+
document.removeEventListener(events.move, onMove, true);
|
|
12504
|
+
document.removeEventListener(events.up, onUp, true);
|
|
12505
|
+
document.removeEventListener("keydown", onGroupResizeKeyDown, true);
|
|
12506
|
+
clearActiveDragCancel(cancelGroupResizeDrag);
|
|
12507
|
+
}
|
|
12508
|
+
|
|
12509
|
+
function cancelGroupResizeDrag() {
|
|
12510
|
+
bridgeGroupResizeController.cancel();
|
|
12511
|
+
cleanupGroupResizeDrag();
|
|
12512
|
+
hideTransformBadge();
|
|
12513
|
+
memberStates.forEach(function (state) {
|
|
12514
|
+
if (!document.documentElement.contains(state.el)) return;
|
|
12515
|
+
state.el.style.position = state.originalInlinePosition;
|
|
12516
|
+
state.el.style.left = state.originalInlineLeft;
|
|
12517
|
+
state.el.style.top = state.originalInlineTop;
|
|
12518
|
+
state.el.style.width = state.originalInlineWidth;
|
|
12519
|
+
state.el.style.height = state.originalInlineHeight;
|
|
12520
|
+
state.el.style.borderWidth = state.originalInlineBorderWidth;
|
|
12521
|
+
state.el.style.fontSize = state.originalInlineFontSize;
|
|
12522
|
+
(state.textTargets || []).forEach(function (target) {
|
|
12523
|
+
target.el.style.fontSize = target.originalInlineFontSize;
|
|
12524
|
+
});
|
|
12525
|
+
});
|
|
12526
|
+
suppressNextShieldClickBriefly();
|
|
12527
|
+
refreshOverlays();
|
|
12528
|
+
return true;
|
|
12529
|
+
}
|
|
12530
|
+
|
|
12531
|
+
function onGroupResizeKeyDown(ev) {
|
|
12532
|
+
if (ev.key !== "Escape") return;
|
|
12533
|
+
stopNativeInteraction(ev);
|
|
12534
|
+
cancelGroupResizeDrag();
|
|
12535
|
+
}
|
|
12536
|
+
|
|
12537
|
+
function onUp(ev) {
|
|
12538
|
+
var controllerEnd = bridgeGroupResizeController.pointerUp(
|
|
12539
|
+
bridgeGesturePointer(ev),
|
|
12540
|
+
);
|
|
12541
|
+
cleanupGroupResizeDrag();
|
|
12542
|
+
hideTransformBadge();
|
|
12543
|
+
if (!controllerEnd.committed) return;
|
|
12544
|
+
// One style-change message per member, in order — the host composes
|
|
12545
|
+
// them against its same-tick content refs exactly like a group move.
|
|
12546
|
+
memberStates.forEach(function (state) {
|
|
12547
|
+
var styles: Record<string, string> = {
|
|
12548
|
+
position: state.el.style.position,
|
|
12549
|
+
left: state.el.style.left,
|
|
12550
|
+
top: state.el.style.top,
|
|
12551
|
+
width: state.el.style.width,
|
|
12552
|
+
height: state.el.style.height,
|
|
12553
|
+
};
|
|
12554
|
+
if (scaleToolEnabled && state.originBorderWidth > 0) {
|
|
12555
|
+
styles.borderWidth = state.el.style.borderWidth;
|
|
12556
|
+
}
|
|
12557
|
+
if (scaleToolEnabled && state.originFontSize > 0) {
|
|
12558
|
+
styles.fontSize = state.el.style.fontSize;
|
|
12559
|
+
}
|
|
12560
|
+
(window.parent as Window).postMessage(
|
|
12561
|
+
{
|
|
12562
|
+
type: "visual-style-change",
|
|
12563
|
+
selector: getSelector(state.el),
|
|
12564
|
+
styles: styles,
|
|
12565
|
+
originalStyles: originalInlineStylesForPatch(state.el, styles),
|
|
12566
|
+
payload: getElementInfo(state.el),
|
|
12567
|
+
},
|
|
12568
|
+
"*",
|
|
12569
|
+
);
|
|
12570
|
+
if (!scaleToolEnabled) return;
|
|
12571
|
+
(state.textTargets || []).forEach(function (target) {
|
|
12572
|
+
var textStyles: Record<string, string> = {
|
|
12573
|
+
fontSize: target.el.style.fontSize,
|
|
12574
|
+
};
|
|
12575
|
+
(window.parent as Window).postMessage(
|
|
12576
|
+
{
|
|
12577
|
+
type: "visual-style-change",
|
|
12578
|
+
selector: getSelector(target.el),
|
|
12579
|
+
styles: textStyles,
|
|
12580
|
+
originalStyles: originalInlineStylesForPatch(
|
|
12581
|
+
target.el,
|
|
12582
|
+
textStyles,
|
|
12583
|
+
),
|
|
12584
|
+
payload: getElementInfo(target.el),
|
|
12585
|
+
// A scaled descendant is a side effect of the gesture, not the
|
|
12586
|
+
// object the user is holding.
|
|
12587
|
+
preserveSelection: true,
|
|
12588
|
+
},
|
|
12589
|
+
"*",
|
|
12590
|
+
);
|
|
12591
|
+
});
|
|
12592
|
+
});
|
|
12593
|
+
}
|
|
12594
|
+
|
|
12595
|
+
document.addEventListener(events.move, onMove, true);
|
|
12596
|
+
document.addEventListener(events.up, onUp, true);
|
|
12597
|
+
document.addEventListener("keydown", onGroupResizeKeyDown, true);
|
|
12598
|
+
setActiveDragCancel(cancelGroupResizeDrag);
|
|
12599
|
+
}
|
|
12600
|
+
|
|
10700
12601
|
function startRotate(e) {
|
|
10701
12602
|
if (readOnly) return;
|
|
10702
12603
|
if (!selectedEl) return;
|
|
@@ -10884,12 +12785,24 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10884
12785
|
preferSelected: selectedLayerDragPriorityEnabled,
|
|
10885
12786
|
});
|
|
10886
12787
|
var clickTarget = hitTarget;
|
|
12788
|
+
if ((window as any).__DND_DEBUG)
|
|
12789
|
+
dndLog("shield:down", {
|
|
12790
|
+
hit: getSelector(hit),
|
|
12791
|
+
dragTarget: getSelector(dragTarget),
|
|
12792
|
+
board: designCanvasBoardSurface,
|
|
12793
|
+
readOnly: readOnly,
|
|
12794
|
+
position: dragTarget
|
|
12795
|
+
? window.getComputedStyle(dragTarget as Element).position
|
|
12796
|
+
: null,
|
|
12797
|
+
flowCandidate: dragTarget ? isFlowReorderCandidate(dragTarget) : null,
|
|
12798
|
+
});
|
|
10887
12799
|
if (
|
|
10888
12800
|
!dragTarget ||
|
|
10889
12801
|
dragTarget === document.body ||
|
|
10890
12802
|
dragTarget === document.documentElement ||
|
|
10891
12803
|
isLayerInteractionBlocked(dragTarget)
|
|
10892
12804
|
) {
|
|
12805
|
+
dndLog("shield:reject", { reason: "no-drag-target" });
|
|
10893
12806
|
return;
|
|
10894
12807
|
}
|
|
10895
12808
|
if (
|
|
@@ -10930,6 +12843,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
10930
12843
|
if (readOnly) return;
|
|
10931
12844
|
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) <= 3) return;
|
|
10932
12845
|
clearPendingShieldDrag();
|
|
12846
|
+
if (!didStartDrag)
|
|
12847
|
+
dndLog("shield:drag-start", { board: designCanvasBoardSurface });
|
|
10933
12848
|
didStartDrag = true;
|
|
10934
12849
|
// Multi-select group move: when the drag starts on a member of the
|
|
10935
12850
|
// current 2+ selection, PRESERVE the whole selection (no
|
|
@@ -13300,6 +15215,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
13300
15215
|
],
|
|
13301
15216
|
});
|
|
13302
15217
|
}
|
|
15218
|
+
captureInitialSourceOwnership();
|
|
13303
15219
|
if (runtimeLayerSnapshotEnabled) scheduleRuntimeLayerSnapshot();
|
|
13304
15220
|
|
|
13305
15221
|
// One-time ready signal: tells the host that every message listener above is
|