@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { generateTabId } from "@agent-native/core/client";
|
|
2
|
+
|
|
3
|
+
/** Stable for the lifetime of this module, including editor component refreshes. */
|
|
4
|
+
export const TAB_ID = generateTabId();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persistence revisions live on a DesignEditor component instance. Give that
|
|
8
|
+
* instance its own operation source too: reusing module-stable TAB_ID after an
|
|
9
|
+
* editor remount would pair reset revision counters with the server's old
|
|
10
|
+
* high-watermark and make fresh saves look stale.
|
|
11
|
+
*/
|
|
12
|
+
export function createEditorSaveOperationSource(
|
|
13
|
+
tabId = TAB_ID,
|
|
14
|
+
editorInstanceId = generateTabId(),
|
|
15
|
+
): string {
|
|
16
|
+
return `${tabId}:save:${editorInstanceId}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Yjs origin tracked by the local undo manager. */
|
|
20
|
+
export const LOCAL_EDIT_ORIGIN = `${TAB_ID}:local`;
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DesignLeftPanel,
|
|
3
|
+
type DesignTool,
|
|
4
|
+
SHOW_DESIGN_CODE_LEFT_PANEL,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
function isStandaloneHttpUrl(value: string): boolean {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!/^https?:\/\//i.test(trimmed)) return false;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(trimmed);
|
|
12
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the source payload behind a localhost-backed Design screen.
|
|
20
|
+
*
|
|
21
|
+
* `design_files.content` intentionally stores the route URL, not its HTML.
|
|
22
|
+
* Writing that value to disk would replace an HTML file with the URL string,
|
|
23
|
+
* so HTML write-back is allowed only after the authenticated snapshot has
|
|
24
|
+
* populated the editor's live source model. CSS can use persisted content,
|
|
25
|
+
* but still fails closed if that payload is a URL-backed screen marker.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveLocalhostSourceWriteContent(args: {
|
|
28
|
+
extension: string;
|
|
29
|
+
persistedContent: string | null | undefined;
|
|
30
|
+
liveSnapshotHtml: string | null | undefined;
|
|
31
|
+
}): string | null {
|
|
32
|
+
const extension = args.extension.trim().toLowerCase();
|
|
33
|
+
const candidate =
|
|
34
|
+
extension === ".html" || extension === ".htm"
|
|
35
|
+
? args.liveSnapshotHtml
|
|
36
|
+
: extension === ".css"
|
|
37
|
+
? args.persistedContent
|
|
38
|
+
: null;
|
|
39
|
+
if (!candidate?.trim() || isStandaloneHttpUrl(candidate)) return null;
|
|
40
|
+
return candidate;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Flash-prevention routing for adopting an ALREADY-server-persisted content
|
|
45
|
+
* snapshot into the editor — the "onApplied host-sync" family: shader fill
|
|
46
|
+
* applies (`apply-shader-fill` → onShaderFillApplied), GLSL shader
|
|
47
|
+
* apply/remove/knob commits (GlslShaderPanel's read-source-file →
|
|
48
|
+
* apply-source-edit → onApplied), and component prop edits.
|
|
49
|
+
*
|
|
50
|
+
* These handlers previously passed `refreshPreview: true` for the active
|
|
51
|
+
* file, but in applyLocalContentUpdate that flag means "skip the in-place
|
|
52
|
+
* replace and force a full srcdoc rebuild" — a real iframe reload of the
|
|
53
|
+
* active screen (white flash, lost scroll/Alpine state, and an onload refire
|
|
54
|
+
* that re-runs screen measurement — observed in the wild as the shader-apply
|
|
55
|
+
* flash plus a zoom-badge drift in the same gesture). The patched content is
|
|
56
|
+
* a normal document update, exactly what the bridge's live in-place
|
|
57
|
+
* full-document replace exists for (see applyLocalContentUpdate's
|
|
58
|
+
* "Holistic flash pipeline" comment), so route it through
|
|
59
|
+
* `forcePreviewFullDocument` instead; applyLocalContentUpdate already falls
|
|
60
|
+
* back to the srcdoc rebuild on its own when the live bridge isn't
|
|
61
|
+
* registered for the surface yet.
|
|
62
|
+
*
|
|
63
|
+
* `persist: false` because the server already owns this content — the
|
|
64
|
+
* updatedAt stamp (when present) records it as the acked base for the next
|
|
65
|
+
* guarded update-file save rather than re-queueing a redundant save.
|
|
66
|
+
*/
|
|
67
|
+
export function getPersistedContentHostSyncOptions(args: {
|
|
68
|
+
fileId: string;
|
|
69
|
+
activeFileId: string | null | undefined;
|
|
70
|
+
updatedAt?: string;
|
|
71
|
+
}): {
|
|
72
|
+
forcePreviewFullDocument: boolean;
|
|
73
|
+
persist: false;
|
|
74
|
+
updatedAt?: string;
|
|
75
|
+
} {
|
|
76
|
+
return {
|
|
77
|
+
forcePreviewFullDocument:
|
|
78
|
+
args.activeFileId !== null &&
|
|
79
|
+
args.activeFileId !== undefined &&
|
|
80
|
+
args.fileId === args.activeFileId,
|
|
81
|
+
persist: false,
|
|
82
|
+
updatedAt: args.updatedAt,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getDesignEditorShareUrl(
|
|
87
|
+
id: string,
|
|
88
|
+
origin: string,
|
|
89
|
+
basePath = "",
|
|
90
|
+
) {
|
|
91
|
+
const normalizedBasePath = basePath.replace(/\/+$/, "");
|
|
92
|
+
const pathname = normalizedBasePath
|
|
93
|
+
? `${normalizedBasePath}/design/${encodeURIComponent(id)}`
|
|
94
|
+
: `/design/${encodeURIComponent(id)}`;
|
|
95
|
+
return new URL(pathname, origin).toString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatDesignEditorUrlZoom(zoom: number): string {
|
|
99
|
+
const rounded = Math.round(zoom * 100) / 100;
|
|
100
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getDesignEditorStateUrlSearch(args: {
|
|
104
|
+
currentSearch: string;
|
|
105
|
+
viewMode: "single" | "overview";
|
|
106
|
+
screenId?: string | null;
|
|
107
|
+
codeFileId?: string | null;
|
|
108
|
+
codeFilename?: string | null;
|
|
109
|
+
selectionId?: string | null;
|
|
110
|
+
leftPanel?: DesignLeftPanel | null;
|
|
111
|
+
zoom?: number | null;
|
|
112
|
+
tool?: DesignTool | null;
|
|
113
|
+
}) {
|
|
114
|
+
const params = new URLSearchParams(args.currentSearch);
|
|
115
|
+
const leftPanel =
|
|
116
|
+
args.leftPanel === "code" && !SHOW_DESIGN_CODE_LEFT_PANEL
|
|
117
|
+
? null
|
|
118
|
+
: args.leftPanel;
|
|
119
|
+
params.set("view", args.viewMode);
|
|
120
|
+
if (leftPanel && leftPanel !== "file") {
|
|
121
|
+
params.set("panel", leftPanel);
|
|
122
|
+
} else {
|
|
123
|
+
params.delete("panel");
|
|
124
|
+
}
|
|
125
|
+
if (args.screenId) {
|
|
126
|
+
params.set("screen", args.screenId);
|
|
127
|
+
} else {
|
|
128
|
+
params.delete("screen");
|
|
129
|
+
}
|
|
130
|
+
if (leftPanel === "code" && args.codeFileId) {
|
|
131
|
+
params.set("fileId", args.codeFileId);
|
|
132
|
+
} else {
|
|
133
|
+
params.delete("fileId");
|
|
134
|
+
}
|
|
135
|
+
if (leftPanel === "code" && !args.codeFileId && args.codeFilename) {
|
|
136
|
+
params.set("filename", args.codeFilename);
|
|
137
|
+
} else {
|
|
138
|
+
params.delete("filename");
|
|
139
|
+
}
|
|
140
|
+
if (args.selectionId) {
|
|
141
|
+
params.set("selection", args.selectionId);
|
|
142
|
+
} else {
|
|
143
|
+
params.delete("selection");
|
|
144
|
+
}
|
|
145
|
+
if (typeof args.zoom === "number" && Number.isFinite(args.zoom)) {
|
|
146
|
+
params.set("zoom", formatDesignEditorUrlZoom(args.zoom));
|
|
147
|
+
} else {
|
|
148
|
+
params.delete("zoom");
|
|
149
|
+
}
|
|
150
|
+
// Keep the shareable navigation mirror aligned with the tool the editor is
|
|
151
|
+
// actually using. `move` is the default and stays implicit so ordinary
|
|
152
|
+
// editor URLs remain compact; every other tool round-trips explicitly.
|
|
153
|
+
if (args.tool && args.tool !== "move") {
|
|
154
|
+
params.set("tool", args.tool);
|
|
155
|
+
} else {
|
|
156
|
+
params.delete("tool");
|
|
157
|
+
}
|
|
158
|
+
const query = params.toString();
|
|
159
|
+
return query ? `?${query}` : "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getLocalhostRouteSourceFile(args: {
|
|
163
|
+
sourceFile?: string;
|
|
164
|
+
source?: string;
|
|
165
|
+
}): string | undefined {
|
|
166
|
+
if (args.sourceFile?.trim()) return args.sourceFile;
|
|
167
|
+
const raw = args.source;
|
|
168
|
+
if (!raw) return undefined;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
171
|
+
if (
|
|
172
|
+
parsed &&
|
|
173
|
+
typeof parsed === "object" &&
|
|
174
|
+
"file" in parsed &&
|
|
175
|
+
typeof (parsed as Record<string, unknown>).file === "string"
|
|
176
|
+
) {
|
|
177
|
+
return (parsed as Record<string, string>).file;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
if (raw.length > 0) return raw;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getLayerMoveSourceContent(args: {
|
|
186
|
+
sourceFileId: string;
|
|
187
|
+
activeFileId?: string | null;
|
|
188
|
+
activeContent: string;
|
|
189
|
+
sourceFileContent?: string;
|
|
190
|
+
sourceContentMap: ReadonlyMap<string, string>;
|
|
191
|
+
}) {
|
|
192
|
+
return (
|
|
193
|
+
args.sourceContentMap.get(args.sourceFileId) ??
|
|
194
|
+
(args.sourceFileId === args.activeFileId
|
|
195
|
+
? args.activeContent
|
|
196
|
+
: args.sourceFileContent) ??
|
|
197
|
+
""
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getFreshActiveFileContent(args: {
|
|
202
|
+
activeContent: string;
|
|
203
|
+
latestContent?: string | null;
|
|
204
|
+
lastLocalContent?: string | null;
|
|
205
|
+
}) {
|
|
206
|
+
return args.latestContent ?? args.lastLocalContent ?? args.activeContent;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getFreshScreenContent(args: {
|
|
210
|
+
screenId: string;
|
|
211
|
+
activeFileId?: string | null;
|
|
212
|
+
freshActiveContentFileId?: string | null;
|
|
213
|
+
freshActiveContent: string;
|
|
214
|
+
fileContentById: ReadonlyMap<string, string>;
|
|
215
|
+
/**
|
|
216
|
+
* Nest-conversion clobber fix: the freshest same-tick local write for this
|
|
217
|
+
* screen (DesignEditor's `pendingLocalFileContentsRef`), or null. The
|
|
218
|
+
* `fileContentById` map is memoized off React state, so a write made by
|
|
219
|
+
* `applyFileContentUpdate` earlier in the SAME task burst (e.g. the bridge's
|
|
220
|
+
* auto-layout conversion `visual-style-change` that lands right before the
|
|
221
|
+
* nest-drop's `visual-structure-change`) is not visible in it yet — the
|
|
222
|
+
* structure handler would rebase off pre-conversion content and its own
|
|
223
|
+
* write would silently clobber the conversion. Preferring the synchronous
|
|
224
|
+
* pending write mirrors what the active file already gets through
|
|
225
|
+
* `getFreshActiveFileContent`'s latest/lastLocal refs.
|
|
226
|
+
*/
|
|
227
|
+
pendingContent?: string | null;
|
|
228
|
+
}) {
|
|
229
|
+
const freshActiveContentFileId =
|
|
230
|
+
args.freshActiveContentFileId ?? args.activeFileId;
|
|
231
|
+
if (
|
|
232
|
+
args.screenId === args.activeFileId &&
|
|
233
|
+
args.screenId === freshActiveContentFileId
|
|
234
|
+
) {
|
|
235
|
+
return args.freshActiveContent;
|
|
236
|
+
}
|
|
237
|
+
return args.pendingContent ?? args.fileContentById.get(args.screenId) ?? "";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function shouldReplacePreviewAfterVisualStyleCommit(args: {
|
|
241
|
+
runtimeApplied?: boolean;
|
|
242
|
+
runtimeStyleApplied: boolean;
|
|
243
|
+
}) {
|
|
244
|
+
return !args.runtimeApplied && !args.runtimeStyleApplied;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* PF12: decide whether a style change from EditPanel (ScrubInput scrub tick /
|
|
249
|
+
* DesignColorPicker drag tick) should skip the expensive source commit
|
|
250
|
+
* (commitVisualStyles / commitStylesToSelectedLayers — projection parse, HTML
|
|
251
|
+
* patch, Yjs write, history entry) and instead only try the cheap live
|
|
252
|
+
* iframe preview.
|
|
253
|
+
*
|
|
254
|
+
* Only a genuine mid-gesture "preview" tick against a single selected element
|
|
255
|
+
* qualifies: `meta` is undefined for every existing call site that never
|
|
256
|
+
* passed gesture metadata (keyboard edits, agent edits, discrete commits),
|
|
257
|
+
* so this returns false for all of them — preserving prior full-commit
|
|
258
|
+
* behavior exactly. Multi-layer-selection edits (`selectedLayerCount > 1`)
|
|
259
|
+
* have no equivalent cheap multi-element preview channel, so those
|
|
260
|
+
* conservatively keep committing on every tick, same as before PF12.
|
|
261
|
+
*/
|
|
262
|
+
export function shouldSkipVisualStyleCommitForPreview(args: {
|
|
263
|
+
phase?: "preview" | "commit";
|
|
264
|
+
selectedLayerCount: number;
|
|
265
|
+
}): boolean {
|
|
266
|
+
return args.phase === "preview" && args.selectedLayerCount <= 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Unit-aware relative-delta application for a per-node multi-select style
|
|
271
|
+
* commit (see commitRelativeStyleDeltaToSelectedLayers). ScrubInput's arrow-
|
|
272
|
+
* key step and the future `relativeDelta` meta both operate on a plain
|
|
273
|
+
* unitless number in the *same numeric domain* the field displays (px count,
|
|
274
|
+
* degree count, raw opacity/line-height number) — the CSS unit suffix is
|
|
275
|
+
* purely a serialization detail added back on here, matching how
|
|
276
|
+
* formatScrubValue/EditPanel already build the CSS string for a single-target
|
|
277
|
+
* commit.
|
|
278
|
+
*
|
|
279
|
+
* Parses the leading numeric portion of `currentValue` (e.g. "12px" -> 12,
|
|
280
|
+
* "45deg" -> 45, "0.5" -> 0.5), adds `delta`, and re-serializes with
|
|
281
|
+
* whatever unit suffix (if any) the ORIGINAL value used — so a per-node
|
|
282
|
+
* relative nudge preserves that node's own unit instead of assuming every
|
|
283
|
+
* selected node shares the same one. Returns `null` when `currentValue`
|
|
284
|
+
* doesn't parse as a leading number (e.g. a keyword like "auto" or "none"),
|
|
285
|
+
* so the caller can skip that node rather than writing garbage.
|
|
286
|
+
*/
|
|
287
|
+
export function applyRelativeDeltaToStyleValue(
|
|
288
|
+
currentValue: string | undefined,
|
|
289
|
+
delta: number,
|
|
290
|
+
): string | null {
|
|
291
|
+
if (typeof currentValue !== "string") return null;
|
|
292
|
+
const match = currentValue.trim().match(/^(-?\d*\.?\d+)(.*)$/);
|
|
293
|
+
if (!match) return null;
|
|
294
|
+
const [, numeric, unit] = match;
|
|
295
|
+
const base = Number(numeric);
|
|
296
|
+
if (!Number.isFinite(base)) return null;
|
|
297
|
+
const next = base + delta;
|
|
298
|
+
// Match formatScrubValue's collapse of float noise (e.g. avoid
|
|
299
|
+
// "12.000000000000002px" from repeated float addition) without imposing a
|
|
300
|
+
// fixed precision the property doesn't use.
|
|
301
|
+
const rounded = Math.round(next * 1e6) / 1e6;
|
|
302
|
+
return `${Object.is(rounded, -0) ? 0 : rounded}${unit ?? ""}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function getLayerMoveIterationOrder<T>(
|
|
306
|
+
orderedIds: readonly T[],
|
|
307
|
+
placement: "before" | "after" | "inside",
|
|
308
|
+
): T[] {
|
|
309
|
+
return placement === "after" ? [...orderedIds].reverse() : [...orderedIds];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function removeUndoRedoOrderKind<T extends string>(
|
|
313
|
+
order: readonly T[],
|
|
314
|
+
kind: T,
|
|
315
|
+
): T[] {
|
|
316
|
+
return order.filter((entry) => entry !== kind);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export type UndoRedoOrderKind =
|
|
320
|
+
| "content"
|
|
321
|
+
| "file-content"
|
|
322
|
+
| "geometry"
|
|
323
|
+
| "file-created";
|
|
324
|
+
|
|
325
|
+
export function getUndoRedoPriorityOrder(
|
|
326
|
+
preferred: UndoRedoOrderKind | undefined,
|
|
327
|
+
): UndoRedoOrderKind[] {
|
|
328
|
+
if (preferred === "geometry") return ["geometry", "content", "file-content"];
|
|
329
|
+
if (preferred === "file-content")
|
|
330
|
+
return ["file-content", "content", "geometry"];
|
|
331
|
+
return ["content", "file-content", "geometry"];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface FileContentSaveRequest {
|
|
335
|
+
id: string;
|
|
336
|
+
content: string;
|
|
337
|
+
syncCollab: boolean;
|
|
338
|
+
/** Stable browser-tab identity used to order normal and keepalive saves. */
|
|
339
|
+
operationSource: string;
|
|
340
|
+
/** Monotonic per-file sequence allocated when the edit enters the queue. */
|
|
341
|
+
operationRevision: number;
|
|
342
|
+
/**
|
|
343
|
+
* Optimistic-concurrency guard forwarded to update-file's
|
|
344
|
+
* `expectedVersionHash` param — the last content hash this client knows
|
|
345
|
+
* the server/collab doc holds for this file. Populated by callers when
|
|
346
|
+
* known (lastAckedFileContentHashRef); omitted when unknown so update-file
|
|
347
|
+
* keeps its legacy unguarded-write behavior.
|
|
348
|
+
*/
|
|
349
|
+
expectedVersionHash?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A completed save may retire the unload retry only when it still represents
|
|
354
|
+
* the latest content queued for that file. A newer edit can enter the debounce
|
|
355
|
+
* slot while an older request is in flight and must remain eligible for the
|
|
356
|
+
* pagehide keepalive.
|
|
357
|
+
*/
|
|
358
|
+
export function shouldClearLatestUnloadSave(
|
|
359
|
+
latest: FileContentSaveRequest | undefined,
|
|
360
|
+
completed: FileContentSaveRequest,
|
|
361
|
+
skippedStaleMirror = false,
|
|
362
|
+
): boolean {
|
|
363
|
+
return Boolean(
|
|
364
|
+
!skippedStaleMirror &&
|
|
365
|
+
latest &&
|
|
366
|
+
latest.id === completed.id &&
|
|
367
|
+
latest.content === completed.content &&
|
|
368
|
+
latest.syncCollab === completed.syncCollab &&
|
|
369
|
+
latest.operationSource === completed.operationSource &&
|
|
370
|
+
latest.operationRevision === completed.operationRevision,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Flushes debounce slots through the ordinary serialized mutation path during
|
|
376
|
+
* React cleanup (route/design changes). Real document unload is handled by the
|
|
377
|
+
* pagehide keepalive; normal navigation must not fire-and-forget or discard
|
|
378
|
+
* the final edit.
|
|
379
|
+
*/
|
|
380
|
+
export function flushPendingFileContentSavesOnCleanup(
|
|
381
|
+
pendingByFileId: Readonly<Record<string, FileContentSaveRequest>>,
|
|
382
|
+
timerIds: readonly number[],
|
|
383
|
+
save: (pending: FileContentSaveRequest) => void,
|
|
384
|
+
clearTimer: (timerId: number) => void,
|
|
385
|
+
): void {
|
|
386
|
+
for (const pending of Object.values(pendingByFileId)) save(pending);
|
|
387
|
+
for (const timerId of timerIds) clearTimer(timerId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Pure decision helper (exported for unit testing) for the pagehide/unload
|
|
392
|
+
* keepalive: should it be sent at all?
|
|
393
|
+
*
|
|
394
|
+
* The keepalive posts a full-document `content` write with `keepalive: true`.
|
|
395
|
+
* Its operation source/revision prevents older same-tab requests from
|
|
396
|
+
* overwriting it, but there is no round trip to recover from a cross-writer
|
|
397
|
+
* hash rejection. When collab is live (this pending save's `syncCollab` is
|
|
398
|
+
* false) and we don't have a known acked hash, skip it: the Yjs document
|
|
399
|
+
* already holds the current content, while an unguarded full replacement
|
|
400
|
+
* could still conflict with a different writer. Every other combination
|
|
401
|
+
* (collab not live, or a hash IS known) sends the versioned keepalive.
|
|
402
|
+
*/
|
|
403
|
+
export function shouldSendKeepalive(
|
|
404
|
+
hashKnown: boolean,
|
|
405
|
+
collabLive: boolean,
|
|
406
|
+
): boolean {
|
|
407
|
+
return hashKnown || !collabLive;
|
|
408
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-chrome overlays that editor-chrome.bridge.ts appends inside the preview
|
|
3
|
+
* iframe (the selection outline + resize handles, hover highlight, marquee,
|
|
4
|
+
* spacing/measurement guides, and badges). They live in the iframe DOM, so image
|
|
5
|
+
* exports must strip them from the clone — otherwise a download captures the
|
|
6
|
+
* editor's selection outline instead of just the design. Keep this in sync with
|
|
7
|
+
* the data-agent-native-* markers set in editor-chrome.bridge.ts.
|
|
8
|
+
*/
|
|
9
|
+
export const EDITOR_CHROME_OVERLAY_SELECTOR = [
|
|
10
|
+
"[data-agent-native-edit-overlay]",
|
|
11
|
+
"[data-agent-native-edit-handle]",
|
|
12
|
+
"[data-agent-native-edge-handle]",
|
|
13
|
+
"[data-agent-native-rotate-handle]",
|
|
14
|
+
"[data-agent-native-transform-badge]",
|
|
15
|
+
"[data-agent-native-spacing-badge]",
|
|
16
|
+
"[data-agent-native-spacing-overlay]",
|
|
17
|
+
"[data-agent-native-spacing-line]",
|
|
18
|
+
"[data-agent-native-spacing-region]",
|
|
19
|
+
"[data-agent-native-insertion-guide]",
|
|
20
|
+
"[data-agent-native-measurement-overlay]",
|
|
21
|
+
].join(",");
|
|
22
|
+
|
|
23
|
+
export interface ExportCropRect {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Return the smallest document-space rectangle containing every valid item. */
|
|
31
|
+
export function unionExportCropRects(
|
|
32
|
+
rects: readonly ExportCropRect[],
|
|
33
|
+
): ExportCropRect | null {
|
|
34
|
+
const valid = rects.filter(
|
|
35
|
+
(rect) =>
|
|
36
|
+
Number.isFinite(rect.x) &&
|
|
37
|
+
Number.isFinite(rect.y) &&
|
|
38
|
+
Number.isFinite(rect.width) &&
|
|
39
|
+
Number.isFinite(rect.height) &&
|
|
40
|
+
rect.width > 0 &&
|
|
41
|
+
rect.height > 0,
|
|
42
|
+
);
|
|
43
|
+
if (valid.length === 0) return null;
|
|
44
|
+
const left = Math.min(...valid.map((rect) => rect.x));
|
|
45
|
+
const top = Math.min(...valid.map((rect) => rect.y));
|
|
46
|
+
const right = Math.max(...valid.map((rect) => rect.x + rect.width));
|
|
47
|
+
const bottom = Math.max(...valid.map((rect) => rect.y + rect.height));
|
|
48
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ExportCompositeFrame extends ExportCropRect {
|
|
52
|
+
rotation?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the world-space bounds of selected screen frames, including rotation,
|
|
57
|
+
* so a multi-frame clipboard image preserves the same spacing as the canvas.
|
|
58
|
+
*/
|
|
59
|
+
export function getExportCompositeBounds(
|
|
60
|
+
frames: readonly ExportCompositeFrame[],
|
|
61
|
+
): ExportCropRect | null {
|
|
62
|
+
return unionExportCropRects(
|
|
63
|
+
frames.flatMap((frame) => {
|
|
64
|
+
if (frame.width <= 0 || frame.height <= 0) return [];
|
|
65
|
+
const radians = ((frame.rotation ?? 0) * Math.PI) / 180;
|
|
66
|
+
if (radians === 0) return [frame];
|
|
67
|
+
const centerX = frame.x + frame.width / 2;
|
|
68
|
+
const centerY = frame.y + frame.height / 2;
|
|
69
|
+
const cosine = Math.cos(radians);
|
|
70
|
+
const sine = Math.sin(radians);
|
|
71
|
+
const corners = [
|
|
72
|
+
[-frame.width / 2, -frame.height / 2],
|
|
73
|
+
[frame.width / 2, -frame.height / 2],
|
|
74
|
+
[frame.width / 2, frame.height / 2],
|
|
75
|
+
[-frame.width / 2, frame.height / 2],
|
|
76
|
+
].map(([x, y]) => ({
|
|
77
|
+
x: centerX + x * cosine - y * sine,
|
|
78
|
+
y: centerY + x * sine + y * cosine,
|
|
79
|
+
}));
|
|
80
|
+
const left = Math.min(...corners.map((corner) => corner.x));
|
|
81
|
+
const top = Math.min(...corners.map((corner) => corner.y));
|
|
82
|
+
const right = Math.max(...corners.map((corner) => corner.x));
|
|
83
|
+
const bottom = Math.max(...corners.map((corner) => corner.y));
|
|
84
|
+
return [{ x: left, y: top, width: right - left, height: bottom - top }];
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map a document-space rect onto pixel coordinates within a rendered canvas of
|
|
91
|
+
* the given size, clamped to stay inside the canvas. `scale` must match the
|
|
92
|
+
* scale passed to html2canvas. Returns null when the crop would be empty or
|
|
93
|
+
* lands fully outside the canvas, so callers can fall back to the full render.
|
|
94
|
+
*/
|
|
95
|
+
export function computeExportCropBox(
|
|
96
|
+
sourceWidth: number,
|
|
97
|
+
sourceHeight: number,
|
|
98
|
+
rect: { x: number; y: number; width: number; height: number },
|
|
99
|
+
scale: number,
|
|
100
|
+
): { sx: number; sy: number; sw: number; sh: number } | null {
|
|
101
|
+
const sx = Math.max(0, Math.round(rect.x * scale));
|
|
102
|
+
const sy = Math.max(0, Math.round(rect.y * scale));
|
|
103
|
+
const right = Math.min(
|
|
104
|
+
sourceWidth,
|
|
105
|
+
Math.round((rect.x + rect.width) * scale),
|
|
106
|
+
);
|
|
107
|
+
const bottom = Math.min(
|
|
108
|
+
sourceHeight,
|
|
109
|
+
Math.round((rect.y + rect.height) * scale),
|
|
110
|
+
);
|
|
111
|
+
const sw = right - sx;
|
|
112
|
+
const sh = bottom - sy;
|
|
113
|
+
if (sw <= 0 || sh <= 0) return null;
|
|
114
|
+
return { sx, sy, sw, sh };
|
|
115
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanvasFrameGeometry,
|
|
3
|
+
CanvasFrameGeometryById,
|
|
4
|
+
} from "@shared/canvas-frames";
|
|
5
|
+
|
|
6
|
+
export function frameGeometryEquals(
|
|
7
|
+
a: CanvasFrameGeometry | undefined,
|
|
8
|
+
b: CanvasFrameGeometry | undefined,
|
|
9
|
+
): boolean {
|
|
10
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function geometrySnapshotsEqual(
|
|
14
|
+
a: CanvasFrameGeometryById,
|
|
15
|
+
b: CanvasFrameGeometryById,
|
|
16
|
+
): boolean {
|
|
17
|
+
if (a === b) return true;
|
|
18
|
+
const aKeys = Object.keys(a);
|
|
19
|
+
const bKeys = Object.keys(b);
|
|
20
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
21
|
+
return aKeys.every((key) => key in b && frameGeometryEquals(a[key], b[key]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const MAX_SANE_FRAME_DIMENSION_PX = 100000;
|
|
25
|
+
export const MAX_SANE_FRAME_ASPECT_RATIO = 50;
|
|
26
|
+
|
|
27
|
+
export function isSaneCanvasFrameGeometryForPersist(
|
|
28
|
+
geometry: CanvasFrameGeometry,
|
|
29
|
+
): boolean {
|
|
30
|
+
const numericFields = [
|
|
31
|
+
geometry.x,
|
|
32
|
+
geometry.y,
|
|
33
|
+
geometry.width,
|
|
34
|
+
geometry.height,
|
|
35
|
+
geometry.rotation,
|
|
36
|
+
geometry.z,
|
|
37
|
+
];
|
|
38
|
+
if (
|
|
39
|
+
numericFields.some(
|
|
40
|
+
(value) => value !== undefined && !Number.isFinite(value),
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const { width, height } = geometry;
|
|
46
|
+
if (
|
|
47
|
+
width !== undefined &&
|
|
48
|
+
(width <= 0 || width > MAX_SANE_FRAME_DIMENSION_PX)
|
|
49
|
+
) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
height !== undefined &&
|
|
54
|
+
(height <= 0 || height > MAX_SANE_FRAME_DIMENSION_PX)
|
|
55
|
+
) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (width !== undefined && height !== undefined) {
|
|
59
|
+
const aspect = Math.max(width / height, height / width);
|
|
60
|
+
if (aspect > MAX_SANE_FRAME_ASPECT_RATIO) return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sanitizeCanvasFrameGeometryForPersist(
|
|
66
|
+
nextById: CanvasFrameGeometryById,
|
|
67
|
+
previousById: CanvasFrameGeometryById,
|
|
68
|
+
exemptFrameIds: readonly string[] = [],
|
|
69
|
+
): { geometryById: CanvasFrameGeometryById; rejectedFrameIds: string[] } {
|
|
70
|
+
const rejectedFrameIds: string[] = [];
|
|
71
|
+
let sanitized: CanvasFrameGeometryById | null = null;
|
|
72
|
+
for (const [frameId, geometry] of Object.entries(nextById)) {
|
|
73
|
+
if (exemptFrameIds.includes(frameId)) continue;
|
|
74
|
+
if (isSaneCanvasFrameGeometryForPersist(geometry)) continue;
|
|
75
|
+
rejectedFrameIds.push(frameId);
|
|
76
|
+
if (sanitized === null) sanitized = { ...nextById };
|
|
77
|
+
const previous = previousById[frameId];
|
|
78
|
+
console.warn(
|
|
79
|
+
"[design] rejected insane canvas frame geometry on persist — reverted to previous geometry",
|
|
80
|
+
{
|
|
81
|
+
frameId,
|
|
82
|
+
rejected: geometry,
|
|
83
|
+
revertedTo: previous ?? null,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
if (previous && isSaneCanvasFrameGeometryForPersist(previous)) {
|
|
87
|
+
sanitized[frameId] = { ...previous };
|
|
88
|
+
} else {
|
|
89
|
+
delete sanitized[frameId];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { geometryById: sanitized ?? nextById, rejectedFrameIds };
|
|
93
|
+
}
|