@agent-native/core 0.92.6 → 0.92.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builtin.ts +1 -1
- package/corpus/core/src/agent/model-config.ts +30 -30
- package/corpus/core/src/agent/production-agent.ts +129 -7
- package/corpus/core/src/cli/design-connect.ts +438 -83
- package/corpus/core/src/cli/recap.ts +2 -2
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +8 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
- package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
- package/corpus/core/src/client/index.ts +4 -0
- package/corpus/core/src/client/org/TeamPage.tsx +41 -26
- package/corpus/core/src/client/use-action.ts +113 -1
- package/corpus/core/src/db/client.ts +12 -2
- package/corpus/core/src/db/create-get-db.ts +4 -1
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
- package/corpus/core/src/server/transcribe-voice.ts +1 -1
- package/corpus/core/src/usage/store.ts +13 -4
- package/corpus/core/src/vite/client.ts +38 -1
- package/corpus/templates/analytics/AGENTS.md +3 -0
- package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
- package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
- package/corpus/templates/analytics/app/global.css +11 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
- package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
- package/corpus/templates/analytics/server/db/index.ts +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
- package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
- package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/content/app/i18n-data.ts +28 -0
- package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
- package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
- package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
- package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
- package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
- package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
- package/corpus/templates/design/actions/capture-design-state.ts +2 -2
- package/corpus/templates/design/actions/connect-localhost.ts +58 -12
- package/corpus/templates/design/actions/create-component.ts +2 -18
- package/corpus/templates/design/actions/create-design-branch.ts +38 -18
- package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
- package/corpus/templates/design/actions/delete-file.ts +19 -34
- package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
- package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
- package/corpus/templates/design/actions/export-pdf.ts +2 -1
- package/corpus/templates/design/actions/export-zip.ts +9 -3
- package/corpus/templates/design/actions/generate-design.ts +107 -70
- package/corpus/templates/design/actions/get-component-details.ts +2 -18
- package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
- package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
- package/corpus/templates/design/actions/get-design.ts +2 -1
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
- package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
- package/corpus/templates/design/actions/index-components.ts +2 -18
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
- package/corpus/templates/design/actions/list-local-files.ts +9 -2
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
- package/corpus/templates/design/actions/open-component-source.ts +2 -18
- package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
- package/corpus/templates/design/actions/present-design-variants.ts +70 -70
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
- package/corpus/templates/design/actions/read-local-file.ts +9 -2
- package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
- package/corpus/templates/design/actions/rename-screen.ts +425 -0
- package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
- package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
- package/corpus/templates/design/actions/update-design.ts +376 -61
- package/corpus/templates/design/actions/update-file.ts +455 -188
- package/corpus/templates/design/actions/write-local-file.ts +10 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
- package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
- package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
- package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
- package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
- package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
- package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
- package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
- package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
- package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
- package/corpus/templates/design/app/components/design/index.ts +2 -2
- package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
- package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
- package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
- package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
- package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
- package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
- package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
- package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
- package/corpus/templates/design/app/i18n-data.ts +223 -0
- package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
- package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
- package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
- package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
- package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
- package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
- package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
- package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
- package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
- package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
- package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
- package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
- package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
- package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
- package/corpus/templates/design/server/db/index.ts +2 -52
- package/corpus/templates/design/server/db/schema.ts +16 -0
- package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
- package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
- package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
- package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
- package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
- package/corpus/templates/design/server/plugins/db.ts +19 -0
- package/corpus/templates/design/server/source-workspace.ts +12 -13
- package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
- package/corpus/templates/design/shared/screen-rename.ts +42 -0
- package/corpus/templates/design/shared/source-mode.ts +84 -4
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builtin.js +1 -1
- package/dist/agent/engine/builtin.js.map +1 -1
- package/dist/agent/model-config.d.ts +9 -9
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +30 -30
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +60 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +106 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +30 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +359 -75
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/recap.js +2 -2
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +1 -1
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +10 -5
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/composer/TiptapComposer.js +1 -1
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +6 -5
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/use-action.d.ts +32 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +68 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -2
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.js +1 -1
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +13 -4
- package/dist/usage/store.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +36 -1
- package/dist/vite/client.js.map +1 -1
- package/docs/content/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/docs/content/pr-visual-recap.mdx +3 -3
- package/docs/content/pure-agent-apps.mdx +22 -0
- package/package.json +2 -2
|
@@ -5,8 +5,21 @@ import {
|
|
|
5
5
|
getDraftGeometryFromPoints,
|
|
6
6
|
} from "@shared/canvas-math";
|
|
7
7
|
import { ensureCodeLayerNodeIdsInHtml } from "@shared/code-layer";
|
|
8
|
+
import {
|
|
9
|
+
appendPenNode,
|
|
10
|
+
clonePenPath,
|
|
11
|
+
closePenPath,
|
|
12
|
+
constrainPointTo45Degrees,
|
|
13
|
+
createCornerNode,
|
|
14
|
+
createSmoothNode,
|
|
15
|
+
isPenCloseTarget,
|
|
16
|
+
serializePenPath,
|
|
17
|
+
type PenPath,
|
|
18
|
+
type PenPoint,
|
|
19
|
+
} from "@shared/pen-path";
|
|
8
20
|
import { IconPlugConnectedX, IconRefresh } from "@tabler/icons-react";
|
|
9
21
|
import { useRef, useEffect, useCallback, useMemo, useState } from "react";
|
|
22
|
+
import { toast } from "sonner";
|
|
10
23
|
|
|
11
24
|
import { Button } from "@/components/ui/button";
|
|
12
25
|
// NOTE: This wires up the NEW shared visual-editor DrawOverlay + comment-pin
|
|
@@ -23,7 +36,6 @@ import { cn } from "@/lib/utils";
|
|
|
23
36
|
|
|
24
37
|
import { editorChromeBridgeScript } from "../../../.generated/bridge/editor-chrome.generated";
|
|
25
38
|
import { embeddedWheelBridgeScript } from "../../../.generated/bridge/embedded-wheel.generated";
|
|
26
|
-
import { hitTestBridgeScript } from "../../../.generated/bridge/hit-test.generated";
|
|
27
39
|
import { motionPreviewBridgeScript } from "../../../.generated/bridge/motion-preview.generated";
|
|
28
40
|
import { navBridgeScript } from "../../../.generated/bridge/nav.generated";
|
|
29
41
|
import { shaderFillPreviewBridgeScript } from "../../../.generated/bridge/shader-fill-preview.generated";
|
|
@@ -31,7 +43,50 @@ import { shaderRuntimeBridgeScript } from "../../../.generated/bridge/shader-run
|
|
|
31
43
|
import { tweakBridgeScript } from "../../../.generated/bridge/tweak.generated";
|
|
32
44
|
import { zoomBridgeScript } from "../../../.generated/bridge/zoom.generated";
|
|
33
45
|
import { isTrustedCanvasBridgeMessage } from "./bridge-security";
|
|
46
|
+
import { submitDesignAnnotations } from "./design-canvas/annotation-submit";
|
|
47
|
+
import {
|
|
48
|
+
getScreenContentPointFromClient,
|
|
49
|
+
getZoomToCursorScrollDelta,
|
|
50
|
+
} from "./design-canvas/coordinate-transforms";
|
|
51
|
+
import {
|
|
52
|
+
collapseDoubleClickPenAnchor,
|
|
53
|
+
type CreatePrimitiveSpec,
|
|
54
|
+
type CreationTool,
|
|
55
|
+
} from "./design-canvas/creation";
|
|
56
|
+
import { isElementInfoPayload } from "./design-canvas/element-payload";
|
|
57
|
+
import {
|
|
58
|
+
embeddedContentOffsetStyle,
|
|
59
|
+
getEmbeddedFrameBackgroundStyle,
|
|
60
|
+
getEmbeddedFrameDocumentContent,
|
|
61
|
+
getEmbeddedIframeBackgroundColor,
|
|
62
|
+
} from "./design-canvas/embedded-frame";
|
|
63
|
+
import {
|
|
64
|
+
getDesignCanvasIframeSandbox,
|
|
65
|
+
getSnapshotRetryDelayMs,
|
|
66
|
+
resolveLiveEditPreviewUrl,
|
|
67
|
+
sanitizeLocalhostSourceSnapshotHtml,
|
|
68
|
+
shouldFetchExternalSourceSnapshot,
|
|
69
|
+
shouldUseIframeLoadReadyFallback,
|
|
70
|
+
} from "./design-canvas/external-preview";
|
|
71
|
+
import { isOsFileDragEvent } from "./design-canvas/file-drop";
|
|
72
|
+
import { LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT } from "./design-canvas/hit-test";
|
|
73
|
+
import type {
|
|
74
|
+
IframeContextMenuPayload,
|
|
75
|
+
IframeFigmaClipboardPastePayload,
|
|
76
|
+
IframeHotkeyPayload,
|
|
77
|
+
} from "./design-canvas/iframe-events";
|
|
78
|
+
import {
|
|
79
|
+
forwardEmbeddedCanvasPanMessage,
|
|
80
|
+
type EmbeddedCanvasPanSession,
|
|
81
|
+
} from "./design-canvas/iframe-pan";
|
|
82
|
+
import type { MotionTrackWire } from "./design-canvas/motion-types";
|
|
83
|
+
import {
|
|
84
|
+
PENDING_TEXT_EDIT_TIMEOUT_MS,
|
|
85
|
+
routePendingTextEditKey,
|
|
86
|
+
schedulePendingTextEditActivation,
|
|
87
|
+
} from "./design-canvas/pending-text-edit";
|
|
34
88
|
import { DeviceFrame } from "./DeviceFrame";
|
|
89
|
+
import { shapeClosingHandles } from "./multi-screen/draft-primitives";
|
|
35
90
|
import type {
|
|
36
91
|
ElementInfo,
|
|
37
92
|
ElementSelectionIntent,
|
|
@@ -77,25 +132,6 @@ function isAllowedFusionOrigin(
|
|
|
77
132
|
return host === "builder.io" || host.endsWith(".builder.io");
|
|
78
133
|
}
|
|
79
134
|
|
|
80
|
-
/**
|
|
81
|
-
* Wire shape for a single motion track sent via the `motion-load-tracks`
|
|
82
|
-
* postMessage. Matches the serialisable subset of `MotionTrack` from
|
|
83
|
-
* `shared/motion-timeline.ts` without requiring an import at the UI layer.
|
|
84
|
-
*/
|
|
85
|
-
export interface MotionTrackWire {
|
|
86
|
-
targetNodeId: string;
|
|
87
|
-
property: string;
|
|
88
|
-
/**
|
|
89
|
-
* Keyframes normalised to the TRACK's own span; `ease` accepts CSS timing
|
|
90
|
-
* functions plus `spring(bounce[, settle])` tokens and `linear(...)` lists.
|
|
91
|
-
*/
|
|
92
|
-
keyframes: Array<{ t: number; value: string; ease?: string }>;
|
|
93
|
-
/** Track start offset within the timeline, ms (staggering). */
|
|
94
|
-
delayMs?: number;
|
|
95
|
-
/** Per-track animation span, ms. Omitted = whole timeline. */
|
|
96
|
-
durationMs?: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
135
|
/**
|
|
100
136
|
* Motion-preview bridge. Injected alongside the other bridge scripts so the
|
|
101
137
|
* MotionDock's scrubbing preview works in ALL editor modes without writing
|
|
@@ -192,59 +228,6 @@ ${navBridgeScript}
|
|
|
192
228
|
</script>
|
|
193
229
|
`;
|
|
194
230
|
|
|
195
|
-
/**
|
|
196
|
-
* Lightweight hit-test bridge: injected into every screen srcdoc iframe so
|
|
197
|
-
* MultiScreenCanvas can ask "what container is under this point?" during a
|
|
198
|
-
* cross-screen drag without needing to synthesise DOM events.
|
|
199
|
-
*
|
|
200
|
-
* Protocol (parent → iframe via postMessage):
|
|
201
|
-
* { type: 'agent-native:hit-test', correlationId: string, x: number, y: number }
|
|
202
|
-
* where x/y are in this iframe's viewport coordinate space.
|
|
203
|
-
* When preview is true, the iframe also renders its local insertion guide.
|
|
204
|
-
* { type: 'agent-native:hit-test-preview-clear' } hides that guide.
|
|
205
|
-
*
|
|
206
|
-
* Reply (iframe → window.parent):
|
|
207
|
-
* { type: 'agent-native:hit-test-result', correlationId: string,
|
|
208
|
-
* anchorNodeId: string, placement: 'before'|'after'|'inside',
|
|
209
|
-
* axis: 'x'|'y',
|
|
210
|
-
* anchorRect: { left: number, top: number, width: number, height: number } }
|
|
211
|
-
*
|
|
212
|
-
* Reads DOM only — no mutations, no event interception. The container-drop and
|
|
213
|
-
* placement logic is intentionally kept in sync with the corresponding helpers
|
|
214
|
-
* inside editor-chrome.bridge.ts (search for "// keep in sync with
|
|
215
|
-
* hit-test.bridge.ts" comments there).
|
|
216
|
-
*
|
|
217
|
-
* Source: app/components/design/bridge/hit-test.bridge.ts
|
|
218
|
-
* Compiled: .generated/bridge/hit-test.generated.ts (run bridge/codegen.ts to update)
|
|
219
|
-
*/
|
|
220
|
-
export const LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT = `
|
|
221
|
-
<script data-agent-native-hit-test-bridge>
|
|
222
|
-
${hitTestBridgeScript}
|
|
223
|
-
</script>
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Append the LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT into a complete HTML string.
|
|
228
|
-
* Injects before </body> when present, before </html> as a fallback, or appends
|
|
229
|
-
* at the end. This is the seam MultiScreenCanvas uses to add the hit-test
|
|
230
|
-
* responder to each screen srcdoc without rebuilding the entire document.
|
|
231
|
-
*/
|
|
232
|
-
export function appendHitTestResponder(html: string): string {
|
|
233
|
-
if (html.includes("</body>")) {
|
|
234
|
-
return html.replace(
|
|
235
|
-
"</body>", // i18n-ignore generated iframe HTML marker
|
|
236
|
-
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT + "</body>", // i18n-ignore generated iframe HTML injection
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
if (html.includes("</html>")) {
|
|
240
|
-
return html.replace(
|
|
241
|
-
"</html>", // i18n-ignore generated iframe HTML marker
|
|
242
|
-
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT + "</html>", // i18n-ignore generated iframe HTML injection
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
return html + LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
231
|
const EDITOR_BRIDGE_VAR_NAMES = [
|
|
249
232
|
"--design-editor-accent-color",
|
|
250
233
|
"--design-editor-accent-hover-color",
|
|
@@ -305,8 +288,10 @@ interface DesignCanvasProps {
|
|
|
305
288
|
/**
|
|
306
289
|
* The runtime source tier for this canvas.
|
|
307
290
|
*
|
|
308
|
-
* - `"inline"` (default) — HTML/Alpine `srcdoc` iframe;
|
|
309
|
-
*
|
|
291
|
+
* - `"inline"` (default) — HTML/Alpine `srcdoc` iframe; all bridge scripts
|
|
292
|
+
* injected by DesignCanvas. Read-only frames get an opaque origin;
|
|
293
|
+
* editable frames temporarily retain the parent origin for the live-DOM
|
|
294
|
+
* editor features documented by getDesignCanvasIframeSandbox.
|
|
310
295
|
* - `"localhost"` — `src=devServerUrl`; dev server is same-origin in most
|
|
311
296
|
* setups; bridge trust: origin must match parent or be "null".
|
|
312
297
|
* - `"fusion"` — `src=builderHostedUrl`; cross-origin Builder-hosted app;
|
|
@@ -351,7 +336,9 @@ interface DesignCanvasProps {
|
|
|
351
336
|
* For `"inline"` and `"localhost"` sources this prop is ignored.
|
|
352
337
|
*/
|
|
353
338
|
fusionUrl?: string;
|
|
354
|
-
|
|
339
|
+
/** Read-only localhost bridge credential. Filesystem write tokens never enter
|
|
340
|
+
* this browser component. */
|
|
341
|
+
previewToken?: string;
|
|
355
342
|
zoom: number;
|
|
356
343
|
onZoomChange?: (zoom: number) => void;
|
|
357
344
|
deviceFrame: DeviceFrameType;
|
|
@@ -680,138 +667,6 @@ interface DesignCanvasProps {
|
|
|
680
667
|
spacePanActive?: boolean;
|
|
681
668
|
}
|
|
682
669
|
|
|
683
|
-
/** Creation tools supported by the single-screen click-to-place overlay. */
|
|
684
|
-
export type CreationTool =
|
|
685
|
-
| "rectangle"
|
|
686
|
-
| "ellipse"
|
|
687
|
-
| "line"
|
|
688
|
-
| "arrow"
|
|
689
|
-
| "text"
|
|
690
|
-
| "pen"
|
|
691
|
-
// Figma parity: the frame tool (F/A) used inside a focused screen places a
|
|
692
|
-
// bare container <div> (a nested "frame") instead of forcing a jump out to
|
|
693
|
-
// overview to draw a new screen.
|
|
694
|
-
| "frame";
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Geometry payload emitted by the single-screen creation overlay. Coordinates
|
|
698
|
-
* are always in screen-content space (see `activeCreationTool` doc above).
|
|
699
|
-
*
|
|
700
|
-
* - `rect` is present for rectangle/ellipse/text (a click uses a sensible
|
|
701
|
-
* default size; a drag uses the dragged bounds via
|
|
702
|
-
* `getDraftGeometryFromPoints`, matching MultiScreenCanvas).
|
|
703
|
-
* - `points` is present for line/arrow (always exactly 2 points) and for a
|
|
704
|
-
* pen click (a single start point — see the deferred multi-click pen note
|
|
705
|
-
* on the overlay component itself).
|
|
706
|
-
* - `fromClick` distinguishes a negligible-movement click (true) from a
|
|
707
|
-
* drag past the movement threshold (false), mirroring MultiScreenCanvas's
|
|
708
|
-
* own click-vs-drag draft creation semantics.
|
|
709
|
-
*/
|
|
710
|
-
export interface CreatePrimitiveSpec {
|
|
711
|
-
tool: CreationTool;
|
|
712
|
-
rect?: { x: number; y: number; width: number; height: number };
|
|
713
|
-
points?: Array<{ x: number; y: number }>;
|
|
714
|
-
fromClick: boolean;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Converts a pointer position in viewport (client) coordinates into
|
|
719
|
-
* screen-content coordinates — the screen's own pixel coordinate system, as
|
|
720
|
-
* used by `getDraftGeometryFromPoints` / `draftPrimitiveToInsert` in
|
|
721
|
-
* MultiScreenCanvas.
|
|
722
|
-
*
|
|
723
|
-
* The iframe's rendered rect (`getBoundingClientRect()`) already reflects any
|
|
724
|
-
* outer zoom `transform: scale(...)` wrapper, while `iframe.clientWidth` /
|
|
725
|
-
* `iframe.clientHeight` are the raw (unscaled) layout size that equals the
|
|
726
|
-
* screen's own content pixel dimensions. Dividing out that ratio undoes the
|
|
727
|
-
* zoom scale, matching the scaleX/scaleY convention already used by the
|
|
728
|
-
* `element-contextmenu` bridge handler below.
|
|
729
|
-
*/
|
|
730
|
-
export function getScreenContentPointFromClient(
|
|
731
|
-
clientX: number,
|
|
732
|
-
clientY: number,
|
|
733
|
-
iframeRect: { left: number; top: number; width: number; height: number },
|
|
734
|
-
iframeContentSize: { width: number; height: number },
|
|
735
|
-
): { x: number; y: number } {
|
|
736
|
-
const scaleX =
|
|
737
|
-
iframeContentSize.width > 0
|
|
738
|
-
? iframeRect.width / iframeContentSize.width
|
|
739
|
-
: 1;
|
|
740
|
-
const scaleY =
|
|
741
|
-
iframeContentSize.height > 0
|
|
742
|
-
? iframeRect.height / iframeContentSize.height
|
|
743
|
-
: 1;
|
|
744
|
-
return {
|
|
745
|
-
x: (clientX - iframeRect.left) / (scaleX || 1),
|
|
746
|
-
y: (clientY - iframeRect.top) / (scaleY || 1),
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* Computes the scroll-container delta needed to keep a given viewport
|
|
752
|
-
* (client) point anchored to the same content underneath it while zooming.
|
|
753
|
-
*
|
|
754
|
-
* This is the cursor-anchor math used by the `pinch-zoom-wheel` bridge
|
|
755
|
-
* handler (forwarded from inside the iframe, since wheel events don't bubble
|
|
756
|
-
* out of iframes — see `zoom.bridge.ts`) to mirror `usePinchZoom`'s own
|
|
757
|
-
* zoom-to-cursor behavior for gestures that occur over the iframe itself.
|
|
758
|
-
*
|
|
759
|
-
* IMPORTANT — this math is only correct when the scaled layer's
|
|
760
|
-
* `transform-origin` is `top left` (or equivalent), so that the layer's own
|
|
761
|
-
* top-left corner is a fixed point in scroll-content space regardless of
|
|
762
|
-
* zoom. If the scaled layer is instead centered via `transform-origin:
|
|
763
|
-
* center center` plus flex/grid centering, the *painted* box re-centers
|
|
764
|
-
* around the container's center on every zoom change — the flex-centering
|
|
765
|
-
* repositions the *layout* box so its center lands at the container's
|
|
766
|
-
* center, and `transform-origin: center` then scales around that already-
|
|
767
|
-
* centered point. That is a well-defined anchor too, but it is NOT the one
|
|
768
|
-
* this function (or usePinchZoom) computes, so cursor-following would drift.
|
|
769
|
-
* See the long comment on the "none"-mode zoom layer in the component render
|
|
770
|
-
* for how DesignCanvas keeps this invariant true while still looking
|
|
771
|
-
* centered at rest.
|
|
772
|
-
*
|
|
773
|
-
* @param anchorClient The viewport point to keep stationary (e.g. cursor
|
|
774
|
-
* position converted from iframe-document space to viewport space).
|
|
775
|
-
* @param containerRect The scroll container's `getBoundingClientRect()`.
|
|
776
|
-
* @param scrollOffset The scroll container's current `{ scrollLeft,
|
|
777
|
-
* scrollTop }`.
|
|
778
|
-
* @param zoomRatio `nextZoom / currentZoom` — how much the zoom is about to
|
|
779
|
-
* change by by (>1 zooming in, <1 zooming out).
|
|
780
|
-
*/
|
|
781
|
-
export function getZoomToCursorScrollDelta(
|
|
782
|
-
anchorClient: { x: number; y: number },
|
|
783
|
-
containerRect: { left: number; top: number },
|
|
784
|
-
scrollOffset: { scrollLeft: number; scrollTop: number },
|
|
785
|
-
zoomRatio: number,
|
|
786
|
-
): { dx: number; dy: number } {
|
|
787
|
-
const cx = anchorClient.x - containerRect.left + scrollOffset.scrollLeft;
|
|
788
|
-
const cy = anchorClient.y - containerRect.top + scrollOffset.scrollTop;
|
|
789
|
-
return {
|
|
790
|
-
dx: cx * (zoomRatio - 1),
|
|
791
|
-
dy: cy * (zoomRatio - 1),
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* True when a drag event is carrying native OS files (e.g. dragged in from
|
|
797
|
-
* Finder/Explorer) rather than an internal HTML5 DOM drag (element reorder,
|
|
798
|
-
* the extensions-panel native-asset drag, etc). A real OS file drag always
|
|
799
|
-
* includes the "Files" type in `dataTransfer.types` — reliably present during
|
|
800
|
-
* dragenter/dragover even before drop, when `dataTransfer.files` itself is
|
|
801
|
-
* empty for security reasons — so this is the correct check to gate
|
|
802
|
-
* preventDefault()/overlay logic on, not `files.length`.
|
|
803
|
-
*/
|
|
804
|
-
export function isOsFileDragEvent(event: {
|
|
805
|
-
dataTransfer: { types: readonly string[] | DOMStringList } | null;
|
|
806
|
-
}): boolean {
|
|
807
|
-
const types = event.dataTransfer?.types;
|
|
808
|
-
if (!types) return false;
|
|
809
|
-
for (let i = 0; i < types.length; i += 1) {
|
|
810
|
-
if (types[i] === "Files") return true;
|
|
811
|
-
}
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
670
|
function getExternalPreviewUrl(content: string): string | null {
|
|
816
671
|
const trimmed = content.trim();
|
|
817
672
|
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
@@ -824,60 +679,12 @@ function getExternalPreviewUrl(content: string): string | null {
|
|
|
824
679
|
}
|
|
825
680
|
}
|
|
826
681
|
|
|
827
|
-
function escapeHtmlAttribute(value: string): string {
|
|
828
|
-
return value
|
|
829
|
-
.replace(/&/g, "&")
|
|
830
|
-
.replace(/"/g, """)
|
|
831
|
-
.replace(/</g, "<")
|
|
832
|
-
.replace(/>/g, ">");
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function addSnapshotBaseHref(html: string, href: string): string {
|
|
836
|
-
if (!html.trim()) return html;
|
|
837
|
-
if (/<base\b/i.test(html)) return html;
|
|
838
|
-
const baseTag = `<base href="${escapeHtmlAttribute(href)}">`;
|
|
839
|
-
if (/<head\b[^>]*>/i.test(html)) {
|
|
840
|
-
return html.replace(/<head\b[^>]*>/i, (match) => `${match}${baseTag}`);
|
|
841
|
-
}
|
|
842
|
-
if (/<html\b[^>]*>/i.test(html)) {
|
|
843
|
-
return html.replace(
|
|
844
|
-
/<html\b[^>]*>/i,
|
|
845
|
-
(match) => `${match}<head>${baseTag}</head>`,
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
return `<!DOCTYPE html><html><head>${baseTag}<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${html}</body></html>`;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function prepareStaticSnapshotHtml(html: string, href: string): string {
|
|
852
|
-
const withBase = addSnapshotBaseHref(html, href);
|
|
853
|
-
if (typeof DOMParser === "undefined") {
|
|
854
|
-
return withBase.replace(/<script\b[^>]*>.*?<\/script>/gis, "");
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const document = new DOMParser().parseFromString(withBase, "text/html");
|
|
858
|
-
document.querySelectorAll("script").forEach((script) => script.remove());
|
|
859
|
-
return `<!DOCTYPE html>${document.documentElement.outerHTML}`;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
682
|
function snapshotEndpointUrl(bridgeUrl: string, previewUrl: string): string {
|
|
863
683
|
const endpoint = new URL("/snapshot", bridgeUrl);
|
|
864
684
|
endpoint.searchParams.set("url", previewUrl);
|
|
865
685
|
return endpoint.toString();
|
|
866
686
|
}
|
|
867
687
|
|
|
868
|
-
export function liveEditEndpointUrl(
|
|
869
|
-
bridgeUrl: string,
|
|
870
|
-
previewUrl: string,
|
|
871
|
-
options?: { includeEditorBridge?: boolean },
|
|
872
|
-
): string {
|
|
873
|
-
const endpoint = new URL("/live-edit", bridgeUrl);
|
|
874
|
-
endpoint.searchParams.set("url", previewUrl);
|
|
875
|
-
if (options?.includeEditorBridge === false) {
|
|
876
|
-
endpoint.searchParams.set("bridge", "0");
|
|
877
|
-
}
|
|
878
|
-
return endpoint.toString();
|
|
879
|
-
}
|
|
880
|
-
|
|
881
688
|
function originFromUrl(value: string | undefined): string | null {
|
|
882
689
|
if (!value) return null;
|
|
883
690
|
try {
|
|
@@ -920,215 +727,6 @@ function contentHash(value: string): string {
|
|
|
920
727
|
return `${value.length}:${hash >>> 0}`;
|
|
921
728
|
}
|
|
922
729
|
|
|
923
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
924
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function isFiniteRect(value: unknown): value is ElementInfo["boundingRect"] {
|
|
928
|
-
if (!isPlainRecord(value)) return false;
|
|
929
|
-
return (
|
|
930
|
-
Number.isFinite(value.x) &&
|
|
931
|
-
Number.isFinite(value.y) &&
|
|
932
|
-
Number.isFinite(value.width) &&
|
|
933
|
-
Number.isFinite(value.height)
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
export function isElementInfoPayload(value: unknown): value is ElementInfo {
|
|
938
|
-
if (!isPlainRecord(value)) return false;
|
|
939
|
-
return (
|
|
940
|
-
typeof value.tagName === "string" &&
|
|
941
|
-
Array.isArray(value.classes) &&
|
|
942
|
-
isPlainRecord(value.computedStyles) &&
|
|
943
|
-
isFiniteRect(value.boundingRect) &&
|
|
944
|
-
typeof value.isFlexChild === "boolean" &&
|
|
945
|
-
typeof value.isFlexContainer === "boolean"
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/**
|
|
950
|
-
* Exponential backoff delay (ms) for the localhost bridge snapshot auto-retry
|
|
951
|
-
* loop, keyed by the zero-based retry attempt number (0 = first retry after
|
|
952
|
-
* the initial failed fetch). Doubles each attempt starting at 1.5s, capped at
|
|
953
|
-
* ~15s so a genuinely-down dev server settles into a slow, low-noise poll
|
|
954
|
-
* instead of hammering the bridge every 1.5s forever.
|
|
955
|
-
*/
|
|
956
|
-
const SNAPSHOT_RETRY_BASE_DELAY_MS = 1500;
|
|
957
|
-
const SNAPSHOT_RETRY_MAX_DELAY_MS = 15000;
|
|
958
|
-
export function getSnapshotRetryDelayMs(attempt: number): number {
|
|
959
|
-
const safeAttempt = Number.isFinite(attempt) ? Math.max(0, attempt) : 0;
|
|
960
|
-
const delay = SNAPSHOT_RETRY_BASE_DELAY_MS * 2 ** safeAttempt;
|
|
961
|
-
return Math.min(SNAPSHOT_RETRY_MAX_DELAY_MS, delay);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const TRANSPARENT_EMBEDDED_FRAME_STYLE =
|
|
965
|
-
"<style data-agent-native-transparent-frame>html,body{background:transparent!important;}body{background-color:transparent!important;}</style>";
|
|
966
|
-
|
|
967
|
-
function embeddedFrameBackgroundStyle(background: string | undefined): string {
|
|
968
|
-
const trimmed = background?.trim();
|
|
969
|
-
if (!trimmed || /[;{}<>]/.test(trimmed)) return "";
|
|
970
|
-
return `<style data-agent-native-frame-background>html,body{background:${trimmed}!important;}body{background-color:${trimmed}!important;}</style>`;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
export function getEmbeddedFrameBackgroundStyle(args: {
|
|
974
|
-
embeddedFrameBackground?: string;
|
|
975
|
-
transparentBackground?: boolean;
|
|
976
|
-
}): string {
|
|
977
|
-
return args.transparentBackground
|
|
978
|
-
? TRANSPARENT_EMBEDDED_FRAME_STYLE
|
|
979
|
-
: embeddedFrameBackgroundStyle(args.embeddedFrameBackground);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
export function getEmbeddedIframeBackgroundColor(args: {
|
|
983
|
-
embeddedFrameBackground?: string;
|
|
984
|
-
transparentBackground?: boolean;
|
|
985
|
-
}): string {
|
|
986
|
-
return args.transparentBackground
|
|
987
|
-
? "transparent"
|
|
988
|
-
: (args.embeddedFrameBackground ?? "transparent");
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* CSS rule that shifts embedded-frame (board) content so board-coordinate
|
|
993
|
-
* (0,0) lands at the center of the board iframe's oversized surface.
|
|
994
|
-
*
|
|
995
|
-
* The selector is deliberately scoped to DIRECT `<body>` children only.
|
|
996
|
-
* `translate` composes per element, so a blanket
|
|
997
|
-
* `[data-agent-native-node-id]{translate:…}` rule shifted every NESTED
|
|
998
|
-
* node-id-bearing descendant by the surface offset AGAIN (+65536px per
|
|
999
|
-
* nesting level) — any board child nested inside another board primitive
|
|
1000
|
-
* rendered tens of thousands of px outside its parent (i.e. visually
|
|
1001
|
-
* vanished) even when its persisted parent-relative left/top were perfectly
|
|
1002
|
-
* correct, and every rect-based coordinate computation on nested board
|
|
1003
|
-
* content was poisoned by the extra offset.
|
|
1004
|
-
*
|
|
1005
|
-
* Scoping (rather than wrapping content in a single offset container) is the
|
|
1006
|
-
* robust fix here because board elements are DOCUMENTED direct `<body>`
|
|
1007
|
-
* children (see shared/board-file.ts module doc + emptyBoardHtml): every
|
|
1008
|
-
* creation path appends at body level (appendCanvasPrimitiveToHtml,
|
|
1009
|
-
* boardObjectEntryToHtmlFragment, moveNodeBetweenDocuments' body-append),
|
|
1010
|
-
* and several consumers key on that structure with `body > …` selectors
|
|
1011
|
-
* (BOARD_SURFACE_RENDER_STYLE) and depth-1 walks
|
|
1012
|
-
* (backfillBoardPrimitiveMarkers, getBoardContentLayerSignature). A wrapper
|
|
1013
|
-
* element would silently break all of those structural contracts.
|
|
1014
|
-
*
|
|
1015
|
-
* With this rule, the offset applies exactly once — to top-level board
|
|
1016
|
-
* elements — and nested children position purely via parent-relative
|
|
1017
|
-
* left/top inside their (absolutely positioned) parent, like any normal
|
|
1018
|
-
* CSS containing-block hierarchy.
|
|
1019
|
-
*/
|
|
1020
|
-
function embeddedContentOffsetStyle(x: number, y: number): string {
|
|
1021
|
-
if (x === 0 && y === 0) return "";
|
|
1022
|
-
return `<style data-agent-native-content-offset>body > [data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function injectEmbeddedFrameStyle(content: string, style: string): string {
|
|
1026
|
-
if (!style) return content;
|
|
1027
|
-
if (/<\/head>/i.test(content)) {
|
|
1028
|
-
return content.replace(/<\/head>/i, `${style}</head>`);
|
|
1029
|
-
}
|
|
1030
|
-
if (/<body\b/i.test(content)) {
|
|
1031
|
-
return content.replace(/<body\b/i, `${style}<body`);
|
|
1032
|
-
}
|
|
1033
|
-
return `${style}${content}`;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
export function getEmbeddedFrameDocumentContent(args: {
|
|
1037
|
-
content: string;
|
|
1038
|
-
embeddedFrameBackground?: string;
|
|
1039
|
-
transparentBackground?: boolean;
|
|
1040
|
-
contentOffsetX?: number;
|
|
1041
|
-
contentOffsetY?: number;
|
|
1042
|
-
}): string {
|
|
1043
|
-
const frameStyle = [
|
|
1044
|
-
getEmbeddedFrameBackgroundStyle({
|
|
1045
|
-
embeddedFrameBackground: args.embeddedFrameBackground,
|
|
1046
|
-
transparentBackground: args.transparentBackground,
|
|
1047
|
-
}),
|
|
1048
|
-
embeddedContentOffsetStyle(
|
|
1049
|
-
args.contentOffsetX ?? 0,
|
|
1050
|
-
args.contentOffsetY ?? 0,
|
|
1051
|
-
),
|
|
1052
|
-
].join("");
|
|
1053
|
-
return injectEmbeddedFrameStyle(args.content, frameStyle);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
export interface IframeHotkeyPayload {
|
|
1057
|
-
key: string;
|
|
1058
|
-
code: string;
|
|
1059
|
-
metaKey: boolean;
|
|
1060
|
-
ctrlKey: boolean;
|
|
1061
|
-
shiftKey: boolean;
|
|
1062
|
-
altKey: boolean;
|
|
1063
|
-
repeat: boolean;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
export interface IframeFigmaClipboardPastePayload {
|
|
1067
|
-
content: string;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
export interface IframeContextMenuPayload {
|
|
1071
|
-
clientX: number;
|
|
1072
|
-
clientY: number;
|
|
1073
|
-
viewportClientX?: number;
|
|
1074
|
-
viewportClientY?: number;
|
|
1075
|
-
info?: ElementInfo | null;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Creation-race keystroke routing: after the host calls `beginTextEdit(nodeId)`
|
|
1080
|
-
* for a JUST-created text element, there is a window (persist round-trip +
|
|
1081
|
-
* bridge activation) where the browser focus still sits on the HOST document.
|
|
1082
|
-
* Keystrokes typed in that window used to fall into host keyboard shortcuts —
|
|
1083
|
-
* letters switched tools, digits zoomed, and Delete/Backspace DELETED the
|
|
1084
|
-
* selected layer/screen — while the characters themselves were silently lost.
|
|
1085
|
-
*
|
|
1086
|
-
* While a text edit is pending, every host keydown is routed through this
|
|
1087
|
-
* pure helper:
|
|
1088
|
-
* - printable characters are BUFFERED (replayed into the editable once the
|
|
1089
|
-
* bridge reports the session active) and swallowed,
|
|
1090
|
-
* - Backspace edits the buffer, Delete/Enter/Tab/arrows are swallowed so they
|
|
1091
|
-
* can never reach host layer-deletion/navigation,
|
|
1092
|
-
* - Escape clears the pending buffer (user aborted) and is swallowed,
|
|
1093
|
-
* - IME composition and Cmd/Ctrl chords (undo!) pass through untouched.
|
|
1094
|
-
*
|
|
1095
|
-
* Exported for unit tests (DesignCanvas.pending-text-edit.test.ts).
|
|
1096
|
-
*/
|
|
1097
|
-
export type PendingTextEditKeyAction =
|
|
1098
|
-
| { action: "buffer"; char: string }
|
|
1099
|
-
| { action: "drop-last" }
|
|
1100
|
-
| { action: "swallow" }
|
|
1101
|
-
| { action: "clear-and-swallow" }
|
|
1102
|
-
| { action: "pass" };
|
|
1103
|
-
|
|
1104
|
-
export function routePendingTextEditKey(e: {
|
|
1105
|
-
key: string;
|
|
1106
|
-
metaKey?: boolean;
|
|
1107
|
-
ctrlKey?: boolean;
|
|
1108
|
-
altKey?: boolean;
|
|
1109
|
-
isComposing?: boolean;
|
|
1110
|
-
}): PendingTextEditKeyAction {
|
|
1111
|
-
if (e.isComposing) return { action: "pass" };
|
|
1112
|
-
if (e.metaKey || e.ctrlKey) return { action: "pass" };
|
|
1113
|
-
if (e.key === "Escape") return { action: "clear-and-swallow" };
|
|
1114
|
-
if (e.key === "Backspace") return { action: "drop-last" };
|
|
1115
|
-
if (
|
|
1116
|
-
e.key === "Delete" ||
|
|
1117
|
-
e.key === "Enter" ||
|
|
1118
|
-
e.key === "Tab" ||
|
|
1119
|
-
e.key.startsWith("Arrow")
|
|
1120
|
-
) {
|
|
1121
|
-
return { action: "swallow" };
|
|
1122
|
-
}
|
|
1123
|
-
if (e.key.length === 1) return { action: "buffer", char: e.key };
|
|
1124
|
-
return { action: "pass" };
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
/** How long a pending text edit may wait for bridge activation before the
|
|
1128
|
-
* keystroke routing above stands down (matches the bridge's own ~2s
|
|
1129
|
-
* begin-text-edit retry window, plus round-trip slack). */
|
|
1130
|
-
export const PENDING_TEXT_EDIT_TIMEOUT_MS = 3000;
|
|
1131
|
-
|
|
1132
730
|
export function DesignCanvas({
|
|
1133
731
|
content,
|
|
1134
732
|
contentKey,
|
|
@@ -1137,7 +735,7 @@ export function DesignCanvas({
|
|
|
1137
735
|
externalSnapshotHtml,
|
|
1138
736
|
onExternalContentSnapshot,
|
|
1139
737
|
fusionUrl,
|
|
1140
|
-
|
|
738
|
+
previewToken,
|
|
1141
739
|
zoom,
|
|
1142
740
|
onZoomChange,
|
|
1143
741
|
deviceFrame,
|
|
@@ -1209,6 +807,9 @@ export function DesignCanvas({
|
|
|
1209
807
|
}: DesignCanvasProps) {
|
|
1210
808
|
const t = useT();
|
|
1211
809
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
810
|
+
const embeddedCanvasPanSessionRef = useRef<EmbeddedCanvasPanSession | null>(
|
|
811
|
+
null,
|
|
812
|
+
);
|
|
1212
813
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
1213
814
|
const zoomLayerRef = useRef<HTMLDivElement>(null);
|
|
1214
815
|
const zoomRef = useRef(zoom);
|
|
@@ -1246,6 +847,7 @@ export function DesignCanvas({
|
|
|
1246
847
|
// fallback for older/interact-mode documents that never inject the chrome
|
|
1247
848
|
// bridge and thus never post ready) and flush in order.
|
|
1248
849
|
const bridgeReadyRef = useRef(false);
|
|
850
|
+
const previousIframeDocumentIdentityRef = useRef<string | null>(null);
|
|
1249
851
|
const pendingOneShotMessagesRef = useRef<unknown[]>([]);
|
|
1250
852
|
const flushPendingOneShotMessages = useCallback(() => {
|
|
1251
853
|
const iframe = iframeRef.current;
|
|
@@ -1331,24 +933,34 @@ export function DesignCanvas({
|
|
|
1331
933
|
}),
|
|
1332
934
|
[boardSurface, contentKey, editMode, readOnly, screenId],
|
|
1333
935
|
);
|
|
1334
|
-
const
|
|
936
|
+
const embeddedGestureBridgeForCurrentState = useMemo(
|
|
1335
937
|
() =>
|
|
1336
938
|
EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
|
|
1337
939
|
"__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
|
|
1338
940
|
isEmbeddedFrame ? "true" : "false",
|
|
941
|
+
).replace(
|
|
942
|
+
"__EMBEDDED_SPACE_KEY_FORWARDING_ENABLED__",
|
|
943
|
+
interactMode || readOnly ? "true" : "false",
|
|
1339
944
|
),
|
|
1340
|
-
[isEmbeddedFrame],
|
|
945
|
+
[interactMode, isEmbeddedFrame, readOnly],
|
|
1341
946
|
);
|
|
947
|
+
const includeLiveEditEditorChrome = !interactMode && !readOnly;
|
|
1342
948
|
const liveEditBridgeScript = useMemo(
|
|
1343
949
|
() =>
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
950
|
+
includeLiveEditEditorChrome
|
|
951
|
+
? MOTION_PREVIEW_BRIDGE_SCRIPT +
|
|
952
|
+
SHADER_FILL_PREVIEW_BRIDGE_SCRIPT +
|
|
953
|
+
TWEAK_BRIDGE_SCRIPT +
|
|
954
|
+
ZOOM_BRIDGE_SCRIPT +
|
|
955
|
+
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
|
|
956
|
+
embeddedGestureBridgeForCurrentState +
|
|
957
|
+
editorChromeBridgeForCurrentState
|
|
958
|
+
: embeddedGestureBridgeForCurrentState,
|
|
959
|
+
[
|
|
1350
960
|
editorChromeBridgeForCurrentState,
|
|
1351
|
-
|
|
961
|
+
embeddedGestureBridgeForCurrentState,
|
|
962
|
+
includeLiveEditEditorChrome,
|
|
963
|
+
],
|
|
1352
964
|
);
|
|
1353
965
|
const liveEditBridgeKey = useMemo(
|
|
1354
966
|
() => contentHash(liveEditBridgeScript),
|
|
@@ -1356,38 +968,52 @@ export function DesignCanvas({
|
|
|
1356
968
|
);
|
|
1357
969
|
const hasLiveEditExternalFrame =
|
|
1358
970
|
sourceType === "localhost" && Boolean(bridgeUrl && rawExternalPreviewUrl);
|
|
1359
|
-
const
|
|
971
|
+
const hasAuthenticatedLiveEditExternalFrame =
|
|
972
|
+
hasLiveEditExternalFrame && Boolean(previewToken);
|
|
973
|
+
const usesLiveEditInjectedBridge =
|
|
1360
974
|
sourceType === "localhost" &&
|
|
1361
|
-
Boolean(bridgeUrl && rawExternalPreviewUrl)
|
|
1362
|
-
|
|
1363
|
-
|
|
975
|
+
Boolean(bridgeUrl && previewToken && rawExternalPreviewUrl);
|
|
976
|
+
const usesLiveEditEditorBridge =
|
|
977
|
+
usesLiveEditInjectedBridge && includeLiveEditEditorChrome;
|
|
1364
978
|
const liveEditBridgeRegistered =
|
|
1365
|
-
|
|
979
|
+
usesLiveEditInjectedBridge &&
|
|
1366
980
|
registeredLiveEditBridgeKey === liveEditBridgeKey;
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
981
|
+
// The live iframe and the editable source model are deliberately separate:
|
|
982
|
+
// render exactly one authenticated /live-edit document while fetching
|
|
983
|
+
// /snapshot in parallel so DesignEditor patches real HTML rather than the
|
|
984
|
+
// persisted URL string. The snapshot never replaces the live iframe.
|
|
985
|
+
const requiresExternalSourceSnapshot = shouldFetchExternalSourceSnapshot({
|
|
986
|
+
sourceType,
|
|
987
|
+
bridgeUrl,
|
|
988
|
+
previewToken,
|
|
989
|
+
previewUrl: rawExternalPreviewUrl,
|
|
990
|
+
hasSnapshotConsumer: Boolean(onExternalContentSnapshot),
|
|
991
|
+
});
|
|
992
|
+
const liveEditExternalPreviewUrl = resolveLiveEditPreviewUrl({
|
|
993
|
+
sourceType,
|
|
994
|
+
bridgeUrl,
|
|
995
|
+
previewToken,
|
|
996
|
+
previewUrl: rawExternalPreviewUrl,
|
|
997
|
+
bridgeKey: liveEditBridgeKey,
|
|
998
|
+
registeredBridgeKey: registeredLiveEditBridgeKey,
|
|
999
|
+
});
|
|
1379
1000
|
const iframeRenderContent =
|
|
1380
|
-
activeExternalSnapshotHtml
|
|
1381
|
-
|
|
1001
|
+
!hasAuthenticatedLiveEditExternalFrame && activeExternalSnapshotHtml
|
|
1002
|
+
? activeExternalSnapshotHtml
|
|
1003
|
+
: renderedContent;
|
|
1382
1004
|
const externalPreviewUrl =
|
|
1383
1005
|
liveEditExternalPreviewUrl ??
|
|
1384
|
-
(
|
|
1006
|
+
(hasLiveEditExternalFrame
|
|
1385
1007
|
? null
|
|
1386
|
-
:
|
|
1387
|
-
|
|
1388
|
-
|
|
1008
|
+
: activeExternalSnapshotHtml
|
|
1009
|
+
? null
|
|
1010
|
+
: rawExternalPreviewUrl);
|
|
1011
|
+
// Source hydration is parallel and must never block or replace the one live
|
|
1012
|
+
// iframe. Apply-to-source remains guarded until the callback has populated
|
|
1013
|
+
// DesignEditor's source snapshot.
|
|
1014
|
+
const waitingForEditableExternalSnapshot = false;
|
|
1389
1015
|
const waitingForLiveEditBridge =
|
|
1390
|
-
|
|
1016
|
+
usesLiveEditInjectedBridge && !liveEditBridgeRegistered;
|
|
1391
1017
|
zoomRef.current = zoom;
|
|
1392
1018
|
runtimeReplacementContentRef.current = runtimeReplacementContent;
|
|
1393
1019
|
runtimeReplacementKeyRef.current = runtimeReplacementKey;
|
|
@@ -1400,7 +1026,7 @@ export function DesignCanvas({
|
|
|
1400
1026
|
// it gets injected into the proxied preview. Re-runs only when the script
|
|
1401
1027
|
// content (liveEditBridgeKey) or the bridge target changes.
|
|
1402
1028
|
useEffect(() => {
|
|
1403
|
-
if (!
|
|
1029
|
+
if (!usesLiveEditInjectedBridge || !bridgeUrl || !previewToken) {
|
|
1404
1030
|
setRegisteredLiveEditBridgeKey(null);
|
|
1405
1031
|
return;
|
|
1406
1032
|
}
|
|
@@ -1415,9 +1041,12 @@ export function DesignCanvas({
|
|
|
1415
1041
|
method: "POST",
|
|
1416
1042
|
headers: {
|
|
1417
1043
|
"content-type": "application/json",
|
|
1418
|
-
|
|
1044
|
+
"x-design-preview-token": previewToken,
|
|
1419
1045
|
},
|
|
1420
|
-
body: JSON.stringify({
|
|
1046
|
+
body: JSON.stringify({
|
|
1047
|
+
script: liveEditBridgeScript,
|
|
1048
|
+
bridgeKey: liveEditBridgeKey,
|
|
1049
|
+
}),
|
|
1421
1050
|
});
|
|
1422
1051
|
if (!response.ok) {
|
|
1423
1052
|
throw new Error(`Bridge registration failed (${response.status})`);
|
|
@@ -1435,18 +1064,19 @@ export function DesignCanvas({
|
|
|
1435
1064
|
};
|
|
1436
1065
|
}, [
|
|
1437
1066
|
bridgeUrl,
|
|
1438
|
-
bridgeToken,
|
|
1439
1067
|
liveEditBridgeKey,
|
|
1440
1068
|
liveEditBridgeScript,
|
|
1441
|
-
|
|
1069
|
+
previewToken,
|
|
1070
|
+
usesLiveEditInjectedBridge,
|
|
1442
1071
|
]);
|
|
1443
1072
|
|
|
1444
1073
|
useEffect(() => {
|
|
1445
1074
|
const previewUrl = rawExternalPreviewUrl;
|
|
1446
1075
|
if (
|
|
1447
|
-
!
|
|
1076
|
+
!requiresExternalSourceSnapshot ||
|
|
1448
1077
|
sourceType !== "localhost" ||
|
|
1449
1078
|
!bridgeUrl ||
|
|
1079
|
+
!previewToken ||
|
|
1450
1080
|
!previewUrl
|
|
1451
1081
|
) {
|
|
1452
1082
|
snapshotRetryAttemptRef.current = 0;
|
|
@@ -1456,17 +1086,12 @@ export function DesignCanvas({
|
|
|
1456
1086
|
setExternalSnapshotState(null);
|
|
1457
1087
|
return;
|
|
1458
1088
|
}
|
|
1459
|
-
if (externalSnapshotHtml) {
|
|
1460
|
-
snapshotRetryAttemptRef.current = 0;
|
|
1461
|
-
setExternalSnapshotState(null);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
1089
|
let cancelled = false;
|
|
1465
1090
|
const controller = new AbortController();
|
|
1466
1091
|
let retryTimer: number | undefined;
|
|
1467
1092
|
const endpoint = snapshotEndpointUrl(bridgeUrl, previewUrl);
|
|
1468
1093
|
const scheduleRetry = () => {
|
|
1469
|
-
if (!
|
|
1094
|
+
if (!requiresExternalSourceSnapshot || cancelled) return;
|
|
1470
1095
|
const delay = getSnapshotRetryDelayMs(snapshotRetryAttemptRef.current);
|
|
1471
1096
|
snapshotRetryAttemptRef.current += 1;
|
|
1472
1097
|
retryTimer = window.setTimeout(() => {
|
|
@@ -1478,7 +1103,10 @@ export function DesignCanvas({
|
|
|
1478
1103
|
try {
|
|
1479
1104
|
const response = await fetch(endpoint, {
|
|
1480
1105
|
method: "GET",
|
|
1481
|
-
headers: {
|
|
1106
|
+
headers: {
|
|
1107
|
+
accept: "application/json",
|
|
1108
|
+
"x-design-preview-token": previewToken,
|
|
1109
|
+
},
|
|
1482
1110
|
signal: controller.signal,
|
|
1483
1111
|
});
|
|
1484
1112
|
const payload = (await response.json().catch(() => null)) as {
|
|
@@ -1502,8 +1130,10 @@ export function DesignCanvas({
|
|
|
1502
1130
|
return;
|
|
1503
1131
|
}
|
|
1504
1132
|
const sourceUrl = payload.url || previewUrl;
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1133
|
+
const sourceHtml = sanitizeLocalhostSourceSnapshotHtml(
|
|
1134
|
+
payload.html ?? "",
|
|
1135
|
+
);
|
|
1136
|
+
const stamped = ensureCodeLayerNodeIdsInHtml(sourceHtml, {
|
|
1507
1137
|
source: {
|
|
1508
1138
|
kind: "remote-url",
|
|
1509
1139
|
sourceType: "localhost",
|
|
@@ -1540,11 +1170,11 @@ export function DesignCanvas({
|
|
|
1540
1170
|
};
|
|
1541
1171
|
}, [
|
|
1542
1172
|
bridgeUrl,
|
|
1543
|
-
externalSnapshotHtml,
|
|
1544
1173
|
externalSnapshotRetryNonce,
|
|
1545
1174
|
rawExternalPreviewUrl,
|
|
1546
|
-
|
|
1175
|
+
requiresExternalSourceSnapshot,
|
|
1547
1176
|
sourceType,
|
|
1177
|
+
previewToken,
|
|
1548
1178
|
]);
|
|
1549
1179
|
|
|
1550
1180
|
// Manual retry (offline-state "Retry" button): reset the backoff so the
|
|
@@ -1666,6 +1296,13 @@ export function DesignCanvas({
|
|
|
1666
1296
|
// never rebuilds srcdoc / reloads every screen iframe.
|
|
1667
1297
|
const srcdoc = useMemo(() => {
|
|
1668
1298
|
if (externalPreviewUrl) return undefined;
|
|
1299
|
+
// Never boot the raw localhost URL (or an editor-enabled srcdoc) while
|
|
1300
|
+
// the keyed live-edit bridge is still registering. The opaque placeholder
|
|
1301
|
+
// stays behind the visible loading surface; the real iframe mounts once,
|
|
1302
|
+
// directly at the authenticated proxied document.
|
|
1303
|
+
if (waitingForLiveEditBridge) {
|
|
1304
|
+
return '<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
|
1305
|
+
}
|
|
1669
1306
|
const editorChromeBridge = interactMode
|
|
1670
1307
|
? ""
|
|
1671
1308
|
: createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
|
|
@@ -1690,10 +1327,6 @@ export function DesignCanvas({
|
|
|
1690
1327
|
"__DESIGN_CANVAS_BOARD_SURFACE__",
|
|
1691
1328
|
boardSurface ? "true" : "false",
|
|
1692
1329
|
);
|
|
1693
|
-
const embeddedWheelBridge = EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
|
|
1694
|
-
"__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
|
|
1695
|
-
isEmbeddedFrame ? "true" : "false",
|
|
1696
|
-
);
|
|
1697
1330
|
// ALWAYS injected (like the other always-on bridges above) so
|
|
1698
1331
|
// MultiScreenCanvas's cross-screen drag hit-testing
|
|
1699
1332
|
// (agent-native:hit-test / agent-native:hit-test-result) resolves an
|
|
@@ -1708,7 +1341,7 @@ export function DesignCanvas({
|
|
|
1708
1341
|
ZOOM_BRIDGE_SCRIPT +
|
|
1709
1342
|
NAV_BRIDGE_SCRIPT +
|
|
1710
1343
|
LIGHTWEIGHT_HIT_TEST_BRIDGE_SCRIPT +
|
|
1711
|
-
|
|
1344
|
+
embeddedGestureBridgeForCurrentState +
|
|
1712
1345
|
editorChromeBridge;
|
|
1713
1346
|
const frameContent = getEmbeddedFrameDocumentContent({
|
|
1714
1347
|
content: iframeRenderContent,
|
|
@@ -1750,10 +1383,22 @@ export function DesignCanvas({
|
|
|
1750
1383
|
embeddedFrame?.contentOffsetX,
|
|
1751
1384
|
embeddedFrame?.contentOffsetY,
|
|
1752
1385
|
embeddedFrameBackground,
|
|
1386
|
+
embeddedGestureBridgeForCurrentState,
|
|
1753
1387
|
iframeRenderContent,
|
|
1754
1388
|
transparentBackground,
|
|
1389
|
+
waitingForLiveEditBridge,
|
|
1755
1390
|
]);
|
|
1756
1391
|
|
|
1392
|
+
const iframeDocumentIdentity = externalPreviewUrl
|
|
1393
|
+
? `src:${externalPreviewUrl}`
|
|
1394
|
+
: waitingForLiveEditBridge
|
|
1395
|
+
? `live-edit-pending:${liveEditBridgeKey}`
|
|
1396
|
+
: `srcdoc:${contentKey ?? ""}:${contentHash(srcdoc ?? "")}`;
|
|
1397
|
+
if (previousIframeDocumentIdentityRef.current !== iframeDocumentIdentity) {
|
|
1398
|
+
previousIframeDocumentIdentityRef.current = iframeDocumentIdentity;
|
|
1399
|
+
bridgeReadyRef.current = false;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1757
1402
|
// Listen for messages from the iframe
|
|
1758
1403
|
useEffect(() => {
|
|
1759
1404
|
function handleMessage(e: MessageEvent) {
|
|
@@ -1768,7 +1413,7 @@ export function DesignCanvas({
|
|
|
1768
1413
|
// somehow shares our window reference still can't be trusted.
|
|
1769
1414
|
const trusted =
|
|
1770
1415
|
sourceType === "fusion"
|
|
1771
|
-
? iframeWindow
|
|
1416
|
+
? iframeWindow !== null &&
|
|
1772
1417
|
e.source === iframeWindow &&
|
|
1773
1418
|
isAllowedFusionOrigin(e.origin, fusionUrl)
|
|
1774
1419
|
: isTrustedCanvasBridgeMessage({
|
|
@@ -2088,6 +1733,21 @@ export function DesignCanvas({
|
|
|
2088
1733
|
}
|
|
2089
1734
|
return;
|
|
2090
1735
|
}
|
|
1736
|
+
if (e.data.type === "embedded-canvas-pan") {
|
|
1737
|
+
const iframe = iframeRef.current;
|
|
1738
|
+
if (!iframe) return;
|
|
1739
|
+
const result = forwardEmbeddedCanvasPanMessage({
|
|
1740
|
+
data: e.data,
|
|
1741
|
+
iframe,
|
|
1742
|
+
hostWindow: window,
|
|
1743
|
+
session: embeddedCanvasPanSessionRef.current,
|
|
1744
|
+
});
|
|
1745
|
+
embeddedCanvasPanSessionRef.current = result.session;
|
|
1746
|
+
// The message type belongs exclusively to the gesture bridge. Even a
|
|
1747
|
+
// malformed/reordered packet must not fall through to another bridge
|
|
1748
|
+
// protocol handler after validation rejects it.
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
2091
1751
|
if (e.data.type === "embedded-canvas-wheel") {
|
|
2092
1752
|
if (!isEmbeddedFrame) return;
|
|
2093
1753
|
const iframe = iframeRef.current;
|
|
@@ -2211,6 +1871,13 @@ export function DesignCanvas({
|
|
|
2211
1871
|
const replayIframeEditorState = useCallback(() => {
|
|
2212
1872
|
const iframe = iframeRef.current;
|
|
2213
1873
|
if (!iframe) return;
|
|
1874
|
+
iframe.contentWindow?.postMessage(
|
|
1875
|
+
{
|
|
1876
|
+
type: "embedded-canvas-pan-mode",
|
|
1877
|
+
leftButtonEnabled: handToolActive || spacePanActive,
|
|
1878
|
+
},
|
|
1879
|
+
"*",
|
|
1880
|
+
);
|
|
2214
1881
|
iframe.contentWindow?.postMessage(
|
|
2215
1882
|
{ type: "tweak-values", values: tweakValues },
|
|
2216
1883
|
"*",
|
|
@@ -2283,6 +1950,7 @@ export function DesignCanvas({
|
|
|
2283
1950
|
);
|
|
2284
1951
|
}
|
|
2285
1952
|
}, [
|
|
1953
|
+
handToolActive,
|
|
2286
1954
|
hoveredSelector,
|
|
2287
1955
|
hoveredSelectorCandidates,
|
|
2288
1956
|
hiddenSelectors,
|
|
@@ -2295,6 +1963,7 @@ export function DesignCanvas({
|
|
|
2295
1963
|
selectedSelectorCandidates,
|
|
2296
1964
|
selectedSelectorGroups,
|
|
2297
1965
|
shaderFillPreview,
|
|
1966
|
+
spacePanActive,
|
|
2298
1967
|
tweakValues,
|
|
2299
1968
|
]);
|
|
2300
1969
|
|
|
@@ -2309,6 +1978,22 @@ export function DesignCanvas({
|
|
|
2309
1978
|
return () => iframe.removeEventListener("load", replayIframeEditorState);
|
|
2310
1979
|
}, [replayIframeEditorState]);
|
|
2311
1980
|
|
|
1981
|
+
// A real top-level focus loss (Cmd+Tab, switching windows, browser chrome)
|
|
1982
|
+
// does not reliably produce pointercancel inside every browser's iframe.
|
|
1983
|
+
// Explicitly reset the child bridge as well as the host-side packet session
|
|
1984
|
+
// so the next middle/hand drag can always start cleanly after focus returns.
|
|
1985
|
+
useEffect(() => {
|
|
1986
|
+
const handleHostWindowBlur = () => {
|
|
1987
|
+
embeddedCanvasPanSessionRef.current = null;
|
|
1988
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
1989
|
+
{ type: "embedded-canvas-pan-cancel" },
|
|
1990
|
+
"*",
|
|
1991
|
+
);
|
|
1992
|
+
};
|
|
1993
|
+
window.addEventListener("blur", handleHostWindowBlur);
|
|
1994
|
+
return () => window.removeEventListener("blur", handleHostWindowBlur);
|
|
1995
|
+
}, []);
|
|
1996
|
+
|
|
2312
1997
|
// Fallback for documents that never inject the editor-chrome bridge (e.g.
|
|
2313
1998
|
// interactMode, or an external-preview iframe where srcdoc is undefined):
|
|
2314
1999
|
// those never post agent-native:editor-chrome-ready, so nothing would ever
|
|
@@ -2322,13 +2007,14 @@ export function DesignCanvas({
|
|
|
2322
2007
|
const iframe = iframeRef.current;
|
|
2323
2008
|
if (!iframe) return;
|
|
2324
2009
|
function handleLoadReadyFallback() {
|
|
2010
|
+
if (!shouldUseIframeLoadReadyFallback(usesLiveEditEditorBridge)) return;
|
|
2325
2011
|
if (bridgeReadyRef.current) return;
|
|
2326
2012
|
bridgeReadyRef.current = true;
|
|
2327
2013
|
flushPendingOneShotMessages();
|
|
2328
2014
|
}
|
|
2329
2015
|
iframe.addEventListener("load", handleLoadReadyFallback);
|
|
2330
2016
|
return () => iframe.removeEventListener("load", handleLoadReadyFallback);
|
|
2331
|
-
}, [flushPendingOneShotMessages]);
|
|
2017
|
+
}, [flushPendingOneShotMessages, usesLiveEditEditorBridge]);
|
|
2332
2018
|
|
|
2333
2019
|
useEffect(() => {
|
|
2334
2020
|
if (clearSelectionRequest === undefined) return;
|
|
@@ -2502,7 +2188,7 @@ export function DesignCanvas({
|
|
|
2502
2188
|
* The bridge enters the same contenteditable text-editing path used on dblclick.
|
|
2503
2189
|
*/
|
|
2504
2190
|
const beginTextEdit = useCallback(
|
|
2505
|
-
(nodeId: string) => {
|
|
2191
|
+
(nodeId: string, options?: { afterPointerGesture?: boolean }) => {
|
|
2506
2192
|
const iframe = iframeRef.current;
|
|
2507
2193
|
if (!iframe?.contentWindow || !nodeId) return;
|
|
2508
2194
|
// Arm the creation-race keystroke buffer (see routePendingTextEditKey):
|
|
@@ -2517,11 +2203,16 @@ export function DesignCanvas({
|
|
|
2517
2203
|
startedAt: Date.now(),
|
|
2518
2204
|
};
|
|
2519
2205
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2206
|
+
schedulePendingTextEditActivation(
|
|
2207
|
+
() => {
|
|
2208
|
+
postOneShotBridgeMessage({
|
|
2209
|
+
type: "begin-text-edit",
|
|
2210
|
+
nodeId,
|
|
2211
|
+
force: true,
|
|
2212
|
+
});
|
|
2213
|
+
},
|
|
2214
|
+
{ afterPointerGesture: options?.afterPointerGesture },
|
|
2215
|
+
);
|
|
2525
2216
|
},
|
|
2526
2217
|
[postOneShotBridgeMessage],
|
|
2527
2218
|
);
|
|
@@ -2623,7 +2314,11 @@ export function DesignCanvas({
|
|
|
2623
2314
|
|
|
2624
2315
|
const lastStyleBaselineResetRequestRef = useRef<number | null>(null);
|
|
2625
2316
|
useEffect(() => {
|
|
2626
|
-
if (
|
|
2317
|
+
if (
|
|
2318
|
+
styleBaselineResetRequest === null ||
|
|
2319
|
+
styleBaselineResetRequest === undefined
|
|
2320
|
+
)
|
|
2321
|
+
return;
|
|
2627
2322
|
if (
|
|
2628
2323
|
lastStyleBaselineResetRequestRef.current === styleBaselineResetRequest
|
|
2629
2324
|
) {
|
|
@@ -2919,7 +2614,9 @@ export function DesignCanvas({
|
|
|
2919
2614
|
// the deviceFrame width so the caller can render the same source at an
|
|
2920
2615
|
// explicit viewport width (e.g. 390 / 768 / 1280 side-by-side breakpoints).
|
|
2921
2616
|
const resolvedWidth =
|
|
2922
|
-
previewWidthPx
|
|
2617
|
+
previewWidthPx !== null && previewWidthPx !== undefined
|
|
2618
|
+
? `${previewWidthPx}px`
|
|
2619
|
+
: iframeWidth;
|
|
2923
2620
|
const focusScrollSurface = useCallback(() => {
|
|
2924
2621
|
const surface = scrollContainerRef.current;
|
|
2925
2622
|
if (!surface || document.activeElement === surface) return;
|
|
@@ -2937,15 +2634,11 @@ export function DesignCanvas({
|
|
|
2937
2634
|
// plain `mousemove`/`mouseup` window listeners for the drag's lifetime,
|
|
2938
2635
|
// same pattern as its `installDragListeners(handleMouseMove, handlePanEnd)`.
|
|
2939
2636
|
//
|
|
2940
|
-
//
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
2943
|
-
//
|
|
2944
|
-
//
|
|
2945
|
-
// than wheel events do (see the pinch-zoom-wheel bridge message above for
|
|
2946
|
-
// the equivalent problem with wheel) — so panning from inside interactive
|
|
2947
|
-
// iframe content is a follow-up that would need its own bridge message,
|
|
2948
|
-
// not a gap introduced by this change.
|
|
2637
|
+
// Drags that begin inside the iframe arrive through the generated embedded
|
|
2638
|
+
// gesture bridge as a synthetic mousedown on the parent iframe element,
|
|
2639
|
+
// followed by window mousemove/mouseup events. That deliberately reuses
|
|
2640
|
+
// this exact path, keeping empty-canvas, inline HTML/Alpine, and localhost
|
|
2641
|
+
// live-edit panning behavior identical without an iframe-local pan model.
|
|
2949
2642
|
const [isPanningState, setIsPanningState] = useState(false);
|
|
2950
2643
|
|
|
2951
2644
|
const handleScrollSurfaceMouseDown = useCallback(
|
|
@@ -2972,13 +2665,15 @@ export function DesignCanvas({
|
|
|
2972
2665
|
scroll.scrollLeft -= dx;
|
|
2973
2666
|
scroll.scrollTop -= dy;
|
|
2974
2667
|
};
|
|
2975
|
-
const
|
|
2668
|
+
const handlePanEnd = () => {
|
|
2976
2669
|
setIsPanningState(false);
|
|
2977
2670
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
2978
|
-
window.removeEventListener("mouseup",
|
|
2671
|
+
window.removeEventListener("mouseup", handlePanEnd);
|
|
2672
|
+
window.removeEventListener("blur", handlePanEnd);
|
|
2979
2673
|
};
|
|
2980
2674
|
window.addEventListener("mousemove", handleMouseMove);
|
|
2981
|
-
window.addEventListener("mouseup",
|
|
2675
|
+
window.addEventListener("mouseup", handlePanEnd);
|
|
2676
|
+
window.addEventListener("blur", handlePanEnd);
|
|
2982
2677
|
},
|
|
2983
2678
|
[handToolActive, spacePanActive],
|
|
2984
2679
|
);
|
|
@@ -3104,7 +2799,9 @@ export function DesignCanvas({
|
|
|
3104
2799
|
// layer's own top-left corner stays a fixed point in scroll-content
|
|
3105
2800
|
// space — is untouched. Base editing (previewWidthPx unset) keeps
|
|
3106
2801
|
// the original inline-block full-width layout unchanged.
|
|
3107
|
-
...(previewWidthPx
|
|
2802
|
+
...(previewWidthPx !== null &&
|
|
2803
|
+
previewWidthPx !== undefined &&
|
|
2804
|
+
!embeddedFrame
|
|
3108
2805
|
? {
|
|
3109
2806
|
display: "block",
|
|
3110
2807
|
marginLeft: "auto",
|
|
@@ -3114,36 +2811,14 @@ export function DesignCanvas({
|
|
|
3114
2811
|
}}
|
|
3115
2812
|
>
|
|
3116
2813
|
<iframe
|
|
2814
|
+
key={iframeDocumentIdentity}
|
|
3117
2815
|
ref={iframeRef}
|
|
3118
2816
|
src={externalPreviewUrl ?? undefined}
|
|
3119
2817
|
srcDoc={externalPreviewUrl ? undefined : srcdoc}
|
|
3120
|
-
sandbox={
|
|
3121
|
-
externalPreviewUrl
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
// branch (the inline srcdoc iframe, where agent/user-generated
|
|
3125
|
-
// design HTML executes) even though allow-scripts +
|
|
3126
|
-
// allow-same-origin together defeat the sandbox in principle.
|
|
3127
|
-
// This is a deliberate, currently-blocked tradeoff, not an
|
|
3128
|
-
// oversight — see the long comment above DesignCanvasProps /
|
|
3129
|
-
// sourceType for the security model, and the PNG/SVG export
|
|
3130
|
-
// handlers in DesignEditor.tsx (handleDownloadPng /
|
|
3131
|
-
// handleDownloadSvg). Those handlers read this same iframe's
|
|
3132
|
-
// `contentDocument` directly and hand the LIVE document element
|
|
3133
|
-
// to html2canvas, and read live CSSOM (getComputedStyle,
|
|
3134
|
-
// doc.styleSheets) for SVG serialization. That cannot be
|
|
3135
|
-
// expressed over the postMessage bridge without moving
|
|
3136
|
-
// html2canvas execution and CSSOM-dependent serialization logic
|
|
3137
|
-
// INTO the sandboxed iframe itself — a separate, larger rewrite,
|
|
3138
|
-
// not a bridge-handler addition. All other former
|
|
3139
|
-
// `contentDocument`/`contentWindow.document` reads on this
|
|
3140
|
-
// iframe (e.g. text-edit-status) now go through bridge
|
|
3141
|
-
// request/response postMessage handlers in
|
|
3142
|
-
// editor-chrome.bridge.ts and no longer need allow-same-origin.
|
|
3143
|
-
// Do not remove this flag until PNG/SVG export is rebuilt to run
|
|
3144
|
-
// inside the iframe.
|
|
3145
|
-
"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
3146
|
-
}
|
|
2818
|
+
sandbox={getDesignCanvasIframeSandbox({
|
|
2819
|
+
externalPreview: Boolean(externalPreviewUrl),
|
|
2820
|
+
readOnly,
|
|
2821
|
+
})}
|
|
3147
2822
|
data-design-preview-iframe
|
|
3148
2823
|
data-screen-iframe-id={
|
|
3149
2824
|
boardSurface ? undefined : (screenId ?? undefined)
|
|
@@ -3285,19 +2960,25 @@ export function DesignCanvas({
|
|
|
3285
2960
|
"",
|
|
3286
2961
|
instruction || "Apply these annotations to the design.",
|
|
3287
2962
|
];
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
2963
|
+
submitDesignAnnotations({
|
|
2964
|
+
message: lines.join("\n"),
|
|
2965
|
+
hasQueuedPins: queuedAnnotationPins.length > 0,
|
|
2966
|
+
send: (message) => {
|
|
2967
|
+
sendToDesignAgentChat({
|
|
2968
|
+
message,
|
|
2969
|
+
submit: true,
|
|
2970
|
+
openSidebar: true,
|
|
2971
|
+
});
|
|
2972
|
+
},
|
|
2973
|
+
markQueuedPinsSubmitted: () => {
|
|
2974
|
+
setPinSubmitSignal((signal) => signal + 1);
|
|
2975
|
+
},
|
|
2976
|
+
exitDrawMode: () => onExitDrawMode?.(),
|
|
2977
|
+
onError: (error) => {
|
|
2978
|
+
console.error("[DesignCanvas] failed to submit drawing:", error);
|
|
2979
|
+
toast.error(t("designEditor.toasts.annotationSendError"));
|
|
2980
|
+
},
|
|
2981
|
+
});
|
|
3301
2982
|
}}
|
|
3302
2983
|
/>
|
|
3303
2984
|
</div>
|
|
@@ -3492,6 +3173,17 @@ interface CreationDragState {
|
|
|
3492
3173
|
moved: boolean;
|
|
3493
3174
|
}
|
|
3494
3175
|
|
|
3176
|
+
interface SingleScreenPenGestureState {
|
|
3177
|
+
pointerId: number;
|
|
3178
|
+
originClient: { x: number; y: number };
|
|
3179
|
+
anchor: PenPoint;
|
|
3180
|
+
pathBefore: PenPath | null;
|
|
3181
|
+
moved: boolean;
|
|
3182
|
+
closing: boolean;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
const SINGLE_SCREEN_PEN_HIT_RADIUS_PX = 10;
|
|
3186
|
+
|
|
3495
3187
|
interface SingleScreenCreationOverlayProps {
|
|
3496
3188
|
tool: CreationTool;
|
|
3497
3189
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -3508,15 +3200,11 @@ interface SingleScreenCreationOverlayProps {
|
|
|
3508
3200
|
* is treated as a drag (draws a rect/line via `getDraftGeometryFromPoints`,
|
|
3509
3201
|
* the same helper MultiScreenCanvas uses for overview drafts); a
|
|
3510
3202
|
* pointerdown→up with negligible movement is treated as a click (places a
|
|
3511
|
-
* default-sized primitive
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3514
|
-
*
|
|
3515
|
-
* pen
|
|
3516
|
-
* is no in-overlay support yet for adding further anchor points before
|
|
3517
|
-
* committing — that's a follow-up once the parent's pen-path commit flow is
|
|
3518
|
-
* wired for single-screen. This intentionally never force-switches to
|
|
3519
|
-
* overview to get full pen authoring.
|
|
3203
|
+
* default-sized primitive). Pen owns a stateful path until the user presses
|
|
3204
|
+
* Enter/Escape, double-clicks, closes on the first anchor, or chooses another
|
|
3205
|
+
* tool. Clicks add corner anchors; pointer drags add Bezier handles; primary
|
|
3206
|
+
* undo/Delete/Backspace remove the latest active segment. This mirrors the
|
|
3207
|
+
* overview pen workflow without forcing a mode switch.
|
|
3520
3208
|
*/
|
|
3521
3209
|
function SingleScreenCreationOverlay({
|
|
3522
3210
|
tool,
|
|
@@ -3525,6 +3213,14 @@ function SingleScreenCreationOverlay({
|
|
|
3525
3213
|
}: SingleScreenCreationOverlayProps) {
|
|
3526
3214
|
const [drag, setDrag] = useState<CreationDragState | null>(null);
|
|
3527
3215
|
const dragRef = useRef<CreationDragState | null>(null);
|
|
3216
|
+
const [penPath, setPenPath] = useState<PenPath | null>(null);
|
|
3217
|
+
const penPathRef = useRef<PenPath | null>(null);
|
|
3218
|
+
const [penGesturePreview, setPenGesturePreview] = useState<PenPath | null>(
|
|
3219
|
+
null,
|
|
3220
|
+
);
|
|
3221
|
+
const penGestureRef = useRef<SingleScreenPenGestureState | null>(null);
|
|
3222
|
+
const [penPointer, setPenPointer] = useState<PenPoint | null>(null);
|
|
3223
|
+
const [penCloseHover, setPenCloseHover] = useState(false);
|
|
3528
3224
|
|
|
3529
3225
|
const toContentPoint = useCallback(
|
|
3530
3226
|
(clientX: number, clientY: number) => {
|
|
@@ -3539,6 +3235,91 @@ function SingleScreenCreationOverlay({
|
|
|
3539
3235
|
[iframeRef],
|
|
3540
3236
|
);
|
|
3541
3237
|
|
|
3238
|
+
const penHitRadius = useCallback(() => {
|
|
3239
|
+
const iframe = iframeRef.current;
|
|
3240
|
+
const rect = iframe?.getBoundingClientRect();
|
|
3241
|
+
if (!iframe || !rect || iframe.clientWidth <= 0) {
|
|
3242
|
+
return SINGLE_SCREEN_PEN_HIT_RADIUS_PX;
|
|
3243
|
+
}
|
|
3244
|
+
const scale = rect.width / iframe.clientWidth;
|
|
3245
|
+
return SINGLE_SCREEN_PEN_HIT_RADIUS_PX / (scale || 1);
|
|
3246
|
+
}, [iframeRef]);
|
|
3247
|
+
|
|
3248
|
+
const updatePenPath = useCallback((nextPath: PenPath | null) => {
|
|
3249
|
+
penPathRef.current = nextPath;
|
|
3250
|
+
setPenPath(nextPath);
|
|
3251
|
+
}, []);
|
|
3252
|
+
|
|
3253
|
+
const clearPenPath = useCallback(() => {
|
|
3254
|
+
updatePenPath(null);
|
|
3255
|
+
penGestureRef.current = null;
|
|
3256
|
+
setPenGesturePreview(null);
|
|
3257
|
+
setPenPointer(null);
|
|
3258
|
+
setPenCloseHover(false);
|
|
3259
|
+
}, [updatePenPath]);
|
|
3260
|
+
|
|
3261
|
+
const finishPenPath = useCallback(
|
|
3262
|
+
(
|
|
3263
|
+
path: PenPath | null = penPathRef.current,
|
|
3264
|
+
options?: { preserveActiveTool?: boolean },
|
|
3265
|
+
) => {
|
|
3266
|
+
const committed = path ? clonePenPath(path) : null;
|
|
3267
|
+
clearPenPath();
|
|
3268
|
+
if (!committed || committed.nodes.length < 2 || !onCreatePrimitive) {
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
onCreatePrimitive({
|
|
3272
|
+
tool: "pen",
|
|
3273
|
+
points: committed.nodes.map((node) => node.point),
|
|
3274
|
+
penPath: committed,
|
|
3275
|
+
fromClick: false,
|
|
3276
|
+
preserveActiveTool: options?.preserveActiveTool,
|
|
3277
|
+
});
|
|
3278
|
+
},
|
|
3279
|
+
[clearPenPath, onCreatePrimitive],
|
|
3280
|
+
);
|
|
3281
|
+
|
|
3282
|
+
const getPenAnchor = useCallback(
|
|
3283
|
+
(clientX: number, clientY: number, shiftKey: boolean) => {
|
|
3284
|
+
const point = toContentPoint(clientX, clientY);
|
|
3285
|
+
const path = penPathRef.current;
|
|
3286
|
+
const lastAnchor = path?.nodes[path.nodes.length - 1]?.point;
|
|
3287
|
+
return shiftKey && lastAnchor
|
|
3288
|
+
? constrainPointTo45Degrees(lastAnchor, point)
|
|
3289
|
+
: point;
|
|
3290
|
+
},
|
|
3291
|
+
[toContentPoint],
|
|
3292
|
+
);
|
|
3293
|
+
|
|
3294
|
+
const updatePenPointer = useCallback(
|
|
3295
|
+
(clientX: number, clientY: number, shiftKey: boolean) => {
|
|
3296
|
+
const path = penPathRef.current;
|
|
3297
|
+
if (!path || path.closed) {
|
|
3298
|
+
setPenPointer(null);
|
|
3299
|
+
setPenCloseHover(false);
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
const rawPoint = toContentPoint(clientX, clientY);
|
|
3303
|
+
const closeHover = isPenCloseTarget(path, rawPoint, penHitRadius());
|
|
3304
|
+
setPenCloseHover(closeHover);
|
|
3305
|
+
setPenPointer(
|
|
3306
|
+
closeHover
|
|
3307
|
+
? path.nodes[0]!.point
|
|
3308
|
+
: getPenAnchor(clientX, clientY, shiftKey),
|
|
3309
|
+
);
|
|
3310
|
+
},
|
|
3311
|
+
[getPenAnchor, penHitRadius, toContentPoint],
|
|
3312
|
+
);
|
|
3313
|
+
|
|
3314
|
+
const previousToolRef = useRef(tool);
|
|
3315
|
+
useEffect(() => {
|
|
3316
|
+
const previousTool = previousToolRef.current;
|
|
3317
|
+
previousToolRef.current = tool;
|
|
3318
|
+
if (previousTool === "pen" && tool !== "pen") {
|
|
3319
|
+
finishPenPath(penPathRef.current, { preserveActiveTool: true });
|
|
3320
|
+
}
|
|
3321
|
+
}, [finishPenPath, tool]);
|
|
3322
|
+
|
|
3542
3323
|
const emit = useCallback(
|
|
3543
3324
|
(state: CreationDragState) => {
|
|
3544
3325
|
if (!onCreatePrimitive) return;
|
|
@@ -3559,17 +3340,8 @@ function SingleScreenCreationOverlay({
|
|
|
3559
3340
|
return;
|
|
3560
3341
|
}
|
|
3561
3342
|
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
// A click starts a path; a drag is treated the same as a click for
|
|
3565
|
-
// now — only the start point is emitted.
|
|
3566
|
-
onCreatePrimitive({
|
|
3567
|
-
tool: "pen",
|
|
3568
|
-
points: [startContent],
|
|
3569
|
-
fromClick: true,
|
|
3570
|
-
});
|
|
3571
|
-
return;
|
|
3572
|
-
}
|
|
3343
|
+
// Pen commits through finishPenPath after collecting multiple gestures.
|
|
3344
|
+
if (activeTool === "pen") return;
|
|
3573
3345
|
|
|
3574
3346
|
if (!moved) {
|
|
3575
3347
|
const size = CREATION_DEFAULT_SIZE[activeTool];
|
|
@@ -3608,6 +3380,36 @@ function SingleScreenCreationOverlay({
|
|
|
3608
3380
|
const handlePointerDown = useCallback(
|
|
3609
3381
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
3610
3382
|
if (e.button !== 0) return;
|
|
3383
|
+
if (tool === "pen") {
|
|
3384
|
+
e.preventDefault();
|
|
3385
|
+
e.stopPropagation();
|
|
3386
|
+
const currentPath = penPathRef.current?.closed
|
|
3387
|
+
? null
|
|
3388
|
+
: penPathRef.current;
|
|
3389
|
+
const pathBefore = currentPath ? clonePenPath(currentPath) : null;
|
|
3390
|
+
const rawPoint = toContentPoint(e.clientX, e.clientY);
|
|
3391
|
+
const closing = isPenCloseTarget(pathBefore, rawPoint, penHitRadius());
|
|
3392
|
+
const anchor = closing
|
|
3393
|
+
? pathBefore!.nodes[0]!.point
|
|
3394
|
+
: getPenAnchor(e.clientX, e.clientY, e.shiftKey);
|
|
3395
|
+
penGestureRef.current = {
|
|
3396
|
+
pointerId: e.pointerId,
|
|
3397
|
+
originClient: { x: e.clientX, y: e.clientY },
|
|
3398
|
+
anchor,
|
|
3399
|
+
pathBefore,
|
|
3400
|
+
moved: false,
|
|
3401
|
+
closing,
|
|
3402
|
+
};
|
|
3403
|
+
const initialPath = closing
|
|
3404
|
+
? closePenPath(pathBefore!)
|
|
3405
|
+
: appendPenNode(pathBefore, createCornerNode(anchor));
|
|
3406
|
+
updatePenPath(initialPath);
|
|
3407
|
+
setPenGesturePreview(initialPath);
|
|
3408
|
+
setPenPointer(null);
|
|
3409
|
+
setPenCloseHover(closing);
|
|
3410
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3611
3413
|
const point = toContentPoint(e.clientX, e.clientY);
|
|
3612
3414
|
const next: CreationDragState = {
|
|
3613
3415
|
tool,
|
|
@@ -3621,11 +3423,49 @@ function SingleScreenCreationOverlay({
|
|
|
3621
3423
|
setDrag(next);
|
|
3622
3424
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
3623
3425
|
},
|
|
3624
|
-
[
|
|
3426
|
+
[
|
|
3427
|
+
finishPenPath,
|
|
3428
|
+
getPenAnchor,
|
|
3429
|
+
penHitRadius,
|
|
3430
|
+
tool,
|
|
3431
|
+
toContentPoint,
|
|
3432
|
+
updatePenPath,
|
|
3433
|
+
],
|
|
3625
3434
|
);
|
|
3626
3435
|
|
|
3627
3436
|
const handlePointerMove = useCallback(
|
|
3628
3437
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
3438
|
+
if (tool === "pen") {
|
|
3439
|
+
const gesture = penGestureRef.current;
|
|
3440
|
+
if (!gesture || gesture.pointerId !== e.pointerId) {
|
|
3441
|
+
updatePenPointer(e.clientX, e.clientY, e.shiftKey);
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
const moved =
|
|
3445
|
+
gesture.moved ||
|
|
3446
|
+
Math.hypot(
|
|
3447
|
+
e.clientX - gesture.originClient.x,
|
|
3448
|
+
e.clientY - gesture.originClient.y,
|
|
3449
|
+
) > CREATION_CLICK_MOVE_THRESHOLD_PX;
|
|
3450
|
+
gesture.moved = moved;
|
|
3451
|
+
const rawHandle = toContentPoint(e.clientX, e.clientY);
|
|
3452
|
+
const handleOut = e.shiftKey
|
|
3453
|
+
? constrainPointTo45Degrees(gesture.anchor, rawHandle)
|
|
3454
|
+
: rawHandle;
|
|
3455
|
+
const nextPath = gesture.closing
|
|
3456
|
+
? shapeClosingHandles(gesture.pathBefore!, moved ? handleOut : null)
|
|
3457
|
+
: appendPenNode(
|
|
3458
|
+
gesture.pathBefore,
|
|
3459
|
+
moved
|
|
3460
|
+
? createSmoothNode(gesture.anchor, handleOut, {
|
|
3461
|
+
breakSymmetry: e.altKey,
|
|
3462
|
+
})
|
|
3463
|
+
: createCornerNode(gesture.anchor),
|
|
3464
|
+
);
|
|
3465
|
+
updatePenPath(nextPath);
|
|
3466
|
+
setPenGesturePreview(nextPath);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3629
3469
|
const current = dragRef.current;
|
|
3630
3470
|
if (!current || current.pointerId !== e.pointerId) return;
|
|
3631
3471
|
// Movement threshold measured in client (viewport) space — a constant
|
|
@@ -3646,32 +3486,138 @@ function SingleScreenCreationOverlay({
|
|
|
3646
3486
|
dragRef.current = next;
|
|
3647
3487
|
setDrag(next);
|
|
3648
3488
|
},
|
|
3649
|
-
[toContentPoint],
|
|
3489
|
+
[tool, toContentPoint, updatePenPath, updatePenPointer],
|
|
3650
3490
|
);
|
|
3651
3491
|
|
|
3652
3492
|
const handlePointerUp = useCallback(
|
|
3653
3493
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
3494
|
+
if (tool === "pen") {
|
|
3495
|
+
const gesture = penGestureRef.current;
|
|
3496
|
+
if (!gesture || gesture.pointerId !== e.pointerId) return;
|
|
3497
|
+
e.preventDefault();
|
|
3498
|
+
e.stopPropagation();
|
|
3499
|
+
const moved =
|
|
3500
|
+
gesture.moved ||
|
|
3501
|
+
Math.hypot(
|
|
3502
|
+
e.clientX - gesture.originClient.x,
|
|
3503
|
+
e.clientY - gesture.originClient.y,
|
|
3504
|
+
) > CREATION_CLICK_MOVE_THRESHOLD_PX;
|
|
3505
|
+
const rawHandle = toContentPoint(e.clientX, e.clientY);
|
|
3506
|
+
const handleOut = e.shiftKey
|
|
3507
|
+
? constrainPointTo45Degrees(gesture.anchor, rawHandle)
|
|
3508
|
+
: rawHandle;
|
|
3509
|
+
const nextPath = gesture.closing
|
|
3510
|
+
? shapeClosingHandles(gesture.pathBefore!, moved ? handleOut : null)
|
|
3511
|
+
: appendPenNode(
|
|
3512
|
+
gesture.pathBefore,
|
|
3513
|
+
moved
|
|
3514
|
+
? createSmoothNode(gesture.anchor, handleOut, {
|
|
3515
|
+
breakSymmetry: e.altKey,
|
|
3516
|
+
})
|
|
3517
|
+
: createCornerNode(gesture.anchor),
|
|
3518
|
+
);
|
|
3519
|
+
penGestureRef.current = null;
|
|
3520
|
+
setPenGesturePreview(null);
|
|
3521
|
+
setPenPointer(null);
|
|
3522
|
+
setPenCloseHover(false);
|
|
3523
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
3524
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
3525
|
+
}
|
|
3526
|
+
if (gesture.closing) {
|
|
3527
|
+
finishPenPath(nextPath);
|
|
3528
|
+
} else {
|
|
3529
|
+
updatePenPath(nextPath);
|
|
3530
|
+
}
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3654
3533
|
const current = dragRef.current;
|
|
3655
3534
|
if (!current || current.pointerId !== e.pointerId) return;
|
|
3656
3535
|
dragRef.current = null;
|
|
3657
3536
|
setDrag(null);
|
|
3658
3537
|
emit(current);
|
|
3659
3538
|
},
|
|
3660
|
-
[emit],
|
|
3539
|
+
[emit, finishPenPath, tool, toContentPoint, updatePenPath],
|
|
3661
3540
|
);
|
|
3662
3541
|
|
|
3663
3542
|
const handlePointerCancel = useCallback(
|
|
3664
3543
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
3544
|
+
if (tool === "pen") {
|
|
3545
|
+
const gesture = penGestureRef.current;
|
|
3546
|
+
if (!gesture || gesture.pointerId !== e.pointerId) return;
|
|
3547
|
+
penGestureRef.current = null;
|
|
3548
|
+
updatePenPath(gesture.pathBefore);
|
|
3549
|
+
setPenGesturePreview(null);
|
|
3550
|
+
setPenPointer(null);
|
|
3551
|
+
setPenCloseHover(false);
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3665
3554
|
const current = dragRef.current;
|
|
3666
3555
|
if (!current || current.pointerId !== e.pointerId) return;
|
|
3667
3556
|
dragRef.current = null;
|
|
3668
3557
|
setDrag(null);
|
|
3669
3558
|
},
|
|
3670
|
-
[],
|
|
3559
|
+
[tool, updatePenPath],
|
|
3671
3560
|
);
|
|
3672
3561
|
|
|
3562
|
+
useEffect(() => {
|
|
3563
|
+
if (tool !== "pen") return;
|
|
3564
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3565
|
+
const path = penPathRef.current;
|
|
3566
|
+
if (!path) return;
|
|
3567
|
+
const target = event.target;
|
|
3568
|
+
if (
|
|
3569
|
+
target instanceof HTMLElement &&
|
|
3570
|
+
(target.isContentEditable ||
|
|
3571
|
+
target.tagName === "INPUT" ||
|
|
3572
|
+
target.tagName === "TEXTAREA")
|
|
3573
|
+
) {
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
const primary = event.metaKey || event.ctrlKey;
|
|
3578
|
+
const undo =
|
|
3579
|
+
primary && !event.shiftKey && event.key.toLowerCase() === "z";
|
|
3580
|
+
const removeSegment =
|
|
3581
|
+
!primary && (event.key === "Delete" || event.key === "Backspace");
|
|
3582
|
+
if (undo || removeSegment) {
|
|
3583
|
+
event.preventDefault();
|
|
3584
|
+
event.stopPropagation();
|
|
3585
|
+
event.stopImmediatePropagation();
|
|
3586
|
+
if (penGestureRef.current) return;
|
|
3587
|
+
const remainingNodes = path.nodes.slice(0, -1);
|
|
3588
|
+
if (remainingNodes.length === 0) {
|
|
3589
|
+
clearPenPath();
|
|
3590
|
+
} else {
|
|
3591
|
+
updatePenPath({ nodes: remainingNodes, closed: false });
|
|
3592
|
+
setPenGesturePreview(null);
|
|
3593
|
+
setPenPointer(null);
|
|
3594
|
+
setPenCloseHover(false);
|
|
3595
|
+
}
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
if (primary || (event.key !== "Enter" && event.key !== "Escape")) {
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
event.preventDefault();
|
|
3603
|
+
event.stopPropagation();
|
|
3604
|
+
event.stopImmediatePropagation();
|
|
3605
|
+
finishPenPath(path);
|
|
3606
|
+
};
|
|
3607
|
+
|
|
3608
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
3609
|
+
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
|
3610
|
+
}, [clearPenPath, finishPenPath, tool, updatePenPath]);
|
|
3611
|
+
|
|
3673
3612
|
const cursorClass = tool === "text" ? "cursor-text" : "cursor-crosshair";
|
|
3674
3613
|
const isLineTool = tool === "line" || tool === "arrow";
|
|
3614
|
+
const displayedPenPath =
|
|
3615
|
+
penGesturePreview ??
|
|
3616
|
+
(penPath && penPointer
|
|
3617
|
+
? penCloseHover
|
|
3618
|
+
? closePenPath(penPath)
|
|
3619
|
+
: appendPenNode(penPath, createCornerNode(penPointer))
|
|
3620
|
+
: penPath);
|
|
3675
3621
|
|
|
3676
3622
|
// Live drag preview, converted back from screen-content space to the
|
|
3677
3623
|
// overlay's own (unscaled, layout-space) coordinates. The overlay div is
|
|
@@ -3700,7 +3646,26 @@ function SingleScreenCreationOverlay({
|
|
|
3700
3646
|
onPointerMove={handlePointerMove}
|
|
3701
3647
|
onPointerUp={handlePointerUp}
|
|
3702
3648
|
onPointerCancel={handlePointerCancel}
|
|
3649
|
+
onDoubleClick={(event) => {
|
|
3650
|
+
if (tool !== "pen" || !penPathRef.current) return;
|
|
3651
|
+
event.preventDefault();
|
|
3652
|
+
event.stopPropagation();
|
|
3653
|
+
finishPenPath(collapseDoubleClickPenAnchor(penPathRef.current));
|
|
3654
|
+
}}
|
|
3655
|
+
onPointerLeave={() => {
|
|
3656
|
+
if (tool === "pen" && !penGestureRef.current) {
|
|
3657
|
+
setPenPointer(null);
|
|
3658
|
+
setPenCloseHover(false);
|
|
3659
|
+
}
|
|
3660
|
+
}}
|
|
3703
3661
|
>
|
|
3662
|
+
{tool === "pen" && displayedPenPath ? (
|
|
3663
|
+
<SingleScreenPenPathOverlay
|
|
3664
|
+
path={displayedPenPath}
|
|
3665
|
+
closeHover={penCloseHover}
|
|
3666
|
+
iframeRef={iframeRef}
|
|
3667
|
+
/>
|
|
3668
|
+
) : null}
|
|
3704
3669
|
{previewRect ? (
|
|
3705
3670
|
<div
|
|
3706
3671
|
data-creation-preview-rect
|
|
@@ -3733,3 +3698,115 @@ function SingleScreenCreationOverlay({
|
|
|
3733
3698
|
</div>
|
|
3734
3699
|
);
|
|
3735
3700
|
}
|
|
3701
|
+
|
|
3702
|
+
function SingleScreenPenPathOverlay({
|
|
3703
|
+
path,
|
|
3704
|
+
closeHover,
|
|
3705
|
+
iframeRef,
|
|
3706
|
+
}: {
|
|
3707
|
+
path: PenPath;
|
|
3708
|
+
closeHover: boolean;
|
|
3709
|
+
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
3710
|
+
}) {
|
|
3711
|
+
const iframe = iframeRef.current;
|
|
3712
|
+
const rect = iframe?.getBoundingClientRect();
|
|
3713
|
+
const renderedScale =
|
|
3714
|
+
iframe && rect && iframe.clientWidth > 0
|
|
3715
|
+
? rect.width / iframe.clientWidth
|
|
3716
|
+
: 1;
|
|
3717
|
+
const chromeScale = 1 / (renderedScale || 1);
|
|
3718
|
+
const anchorSize = 8 * chromeScale;
|
|
3719
|
+
const handleSize = 6 * chromeScale;
|
|
3720
|
+
const pathData = serializePenPath(path);
|
|
3721
|
+
|
|
3722
|
+
return (
|
|
3723
|
+
<div
|
|
3724
|
+
data-pen-path-overlay
|
|
3725
|
+
className="pointer-events-none absolute inset-0 z-[90]"
|
|
3726
|
+
>
|
|
3727
|
+
<svg className="absolute inset-0 size-full overflow-visible">
|
|
3728
|
+
{path.nodes.map((node, index) => (
|
|
3729
|
+
<g key={`single-screen-pen-handles-${index}`}>
|
|
3730
|
+
{node.handleIn ? (
|
|
3731
|
+
<line
|
|
3732
|
+
x1={node.point.x}
|
|
3733
|
+
y1={node.point.y}
|
|
3734
|
+
x2={node.handleIn.x}
|
|
3735
|
+
y2={node.handleIn.y}
|
|
3736
|
+
stroke="var(--design-editor-accent-color)"
|
|
3737
|
+
strokeDasharray="3 3"
|
|
3738
|
+
strokeWidth={Math.max(1, chromeScale)}
|
|
3739
|
+
/>
|
|
3740
|
+
) : null}
|
|
3741
|
+
{node.handleOut ? (
|
|
3742
|
+
<line
|
|
3743
|
+
x1={node.point.x}
|
|
3744
|
+
y1={node.point.y}
|
|
3745
|
+
x2={node.handleOut.x}
|
|
3746
|
+
y2={node.handleOut.y}
|
|
3747
|
+
stroke="var(--design-editor-accent-color)"
|
|
3748
|
+
strokeDasharray="3 3"
|
|
3749
|
+
strokeWidth={Math.max(1, chromeScale)}
|
|
3750
|
+
/>
|
|
3751
|
+
) : null}
|
|
3752
|
+
</g>
|
|
3753
|
+
))}
|
|
3754
|
+
<path
|
|
3755
|
+
d={pathData}
|
|
3756
|
+
fill="none"
|
|
3757
|
+
stroke="rgba(255,255,255,0.95)"
|
|
3758
|
+
strokeLinecap="round"
|
|
3759
|
+
strokeLinejoin="round"
|
|
3760
|
+
strokeWidth={5 * chromeScale}
|
|
3761
|
+
/>
|
|
3762
|
+
<path
|
|
3763
|
+
d={pathData}
|
|
3764
|
+
fill="none"
|
|
3765
|
+
stroke="var(--design-editor-accent-color)"
|
|
3766
|
+
strokeLinecap="round"
|
|
3767
|
+
strokeLinejoin="round"
|
|
3768
|
+
strokeWidth={2 * chromeScale}
|
|
3769
|
+
/>
|
|
3770
|
+
</svg>
|
|
3771
|
+
{path.nodes.map((node, index) => (
|
|
3772
|
+
<span
|
|
3773
|
+
key={`single-screen-pen-anchor-${index}`}
|
|
3774
|
+
data-pen-anchor
|
|
3775
|
+
className={cn(
|
|
3776
|
+
"absolute rounded-[2px] border shadow-sm",
|
|
3777
|
+
index === 0 && closeHover
|
|
3778
|
+
? "scale-125 border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)] ring-4 ring-[var(--design-editor-selection-color)]"
|
|
3779
|
+
: "border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-contrast-color)]",
|
|
3780
|
+
)}
|
|
3781
|
+
style={{
|
|
3782
|
+
left: node.point.x - anchorSize / 2,
|
|
3783
|
+
top: node.point.y - anchorSize / 2,
|
|
3784
|
+
width: anchorSize,
|
|
3785
|
+
height: anchorSize,
|
|
3786
|
+
borderWidth: Math.max(1, chromeScale),
|
|
3787
|
+
}}
|
|
3788
|
+
/>
|
|
3789
|
+
))}
|
|
3790
|
+
{path.nodes.flatMap((node, index) =>
|
|
3791
|
+
[node.handleIn, node.handleOut].flatMap((handle, handleIndex) =>
|
|
3792
|
+
handle
|
|
3793
|
+
? [
|
|
3794
|
+
<span
|
|
3795
|
+
key={`single-screen-pen-handle-${index}-${handleIndex}`}
|
|
3796
|
+
data-pen-handle
|
|
3797
|
+
className="absolute rounded-full border border-[var(--design-editor-accent-color)] bg-background shadow-sm"
|
|
3798
|
+
style={{
|
|
3799
|
+
left: handle.x - handleSize / 2,
|
|
3800
|
+
top: handle.y - handleSize / 2,
|
|
3801
|
+
width: handleSize,
|
|
3802
|
+
height: handleSize,
|
|
3803
|
+
borderWidth: Math.max(1, chromeScale),
|
|
3804
|
+
}}
|
|
3805
|
+
/>,
|
|
3806
|
+
]
|
|
3807
|
+
: [],
|
|
3808
|
+
),
|
|
3809
|
+
)}
|
|
3810
|
+
</div>
|
|
3811
|
+
);
|
|
3812
|
+
}
|