@agent-native/core 0.92.7 → 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 +12 -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/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/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/struct-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/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- 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,43 @@
|
|
|
1
|
+
import { shouldUseLiveFileContent } from "@shared/html-content";
|
|
2
|
+
|
|
3
|
+
export function shouldRebaseCollabDocFromStoredContent({
|
|
4
|
+
liveContent,
|
|
5
|
+
storedContent,
|
|
6
|
+
storedUpdatedAt,
|
|
7
|
+
lastAppliedUpdatedAt,
|
|
8
|
+
fileType,
|
|
9
|
+
}: {
|
|
10
|
+
liveContent: string;
|
|
11
|
+
storedContent: string;
|
|
12
|
+
storedUpdatedAt: string | null | undefined;
|
|
13
|
+
lastAppliedUpdatedAt: string | null;
|
|
14
|
+
fileType: string;
|
|
15
|
+
}): boolean {
|
|
16
|
+
if (liveContent === storedContent) return false;
|
|
17
|
+
if (
|
|
18
|
+
!shouldUseLiveFileContent({
|
|
19
|
+
liveContent,
|
|
20
|
+
storedContent,
|
|
21
|
+
fileType,
|
|
22
|
+
})
|
|
23
|
+
) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (fileType.toLowerCase() !== "html") return false;
|
|
27
|
+
if (!lastAppliedUpdatedAt) return !!storedUpdatedAt;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveScreenCollabSyncTarget({
|
|
32
|
+
fileId,
|
|
33
|
+
overviewPresenceFileId,
|
|
34
|
+
overviewDocConnected,
|
|
35
|
+
}: {
|
|
36
|
+
fileId: string;
|
|
37
|
+
overviewPresenceFileId: string | null;
|
|
38
|
+
overviewDocConnected: boolean;
|
|
39
|
+
}): { writeLiveDoc: boolean; syncCollab: boolean } {
|
|
40
|
+
const writeLiveDoc =
|
|
41
|
+
overviewDocConnected && overviewPresenceFileId === fileId;
|
|
42
|
+
return { writeLiveDoc, syncCollab: !writeLiveDoc };
|
|
43
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { parseCssColorExtended } from "@shared/color-utils";
|
|
2
|
+
|
|
3
|
+
export const BOARD_TEXT_AUTO_COLOR_MARKER = "data-an-auto-text-color";
|
|
4
|
+
|
|
5
|
+
// Relative luminance threshold mirroring containerBackgroundIsLight in
|
|
6
|
+
// editor-chrome.bridge.ts (keep both in sync) — used by the pure decision
|
|
7
|
+
// helper below so its threshold can't drift from the in-screen bridge path.
|
|
8
|
+
const AUTO_TEXT_COLOR_LIGHT_LUMINANCE_THRESHOLD = 150;
|
|
9
|
+
|
|
10
|
+
function relativeLuminance(rgb: { r: number; g: number; b: number }): number {
|
|
11
|
+
return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Tolerant "is this the board's auto-applied default white" check, shared
|
|
15
|
+
// by shouldAdaptAutoTextColorForCrossScreenMove and
|
|
16
|
+
// isStaleAutoTextColorMarker (finding 2) — both need the exact same
|
|
17
|
+
// definition of "still the auto-default", just applied to different
|
|
18
|
+
// questions (safe to adapt vs. safe to keep trusting the marker).
|
|
19
|
+
function isAutoDefaultWhiteColor(
|
|
20
|
+
inlineColor: string | null | undefined,
|
|
21
|
+
): boolean {
|
|
22
|
+
const normalized = (inlineColor || "")
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/\s+/g, " ");
|
|
26
|
+
return (
|
|
27
|
+
normalized === "#ffffff" ||
|
|
28
|
+
normalized === "#fff" ||
|
|
29
|
+
normalized === "rgb(255,255,255)" ||
|
|
30
|
+
normalized === "rgb(255, 255, 255)" ||
|
|
31
|
+
normalized === "white"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Finding 2(a): BOARD_TEXT_AUTO_COLOR_MARKER is stamped once at board-text
|
|
37
|
+
* creation time and never removed by the bridge, so it goes stale the
|
|
38
|
+
* moment a user explicitly recolors previously-auto-white text (e.g. picks
|
|
39
|
+
* a brand color) without the marker being cleared in the same edit. Trusting
|
|
40
|
+
* a stale marker unconditionally (the previous behavior) forced that
|
|
41
|
+
* deliberately-chosen color back to `inherit` on the next reparent/
|
|
42
|
+
* cross-screen move. A marker is only trustworthy evidence of "this color is
|
|
43
|
+
* still auto-applied" when the node's CURRENT inline color is still exactly
|
|
44
|
+
* the auto-default white — any other color means the user (or some other
|
|
45
|
+
* edit) changed it after the marker was stamped, so the marker is stale and
|
|
46
|
+
* must not be honored (callers should also strip it — see
|
|
47
|
+
* stripStaleAutoTextColorMarkerFromHtml below).
|
|
48
|
+
*/
|
|
49
|
+
export function isStaleAutoTextColorMarker(params: {
|
|
50
|
+
inlineColor: string | null | undefined;
|
|
51
|
+
hasAutoMarker: boolean;
|
|
52
|
+
}): boolean {
|
|
53
|
+
return params.hasAutoMarker && !isAutoDefaultWhiteColor(params.inlineColor);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pure decision: should a moved text node's auto-applied board color be
|
|
58
|
+
* rewritten to `inherit` once it lands in its cross-screen destination?
|
|
59
|
+
*
|
|
60
|
+
* Mirrors adaptAutoTextColorForNest's decision in editor-chrome.bridge.ts
|
|
61
|
+
* (keep both in sync), adapted to the host's HTML-string world instead of a
|
|
62
|
+
* live DOM re-parent check — the cross-screen drop path
|
|
63
|
+
* (handleCrossScreenElementDrop) always represents an actual re-parent (the
|
|
64
|
+
* node moves from one document's body into another), so there is no
|
|
65
|
+
* same-parent short-circuit to mirror here.
|
|
66
|
+
*
|
|
67
|
+
* - `hasAutoMarker` AND the color is still the auto-default white: always
|
|
68
|
+
* safe to adapt regardless of the destination background — the color is
|
|
69
|
+
* definitely still auto-applied, not user-set (finding 2: a marker whose
|
|
70
|
+
* color has since diverged from white is STALE and must not short-circuit
|
|
71
|
+
* here — falls through to the same conservative heuristic as no marker).
|
|
72
|
+
* - No marker (pre-marker content, a node the stamp missed, or a stale
|
|
73
|
+
* marker whose color moved off white): fall back to the conservative
|
|
74
|
+
* default-white + light-destination heuristic so a deliberately-chosen
|
|
75
|
+
* color is never touched.
|
|
76
|
+
*/
|
|
77
|
+
export function shouldAdaptAutoTextColorForCrossScreenMove(params: {
|
|
78
|
+
inlineColor: string | null | undefined;
|
|
79
|
+
hasAutoMarker: boolean;
|
|
80
|
+
destinationBackgroundIsLight: boolean;
|
|
81
|
+
}): boolean {
|
|
82
|
+
const { inlineColor, hasAutoMarker, destinationBackgroundIsLight } = params;
|
|
83
|
+
const normalized = (inlineColor || "").trim().toLowerCase();
|
|
84
|
+
if (
|
|
85
|
+
!normalized ||
|
|
86
|
+
normalized === "inherit" ||
|
|
87
|
+
normalized === "currentcolor"
|
|
88
|
+
) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const isDefaultWhite = isAutoDefaultWhiteColor(inlineColor);
|
|
92
|
+
if (hasAutoMarker && isDefaultWhite) return true;
|
|
93
|
+
if (!isDefaultWhite) return false;
|
|
94
|
+
return destinationBackgroundIsLight;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pure decision for finding 1: given an ordered chain of background signals
|
|
99
|
+
* read from an element's ancestor chain (innermost first — same walk order
|
|
100
|
+
* `collectDestinationBackgroundSignals` below produces), decide whether the
|
|
101
|
+
* destination background is light.
|
|
102
|
+
*
|
|
103
|
+
* Each entry is either a CSS color string (from an inline `background`/
|
|
104
|
+
* `background-color` declaration, or a live `getComputedStyle().
|
|
105
|
+
* backgroundColor` read when a live document is available — see call site)
|
|
106
|
+
* or a `{ darkClassHint: true }` marker for a cheap utility-class signal
|
|
107
|
+
* (e.g. `bg-black`, `bg-gray-900`, `dark:bg-*`) when no inline/computed
|
|
108
|
+
* color was found on that same element. The first entry that resolves to a
|
|
109
|
+
* non-transparent (alpha >= 0.4) color wins; alpha below that threshold is
|
|
110
|
+
* treated as transparent so the walk keeps climbing instead of trusting a
|
|
111
|
+
* near-invisible tint. A dark-class hint only counts when no color signal
|
|
112
|
+
* was present on that element (a real color always wins over a guessed
|
|
113
|
+
* class). No signal anywhere in the chain conservatively reports "light" so
|
|
114
|
+
* the default-white heuristic can still fire and prevent invisible text.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveDestinationBackgroundLightness(
|
|
117
|
+
chain: ReadonlyArray<{ color: string | null } | { darkClassHint: boolean }>,
|
|
118
|
+
): boolean {
|
|
119
|
+
for (const entry of chain) {
|
|
120
|
+
if ("color" in entry && entry.color) {
|
|
121
|
+
const rgba = parseCssColorExtended(entry.color);
|
|
122
|
+
if (rgba && rgba.a >= 0.4) {
|
|
123
|
+
return (
|
|
124
|
+
relativeLuminance(rgba) > AUTO_TEXT_COLOR_LIGHT_LUMINANCE_THRESHOLD
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if ("darkClassHint" in entry && entry.darkClassHint) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cheap, best-effort utility-class signal for "this element's classes look
|
|
137
|
+
// like a dark background" — used only when an element carries no inline or
|
|
138
|
+
// computed background color at all (a real color signal always wins). Not
|
|
139
|
+
// an attempt to parse Tailwind's full config: just the small set of
|
|
140
|
+
// class-name shapes generated designs actually use for dark surfaces
|
|
141
|
+
// (`bg-black`, `bg-gray-900`, `bg-neutral-950`, `dark:bg-slate-900`, etc.).
|
|
142
|
+
const DARK_BACKGROUND_CLASS_RE =
|
|
143
|
+
/(?:^|:)bg-(?:black|(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:800|900|950))\b/;
|
|
144
|
+
|
|
145
|
+
function elementHasDarkBackgroundClassHint(element: Element): boolean {
|
|
146
|
+
const className =
|
|
147
|
+
typeof element.className === "string"
|
|
148
|
+
? element.className
|
|
149
|
+
: (element.getAttribute("class") ?? "");
|
|
150
|
+
return DARK_BACKGROUND_CLASS_RE.test(className);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds the ancestor background-signal chain `resolveDestinationBackgroundLightness`
|
|
155
|
+
* consumes, walking up from `element`. Prefers a LIVE document's computed
|
|
156
|
+
* style (real cascade, resolves stylesheet/utility classes correctly —
|
|
157
|
+
* mirrors the bridge's `containerBackgroundIsLight`) when `liveElement` is
|
|
158
|
+
* supplied (see `destinationBackgroundIsLightForNode`'s doc for how the
|
|
159
|
+
* live node is resolved); otherwise falls back to reading INLINE
|
|
160
|
+
* `background`/`background-color` declarations plus the dark-class-name
|
|
161
|
+
* heuristic on the detached parsed-doc element, since a DOMParser document
|
|
162
|
+
* has no `defaultView` in real browsers and can't run `getComputedStyle`.
|
|
163
|
+
*/
|
|
164
|
+
function collectDestinationBackgroundSignals(
|
|
165
|
+
element: Element,
|
|
166
|
+
liveElement: Element | null,
|
|
167
|
+
): Array<{ color: string | null } | { darkClassHint: boolean }> {
|
|
168
|
+
const chain: Array<{ color: string | null } | { darkClassHint: boolean }> =
|
|
169
|
+
[];
|
|
170
|
+
if (liveElement) {
|
|
171
|
+
const liveView = liveElement.ownerDocument?.defaultView;
|
|
172
|
+
let cursor: Element | null = liveElement;
|
|
173
|
+
while (
|
|
174
|
+
liveView &&
|
|
175
|
+
cursor &&
|
|
176
|
+
cursor !== liveElement.ownerDocument.documentElement
|
|
177
|
+
) {
|
|
178
|
+
chain.push({ color: liveView.getComputedStyle(cursor).backgroundColor });
|
|
179
|
+
cursor = cursor.parentElement;
|
|
180
|
+
}
|
|
181
|
+
return chain;
|
|
182
|
+
}
|
|
183
|
+
let cursor: Element | null = element;
|
|
184
|
+
while (cursor && cursor !== element.ownerDocument.documentElement) {
|
|
185
|
+
const inline = (cursor as HTMLElement).style;
|
|
186
|
+
const inlineColor = inline?.backgroundColor || inline?.background || null;
|
|
187
|
+
if (inlineColor) {
|
|
188
|
+
chain.push({ color: inlineColor });
|
|
189
|
+
} else {
|
|
190
|
+
chain.push({ darkClassHint: elementHasDarkBackgroundClassHint(cursor) });
|
|
191
|
+
}
|
|
192
|
+
cursor = cursor.parentElement;
|
|
193
|
+
}
|
|
194
|
+
return chain;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolves whether the destination background around `element` (a node in a
|
|
199
|
+
* DOMParser-parsed detached document) is light. Prefers reading the LIVE
|
|
200
|
+
* destination screen iframe's computed style for the corresponding node
|
|
201
|
+
* when `liveDoc` is supplied — MultiScreenCanvas mounts every visible
|
|
202
|
+
* screen as a same-origin iframe reachable via
|
|
203
|
+
* `[data-screen-iframe-id="<screenId>"]` (see `getPrimaryIframeId` in
|
|
204
|
+
* MultiScreenCanvas.tsx), so the real cascade (including Tailwind utility
|
|
205
|
+
* classes, `<style>` rules, `dark:` variants, etc.) is available exactly
|
|
206
|
+
* like the in-iframe bridge's `containerBackgroundIsLight` — that live path
|
|
207
|
+
* is what makes class-based dark destinations (not just inline-styled ones)
|
|
208
|
+
* resolve correctly. Falls back to the detached doc's inline-style +
|
|
209
|
+
* dark-class-hint chain when no live document is available (e.g. the
|
|
210
|
+
* destination screen isn't currently mounted).
|
|
211
|
+
*/
|
|
212
|
+
function destinationBackgroundIsLightForNode(
|
|
213
|
+
element: Element,
|
|
214
|
+
liveDoc?: Document | null,
|
|
215
|
+
): boolean {
|
|
216
|
+
try {
|
|
217
|
+
const liveElement =
|
|
218
|
+
liveDoc && element.hasAttribute("data-agent-native-node-id")
|
|
219
|
+
? liveDoc.querySelector(
|
|
220
|
+
`[data-agent-native-node-id="${CSS.escape(
|
|
221
|
+
element.getAttribute("data-agent-native-node-id") ?? "",
|
|
222
|
+
)}"]`,
|
|
223
|
+
)
|
|
224
|
+
: null;
|
|
225
|
+
const chain = collectDestinationBackgroundSignals(element, liveElement);
|
|
226
|
+
return resolveDestinationBackgroundLightness(chain);
|
|
227
|
+
} catch {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Cross-screen counterpart to adaptAutoTextColorForNest — runs HOST-SIDE
|
|
234
|
+
* after moveNodeBetweenDocuments has already re-parented the text node into
|
|
235
|
+
* `destContent`'s DOM (identified by `destNodeAttrId`). Board-drawn text
|
|
236
|
+
* carries an explicit inline `color:#ffffff` default (see
|
|
237
|
+
* defaultCanvasTextColor / appendCanvasPrimitiveToHtml) because
|
|
238
|
+
* `currentColor` would inherit black on the always-dark board; dropped into
|
|
239
|
+
* a light destination screen/container, that stale inline white is
|
|
240
|
+
* invisible white-on-white. The in-screen drag path already handles this
|
|
241
|
+
* via the bridge's adaptAutoTextColorForNest; this is the missing
|
|
242
|
+
* cross-screen mirror (finding 8).
|
|
243
|
+
*
|
|
244
|
+
* No-op (returns `content` unchanged) when the moved node isn't a text
|
|
245
|
+
* primitive, carries no color needing adaptation, or the DOM can't be
|
|
246
|
+
* parsed — always best-effort, never a hard requirement for the move to
|
|
247
|
+
* succeed.
|
|
248
|
+
*
|
|
249
|
+
* `liveDestDoc` (optional) is the destination screen's LIVE iframe document
|
|
250
|
+
* when it's currently mounted — see `destinationBackgroundIsLightForNode`'s
|
|
251
|
+
* doc comment for why callers should prefer passing it when available
|
|
252
|
+
* (correct resolution for class-based/cascaded dark backgrounds, not just
|
|
253
|
+
* inline-styled ones).
|
|
254
|
+
*/
|
|
255
|
+
export function adaptAutoTextColorForCrossScreenNode(
|
|
256
|
+
content: string,
|
|
257
|
+
destNodeAttrId: string,
|
|
258
|
+
liveDestDoc?: Document | null,
|
|
259
|
+
): string {
|
|
260
|
+
if (typeof window === "undefined" || !destNodeAttrId) return content;
|
|
261
|
+
try {
|
|
262
|
+
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
263
|
+
const moved = doc.querySelector(
|
|
264
|
+
`[data-agent-native-node-id="${CSS.escape(destNodeAttrId)}"]`,
|
|
265
|
+
);
|
|
266
|
+
if (!moved) return content;
|
|
267
|
+
const kind = (
|
|
268
|
+
moved.getAttribute("data-an-primitive") ||
|
|
269
|
+
moved.getAttribute("data-agent-native-primitive") ||
|
|
270
|
+
""
|
|
271
|
+
).toLowerCase();
|
|
272
|
+
if (kind !== "text") return content;
|
|
273
|
+
const el = moved as HTMLElement;
|
|
274
|
+
const hasAutoMarker = moved.hasAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
|
|
275
|
+
// Finding 2(a): a marker whose color has since diverged from the
|
|
276
|
+
// auto-default white is stale — strip it here (in addition to falling
|
|
277
|
+
// through to the conservative heuristic below) so it can't mislead a
|
|
278
|
+
// LATER move/commit into re-honoring it once the color happens to be
|
|
279
|
+
// reset back to white for unrelated reasons.
|
|
280
|
+
let markerStripped = false;
|
|
281
|
+
if (
|
|
282
|
+
isStaleAutoTextColorMarker({
|
|
283
|
+
inlineColor: el.style.color,
|
|
284
|
+
hasAutoMarker,
|
|
285
|
+
})
|
|
286
|
+
) {
|
|
287
|
+
moved.removeAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
|
|
288
|
+
markerStripped = true;
|
|
289
|
+
}
|
|
290
|
+
const shouldAdapt = shouldAdaptAutoTextColorForCrossScreenMove({
|
|
291
|
+
inlineColor: el.style.color,
|
|
292
|
+
hasAutoMarker: hasAutoMarker && !markerStripped,
|
|
293
|
+
destinationBackgroundIsLight: destinationBackgroundIsLightForNode(
|
|
294
|
+
moved,
|
|
295
|
+
liveDestDoc,
|
|
296
|
+
),
|
|
297
|
+
});
|
|
298
|
+
if (!shouldAdapt && !markerStripped) return content;
|
|
299
|
+
if (shouldAdapt) el.style.color = "inherit";
|
|
300
|
+
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
301
|
+
} catch {
|
|
302
|
+
return content;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Finding 2(b): the other half of the stale-marker fix. Stripping only
|
|
308
|
+
* happens lazily (finding 2(a), inside adaptAutoTextColorForCrossScreenNode)
|
|
309
|
+
* when a move/adapt pass actually re-reads the node — a user who sets an
|
|
310
|
+
* explicit color and never triggers another move/reparent would otherwise
|
|
311
|
+
* keep carrying a now-stale marker indefinitely. Called from
|
|
312
|
+
* commitVisualStyles whenever a "color" property is part of the committed
|
|
313
|
+
* style patch, so the marker is cleared at the moment the user's explicit
|
|
314
|
+
* choice is actually persisted, not just the next time something happens to
|
|
315
|
+
* re-check it. No-op (returns `content` unchanged) when the node has no
|
|
316
|
+
* marker, isn't found, or the DOM can't be parsed.
|
|
317
|
+
*/
|
|
318
|
+
export function clearAutoTextColorMarkerOnExplicitColorCommit(
|
|
319
|
+
content: string,
|
|
320
|
+
nodeId: string | null | undefined,
|
|
321
|
+
): string {
|
|
322
|
+
if (typeof window === "undefined" || !nodeId) return content;
|
|
323
|
+
try {
|
|
324
|
+
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
325
|
+
const node = doc.querySelector(
|
|
326
|
+
`[data-agent-native-node-id="${CSS.escape(nodeId)}"]`,
|
|
327
|
+
);
|
|
328
|
+
if (!node || !node.hasAttribute(BOARD_TEXT_AUTO_COLOR_MARKER)) {
|
|
329
|
+
return content;
|
|
330
|
+
}
|
|
331
|
+
node.removeAttribute(BOARD_TEXT_AUTO_COLOR_MARKER);
|
|
332
|
+
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
333
|
+
} catch {
|
|
334
|
+
return content;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanvasFrameGeometry,
|
|
3
|
+
CanvasFrameGeometryById,
|
|
4
|
+
} from "@shared/canvas-frames";
|
|
5
|
+
|
|
6
|
+
export const KEEPALIVE_ACTION_MAX_BYTES = 60_000;
|
|
7
|
+
|
|
8
|
+
export type DesignDataOperation =
|
|
9
|
+
| {
|
|
10
|
+
op: "set";
|
|
11
|
+
path: [string, ...string[]];
|
|
12
|
+
value: unknown;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
op: "delete";
|
|
16
|
+
path: [string, ...string[]];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface PendingDesignDataOperation {
|
|
20
|
+
operation: DesignDataOperation;
|
|
21
|
+
revision: number;
|
|
22
|
+
order: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PendingDesignDataOperations = Record<
|
|
26
|
+
string,
|
|
27
|
+
PendingDesignDataOperation
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recordValue(
|
|
35
|
+
data: Record<string, unknown>,
|
|
36
|
+
key: string,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
const value = data[key];
|
|
39
|
+
return isRecord(value) ? value : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function operationPathKey(operation: DesignDataOperation): string {
|
|
43
|
+
return JSON.stringify(operation.path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function valuesEqual(left: unknown, right: unknown): boolean {
|
|
47
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function viewportSizeFromFrameGeometry(
|
|
51
|
+
geometry: CanvasFrameGeometry | undefined,
|
|
52
|
+
): { width: number; height: number } | null {
|
|
53
|
+
if (
|
|
54
|
+
typeof geometry?.width !== "number" ||
|
|
55
|
+
!Number.isFinite(geometry.width) ||
|
|
56
|
+
typeof geometry.height !== "number" ||
|
|
57
|
+
!Number.isFinite(geometry.height)
|
|
58
|
+
) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
width: Math.max(1, Math.round(geometry.width)),
|
|
63
|
+
height: Math.max(1, Math.round(geometry.height)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildFrameGeometryDataOperations(args: {
|
|
68
|
+
previousGeometry: CanvasFrameGeometryById;
|
|
69
|
+
nextGeometry: CanvasFrameGeometryById;
|
|
70
|
+
designData: Record<string, unknown>;
|
|
71
|
+
syncViewportFrameIds?: readonly string[];
|
|
72
|
+
}): DesignDataOperation[] {
|
|
73
|
+
const operations: DesignDataOperation[] = [];
|
|
74
|
+
const frameIds = new Set([
|
|
75
|
+
...Object.keys(args.previousGeometry),
|
|
76
|
+
...Object.keys(args.nextGeometry),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
for (const frameId of frameIds) {
|
|
80
|
+
const previous = args.previousGeometry[frameId];
|
|
81
|
+
const next = args.nextGeometry[frameId];
|
|
82
|
+
if (valuesEqual(previous, next)) continue;
|
|
83
|
+
if (next === undefined) {
|
|
84
|
+
operations.push({ op: "delete", path: ["canvasFrames", frameId] });
|
|
85
|
+
} else {
|
|
86
|
+
operations.push({
|
|
87
|
+
op: "set",
|
|
88
|
+
path: ["canvasFrames", frameId],
|
|
89
|
+
value: { ...next },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const screenMetadata = recordValue(args.designData, "screenMetadata");
|
|
95
|
+
const localhostScreens = recordValue(args.designData, "localhostScreens");
|
|
96
|
+
for (const frameId of new Set(args.syncViewportFrameIds ?? [])) {
|
|
97
|
+
const viewport = viewportSizeFromFrameGeometry(args.nextGeometry[frameId]);
|
|
98
|
+
if (!viewport) continue;
|
|
99
|
+
const metadataEntry = recordValue(screenMetadata, frameId);
|
|
100
|
+
if (metadataEntry.width !== viewport.width) {
|
|
101
|
+
operations.push({
|
|
102
|
+
op: "set",
|
|
103
|
+
path: ["screenMetadata", frameId, "width"],
|
|
104
|
+
value: viewport.width,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (metadataEntry.height !== viewport.height) {
|
|
108
|
+
operations.push({
|
|
109
|
+
op: "set",
|
|
110
|
+
path: ["screenMetadata", frameId, "height"],
|
|
111
|
+
value: viewport.height,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const localhostEntry = recordValue(localhostScreens, frameId);
|
|
116
|
+
if (Object.keys(localhostEntry).length === 0) continue;
|
|
117
|
+
if (localhostEntry.width !== viewport.width) {
|
|
118
|
+
operations.push({
|
|
119
|
+
op: "set",
|
|
120
|
+
path: ["localhostScreens", frameId, "width"],
|
|
121
|
+
value: viewport.width,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (localhostEntry.height !== viewport.height) {
|
|
125
|
+
operations.push({
|
|
126
|
+
op: "set",
|
|
127
|
+
path: ["localhostScreens", frameId, "height"],
|
|
128
|
+
value: viewport.height,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return operations;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function applyDesignDataOperations(
|
|
137
|
+
data: Record<string, unknown>,
|
|
138
|
+
operations: readonly DesignDataOperation[],
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
const root = { ...data };
|
|
141
|
+
for (const operation of operations) {
|
|
142
|
+
let target = root;
|
|
143
|
+
let missingDeleteParent = false;
|
|
144
|
+
for (const segment of operation.path.slice(0, -1)) {
|
|
145
|
+
const current = target[segment];
|
|
146
|
+
if (current === undefined) {
|
|
147
|
+
if (operation.op === "delete") {
|
|
148
|
+
missingDeleteParent = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const next: Record<string, unknown> = {};
|
|
152
|
+
target[segment] = next;
|
|
153
|
+
target = next;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!isRecord(current)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Cannot apply design data operation through non-object path "${operation.path.join(".")}".`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const cloned = { ...current };
|
|
162
|
+
target[segment] = cloned;
|
|
163
|
+
target = cloned;
|
|
164
|
+
}
|
|
165
|
+
if (missingDeleteParent) continue;
|
|
166
|
+
const leaf = operation.path[operation.path.length - 1]!;
|
|
167
|
+
if (operation.op === "delete") delete target[leaf];
|
|
168
|
+
else target[leaf] = operation.value;
|
|
169
|
+
}
|
|
170
|
+
return root;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function compactDesignDataOperations(
|
|
174
|
+
operations: readonly DesignDataOperation[],
|
|
175
|
+
): DesignDataOperation[] {
|
|
176
|
+
const byPath = new Map<string, DesignDataOperation>();
|
|
177
|
+
for (const operation of operations) {
|
|
178
|
+
const key = operationPathKey(operation);
|
|
179
|
+
byPath.delete(key);
|
|
180
|
+
byPath.set(key, operation);
|
|
181
|
+
}
|
|
182
|
+
return [...byPath.values()];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function stagePendingDesignDataOperations(
|
|
186
|
+
pending: PendingDesignDataOperations,
|
|
187
|
+
operations: readonly DesignDataOperation[],
|
|
188
|
+
revision: number,
|
|
189
|
+
): PendingDesignDataOperations {
|
|
190
|
+
const next = { ...pending };
|
|
191
|
+
operations.forEach((operation, order) => {
|
|
192
|
+
next[operationPathKey(operation)] = { operation, revision, order };
|
|
193
|
+
});
|
|
194
|
+
return next;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function clearAcknowledgedDesignDataOperations(
|
|
198
|
+
pending: PendingDesignDataOperations,
|
|
199
|
+
operations: readonly DesignDataOperation[],
|
|
200
|
+
revision: number,
|
|
201
|
+
): PendingDesignDataOperations {
|
|
202
|
+
const next = { ...pending };
|
|
203
|
+
for (const operation of operations) {
|
|
204
|
+
const key = operationPathKey(operation);
|
|
205
|
+
if (next[key]?.revision === revision) delete next[key];
|
|
206
|
+
}
|
|
207
|
+
return next;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Clears every operation included in a compacted save through `revision`
|
|
212
|
+
* while preserving edits that entered the queue after that request began.
|
|
213
|
+
*/
|
|
214
|
+
export function clearAcknowledgedDesignDataOperationsThroughRevision(
|
|
215
|
+
pending: PendingDesignDataOperations,
|
|
216
|
+
revision: number,
|
|
217
|
+
): PendingDesignDataOperations {
|
|
218
|
+
return Object.fromEntries(
|
|
219
|
+
Object.entries(pending).filter(
|
|
220
|
+
([, operation]) => operation.revision > revision,
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function pendingDesignDataOperations(
|
|
226
|
+
pending: PendingDesignDataOperations,
|
|
227
|
+
): DesignDataOperation[] {
|
|
228
|
+
return Object.values(pending)
|
|
229
|
+
.sort(
|
|
230
|
+
(left, right) =>
|
|
231
|
+
left.revision - right.revision || left.order - right.order,
|
|
232
|
+
)
|
|
233
|
+
.map(({ operation }) => operation);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function byteLength(value: string): number {
|
|
237
|
+
if (typeof TextEncoder === "undefined") return value.length;
|
|
238
|
+
return new TextEncoder().encode(value).length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function buildDataOperationsKeepalivePayload(
|
|
242
|
+
designId: string | undefined,
|
|
243
|
+
operations: readonly DesignDataOperation[],
|
|
244
|
+
operationSource: string,
|
|
245
|
+
operationRevision: number,
|
|
246
|
+
maxBytes = KEEPALIVE_ACTION_MAX_BYTES,
|
|
247
|
+
): {
|
|
248
|
+
id: string;
|
|
249
|
+
dataOperations: DesignDataOperation[];
|
|
250
|
+
operationSource: string;
|
|
251
|
+
operationRevision: number;
|
|
252
|
+
} | null {
|
|
253
|
+
if (!designId) return null;
|
|
254
|
+
const dataOperations = compactDesignDataOperations(operations);
|
|
255
|
+
if (dataOperations.length === 0) return null;
|
|
256
|
+
if (
|
|
257
|
+
!operationSource ||
|
|
258
|
+
!Number.isSafeInteger(operationRevision) ||
|
|
259
|
+
operationRevision < 0
|
|
260
|
+
) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const payload = {
|
|
264
|
+
id: designId,
|
|
265
|
+
dataOperations,
|
|
266
|
+
operationSource,
|
|
267
|
+
operationRevision,
|
|
268
|
+
};
|
|
269
|
+
return byteLength(JSON.stringify(payload)) <= maxBytes ? payload : null;
|
|
270
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function isRadixOverlayOpen(wrapperEl: Element): boolean {
|
|
2
|
+
if (wrapperEl.hasAttribute("data-agent-native-tooltip")) return false;
|
|
3
|
+
const isOpenState = (el: Element) =>
|
|
4
|
+
el.getAttribute("data-state") !== "closed";
|
|
5
|
+
const stateful = wrapperEl.querySelectorAll("[data-state]");
|
|
6
|
+
if (stateful.length > 0) {
|
|
7
|
+
const allTooltips = Array.from(stateful).every((content) =>
|
|
8
|
+
content.hasAttribute("data-agent-native-tooltip"),
|
|
9
|
+
);
|
|
10
|
+
if (allTooltips) return false;
|
|
11
|
+
if (wrapperEl.hasAttribute("data-state")) {
|
|
12
|
+
return isOpenState(wrapperEl);
|
|
13
|
+
}
|
|
14
|
+
return Array.from(stateful).some(
|
|
15
|
+
(content) =>
|
|
16
|
+
!content.hasAttribute("data-agent-native-tooltip") &&
|
|
17
|
+
isOpenState(content),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (wrapperEl.hasAttribute("data-state")) {
|
|
21
|
+
return isOpenState(wrapperEl);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|