@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
|
@@ -105,6 +105,71 @@ interface Stroke {
|
|
|
105
105
|
createdAt: number;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
interface PendingTextInput {
|
|
109
|
+
/** Fractional x (0..1) of the visual rect. */
|
|
110
|
+
xFrac: number;
|
|
111
|
+
/** Fractional y (0..1) of the visual rect. */
|
|
112
|
+
yFrac: number;
|
|
113
|
+
value: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Keep long pen/stylus gestures bounded before they become agent prompt data.
|
|
118
|
+
* Once this limit is reached, older samples are progressively decimated while
|
|
119
|
+
* preserving the first, latest, and future points. At normal pointer rates the
|
|
120
|
+
* sub-pixel filter below is the only sampling users will notice.
|
|
121
|
+
*/
|
|
122
|
+
const MAX_STROKE_POINTS = 2048;
|
|
123
|
+
const MIN_POINT_DISTANCE_PX = 0.35;
|
|
124
|
+
|
|
125
|
+
function pointFromClient(
|
|
126
|
+
clientX: number,
|
|
127
|
+
clientY: number,
|
|
128
|
+
rect: DOMRect,
|
|
129
|
+
): Point | null {
|
|
130
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
131
|
+
return {
|
|
132
|
+
x: Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
|
|
133
|
+
y: Math.max(0, Math.min(1, (clientY - rect.top) / rect.height)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appendStrokePoint(
|
|
138
|
+
points: Point[],
|
|
139
|
+
point: Point,
|
|
140
|
+
rect: DOMRect,
|
|
141
|
+
force = false,
|
|
142
|
+
): Point[] {
|
|
143
|
+
const last = points[points.length - 1];
|
|
144
|
+
if (last) {
|
|
145
|
+
const dx = (point.x - last.x) * rect.width;
|
|
146
|
+
const dy = (point.y - last.y) * rect.height;
|
|
147
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
148
|
+
if (
|
|
149
|
+
distanceSquared === 0 ||
|
|
150
|
+
(!force && distanceSquared < MIN_POINT_DISTANCE_PX ** 2)
|
|
151
|
+
) {
|
|
152
|
+
return points;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let next = points;
|
|
157
|
+
if (next.length >= MAX_STROKE_POINTS) {
|
|
158
|
+
// Preserve the full trajectory instead of dropping the tail of a long
|
|
159
|
+
// gesture. Repeated compaction gradually lowers only the oldest sampling
|
|
160
|
+
// density, which is visually preferable to a path that suddenly stops.
|
|
161
|
+
const compacted: Point[] = [next[0]];
|
|
162
|
+
for (let index = 2; index < next.length - 1; index += 2) {
|
|
163
|
+
compacted.push(next[index]);
|
|
164
|
+
}
|
|
165
|
+
compacted.push(next[next.length - 1]);
|
|
166
|
+
next = compacted;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
next.push(point);
|
|
170
|
+
return next;
|
|
171
|
+
}
|
|
172
|
+
|
|
108
173
|
/**
|
|
109
174
|
* Draw-to-prompt overlay for the slide canvas.
|
|
110
175
|
*
|
|
@@ -142,21 +207,26 @@ export function DrawOverlay({
|
|
|
142
207
|
const [color, setColor] = useState(PRESET_COLORS[0].color);
|
|
143
208
|
const [lineWidth, setLineWidth] = useState(LINE_WIDTHS[1].value);
|
|
144
209
|
const [strokes, setStrokes] = useState<Stroke[]>([]);
|
|
210
|
+
const strokesRef = useRef<Stroke[]>([]);
|
|
145
211
|
const [textAnnotations, setTextAnnotations] = useState<DrawAnnotation[]>([]);
|
|
212
|
+
const textAnnotationsRef = useRef<DrawAnnotation[]>([]);
|
|
146
213
|
// Unified redo stack. Each entry is either a Stroke or a DrawAnnotation so
|
|
147
214
|
// undo/redo work in creation order across both types.
|
|
148
215
|
const [redoStack, setRedoStack] = useState<Array<Stroke | DrawAnnotation>>(
|
|
149
216
|
[],
|
|
150
217
|
);
|
|
151
218
|
const [currentStroke, setCurrentStroke] = useState<Point[] | null>(null);
|
|
219
|
+
// Pointer events can arrive down -> move -> up before React renders once.
|
|
220
|
+
// This ref is the authoritative in-progress gesture; state is only a
|
|
221
|
+
// frame-throttled snapshot used to redraw the canvas.
|
|
222
|
+
const currentStrokeRef = useRef<Point[] | null>(null);
|
|
223
|
+
const currentStrokeFrameRef = useRef<number | null>(null);
|
|
224
|
+
const activePointerIdRef = useRef<number | null>(null);
|
|
225
|
+
const activeStrokeStyleRef = useRef({ color, lineWidth });
|
|
226
|
+
const lastCreatedAtRef = useRef(0);
|
|
152
227
|
const [textMode, setTextMode] = useState(false);
|
|
153
|
-
const [textInput, setTextInput] = useState<
|
|
154
|
-
|
|
155
|
-
xFrac: number;
|
|
156
|
-
/** Fractional y (0..1) of the visual rect. */
|
|
157
|
-
yFrac: number;
|
|
158
|
-
value: string;
|
|
159
|
-
} | null>(null);
|
|
228
|
+
const [textInput, setTextInput] = useState<PendingTextInput | null>(null);
|
|
229
|
+
const textInputStateRef = useRef<PendingTextInput | null>(null);
|
|
160
230
|
const textInputRef = useRef<HTMLInputElement>(null);
|
|
161
231
|
// Escape cancels the pending text annotation, but unmounting the input also
|
|
162
232
|
// fires its blur handler, which would commit the very annotation the user
|
|
@@ -169,33 +239,100 @@ export function DrawOverlay({
|
|
|
169
239
|
// the canvas element's CSS size changes (e.g. device frame switch).
|
|
170
240
|
const [resizeTick, setResizeTick] = useState(0);
|
|
171
241
|
|
|
172
|
-
const scale = zoom / 100;
|
|
242
|
+
const scale = Math.max(zoom / 100, 0.01);
|
|
243
|
+
|
|
244
|
+
const cancelScheduledStrokeFrame = useCallback(() => {
|
|
245
|
+
if (currentStrokeFrameRef.current === null) return;
|
|
246
|
+
window.cancelAnimationFrame(currentStrokeFrameRef.current);
|
|
247
|
+
currentStrokeFrameRef.current = null;
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
const scheduleCurrentStrokeRedraw = useCallback(() => {
|
|
251
|
+
if (currentStrokeFrameRef.current !== null) return;
|
|
252
|
+
currentStrokeFrameRef.current = window.requestAnimationFrame(() => {
|
|
253
|
+
currentStrokeFrameRef.current = null;
|
|
254
|
+
const points = currentStrokeRef.current;
|
|
255
|
+
setCurrentStroke(points ? [...points] : null);
|
|
256
|
+
});
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
const resetActiveStroke = useCallback(() => {
|
|
260
|
+
drawing.current = false;
|
|
261
|
+
activePointerIdRef.current = null;
|
|
262
|
+
currentStrokeRef.current = null;
|
|
263
|
+
cancelScheduledStrokeFrame();
|
|
264
|
+
setCurrentStroke(null);
|
|
265
|
+
}, [cancelScheduledStrokeFrame]);
|
|
266
|
+
|
|
267
|
+
const nextCreatedAt = useCallback(() => {
|
|
268
|
+
const next = Math.max(Date.now(), lastCreatedAtRef.current + 1);
|
|
269
|
+
lastCreatedAtRef.current = next;
|
|
270
|
+
return next;
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
const setPendingTextInput = useCallback(
|
|
274
|
+
(
|
|
275
|
+
update:
|
|
276
|
+
| PendingTextInput
|
|
277
|
+
| null
|
|
278
|
+
| ((current: PendingTextInput | null) => PendingTextInput | null),
|
|
279
|
+
) => {
|
|
280
|
+
const next =
|
|
281
|
+
typeof update === "function"
|
|
282
|
+
? update(textInputStateRef.current)
|
|
283
|
+
: update;
|
|
284
|
+
textInputStateRef.current = next;
|
|
285
|
+
setTextInput(next);
|
|
286
|
+
},
|
|
287
|
+
[],
|
|
288
|
+
);
|
|
173
289
|
|
|
174
290
|
// Clear all state on hide so the next open starts fresh.
|
|
175
291
|
useEffect(() => {
|
|
176
292
|
if (!visible) {
|
|
293
|
+
resetActiveStroke();
|
|
294
|
+
strokesRef.current = [];
|
|
295
|
+
textAnnotationsRef.current = [];
|
|
177
296
|
setStrokes([]);
|
|
178
297
|
setTextAnnotations([]);
|
|
179
298
|
setRedoStack([]);
|
|
180
|
-
|
|
181
|
-
|
|
299
|
+
setPendingTextInput(null);
|
|
300
|
+
setTextMode(false);
|
|
182
301
|
setInstruction("");
|
|
302
|
+
cancelingTextRef.current = false;
|
|
303
|
+
lastCreatedAtRef.current = 0;
|
|
183
304
|
}
|
|
184
|
-
}, [visible]);
|
|
305
|
+
}, [resetActiveStroke, setPendingTextInput, visible]);
|
|
185
306
|
|
|
307
|
+
// Comment-pin mode can temporarily leave the toolbar visible while making
|
|
308
|
+
// the drawing surface inert. Never retain a half-finished gesture across
|
|
309
|
+
// that tool switch.
|
|
186
310
|
useEffect(() => {
|
|
187
|
-
if (!
|
|
311
|
+
if (!canvasInteractive) resetActiveStroke();
|
|
312
|
+
}, [canvasInteractive, resetActiveStroke]);
|
|
313
|
+
|
|
314
|
+
useEffect(
|
|
315
|
+
() => () => {
|
|
316
|
+
cancelScheduledStrokeFrame();
|
|
317
|
+
},
|
|
318
|
+
[cancelScheduledStrokeFrame],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const shouldFocusTextInput = textInput !== null;
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!shouldFocusTextInput) return;
|
|
188
324
|
const id = window.requestAnimationFrame(() => {
|
|
189
325
|
textInputRef.current?.focus();
|
|
190
326
|
});
|
|
191
327
|
return () => window.cancelAnimationFrame(id);
|
|
192
|
-
}, [
|
|
328
|
+
}, [shouldFocusTextInput]);
|
|
193
329
|
|
|
194
330
|
// ResizeObserver: bump resizeTick whenever the canvas changes CSS size so
|
|
195
331
|
// the redraw effect re-runs and the backing store is resized correctly.
|
|
196
332
|
useEffect(() => {
|
|
197
333
|
const canvas = canvasRef.current;
|
|
198
334
|
if (!canvas) return;
|
|
335
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
199
336
|
const ro = new ResizeObserver(() => setResizeTick((t) => t + 1));
|
|
200
337
|
ro.observe(canvas);
|
|
201
338
|
return () => ro.disconnect();
|
|
@@ -235,14 +372,15 @@ export function DrawOverlay({
|
|
|
235
372
|
}
|
|
236
373
|
|
|
237
374
|
if (currentStroke && currentStroke.length > 0) {
|
|
375
|
+
const activeStyle = activeStrokeStyleRef.current;
|
|
238
376
|
drawStroke(
|
|
239
377
|
ctx,
|
|
240
378
|
currentStroke.map((p) => ({
|
|
241
379
|
x: p.x * rect.width,
|
|
242
380
|
y: p.y * rect.height,
|
|
243
381
|
})),
|
|
244
|
-
color,
|
|
245
|
-
lineWidth,
|
|
382
|
+
activeStyle.color,
|
|
383
|
+
activeStyle.lineWidth,
|
|
246
384
|
);
|
|
247
385
|
}
|
|
248
386
|
// `zoom` is a dependency because it scales the canvas via a CSS transform,
|
|
@@ -254,77 +392,151 @@ export function DrawOverlay({
|
|
|
254
392
|
const handlePointerDown = useCallback(
|
|
255
393
|
(e: React.PointerEvent) => {
|
|
256
394
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
257
|
-
if (!rect) return;
|
|
395
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
|
396
|
+
if (!canvasInteractive || drawing.current) return;
|
|
397
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
398
|
+
|
|
258
399
|
if (textMode) {
|
|
400
|
+
e.preventDefault();
|
|
259
401
|
cancelingTextRef.current = false;
|
|
260
|
-
|
|
402
|
+
setPendingTextInput({
|
|
261
403
|
xFrac: (e.clientX - rect.left) / rect.width,
|
|
262
404
|
yFrac: (e.clientY - rect.top) / rect.height,
|
|
263
405
|
value: "",
|
|
264
406
|
});
|
|
265
407
|
return;
|
|
266
408
|
}
|
|
409
|
+
|
|
410
|
+
const point = pointFromClient(e.clientX, e.clientY, rect);
|
|
411
|
+
if (!point) return;
|
|
412
|
+
e.preventDefault();
|
|
267
413
|
drawing.current = true;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
y: (e.clientY - rect.top) / rect.height,
|
|
272
|
-
};
|
|
414
|
+
activePointerIdRef.current = e.pointerId;
|
|
415
|
+
activeStrokeStyleRef.current = { color, lineWidth };
|
|
416
|
+
currentStrokeRef.current = [point];
|
|
273
417
|
setCurrentStroke([point]);
|
|
274
|
-
|
|
418
|
+
try {
|
|
419
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
420
|
+
} catch {
|
|
421
|
+
// Pointer capture may fail if the browser already cancelled the
|
|
422
|
+
// gesture. The pointer-id guard still prevents cross-pointer mixing.
|
|
423
|
+
}
|
|
275
424
|
},
|
|
276
|
-
[textMode],
|
|
425
|
+
[canvasInteractive, color, lineWidth, setPendingTextInput, textMode],
|
|
277
426
|
);
|
|
278
427
|
|
|
279
428
|
const handlePointerMove = useCallback(
|
|
280
429
|
(e: React.PointerEvent) => {
|
|
281
430
|
if (!drawing.current || textMode) return;
|
|
431
|
+
if (activePointerIdRef.current !== e.pointerId) return;
|
|
282
432
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
283
|
-
if (!rect) return;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
433
|
+
if (!rect || !currentStrokeRef.current) return;
|
|
434
|
+
|
|
435
|
+
const nativeEvent = e.nativeEvent;
|
|
436
|
+
const coalesced = nativeEvent.getCoalescedEvents?.() ?? [];
|
|
437
|
+
const samples = [...coalesced, nativeEvent];
|
|
438
|
+
let points = currentStrokeRef.current;
|
|
439
|
+
let changed = false;
|
|
440
|
+
for (const sample of samples) {
|
|
441
|
+
const point = pointFromClient(sample.clientX, sample.clientY, rect);
|
|
442
|
+
if (!point) continue;
|
|
443
|
+
const previousLength = points.length;
|
|
444
|
+
const next = appendStrokePoint(points, point, rect);
|
|
445
|
+
if (next !== points || next.length !== previousLength) changed = true;
|
|
446
|
+
points = next;
|
|
447
|
+
}
|
|
448
|
+
currentStrokeRef.current = points;
|
|
449
|
+
if (changed) scheduleCurrentStrokeRedraw();
|
|
289
450
|
},
|
|
290
|
-
[textMode],
|
|
451
|
+
[scheduleCurrentStrokeRedraw, textMode],
|
|
291
452
|
);
|
|
292
453
|
|
|
293
|
-
const handlePointerUp = useCallback(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
454
|
+
const handlePointerUp = useCallback(
|
|
455
|
+
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
456
|
+
if (!drawing.current || activePointerIdRef.current !== e.pointerId)
|
|
457
|
+
return;
|
|
458
|
+
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
const rect = canvasRef.current?.getBoundingClientRect();
|
|
461
|
+
let points = currentStrokeRef.current;
|
|
462
|
+
if (rect && points) {
|
|
463
|
+
const coalesced = e.nativeEvent.getCoalescedEvents?.() ?? [];
|
|
464
|
+
for (const sample of [...coalesced, e.nativeEvent]) {
|
|
465
|
+
const point = pointFromClient(sample.clientX, sample.clientY, rect);
|
|
466
|
+
if (point) points = appendStrokePoint(points, point, rect, true);
|
|
467
|
+
}
|
|
468
|
+
currentStrokeRef.current = points;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
drawing.current = false;
|
|
472
|
+
activePointerIdRef.current = null;
|
|
473
|
+
cancelScheduledStrokeFrame();
|
|
474
|
+
if (points && points.length > 1) {
|
|
475
|
+
const activeStyle = activeStrokeStyleRef.current;
|
|
476
|
+
const stroke: Stroke = {
|
|
300
477
|
id: crypto.randomUUID(),
|
|
301
|
-
points:
|
|
302
|
-
color,
|
|
303
|
-
lineWidth,
|
|
304
|
-
createdAt:
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
478
|
+
points: [...points],
|
|
479
|
+
color: activeStyle.color,
|
|
480
|
+
lineWidth: activeStyle.lineWidth,
|
|
481
|
+
createdAt: nextCreatedAt(),
|
|
482
|
+
};
|
|
483
|
+
const nextStrokes = [...strokesRef.current, stroke];
|
|
484
|
+
strokesRef.current = nextStrokes;
|
|
485
|
+
setStrokes(nextStrokes);
|
|
486
|
+
// A new stroke clears the redo stack (standard editor convention).
|
|
487
|
+
setRedoStack([]);
|
|
488
|
+
}
|
|
489
|
+
currentStrokeRef.current = null;
|
|
490
|
+
setCurrentStroke(null);
|
|
312
491
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
492
|
+
try {
|
|
493
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
494
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
// The browser can release capture before React handles pointerup.
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
[cancelScheduledStrokeFrame, nextCreatedAt],
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const handlePointerCancel = useCallback(
|
|
504
|
+
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
505
|
+
if (activePointerIdRef.current !== e.pointerId) return;
|
|
506
|
+
resetActiveStroke();
|
|
507
|
+
try {
|
|
508
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
509
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
// Pointer cancellation often releases capture before this event.
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
[resetActiveStroke],
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const handleLostPointerCapture = useCallback(
|
|
519
|
+
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
520
|
+
if (drawing.current && activePointerIdRef.current === e.pointerId) {
|
|
521
|
+
resetActiveStroke();
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
[resetActiveStroke],
|
|
525
|
+
);
|
|
317
526
|
|
|
318
527
|
/**
|
|
319
528
|
* Undo removes the most recently created annotation (stroke or text) in
|
|
320
529
|
* creation order and pushes it onto the unified redo stack.
|
|
321
530
|
*/
|
|
322
531
|
const undo = () => {
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
532
|
+
const currentStrokes = strokesRef.current;
|
|
533
|
+
const currentTexts = textAnnotationsRef.current;
|
|
534
|
+
const lastStroke =
|
|
535
|
+
currentStrokes.length > 0
|
|
536
|
+
? currentStrokes[currentStrokes.length - 1]
|
|
327
537
|
: null;
|
|
538
|
+
const lastText =
|
|
539
|
+
currentTexts.length > 0 ? currentTexts[currentTexts.length - 1] : null;
|
|
328
540
|
|
|
329
541
|
if (!lastStroke && !lastText) return;
|
|
330
542
|
|
|
@@ -333,10 +545,14 @@ export function DrawOverlay({
|
|
|
333
545
|
const textTime = lastText?.createdAt ?? 0;
|
|
334
546
|
|
|
335
547
|
if (lastStroke && strokeTime >= textTime) {
|
|
336
|
-
|
|
548
|
+
const nextStrokes = currentStrokes.slice(0, -1);
|
|
549
|
+
strokesRef.current = nextStrokes;
|
|
550
|
+
setStrokes(nextStrokes);
|
|
337
551
|
setRedoStack((stack) => [...stack, lastStroke]);
|
|
338
552
|
} else if (lastText) {
|
|
339
|
-
|
|
553
|
+
const nextTexts = currentTexts.slice(0, -1);
|
|
554
|
+
textAnnotationsRef.current = nextTexts;
|
|
555
|
+
setTextAnnotations(nextTexts);
|
|
340
556
|
setRedoStack((stack) => [...stack, lastText]);
|
|
341
557
|
}
|
|
342
558
|
};
|
|
@@ -348,10 +564,17 @@ export function DrawOverlay({
|
|
|
348
564
|
const remaining = stack.slice(0, -1);
|
|
349
565
|
if ("points" in top) {
|
|
350
566
|
// It's a Stroke
|
|
351
|
-
|
|
567
|
+
const nextStrokes = [...strokesRef.current, top as Stroke];
|
|
568
|
+
strokesRef.current = nextStrokes;
|
|
569
|
+
setStrokes(nextStrokes);
|
|
352
570
|
} else {
|
|
353
571
|
// It's a DrawAnnotation (text)
|
|
354
|
-
|
|
572
|
+
const nextTexts = [
|
|
573
|
+
...textAnnotationsRef.current,
|
|
574
|
+
top as DrawAnnotation,
|
|
575
|
+
];
|
|
576
|
+
textAnnotationsRef.current = nextTexts;
|
|
577
|
+
setTextAnnotations(nextTexts);
|
|
355
578
|
}
|
|
356
579
|
return remaining;
|
|
357
580
|
});
|
|
@@ -359,9 +582,11 @@ export function DrawOverlay({
|
|
|
359
582
|
|
|
360
583
|
const clear = () => {
|
|
361
584
|
if (strokes.length === 0 && textAnnotations.length === 0) return;
|
|
362
|
-
const prevStrokes =
|
|
363
|
-
const prevTexts =
|
|
585
|
+
const prevStrokes = strokesRef.current;
|
|
586
|
+
const prevTexts = textAnnotationsRef.current;
|
|
364
587
|
const prevRedo = redoStack;
|
|
588
|
+
strokesRef.current = [];
|
|
589
|
+
textAnnotationsRef.current = [];
|
|
365
590
|
setStrokes([]);
|
|
366
591
|
setTextAnnotations([]);
|
|
367
592
|
setRedoStack([]);
|
|
@@ -371,8 +596,12 @@ export function DrawOverlay({
|
|
|
371
596
|
onClick: () => {
|
|
372
597
|
// Merge snapshot with any strokes/texts drawn during the toast window
|
|
373
598
|
// so new work is not discarded; also restore the pre-clear redo stack.
|
|
374
|
-
|
|
375
|
-
|
|
599
|
+
const restoredStrokes = [...prevStrokes, ...strokesRef.current];
|
|
600
|
+
const restoredTexts = [...prevTexts, ...textAnnotationsRef.current];
|
|
601
|
+
strokesRef.current = restoredStrokes;
|
|
602
|
+
textAnnotationsRef.current = restoredTexts;
|
|
603
|
+
setStrokes(restoredStrokes);
|
|
604
|
+
setTextAnnotations(restoredTexts);
|
|
376
605
|
setRedoStack(prevRedo);
|
|
377
606
|
},
|
|
378
607
|
},
|
|
@@ -381,35 +610,42 @@ export function DrawOverlay({
|
|
|
381
610
|
};
|
|
382
611
|
|
|
383
612
|
const commitTextAnnotation = () => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
613
|
+
const pendingText = textInputStateRef.current;
|
|
614
|
+
// Clear the authoritative pending value first. Enter, blur, and Send can
|
|
615
|
+
// all occur in the same browser turn; any later handler becomes a no-op
|
|
616
|
+
// instead of duplicating the label.
|
|
617
|
+
setPendingTextInput(null);
|
|
618
|
+
if (!pendingText || !pendingText.value.trim()) return;
|
|
619
|
+
|
|
388
620
|
const ann: DrawAnnotation = {
|
|
389
621
|
id: crypto.randomUUID(),
|
|
390
622
|
type: "text",
|
|
391
|
-
text:
|
|
623
|
+
text: pendingText.value.trim(),
|
|
392
624
|
// Store fractional position so the label stays anchored across zoom changes.
|
|
393
|
-
position: { x:
|
|
625
|
+
position: { x: pendingText.xFrac, y: pendingText.yFrac },
|
|
394
626
|
color,
|
|
395
627
|
lineWidth,
|
|
396
|
-
createdAt:
|
|
628
|
+
createdAt: nextCreatedAt(),
|
|
397
629
|
};
|
|
398
|
-
|
|
630
|
+
const nextTexts = [...textAnnotationsRef.current, ann];
|
|
631
|
+
textAnnotationsRef.current = nextTexts;
|
|
632
|
+
setTextAnnotations(nextTexts);
|
|
399
633
|
// A new text annotation also clears the redo stack.
|
|
400
634
|
setRedoStack([]);
|
|
401
|
-
setTextInput(null);
|
|
402
635
|
};
|
|
403
636
|
|
|
404
637
|
const send = () => {
|
|
405
638
|
const canvas = canvasRef.current;
|
|
406
639
|
if (!canvas) return;
|
|
640
|
+
// A focused text label normally commits on blur before the Send click, but
|
|
641
|
+
// keyboard/programmatic activation does not guarantee that ordering.
|
|
642
|
+
commitTextAnnotation();
|
|
407
643
|
const rect = canvas.getBoundingClientRect();
|
|
408
644
|
// Layout-space dimensions = visual rect / scale factor.
|
|
409
645
|
const layoutW = rect.width / scale;
|
|
410
646
|
const layoutH = rect.height / scale;
|
|
411
647
|
|
|
412
|
-
const pathAnnotations: DrawAnnotation[] =
|
|
648
|
+
const pathAnnotations: DrawAnnotation[] = strokesRef.current.map((s) => ({
|
|
413
649
|
id: s.id,
|
|
414
650
|
type: "path",
|
|
415
651
|
// Convert fractional points to layout-space absolute pixels for the agent.
|
|
@@ -425,18 +661,18 @@ export function DrawOverlay({
|
|
|
425
661
|
}));
|
|
426
662
|
|
|
427
663
|
// Convert fractional text positions to layout-space absolute pixels.
|
|
428
|
-
const layoutTextAnnotations: DrawAnnotation[] =
|
|
429
|
-
(a) => ({
|
|
664
|
+
const layoutTextAnnotations: DrawAnnotation[] =
|
|
665
|
+
textAnnotationsRef.current.map((a) => ({
|
|
430
666
|
...a,
|
|
431
667
|
position: {
|
|
432
668
|
x: a.position.x * layoutW,
|
|
433
669
|
y: a.position.y * layoutH,
|
|
434
670
|
},
|
|
435
|
-
})
|
|
436
|
-
);
|
|
671
|
+
}));
|
|
437
672
|
|
|
438
673
|
const all = [...pathAnnotations, ...layoutTextAnnotations];
|
|
439
|
-
if (all.length === 0 && !instruction.trim())
|
|
674
|
+
if (all.length === 0 && !instruction.trim() && queuedAnnotationCount === 0)
|
|
675
|
+
return;
|
|
440
676
|
|
|
441
677
|
onSend(all, instruction.trim(), {
|
|
442
678
|
width: layoutW,
|
|
@@ -449,6 +685,7 @@ export function DrawOverlay({
|
|
|
449
685
|
const hasContent =
|
|
450
686
|
strokes.length > 0 ||
|
|
451
687
|
textAnnotations.length > 0 ||
|
|
688
|
+
!!textInput?.value.trim() ||
|
|
452
689
|
instruction.trim() ||
|
|
453
690
|
queuedAnnotationCount > 0;
|
|
454
691
|
|
|
@@ -648,7 +885,7 @@ export function DrawOverlay({
|
|
|
648
885
|
ref={canvasRef}
|
|
649
886
|
data-draw-canvas
|
|
650
887
|
className={cn(
|
|
651
|
-
"absolute inset-0 h-full w-full",
|
|
888
|
+
"absolute inset-0 h-full w-full touch-none",
|
|
652
889
|
textMode ? "cursor-text" : "cursor-crosshair",
|
|
653
890
|
!canvasInteractive && "pointer-events-none",
|
|
654
891
|
)}
|
|
@@ -656,6 +893,7 @@ export function DrawOverlay({
|
|
|
656
893
|
onPointerMove={handlePointerMove}
|
|
657
894
|
onPointerUp={handlePointerUp}
|
|
658
895
|
onPointerCancel={handlePointerCancel}
|
|
896
|
+
onLostPointerCapture={handleLostPointerCapture}
|
|
659
897
|
/>
|
|
660
898
|
|
|
661
899
|
{/* Rendered text annotations.
|
|
@@ -708,7 +946,7 @@ export function DrawOverlay({
|
|
|
708
946
|
ref={textInputRef}
|
|
709
947
|
value={textInput.value}
|
|
710
948
|
onChange={(e) =>
|
|
711
|
-
|
|
949
|
+
setPendingTextInput((prev) =>
|
|
712
950
|
prev ? { ...prev, value: e.target.value } : null,
|
|
713
951
|
)
|
|
714
952
|
}
|
|
@@ -719,16 +957,19 @@ export function DrawOverlay({
|
|
|
719
957
|
cancelingTextRef.current = false;
|
|
720
958
|
return;
|
|
721
959
|
}
|
|
722
|
-
|
|
960
|
+
commitTextAnnotation();
|
|
723
961
|
}}
|
|
724
962
|
onKeyDown={(e) => {
|
|
725
963
|
if (e.key === "Enter") {
|
|
726
964
|
e.preventDefault();
|
|
965
|
+
// Committing unmounts the focused input, which can emit a
|
|
966
|
+
// blur event in the same turn. Skip that second commit.
|
|
967
|
+
cancelingTextRef.current = true;
|
|
727
968
|
commitTextAnnotation();
|
|
728
969
|
}
|
|
729
970
|
if (e.key === "Escape") {
|
|
730
971
|
cancelingTextRef.current = true;
|
|
731
|
-
|
|
972
|
+
setPendingTextInput(null);
|
|
732
973
|
}
|
|
733
974
|
}}
|
|
734
975
|
className="h-7 w-48 border-primary bg-background text-sm"
|
|
@@ -95,6 +95,20 @@ export function designSelectionStateKeysForTab(
|
|
|
95
95
|
: ["design-selection"];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Route-level cleanup only owns this tab's scoped selection. The editor's
|
|
100
|
+
* owner-aware unmount cleanup is responsible for the global compatibility
|
|
101
|
+
* mirror; clearing that mirror here would let any tab that leaves /design
|
|
102
|
+
* erase another still-open editor tab's current agent context.
|
|
103
|
+
*/
|
|
104
|
+
export function designSelectionCleanupKeysForTab(
|
|
105
|
+
browserTabId?: string,
|
|
106
|
+
): string[] {
|
|
107
|
+
return [
|
|
108
|
+
browserTabId ? `design-selection:${browserTabId}` : "design-selection",
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
function normalizeEditorView(
|
|
99
113
|
value: unknown,
|
|
100
114
|
): "single" | "overview" | undefined {
|
|
@@ -206,7 +220,7 @@ export function useNavigationState(enabled = true) {
|
|
|
206
220
|
useEffect(() => {
|
|
207
221
|
if (!enabled) return;
|
|
208
222
|
if (location.pathname.startsWith("/design/")) return;
|
|
209
|
-
for (const key of
|
|
223
|
+
for (const key of designSelectionCleanupKeysForTab(browserTabId)) {
|
|
210
224
|
setClientAppState(key, null).catch(() => {});
|
|
211
225
|
}
|
|
212
226
|
}, [browserTabId, enabled, location.pathname]);
|