@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
|
@@ -63,6 +63,39 @@ export interface CanvasSnapOptions {
|
|
|
63
63
|
thresholdScreenPx?: number;
|
|
64
64
|
zoom: number;
|
|
65
65
|
bypass?: boolean;
|
|
66
|
+
/** Axes the caller has already pinned (a Shift dominant-axis drag). Both the
|
|
67
|
+
* offset and the guides skip these, so the chrome never describes a
|
|
68
|
+
* position the object is not at. */
|
|
69
|
+
lockedAxes?: { x?: boolean; y?: boolean };
|
|
70
|
+
/** Pre-resolved AABBs, so a per-rAF caller does not rebuild them for each
|
|
71
|
+
* stage of the drag-snap stack. */
|
|
72
|
+
stationaryBounds?: FrameBounds[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type SpacingSnapOptions = CanvasSnapOptions;
|
|
76
|
+
|
|
77
|
+
export interface DragSnapOptions extends CanvasSnapOptions {
|
|
78
|
+
/** Figma's "snap to pixel grid": land on whole canvas pixels. */
|
|
79
|
+
pixelGrid?: boolean;
|
|
80
|
+
/** How far a neighbour can be, in screen px, and still get a distance
|
|
81
|
+
* readout. Beyond this the measurement is noise stretched across empty
|
|
82
|
+
* canvas rather than something the user is positioning against. */
|
|
83
|
+
proximityRangeScreenPx?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The distance from the moving frame to its nearest neighbour on one axis. */
|
|
87
|
+
export interface ProximityMeasurement {
|
|
88
|
+
orientation: "vertical" | "horizontal";
|
|
89
|
+
gap: number;
|
|
90
|
+
band: DistanceGuideBand;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DragSnapResult {
|
|
94
|
+
dx: number;
|
|
95
|
+
dy: number;
|
|
96
|
+
guides: AlignmentGuide[];
|
|
97
|
+
spacingGuides: EqualGapGuide[];
|
|
98
|
+
measurements: ProximityMeasurement[];
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
export interface ResizeSnapOptions extends CanvasSnapOptions {
|
|
@@ -134,14 +167,12 @@ export interface DistanceGuideBand {
|
|
|
134
167
|
* highlights both gaps with the shared distance. */
|
|
135
168
|
export interface EqualGapGuide {
|
|
136
169
|
orientation: "vertical" | "horizontal";
|
|
137
|
-
/** The shared gap size, in canvas units,
|
|
170
|
+
/** The shared gap size, in canvas units, every band agrees on. */
|
|
138
171
|
gap: number;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
/** How close two gaps must be (in canvas units) to count as "equal". */
|
|
144
|
-
toleranceCanvasPx?: number;
|
|
172
|
+
/** At least two: the gap(s) around the moving frame, plus every other gap
|
|
173
|
+
* in the same row/column that matches, so a run of evenly spaced frames
|
|
174
|
+
* lights up as one rhythm instead of just the pair being snapped. */
|
|
175
|
+
bands: DistanceGuideBand[];
|
|
145
176
|
}
|
|
146
177
|
|
|
147
178
|
export interface RotateFrameMetadata {
|
|
@@ -225,6 +256,7 @@ interface SnapCandidate {
|
|
|
225
256
|
}
|
|
226
257
|
|
|
227
258
|
export const DEFAULT_SNAP_THRESHOLD_SCREEN_PX = 6;
|
|
259
|
+
export const DEFAULT_PROXIMITY_RANGE_SCREEN_PX = 160;
|
|
228
260
|
export const DEFAULT_ROTATION_SNAP_DEGREES = 15;
|
|
229
261
|
export const DEFAULT_PIXEL_GRID_MIN_ZOOM = 800;
|
|
230
262
|
export const MIN_CANVAS_FRAME_WIDTH = 120;
|
|
@@ -638,76 +670,260 @@ export function computeMoveSnap(
|
|
|
638
670
|
return { dx: 0, dy: 0, guides: [] as AlignmentGuide[] };
|
|
639
671
|
}
|
|
640
672
|
|
|
641
|
-
let bestX: SnapCandidate | null = null;
|
|
642
|
-
let bestY: SnapCandidate | null = null;
|
|
643
673
|
const threshold = getCanvasSnapThreshold(options);
|
|
644
674
|
// Use the rotated (world-space) AABB rather than the unrotated local
|
|
645
675
|
// bounds, so a rotated frame snaps by its visual silhouette instead of an
|
|
646
676
|
// AABB that doesn't match anything on screen.
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
677
|
+
const movingBounds = moving.map((entry) =>
|
|
678
|
+
getRotatedFrameAABB(entry.geometry),
|
|
679
|
+
);
|
|
680
|
+
const stationaryBounds =
|
|
681
|
+
options.stationaryBounds ??
|
|
682
|
+
stationary.map((entry) => getRotatedFrameAABB(entry.geometry));
|
|
683
|
+
|
|
684
|
+
// A locked axis must be locked before the guides are built: derive both the
|
|
685
|
+
// offset and the guides from the same position, or the lines describe a
|
|
686
|
+
// place the frame is not.
|
|
687
|
+
const dx = options.lockedAxes?.x
|
|
688
|
+
? null
|
|
689
|
+
: findAxisSnapOffset("x", movingBounds, stationaryBounds, threshold);
|
|
690
|
+
const dy = options.lockedAxes?.y
|
|
691
|
+
? null
|
|
692
|
+
: findAxisSnapOffset("y", movingBounds, stationaryBounds, threshold);
|
|
693
|
+
|
|
694
|
+
const snappedBounds = movingBounds.map((bounds) =>
|
|
695
|
+
translateBounds(bounds, dx ?? 0, dy ?? 0),
|
|
696
|
+
);
|
|
651
697
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
698
|
+
return {
|
|
699
|
+
dx: dx ?? 0,
|
|
700
|
+
dy: dy ?? 0,
|
|
701
|
+
guides: [
|
|
702
|
+
...(options.lockedAxes?.x
|
|
703
|
+
? []
|
|
704
|
+
: buildAxisGuides("x", snappedBounds, stationaryBounds)),
|
|
705
|
+
...(options.lockedAxes?.y
|
|
706
|
+
? []
|
|
707
|
+
: buildAxisGuides("y", snappedBounds, stationaryBounds)),
|
|
708
|
+
],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Figma-style "smart spacing" snap: centers the frame between two neighbors,
|
|
714
|
+
* or matches an existing gap in the same row/column. `lockedAxes` are the
|
|
715
|
+
* axes computeMoveSnap already claimed — moving one would pull the frame off
|
|
716
|
+
* the alignment guide the user can already see.
|
|
717
|
+
*/
|
|
718
|
+
export function computeSpacingSnap(
|
|
719
|
+
moving: FrameGeometry,
|
|
720
|
+
stationary: FrameEntry[],
|
|
721
|
+
options: SpacingSnapOptions,
|
|
722
|
+
): {
|
|
723
|
+
dx: number;
|
|
724
|
+
dy: number;
|
|
725
|
+
guides: EqualGapGuide[];
|
|
726
|
+
movedAxes?: { x: boolean; y: boolean };
|
|
727
|
+
} {
|
|
728
|
+
if (options.bypass) return { dx: 0, dy: 0, guides: [] };
|
|
675
729
|
|
|
730
|
+
const tolerance = getCanvasSnapThreshold(options);
|
|
731
|
+
const spacingMatchTolerance = getCanvasSnapThreshold({
|
|
732
|
+
...options,
|
|
733
|
+
thresholdScreenPx: SPACING_MATCH_SCREEN_PX,
|
|
734
|
+
});
|
|
735
|
+
const bounds = getRotatedFrameAABB(moving);
|
|
736
|
+
const stationaryBounds =
|
|
737
|
+
options.stationaryBounds ??
|
|
738
|
+
stationary.map((entry) => getRotatedFrameAABB(entry.geometry));
|
|
739
|
+
|
|
740
|
+
const idle = { offset: 0, side: null } as const;
|
|
741
|
+
const x = options.lockedAxes?.x
|
|
742
|
+
? idle
|
|
743
|
+
: findSpacingSnapOffset("x", bounds, stationaryBounds, tolerance);
|
|
744
|
+
const y = options.lockedAxes?.y
|
|
745
|
+
? idle
|
|
746
|
+
: findSpacingSnapOffset("y", bounds, stationaryBounds, tolerance);
|
|
747
|
+
|
|
748
|
+
// Guides are drawn at the tight tolerance, not the snap pull: a near-equal
|
|
749
|
+
// pair must not get labelled as an equal gap it does not actually have.
|
|
750
|
+
const snapped = translateBounds(bounds, x.offset, y.offset);
|
|
676
751
|
return {
|
|
677
|
-
dx:
|
|
678
|
-
dy:
|
|
679
|
-
guides: [
|
|
752
|
+
dx: x.offset,
|
|
753
|
+
dy: y.offset,
|
|
754
|
+
guides: [
|
|
755
|
+
...(options.lockedAxes?.x
|
|
756
|
+
? []
|
|
757
|
+
: buildSpacingGuides(
|
|
758
|
+
"x",
|
|
759
|
+
snapped,
|
|
760
|
+
stationaryBounds,
|
|
761
|
+
spacingMatchTolerance,
|
|
762
|
+
x.side,
|
|
763
|
+
)),
|
|
764
|
+
...(options.lockedAxes?.y
|
|
765
|
+
? []
|
|
766
|
+
: buildSpacingGuides(
|
|
767
|
+
"y",
|
|
768
|
+
snapped,
|
|
769
|
+
stationaryBounds,
|
|
770
|
+
spacingMatchTolerance,
|
|
771
|
+
y.side,
|
|
772
|
+
)),
|
|
773
|
+
],
|
|
774
|
+
movedAxes: { x: x.offset !== 0, y: y.offset !== 0 },
|
|
680
775
|
};
|
|
681
776
|
}
|
|
682
777
|
|
|
683
778
|
/**
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* independently of, and after, any snap offset has already been applied to
|
|
690
|
-
* the moving frame's geometry.
|
|
691
|
-
*
|
|
692
|
-
* Only single-frame moves are supported (matching how Figma's own equal-gap
|
|
693
|
-
* guides only appear while dragging one object) — pass the already-resolved
|
|
694
|
-
* single moving frame, not a multi-select group.
|
|
779
|
+
* Distance to the nearest neighbour on each axis, for the frame's settled
|
|
780
|
+
* position. Stock Figma only surfaces a distance when it matches an existing
|
|
781
|
+
* gap; showing the nearest one unconditionally is a deliberate divergence,
|
|
782
|
+
* range-limited so a lone frame across the canvas does not draw a measurement
|
|
783
|
+
* over empty space.
|
|
695
784
|
*/
|
|
696
|
-
export function
|
|
785
|
+
export function computeProximityMeasurements(
|
|
697
786
|
moving: FrameGeometry,
|
|
698
787
|
stationary: FrameEntry[],
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
788
|
+
options: {
|
|
789
|
+
zoom: number;
|
|
790
|
+
rangeScreenPx?: number;
|
|
791
|
+
bypass?: boolean;
|
|
792
|
+
stationaryBounds?: FrameBounds[];
|
|
793
|
+
},
|
|
794
|
+
): ProximityMeasurement[] {
|
|
795
|
+
if (options.bypass) return [];
|
|
796
|
+
const range =
|
|
797
|
+
(options.rangeScreenPx ?? DEFAULT_PROXIMITY_RANGE_SCREEN_PX) /
|
|
798
|
+
getCameraScale(options.zoom);
|
|
799
|
+
const bounds = getRotatedFrameAABB(moving);
|
|
800
|
+
const stationaryBounds =
|
|
801
|
+
options.stationaryBounds ??
|
|
802
|
+
stationary.map((entry) => getRotatedFrameAABB(entry.geometry));
|
|
803
|
+
|
|
804
|
+
const measurements: ProximityMeasurement[] = [];
|
|
805
|
+
for (const axis of ["x", "y"] as const) {
|
|
806
|
+
const candidates = collectAxisGapCandidates(axis, bounds, stationaryBounds);
|
|
807
|
+
const nearest = candidates.reduce<GapCandidate | null>(
|
|
808
|
+
(best, candidate) =>
|
|
809
|
+
!best || candidate.gap < best.gap ? candidate : best,
|
|
810
|
+
null,
|
|
811
|
+
);
|
|
812
|
+
if (!nearest || nearest.gap > range) continue;
|
|
813
|
+
measurements.push({
|
|
814
|
+
orientation: axis === "x" ? "vertical" : "horizontal",
|
|
815
|
+
gap: nearest.gap,
|
|
816
|
+
band: gapCandidateBand(nearest),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return measurements;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The whole Figma move-snap stack in one call, in precedence order: an
|
|
824
|
+
* edge/center alignment wins, spacing only gets the axes alignment left free,
|
|
825
|
+
* and the pixel grid only gets the axes with no guide drawn on them at all —
|
|
826
|
+
* rounding an axis that has a visible guide would move the frame off the line
|
|
827
|
+
* the user is looking at.
|
|
828
|
+
*/
|
|
829
|
+
export function computeDragSnap(
|
|
830
|
+
moving: FrameEntry[],
|
|
831
|
+
stationary: FrameEntry[],
|
|
832
|
+
options: DragSnapOptions,
|
|
833
|
+
): DragSnapResult {
|
|
834
|
+
// One AABB pass feeds all three stages; this runs every rAF of a drag.
|
|
835
|
+
const stationaryBounds = stationary.map((entry) =>
|
|
836
|
+
getRotatedFrameAABB(entry.geometry),
|
|
837
|
+
);
|
|
838
|
+
const alignment = computeMoveSnap(moving, stationary, {
|
|
839
|
+
...options,
|
|
840
|
+
stationaryBounds,
|
|
841
|
+
});
|
|
842
|
+
const claimedX = alignment.guides.some((g) => g.orientation === "vertical");
|
|
843
|
+
const claimedY = alignment.guides.some((g) => g.orientation === "horizontal");
|
|
703
844
|
|
|
704
|
-
|
|
705
|
-
|
|
845
|
+
// Figma drops spacing guides for a multi-select drag; so do we.
|
|
846
|
+
const single = moving.length === 1 ? moving[0] : null;
|
|
847
|
+
const spacing = single
|
|
848
|
+
? computeSpacingSnap(
|
|
849
|
+
{
|
|
850
|
+
...single.geometry,
|
|
851
|
+
x: single.geometry.x + alignment.dx,
|
|
852
|
+
y: single.geometry.y + alignment.dy,
|
|
853
|
+
},
|
|
854
|
+
stationary,
|
|
855
|
+
{
|
|
856
|
+
zoom: options.zoom,
|
|
857
|
+
bypass: options.bypass,
|
|
858
|
+
thresholdScreenPx: options.thresholdScreenPx,
|
|
859
|
+
stationaryBounds,
|
|
860
|
+
lockedAxes: {
|
|
861
|
+
x: claimedX || options.lockedAxes?.x,
|
|
862
|
+
y: claimedY || options.lockedAxes?.y,
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
)
|
|
866
|
+
: {
|
|
867
|
+
dx: 0,
|
|
868
|
+
dy: 0,
|
|
869
|
+
guides: [] as EqualGapGuide[],
|
|
870
|
+
movedAxes: { x: false, y: false },
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
let dx = alignment.dx + spacing.dx;
|
|
874
|
+
let dy = alignment.dy + spacing.dy;
|
|
875
|
+
|
|
876
|
+
const anchor = moving[0];
|
|
877
|
+
if (options.pixelGrid && anchor && !options.bypass) {
|
|
878
|
+
// Keyed on what actually moved the frame, not on what got drawn: a
|
|
879
|
+
// spacing snap that produced no chrome is still a position the user
|
|
880
|
+
// chose, and rounding it away undoes the snap they just felt.
|
|
881
|
+
const claimed = (orientation: "vertical" | "horizontal") =>
|
|
882
|
+
alignment.guides.some((g) => g.orientation === orientation) ||
|
|
883
|
+
spacing.guides.some((g) => g.orientation === orientation) ||
|
|
884
|
+
(orientation === "vertical"
|
|
885
|
+
? spacing.movedAxes?.x
|
|
886
|
+
: spacing.movedAxes?.y);
|
|
887
|
+
// Only the anchor rounds; the rest of a multi-select rides the same
|
|
888
|
+
// delta, so a group keeps its internal fractional offsets.
|
|
889
|
+
if (!claimed("vertical") && !options.lockedAxes?.x) {
|
|
890
|
+
dx = Math.round(anchor.geometry.x + dx) - anchor.geometry.x;
|
|
891
|
+
}
|
|
892
|
+
if (!claimed("horizontal") && !options.lockedAxes?.y) {
|
|
893
|
+
dy = Math.round(anchor.geometry.y + dy) - anchor.geometry.y;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
706
896
|
|
|
707
|
-
|
|
708
|
-
|
|
897
|
+
// An axis whose spacing guide already prints the gap does not also need a
|
|
898
|
+
// proximity readout of the same number.
|
|
899
|
+
const settled = single
|
|
900
|
+
? {
|
|
901
|
+
...single.geometry,
|
|
902
|
+
x: single.geometry.x + dx,
|
|
903
|
+
y: single.geometry.y + dy,
|
|
904
|
+
}
|
|
905
|
+
: null;
|
|
906
|
+
const measurements = settled
|
|
907
|
+
? computeProximityMeasurements(settled, stationary, {
|
|
908
|
+
zoom: options.zoom,
|
|
909
|
+
rangeScreenPx: options.proximityRangeScreenPx,
|
|
910
|
+
bypass: options.bypass,
|
|
911
|
+
stationaryBounds,
|
|
912
|
+
}).filter(
|
|
913
|
+
(measurement) =>
|
|
914
|
+
!spacing.guides.some(
|
|
915
|
+
(guide) => guide.orientation === measurement.orientation,
|
|
916
|
+
),
|
|
917
|
+
)
|
|
918
|
+
: [];
|
|
709
919
|
|
|
710
|
-
return
|
|
920
|
+
return {
|
|
921
|
+
dx,
|
|
922
|
+
dy,
|
|
923
|
+
guides: alignment.guides,
|
|
924
|
+
spacingGuides: spacing.guides,
|
|
925
|
+
measurements,
|
|
926
|
+
};
|
|
711
927
|
}
|
|
712
928
|
|
|
713
929
|
interface GapCandidate {
|
|
@@ -724,122 +940,102 @@ interface GapCandidate {
|
|
|
724
940
|
function collectAxisGapCandidates(
|
|
725
941
|
axis: "x" | "y",
|
|
726
942
|
movingBounds: FrameBounds,
|
|
727
|
-
stationary:
|
|
943
|
+
stationary: FrameBounds[],
|
|
728
944
|
): GapCandidate[] {
|
|
729
945
|
const candidates: GapCandidate[] = [];
|
|
730
|
-
for (const
|
|
731
|
-
const bounds = getRotatedFrameAABB(entry.geometry);
|
|
946
|
+
for (const bounds of stationary) {
|
|
732
947
|
// Only frames that overlap the moving frame's extent on the OTHER axis
|
|
733
948
|
// produce a meaningful "gap between them" — otherwise the empty space
|
|
734
949
|
// isn't really a corridor connecting the two shapes.
|
|
735
|
-
|
|
736
|
-
axis === "x"
|
|
737
|
-
? bounds.top < movingBounds.bottom && bounds.bottom > movingBounds.top
|
|
738
|
-
: bounds.left < movingBounds.right && bounds.right > movingBounds.left;
|
|
739
|
-
if (!crossOverlaps) continue;
|
|
950
|
+
if (!crossAxisOverlaps(axis, bounds, movingBounds)) continue;
|
|
740
951
|
|
|
741
|
-
const crossStart =
|
|
742
|
-
axis
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const crossEnd =
|
|
746
|
-
axis
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (axis
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
} else {
|
|
771
|
-
if (bounds.bottom <= movingBounds.top) {
|
|
772
|
-
candidates.push({
|
|
773
|
-
side: "before",
|
|
774
|
-
gap: movingBounds.top - bounds.bottom,
|
|
775
|
-
gapStart: bounds.bottom,
|
|
776
|
-
gapEnd: movingBounds.top,
|
|
777
|
-
crossStart,
|
|
778
|
-
crossEnd,
|
|
779
|
-
});
|
|
780
|
-
} else if (bounds.top >= movingBounds.bottom) {
|
|
781
|
-
candidates.push({
|
|
782
|
-
side: "after",
|
|
783
|
-
gap: bounds.top - movingBounds.bottom,
|
|
784
|
-
gapStart: movingBounds.bottom,
|
|
785
|
-
gapEnd: bounds.top,
|
|
786
|
-
crossStart,
|
|
787
|
-
crossEnd,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
952
|
+
const crossStart = Math.max(
|
|
953
|
+
getCrossStart(bounds, axis),
|
|
954
|
+
getCrossStart(movingBounds, axis),
|
|
955
|
+
);
|
|
956
|
+
const crossEnd = Math.min(
|
|
957
|
+
getCrossEnd(bounds, axis),
|
|
958
|
+
getCrossEnd(movingBounds, axis),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
if (getAxisEnd(bounds, axis) <= getAxisStart(movingBounds, axis)) {
|
|
962
|
+
candidates.push({
|
|
963
|
+
side: "before",
|
|
964
|
+
gap: getAxisStart(movingBounds, axis) - getAxisEnd(bounds, axis),
|
|
965
|
+
gapStart: getAxisEnd(bounds, axis),
|
|
966
|
+
gapEnd: getAxisStart(movingBounds, axis),
|
|
967
|
+
crossStart,
|
|
968
|
+
crossEnd,
|
|
969
|
+
});
|
|
970
|
+
} else if (getAxisStart(bounds, axis) >= getAxisEnd(movingBounds, axis)) {
|
|
971
|
+
candidates.push({
|
|
972
|
+
side: "after",
|
|
973
|
+
gap: getAxisStart(bounds, axis) - getAxisEnd(movingBounds, axis),
|
|
974
|
+
gapStart: getAxisEnd(movingBounds, axis),
|
|
975
|
+
gapEnd: getAxisStart(bounds, axis),
|
|
976
|
+
crossStart,
|
|
977
|
+
crossEnd,
|
|
978
|
+
});
|
|
790
979
|
}
|
|
791
980
|
}
|
|
792
981
|
return candidates;
|
|
793
982
|
}
|
|
794
983
|
|
|
795
|
-
|
|
796
|
-
|
|
984
|
+
// Closest gap on each side is the one a user is most likely dragging toward,
|
|
985
|
+
// so only the single closest candidate per side is ever paired — otherwise a
|
|
986
|
+
// busy canvas surfaces every combinatorial pair of same-ish gaps at once.
|
|
987
|
+
function closestGapCandidate(
|
|
797
988
|
candidates: GapCandidate[],
|
|
798
|
-
|
|
799
|
-
):
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
// toward, so only pair the single closest "before" candidate against the
|
|
806
|
-
// single closest "after" candidate — otherwise a busy canvas would surface
|
|
807
|
-
// every combinatorial pair of same-ish gaps at once.
|
|
808
|
-
const closestBefore = before.reduce<GapCandidate | null>(
|
|
809
|
-
(best, c) => (!best || c.gap < best.gap ? c : best),
|
|
810
|
-
null,
|
|
811
|
-
);
|
|
812
|
-
const closestAfter = after.reduce<GapCandidate | null>(
|
|
813
|
-
(best, c) => (!best || c.gap < best.gap ? c : best),
|
|
989
|
+
side: "before" | "after",
|
|
990
|
+
): GapCandidate | null {
|
|
991
|
+
return candidates.reduce<GapCandidate | null>(
|
|
992
|
+
(best, candidate) =>
|
|
993
|
+
candidate.side === side && (!best || candidate.gap < best.gap)
|
|
994
|
+
? candidate
|
|
995
|
+
: best,
|
|
814
996
|
null,
|
|
815
997
|
);
|
|
998
|
+
}
|
|
816
999
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
bands: [
|
|
826
|
-
{
|
|
827
|
-
gapStart: closestBefore.gapStart,
|
|
828
|
-
gapEnd: closestBefore.gapEnd,
|
|
829
|
-
crossStart: closestBefore.crossStart,
|
|
830
|
-
crossEnd: closestBefore.crossEnd,
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
gapStart: closestAfter.gapStart,
|
|
834
|
-
gapEnd: closestAfter.gapEnd,
|
|
835
|
-
crossStart: closestAfter.crossStart,
|
|
836
|
-
crossEnd: closestAfter.crossEnd,
|
|
837
|
-
},
|
|
838
|
-
],
|
|
839
|
-
});
|
|
840
|
-
}
|
|
1000
|
+
function gapCandidateBand(candidate: GapCandidate): DistanceGuideBand {
|
|
1001
|
+
return {
|
|
1002
|
+
gapStart: candidate.gapStart,
|
|
1003
|
+
gapEnd: candidate.gapEnd,
|
|
1004
|
+
crossStart: candidate.crossStart,
|
|
1005
|
+
crossEnd: candidate.crossEnd,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
841
1008
|
|
|
842
|
-
|
|
1009
|
+
function buildEqualGapPair(
|
|
1010
|
+
axis: "x" | "y",
|
|
1011
|
+
movingBounds: FrameBounds,
|
|
1012
|
+
stationary: FrameBounds[],
|
|
1013
|
+
toleranceCanvasPx: number,
|
|
1014
|
+
): EqualGapGuide[] {
|
|
1015
|
+
const candidates = collectAxisGapCandidates(axis, movingBounds, stationary);
|
|
1016
|
+
const before = closestGapCandidate(candidates, "before");
|
|
1017
|
+
const after = closestGapCandidate(candidates, "after");
|
|
1018
|
+
if (!before || !after) return [];
|
|
1019
|
+
if (Math.abs(before.gap - after.gap) > toleranceCanvasPx) return [];
|
|
1020
|
+
return [
|
|
1021
|
+
{
|
|
1022
|
+
orientation: axis === "x" ? "vertical" : "horizontal",
|
|
1023
|
+
gap: (before.gap + after.gap) / 2,
|
|
1024
|
+
bands: [gapCandidateBand(before), gapCandidateBand(after)],
|
|
1025
|
+
},
|
|
1026
|
+
];
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** Every gap already in the row/column that matches `gap`. Figma and tldraw
|
|
1030
|
+
* both light the whole run, not only the pair the snap landed on. */
|
|
1031
|
+
function matchingRhythmBands(
|
|
1032
|
+
rhythms: { gap: number; band: DistanceGuideBand }[],
|
|
1033
|
+
gap: number,
|
|
1034
|
+
tolerance: number,
|
|
1035
|
+
): DistanceGuideBand[] {
|
|
1036
|
+
return rhythms
|
|
1037
|
+
.filter((rhythm) => Math.abs(rhythm.gap - gap) <= tolerance)
|
|
1038
|
+
.map((rhythm) => rhythm.band);
|
|
843
1039
|
}
|
|
844
1040
|
|
|
845
1041
|
export function resizeFrameFromDelta(
|
|
@@ -2001,54 +2197,264 @@ function getCanvasSnapThreshold({
|
|
|
2001
2197
|
return thresholdScreenPx / scale;
|
|
2002
2198
|
}
|
|
2003
2199
|
|
|
2004
|
-
|
|
2200
|
+
const SNAP_ALIGN_EPSILON = 1e-6;
|
|
2201
|
+
/** Screen px. Every other tolerance in this module is screen-constant via
|
|
2202
|
+
* getCanvasSnapThreshold; a fixed canvas-px value would make the equal-gap
|
|
2203
|
+
* claim 16x stricter at 25% zoom than at 400%. */
|
|
2204
|
+
const SPACING_MATCH_SCREEN_PX = 0.5;
|
|
2205
|
+
|
|
2206
|
+
function getAxisSnapValues(bounds: FrameBounds, axis: "x" | "y") {
|
|
2207
|
+
return axis === "x"
|
|
2208
|
+
? [bounds.left, bounds.centerX, bounds.right]
|
|
2209
|
+
: [bounds.top, bounds.centerY, bounds.bottom];
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function getAxisStart(bounds: FrameBounds, axis: "x" | "y") {
|
|
2213
|
+
return axis === "x" ? bounds.left : bounds.top;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function getAxisEnd(bounds: FrameBounds, axis: "x" | "y") {
|
|
2217
|
+
return axis === "x" ? bounds.right : bounds.bottom;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function getCrossStart(bounds: FrameBounds, axis: "x" | "y") {
|
|
2221
|
+
return axis === "x" ? bounds.top : bounds.left;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function getCrossEnd(bounds: FrameBounds, axis: "x" | "y") {
|
|
2225
|
+
return axis === "x" ? bounds.bottom : bounds.right;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function crossAxisOverlaps(
|
|
2005
2229
|
axis: "x" | "y",
|
|
2006
|
-
|
|
2007
|
-
|
|
2230
|
+
a: FrameBounds,
|
|
2231
|
+
b: FrameBounds,
|
|
2232
|
+
): boolean {
|
|
2233
|
+
return (
|
|
2234
|
+
getCrossStart(a, axis) < getCrossEnd(b, axis) &&
|
|
2235
|
+
getCrossEnd(a, axis) > getCrossStart(b, axis)
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function translateBounds(
|
|
2240
|
+
bounds: FrameBounds,
|
|
2241
|
+
dx: number,
|
|
2242
|
+
dy: number,
|
|
2243
|
+
): FrameBounds {
|
|
2244
|
+
return {
|
|
2245
|
+
left: bounds.left + dx,
|
|
2246
|
+
right: bounds.right + dx,
|
|
2247
|
+
centerX: bounds.centerX + dx,
|
|
2248
|
+
top: bounds.top + dy,
|
|
2249
|
+
bottom: bounds.bottom + dy,
|
|
2250
|
+
centerY: bounds.centerY + dy,
|
|
2251
|
+
width: bounds.width,
|
|
2252
|
+
height: bounds.height,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function findAxisSnapOffset(
|
|
2257
|
+
axis: "x" | "y",
|
|
2258
|
+
moving: FrameBounds[],
|
|
2259
|
+
stationary: FrameBounds[],
|
|
2008
2260
|
threshold: number,
|
|
2009
|
-
):
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2261
|
+
): number | null {
|
|
2262
|
+
let offset: number | null = null;
|
|
2263
|
+
let bestDistance = Infinity;
|
|
2264
|
+
for (const movingBounds of moving) {
|
|
2265
|
+
for (const movingValue of getAxisSnapValues(movingBounds, axis)) {
|
|
2266
|
+
for (const stationaryBounds of stationary) {
|
|
2267
|
+
for (const stationaryValue of getAxisSnapValues(
|
|
2268
|
+
stationaryBounds,
|
|
2269
|
+
axis,
|
|
2270
|
+
)) {
|
|
2271
|
+
const candidate = stationaryValue - movingValue;
|
|
2272
|
+
const distance = Math.abs(candidate);
|
|
2273
|
+
if (distance > threshold || distance >= bestDistance) continue;
|
|
2274
|
+
bestDistance = distance;
|
|
2275
|
+
offset = candidate;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return offset;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/** Every guide line the snapped position actually sits on, not just the one
|
|
2284
|
+
* that won the snap: three frames sharing a left edge draw one line through
|
|
2285
|
+
* all three, and an edge match plus a center match on the same axis draw
|
|
2286
|
+
* both, matching Figma. Call with post-snap bounds — the line's extent is
|
|
2287
|
+
* the union of the frames it connects. */
|
|
2288
|
+
function buildAxisGuides(
|
|
2289
|
+
axis: "x" | "y",
|
|
2290
|
+
moving: FrameBounds[],
|
|
2291
|
+
stationary: FrameBounds[],
|
|
2292
|
+
): AlignmentGuide[] {
|
|
2293
|
+
const spans = new Map<number, { start: number; end: number }>();
|
|
2294
|
+
for (const movingBounds of moving) {
|
|
2295
|
+
for (const movingValue of getAxisSnapValues(movingBounds, axis)) {
|
|
2296
|
+
for (const stationaryBounds of stationary) {
|
|
2297
|
+
for (const stationaryValue of getAxisSnapValues(
|
|
2298
|
+
stationaryBounds,
|
|
2299
|
+
axis,
|
|
2300
|
+
)) {
|
|
2301
|
+
if (Math.abs(stationaryValue - movingValue) > SNAP_ALIGN_EPSILON) {
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const existing = spans.get(stationaryValue);
|
|
2305
|
+
const start = Math.min(
|
|
2306
|
+
existing?.start ?? Infinity,
|
|
2307
|
+
getCrossStart(movingBounds, axis),
|
|
2308
|
+
getCrossStart(stationaryBounds, axis),
|
|
2309
|
+
);
|
|
2310
|
+
const end = Math.max(
|
|
2311
|
+
existing?.end ?? -Infinity,
|
|
2312
|
+
getCrossEnd(movingBounds, axis),
|
|
2313
|
+
getCrossEnd(stationaryBounds, axis),
|
|
2314
|
+
);
|
|
2315
|
+
spans.set(stationaryValue, { start, end });
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const orientation = axis === "x" ? "vertical" : "horizontal";
|
|
2322
|
+
return Array.from(spans, ([position, span]) => ({
|
|
2323
|
+
orientation,
|
|
2324
|
+
position,
|
|
2325
|
+
start: span.start,
|
|
2326
|
+
end: span.end,
|
|
2327
|
+
})) as AlignmentGuide[];
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** Gaps that already exist between two side-by-side stationary frames in the
|
|
2331
|
+
* moving frame's own row/column — the rhythm a Figma spacing snap matches. */
|
|
2332
|
+
function collectRhythmGaps(
|
|
2333
|
+
axis: "x" | "y",
|
|
2334
|
+
movingBounds: FrameBounds,
|
|
2335
|
+
stationary: FrameBounds[],
|
|
2336
|
+
): { gap: number; band: DistanceGuideBand }[] {
|
|
2337
|
+
const row = stationary
|
|
2338
|
+
.filter(
|
|
2339
|
+
(bounds) =>
|
|
2340
|
+
crossAxisOverlaps(axis, bounds, movingBounds) &&
|
|
2341
|
+
// A frame that spans the whole moving frame — a parent content box,
|
|
2342
|
+
// a backdrop — is a wrapper, not a neighbour in the rhythm, and its
|
|
2343
|
+
// span would swallow every real gap in the row.
|
|
2344
|
+
!(
|
|
2345
|
+
getAxisStart(bounds, axis) <= getAxisStart(movingBounds, axis) &&
|
|
2346
|
+
getAxisEnd(bounds, axis) >= getAxisEnd(movingBounds, axis)
|
|
2347
|
+
),
|
|
2348
|
+
)
|
|
2349
|
+
.sort((a, b) => getAxisStart(a, axis) - getAxisStart(b, axis));
|
|
2350
|
+
|
|
2351
|
+
const gaps: { gap: number; band: DistanceGuideBand }[] = [];
|
|
2352
|
+
let previous: FrameBounds | null = null;
|
|
2353
|
+
for (const bounds of row) {
|
|
2354
|
+
const gap = previous
|
|
2355
|
+
? getAxisStart(bounds, axis) - getAxisEnd(previous, axis)
|
|
2356
|
+
: 0;
|
|
2357
|
+
if (previous && gap > 0 && crossAxisOverlaps(axis, previous, bounds)) {
|
|
2358
|
+
gaps.push({
|
|
2359
|
+
gap,
|
|
2360
|
+
band: {
|
|
2361
|
+
gapStart: getAxisEnd(previous, axis),
|
|
2362
|
+
gapEnd: getAxisStart(bounds, axis),
|
|
2363
|
+
crossStart: Math.max(
|
|
2364
|
+
getCrossStart(previous, axis),
|
|
2365
|
+
getCrossStart(bounds, axis),
|
|
2366
|
+
),
|
|
2367
|
+
crossEnd: Math.min(
|
|
2368
|
+
getCrossEnd(previous, axis),
|
|
2369
|
+
getCrossEnd(bounds, axis),
|
|
2370
|
+
),
|
|
2371
|
+
},
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
if (!previous || getAxisEnd(bounds, axis) > getAxisEnd(previous, axis)) {
|
|
2375
|
+
previous = bounds;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return gaps;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/** The offset plus which neighbour it was measured against, so the guide is
|
|
2382
|
+
* drawn on the side the snap actually used. Picking a side independently
|
|
2383
|
+
* builds chrome for a gap that was never matched. */
|
|
2384
|
+
function findSpacingSnapOffset(
|
|
2385
|
+
axis: "x" | "y",
|
|
2386
|
+
movingBounds: FrameBounds,
|
|
2387
|
+
stationary: FrameBounds[],
|
|
2388
|
+
tolerance: number,
|
|
2389
|
+
): { offset: number; side: "before" | "after" | "both" | null } {
|
|
2390
|
+
const candidates = collectAxisGapCandidates(axis, movingBounds, stationary);
|
|
2391
|
+
const before = closestGapCandidate(candidates, "before");
|
|
2392
|
+
const after = closestGapCandidate(candidates, "after");
|
|
2393
|
+
if (!before && !after) return { offset: 0, side: null };
|
|
2394
|
+
|
|
2395
|
+
let offset = 0;
|
|
2396
|
+
let side: "before" | "after" | "both" | null = null;
|
|
2397
|
+
let bestDistance = Infinity;
|
|
2398
|
+
const consider = (value: number, matched: "before" | "after" | "both") => {
|
|
2399
|
+
const distance = Math.abs(value);
|
|
2400
|
+
if (distance > tolerance || distance >= bestDistance) return;
|
|
2401
|
+
bestDistance = distance;
|
|
2402
|
+
offset = value;
|
|
2403
|
+
side = matched;
|
|
2404
|
+
};
|
|
2405
|
+
|
|
2406
|
+
if (before && after) consider((after.gap - before.gap) / 2, "both");
|
|
2407
|
+
for (const rhythm of collectRhythmGaps(axis, movingBounds, stationary)) {
|
|
2408
|
+
if (before) consider(rhythm.gap - before.gap, "before");
|
|
2409
|
+
if (after) consider(after.gap - rhythm.gap, "after");
|
|
2410
|
+
}
|
|
2411
|
+
return { offset, side };
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/** The pair of gap bands to draw once `findSpacingSnapOffset` has landed:
|
|
2415
|
+
* the two equal gaps around the frame, or the frame's gap plus the existing
|
|
2416
|
+
* gap it matched. Call with post-snap bounds. */
|
|
2417
|
+
function buildSpacingGuides(
|
|
2418
|
+
axis: "x" | "y",
|
|
2419
|
+
movingBounds: FrameBounds,
|
|
2420
|
+
stationary: FrameBounds[],
|
|
2421
|
+
tolerance: number,
|
|
2422
|
+
matchedSide?: "before" | "after" | "both" | null,
|
|
2423
|
+
): EqualGapGuide[] {
|
|
2424
|
+
const rhythms = collectRhythmGaps(axis, movingBounds, stationary);
|
|
2425
|
+
const pair = buildEqualGapPair(axis, movingBounds, stationary, tolerance);
|
|
2426
|
+
if (pair.length) {
|
|
2427
|
+
const guide = pair[0];
|
|
2428
|
+
return [
|
|
2429
|
+
{
|
|
2430
|
+
...guide,
|
|
2431
|
+
bands: [
|
|
2432
|
+
...guide.bands,
|
|
2433
|
+
...matchingRhythmBands(rhythms, guide.gap, tolerance),
|
|
2434
|
+
],
|
|
2435
|
+
},
|
|
2436
|
+
];
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const candidates = collectAxisGapCandidates(axis, movingBounds, stationary);
|
|
2440
|
+
const neighbor =
|
|
2441
|
+
matchedSide === "after"
|
|
2442
|
+
? closestGapCandidate(candidates, "after")
|
|
2443
|
+
: matchedSide === "before"
|
|
2444
|
+
? closestGapCandidate(candidates, "before")
|
|
2445
|
+
: (closestGapCandidate(candidates, "before") ??
|
|
2446
|
+
closestGapCandidate(candidates, "after"));
|
|
2447
|
+
if (!neighbor) return [];
|
|
2448
|
+
|
|
2449
|
+
const matched = matchingRhythmBands(rhythms, neighbor.gap, tolerance);
|
|
2450
|
+
if (!matched.length) return [];
|
|
2451
|
+
return [
|
|
2452
|
+
{
|
|
2453
|
+
orientation: axis === "x" ? "vertical" : "horizontal",
|
|
2454
|
+
gap: neighbor.gap,
|
|
2455
|
+
bands: [gapCandidateBand(neighbor), ...matched],
|
|
2456
|
+
},
|
|
2457
|
+
];
|
|
2052
2458
|
}
|
|
2053
2459
|
|
|
2054
2460
|
function getBestCandidate(
|