@agent-native/core 0.163.4 → 0.164.3

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.
Files changed (228) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
  3. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  4. package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
  5. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
  7. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  8. package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
  9. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
  10. package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
  11. package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
  12. package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
  13. package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
  14. package/corpus/templates/analytics/netlify.toml +2 -0
  15. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
  16. package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
  17. package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
  18. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
  19. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
  20. package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
  21. package/corpus/templates/assets/app/routes/picker.tsx +4 -1
  22. package/corpus/templates/assets/netlify.toml +2 -0
  23. package/corpus/templates/brain/netlify.toml +2 -0
  24. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  25. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  26. package/corpus/templates/calendar/netlify.toml +2 -0
  27. package/corpus/templates/chat/netlify.toml +2 -0
  28. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  29. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  30. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  31. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  32. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  33. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  34. package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
  35. package/corpus/templates/clips/app/routes/_index.tsx +7 -3
  36. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  37. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  38. package/corpus/templates/clips/netlify.toml +2 -0
  39. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  40. package/corpus/templates/content/netlify.toml +2 -0
  41. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  42. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  43. package/corpus/templates/design/AGENTS.md +3 -1
  44. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  45. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  46. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  47. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  48. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  49. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  50. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  51. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  52. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  53. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  54. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  55. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  56. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  126. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  127. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  128. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  129. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  130. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  131. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  132. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  133. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  134. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  135. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  136. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  137. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  138. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  139. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  140. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  141. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  142. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  143. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  144. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  145. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  146. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  147. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  148. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  149. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  150. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  151. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  152. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  153. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  154. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  155. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  156. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  157. package/corpus/templates/design/app/routes/examples.tsx +2 -1
  158. package/corpus/templates/design/netlify.toml +2 -0
  159. package/corpus/templates/design/shared/board-file.ts +2 -1
  160. package/corpus/templates/dispatch/netlify.toml +2 -0
  161. package/corpus/templates/forms/app/routes/_index.tsx +3 -2
  162. package/corpus/templates/forms/netlify.toml +2 -0
  163. package/corpus/templates/mail/app/routes/_index.tsx +3 -2
  164. package/corpus/templates/mail/netlify.toml +2 -0
  165. package/corpus/templates/plan/netlify.toml +2 -0
  166. package/corpus/templates/slides/netlify.toml +2 -0
  167. package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
  168. package/dist/agent/engine/builder-engine.d.ts +6 -4
  169. package/dist/agent/engine/builder-engine.js +91 -26
  170. package/dist/agent/engine/registry.js +16 -0
  171. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  172. package/dist/cli/code-agent-agent-tools.js +188 -0
  173. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  174. package/dist/cli/code-agent-collaboration.js +104 -0
  175. package/dist/cli/code-agent-executor.js +6 -0
  176. package/dist/cli/code-agent-schedules.d.ts +54 -0
  177. package/dist/cli/code-agent-schedules.js +198 -0
  178. package/dist/client/AgentPanel.js +1 -1
  179. package/dist/client/ConnectBuilderCard.js +5 -2
  180. package/dist/client/EnvironmentBadge.js +6 -5
  181. package/dist/client/chat/run-recovery.js +3 -2
  182. package/dist/client/chat/tool-call-display.js +2 -3
  183. package/dist/client/components/CodeRequiredDialog.js +4 -2
  184. package/dist/client/i18n.js +5 -4
  185. package/dist/client/settings/SettingsPanel.js +1 -1
  186. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  187. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  188. package/dist/client/settings/useBuilderStatus.js +22 -33
  189. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  190. package/dist/code-agents/index.d.ts +2 -0
  191. package/dist/code-agents/index.js +2 -0
  192. package/dist/collab/awareness.d.ts +2 -2
  193. package/dist/collab/routes.d.ts +1 -1
  194. package/dist/deploy/build.js +10 -4
  195. package/dist/localization/shared.d.ts +2 -0
  196. package/dist/localization/shared.js +5 -0
  197. package/dist/mcp-client/oauth-client.d.ts +1 -0
  198. package/dist/mcp-client/oauth-client.js +50 -1
  199. package/dist/notifications/routes.d.ts +3 -3
  200. package/dist/observability/routes.d.ts +1 -1
  201. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  202. package/dist/server/auth-marketing-locales.d.ts +7 -0
  203. package/dist/server/auth-marketing-locales.js +1322 -0
  204. package/dist/server/auth-marketing.d.ts +1 -0
  205. package/dist/server/auth-marketing.js +35 -6
  206. package/dist/server/auth.js +4 -17
  207. package/dist/server/builder-browser.d.ts +22 -0
  208. package/dist/server/builder-browser.js +77 -1
  209. package/dist/server/builder-oauth.d.ts +50 -0
  210. package/dist/server/builder-oauth.js +263 -0
  211. package/dist/server/core-routes-plugin.d.ts +11 -0
  212. package/dist/server/core-routes-plugin.js +291 -415
  213. package/dist/server/credential-provider.d.ts +1 -1
  214. package/dist/server/credential-provider.js +1 -1
  215. package/dist/server/csrf.js +2 -2
  216. package/dist/server/onboarding-html.js +52 -120
  217. package/dist/server/ssr-handler.js +13 -4
  218. package/dist/settings/store.d.ts +9 -0
  219. package/dist/settings/store.js +19 -0
  220. package/dist/shared/cache-control.d.ts +22 -0
  221. package/dist/shared/cache-control.js +25 -0
  222. package/dist/shared/index.d.ts +1 -0
  223. package/dist/shared/index.js +1 -0
  224. package/dist/templates/chat/netlify.toml +2 -0
  225. package/docs/content/deployment.mdx +2 -0
  226. package/docs/content/multi-app-workspace.mdx +1 -1
  227. package/package.json +2 -2
  228. package/src/templates/chat/netlify.toml +2 -0
@@ -0,0 +1,279 @@
1
+ import type { CodeLayerNode } from "@shared/code-layer";
2
+ import { buildCodeLayerProjection } from "@shared/code-layer";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+ import { toast } from "sonner";
5
+ import * as Y from "yjs";
6
+
7
+ import { trace } from "@/components/design/design-trace";
8
+ import type { ElementInfo } from "@/components/design/types";
9
+ import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
10
+ import {
11
+ extractLayerPosition,
12
+ getElementOuterHtml,
13
+ insertClonedHtmlLayer,
14
+ insertClonedHtmlLayers,
15
+ } from "@/pages/design-editor/clone-and-pen-edit";
16
+ import {
17
+ codeLayerSelectorAliases,
18
+ elementInfoFromCodeLayerNode,
19
+ } from "@/pages/design-editor/code-layer-state";
20
+ import type { SelectedCanvasLayerSnapshot } from "@/pages/design-editor/command-types";
21
+ import type { DesignFile } from "@/pages/design-editor/types";
22
+
23
+ export interface DuplicateSelectionArgs {
24
+ activeFile: DesignFile;
25
+ applyFileContentUpdate: (
26
+ fileId: string,
27
+ nextContent: string,
28
+ options?: {
29
+ refreshPreview?: boolean;
30
+ skipPreview?: boolean;
31
+ forcePreviewFullDocument?: boolean;
32
+ persist?: boolean;
33
+ recordHistory?: boolean;
34
+ updatedAt?: string;
35
+ clipboardMutation?: ClipboardContentMutationPublication;
36
+ },
37
+ ) => void;
38
+ applyLocalContentUpdate: (
39
+ nextContent: string,
40
+ options?: {
41
+ refreshPreview?: boolean;
42
+ skipPreview?: boolean;
43
+ forcePreviewFullDocument?: boolean;
44
+ immediateSave?: boolean;
45
+ persist?: boolean;
46
+ recordHistory?: boolean;
47
+ historyBeforeContent?: string;
48
+ updatedAt?: string;
49
+ clipboardMutation?: ClipboardContentMutationPublication;
50
+ },
51
+ ) => void;
52
+ canEditDesign: boolean;
53
+ files: DesignFile[];
54
+ getFreshActiveContent: () => string;
55
+ getScreenContent: (screenId: string) => string;
56
+ getSelectedLayerSnapshots: () => SelectedCanvasLayerSnapshot[];
57
+ handleDuplicateScreen: (
58
+ screenId: string,
59
+ request?: { canvasPosition?: { x: number; y: number } },
60
+ ) => void;
61
+ lastDuplicateTransformRef: RefObject<{
62
+ rootNodeIds: string[];
63
+ dx: number;
64
+ dy: number;
65
+ } | null>;
66
+ overviewSelectedScreenIds: string[];
67
+ remapMotionTracksForClone: (
68
+ nodeIdMap: Map<string, string>,
69
+ targetFileId: string,
70
+ ) => void;
71
+ selectedCanvasSelector: string;
72
+ selectedElement: ElementInfo | null;
73
+ selectedLayerIdsState: string[];
74
+ setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
75
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
76
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
77
+ t: (key: string, options?: Record<string, unknown>) => string;
78
+ undoManagerRef: RefObject<Y.UndoManager | null>;
79
+ viewModeRef: RefObject<"single" | "overview">;
80
+ }
81
+
82
+ export function runDuplicateSelection({
83
+ activeFile,
84
+ applyFileContentUpdate,
85
+ applyLocalContentUpdate,
86
+ canEditDesign,
87
+ files,
88
+ getFreshActiveContent,
89
+ getScreenContent,
90
+ getSelectedLayerSnapshots,
91
+ handleDuplicateScreen,
92
+ lastDuplicateTransformRef,
93
+ overviewSelectedScreenIds,
94
+ remapMotionTracksForClone,
95
+ selectedCanvasSelector,
96
+ selectedElement,
97
+ selectedLayerIdsState,
98
+ setOverviewSelectedScreenIds,
99
+ setSelectedElement,
100
+ setSelectedLayerIdsState,
101
+ t,
102
+ undoManagerRef,
103
+ viewModeRef,
104
+ }: DuplicateSelectionArgs) {
105
+ trace("structure", "duplicate-selection", {
106
+ canEdit: canEditDesign,
107
+ selectedLayers: selectedLayerIdsState.length,
108
+ });
109
+ if (!canEditDesign) return;
110
+ // U19: duplicate is a discrete one-shot action — see the matching note
111
+ // in handlePasteSelection.
112
+ undoManagerRef.current?.stopCapturing();
113
+ const snapshots = getSelectedLayerSnapshots();
114
+ if (snapshots.length > 0) {
115
+ const selectedIds: string[] = [];
116
+ const selectedScreenIds: string[] = [];
117
+ let lastActiveNode: CodeLayerNode | null = null;
118
+
119
+ // U7: replay the last recorded move delta when duplicating the exact
120
+ // same selection again (e.g. dup, drag it, dup again repeats the same
121
+ // offset — matching Figma). Otherwise an absolutely-positioned source
122
+ // duplicates with zero offset (lands exactly in place) instead of the
123
+ // previous unconditional stripRootPosition cascade.
124
+ const currentSourceIds = snapshots
125
+ .map((snapshot) => snapshot.rootNodeId ?? snapshot.node.id)
126
+ .sort();
127
+ const repeatTransform =
128
+ lastDuplicateTransformRef.current &&
129
+ lastDuplicateTransformRef.current.rootNodeIds.length ===
130
+ currentSourceIds.length &&
131
+ lastDuplicateTransformRef.current.rootNodeIds.every(
132
+ (id, index) => id === currentSourceIds[index],
133
+ )
134
+ ? lastDuplicateTransformRef.current
135
+ : null;
136
+ const nextDuplicateRootNodeIds: string[] = [];
137
+
138
+ for (const file of files) {
139
+ const group = snapshots.filter(
140
+ (snapshot) => snapshot.sourceFileId === file.id,
141
+ );
142
+ if (group.length === 0) continue;
143
+ let content = getScreenContent(file.id);
144
+ const insertedRootNodeIds: string[] = [];
145
+ for (const snapshot of [...group].sort(
146
+ (a, b) => b.sourceIndex - a.sourceIndex,
147
+ )) {
148
+ const projection = buildCodeLayerProjection(content);
149
+ const anchorNode =
150
+ projection.nodes.find(
151
+ (node) =>
152
+ node.id === snapshot.node.id ||
153
+ node.dataAttributes["data-agent-native-node-id"] ===
154
+ snapshot.rootNodeId,
155
+ ) ?? snapshot.node;
156
+ const sourcePosition = extractLayerPosition(snapshot.html);
157
+ const result = insertClonedHtmlLayers(content, [snapshot.html], {
158
+ targetSelectors: codeLayerSelectorAliases(anchorNode),
159
+ placement: "after",
160
+ // Absolutely-positioned board items land exactly in place (or at
161
+ // the replayed delta); only in-flow elements (no left/top) use
162
+ // stripRootPosition so they join the document as a plain sibling.
163
+ stripRootPosition: !sourcePosition,
164
+ positions: sourcePosition
165
+ ? [
166
+ {
167
+ x: sourcePosition.x + (repeatTransform?.dx ?? 0),
168
+ y: sourcePosition.y + (repeatTransform?.dy ?? 0),
169
+ },
170
+ ]
171
+ : undefined,
172
+ });
173
+ if (!result) continue;
174
+ content = result.content;
175
+ insertedRootNodeIds.unshift(...result.rootNodeIds);
176
+ // U14: duplicate keeps the clone's animation.
177
+ remapMotionTracksForClone(result.nodeIdMap, file.id);
178
+ }
179
+ if (insertedRootNodeIds.length === 0) continue;
180
+ applyFileContentUpdate(file.id, content, {
181
+ forcePreviewFullDocument: true,
182
+ refreshPreview: false,
183
+ });
184
+ selectedScreenIds.push(file.id);
185
+ const finalProjection = buildCodeLayerProjection(content);
186
+ insertedRootNodeIds.forEach((rootNodeId) => {
187
+ const insertedNode = finalProjection.nodes.find(
188
+ (node) =>
189
+ node.id === rootNodeId ||
190
+ node.dataAttributes["data-agent-native-node-id"] === rootNodeId,
191
+ );
192
+ if (!insertedNode) return;
193
+ selectedIds.push(insertedNode.id);
194
+ nextDuplicateRootNodeIds.push(
195
+ insertedNode.dataAttributes["data-agent-native-node-id"] ??
196
+ insertedNode.id,
197
+ );
198
+ if (file.id === activeFile?.id) lastActiveNode = insertedNode;
199
+ });
200
+ }
201
+
202
+ if (selectedIds.length > 0) {
203
+ setSelectedLayerIdsState(selectedIds);
204
+ setSelectedElement(
205
+ lastActiveNode ? elementInfoFromCodeLayerNode(lastActiveNode) : null,
206
+ );
207
+ if (viewModeRef.current === "overview") {
208
+ setOverviewSelectedScreenIds(selectedScreenIds);
209
+ }
210
+ // Track this duplicate as the new "last duplicate" so a subsequent
211
+ // drag-then-Cmd+D can record/replay a delta against it. Preserve the
212
+ // previous delta across repeated Cmd+D so chained duplicates (no drag
213
+ // in between) keep applying the same recorded offset, matching Figma.
214
+ lastDuplicateTransformRef.current = {
215
+ rootNodeIds: [...nextDuplicateRootNodeIds].sort(),
216
+ dx: repeatTransform?.dx ?? 0,
217
+ dy: repeatTransform?.dy ?? 0,
218
+ };
219
+ return;
220
+ }
221
+ }
222
+
223
+ if (selectedElement?.selector) {
224
+ const baseContent = getFreshActiveContent();
225
+ const html = getElementOuterHtml(baseContent, selectedElement.selector);
226
+ if (!html) {
227
+ toast.error(t("designEditor.toasts.duplicateElementFailed"));
228
+ return;
229
+ }
230
+ // B7 fix: duplicate inserts the clone as an in-flow sibling right AFTER
231
+ // the original — not as an absolutely-positioned body child. Strip
232
+ // position/left/top so it joins normal document flow.
233
+ const selector = selectedCanvasSelector ?? selectedElement.selector;
234
+ const strippedHtml = (() => {
235
+ try {
236
+ const parser = new DOMParser();
237
+ const tmp = parser.parseFromString(
238
+ `<template>${html}</template>`,
239
+ "text/html",
240
+ );
241
+ const root =
242
+ tmp.querySelector("template")?.content.firstElementChild ??
243
+ tmp.body.firstElementChild;
244
+ if (root && root instanceof HTMLElement) {
245
+ root.style.position = "";
246
+ root.style.left = "";
247
+ root.style.top = "";
248
+ root.style.right = "";
249
+ root.style.bottom = "";
250
+ }
251
+ return root?.outerHTML ?? html;
252
+ } catch {
253
+ return html;
254
+ }
255
+ })();
256
+ const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
257
+ targetSelectors: [selector],
258
+ placement: "after",
259
+ });
260
+ if (nextContent) {
261
+ applyLocalContentUpdate(nextContent, {
262
+ forcePreviewFullDocument: true,
263
+ });
264
+ } else {
265
+ toast.error(t("designEditor.toasts.duplicateElementFailed"));
266
+ }
267
+ return;
268
+ }
269
+ // U17: duplicate every selected screen, not just the active one — a
270
+ // multi-screen overview selection (no deeper layer focus) previously
271
+ // silently duplicated only activeFile.id and dropped the rest.
272
+ const screenIdsToDuplicate =
273
+ viewModeRef.current === "overview" && overviewSelectedScreenIds.length > 1
274
+ ? overviewSelectedScreenIds
275
+ : activeFile
276
+ ? [activeFile.id]
277
+ : [];
278
+ screenIdsToDuplicate.forEach((screenId) => handleDuplicateScreen(screenId));
279
+ }
@@ -0,0 +1,81 @@
1
+ import type { RefObject } from "react";
2
+
3
+ import { isDesignHotkeyEditableTarget } from "@/hooks/useDesignHotkeys";
4
+ import { readDesignClipboardPayloadFromDataTransfer } from "@/lib/design-clipboard";
5
+ import type { DesignClipboardPayload } from "@/lib/design-import";
6
+ import { getFigmaClipboardContent } from "@/lib/design-import";
7
+
8
+ export interface EditorPasteArgs {
9
+ adoptDesignClipboardPayload: (
10
+ payload: DesignClipboardPayload,
11
+ markerText: string,
12
+ plainText?: string,
13
+ ) => void;
14
+ canEditDesign: boolean;
15
+ handlePasteSelection: (position?: { x: number; y: number }) => Promise<void>;
16
+ handlePastedImageFiles: (files: File[]) => boolean;
17
+ hasCanvasClipboard: boolean;
18
+ importFigmaClipboardIntoDesign: (content: string) => Promise<void>;
19
+ lastWrittenClipboardMarkerRef: RefObject<string | null>;
20
+ lastWrittenClipboardPlainTextRef: RefObject<string | null>;
21
+ }
22
+
23
+ export function runEditorPaste(
24
+ {
25
+ adoptDesignClipboardPayload,
26
+ canEditDesign,
27
+ handlePasteSelection,
28
+ handlePastedImageFiles,
29
+ hasCanvasClipboard,
30
+ importFigmaClipboardIntoDesign,
31
+ lastWrittenClipboardMarkerRef,
32
+ lastWrittenClipboardPlainTextRef,
33
+ }: EditorPasteArgs,
34
+ event: ClipboardEvent,
35
+ ) {
36
+ if (event.defaultPrevented) return;
37
+ if (isDesignHotkeyEditableTarget(event.target)) return;
38
+ // U8/paste-multi: collect every pasted image file, not just the first —
39
+ // see handlePastedImageFiles' doc comment for the full rationale.
40
+ const imageFiles = Array.from(event.clipboardData?.items ?? [])
41
+ .filter((item) => item.kind === "file" && item.type.startsWith("image/"))
42
+ .map((item) => item.getAsFile())
43
+ .filter((file): file is File => Boolean(file));
44
+ if (imageFiles.length > 0 && canEditDesign) {
45
+ if (handlePastedImageFiles(imageFiles)) {
46
+ event.preventDefault();
47
+ return;
48
+ }
49
+ }
50
+ const figmaContent = getFigmaClipboardContent(event.clipboardData);
51
+ if (figmaContent) {
52
+ event.preventDefault();
53
+ void importFigmaClipboardIntoDesign(figmaContent);
54
+ return;
55
+ }
56
+ if (!canEditDesign) return;
57
+ // The native paste event reflects the current clipboard synchronously.
58
+ // New copies carry their lossless marker in text/html; text/plain remains
59
+ // readable. The helper also accepts legacy markers from text/plain.
60
+ const clipboardResult = readDesignClipboardPayloadFromDataTransfer(
61
+ event.clipboardData,
62
+ );
63
+ if (
64
+ clipboardResult &&
65
+ clipboardResult.markerText !== lastWrittenClipboardMarkerRef.current
66
+ ) {
67
+ adoptDesignClipboardPayload(
68
+ clipboardResult.payload,
69
+ clipboardResult.markerText,
70
+ clipboardResult.plainText,
71
+ );
72
+ }
73
+ const clipboardPlainText = event.clipboardData?.getData("text/plain") ?? "";
74
+ const matchesInMemoryClipboard =
75
+ lastWrittenClipboardPlainTextRef.current !== null &&
76
+ clipboardPlainText === lastWrittenClipboardPlainTextRef.current;
77
+ if (clipboardResult || (hasCanvasClipboard && matchesInMemoryClipboard)) {
78
+ event.preventDefault();
79
+ void handlePasteSelection();
80
+ }
81
+ }
@@ -0,0 +1,155 @@
1
+ import type { CodeLayerNode, CodeLayerTreeNode } from "@shared/code-layer";
2
+ import { buildCodeLayerProjection } from "@shared/code-layer";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+
5
+ import { getOverviewEnterTarget } from "@/pages/design-editor/selection-state";
6
+ import { scheduleBeginTextEditForScreen } from "@/pages/design-editor/text-edit-utils";
7
+ import type { DesignFile } from "@/pages/design-editor/types";
8
+
9
+ export interface EnterHotkeyArgs {
10
+ SINGLE_MODE_TEXT_TAGS: Set<string>;
11
+ activeFile: DesignFile;
12
+ activeFileId: string | null;
13
+ boardFileId: string | undefined;
14
+ codeLayerOwnerByNodeIdRef: RefObject<
15
+ Map<
16
+ string,
17
+ {
18
+ fileId: string;
19
+ node: CodeLayerNode;
20
+ tree: CodeLayerTreeNode[];
21
+ runtimeOnly: boolean;
22
+ }
23
+ >
24
+ >;
25
+ enterVectorEditForSelection: (owner: {
26
+ fileId: string;
27
+ node: CodeLayerNode;
28
+ }) => boolean;
29
+ getProjectionContentForScreen: (screenId: string) => string;
30
+ overviewSelectedScreenIds: string[];
31
+ selectCodeLayerNodesForHotkey: (
32
+ fileId: string,
33
+ nodes: CodeLayerNode[],
34
+ expandedIds?: readonly string[],
35
+ ) => boolean;
36
+ selectedLayerIdsState: string[];
37
+ setActiveFileId: Dispatch<SetStateAction<string | null>>;
38
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
39
+ viewMode: "single" | "overview";
40
+ }
41
+
42
+ export function runEnterHotkey({
43
+ SINGLE_MODE_TEXT_TAGS,
44
+ activeFile,
45
+ activeFileId,
46
+ boardFileId,
47
+ codeLayerOwnerByNodeIdRef,
48
+ enterVectorEditForSelection,
49
+ getProjectionContentForScreen,
50
+ overviewSelectedScreenIds,
51
+ selectCodeLayerNodesForHotkey,
52
+ selectedLayerIdsState,
53
+ setActiveFileId,
54
+ setSelectedLayerIdsState,
55
+ viewMode,
56
+ }: EnterHotkeyArgs) {
57
+ if (viewMode !== "overview") {
58
+ if (selectedLayerIdsState.length === 1) {
59
+ const layerId = selectedLayerIdsState[0]!;
60
+ const owner = codeLayerOwnerByNodeIdRef.current.get(layerId);
61
+ if (owner && owner.fileId === (activeFile?.id ?? activeFileId)) {
62
+ const isTextNode =
63
+ SINGLE_MODE_TEXT_TAGS.has(owner.node.tag) ||
64
+ owner.node.dataAttributes["data-an-primitive"] === "text";
65
+ if (isTextNode) {
66
+ const nodeAttrId =
67
+ owner.node.dataAttributes["data-agent-native-node-id"] ??
68
+ owner.node.id;
69
+ scheduleBeginTextEditForScreen(owner.fileId, nodeAttrId, {
70
+ boardFileId,
71
+ });
72
+ return;
73
+ }
74
+ const childNodes = owner.node.children
75
+ .map(
76
+ (childId) => codeLayerOwnerByNodeIdRef.current.get(childId)?.node,
77
+ )
78
+ .filter((node): node is CodeLayerNode => Boolean(node));
79
+ if (
80
+ selectCodeLayerNodesForHotkey(owner.fileId, childNodes, [
81
+ owner.node.id,
82
+ ])
83
+ ) {
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ return;
89
+ }
90
+ // P5/vector-edit: Enter on a single selected committed pen path (has
91
+ // data-an-pen-nodes — see setPenNodesAttributeOnElement) enters vector
92
+ // edit mode instead of drilling into the owning screen. Vector edit's
93
+ // overlay only exists on MultiScreenCanvas (overview), so this
94
+ // deliberately only fires here — see enterVectorEditForSelection's doc
95
+ // comment for the single-screen deferral.
96
+ if (selectedLayerIdsState.length === 1) {
97
+ const layerId = selectedLayerIdsState[0]!;
98
+ const owner = codeLayerOwnerByNodeIdRef.current.get(layerId);
99
+ const penNodesAttr = owner?.node.dataAttributes["data-an-pen-nodes"];
100
+ if (owner && penNodesAttr && enterVectorEditForSelection(owner)) {
101
+ return;
102
+ }
103
+ // Figma parity: Enter on a selected TEXT layer always begins inline
104
+ // text editing, in overview mode too — not just single-screen mode
105
+ // (see the T22 branch above). Reuses the exact same owner lookup /
106
+ // SINGLE_MODE_TEXT_TAGS / data-an-primitive==="text" classification
107
+ // and scheduleBeginTextEditForScreen path; only the screen-drill
108
+ // fallback below is skipped when the selected layer is text.
109
+ if (owner) {
110
+ const isTextNode =
111
+ SINGLE_MODE_TEXT_TAGS.has(owner.node.tag) ||
112
+ owner.node.dataAttributes["data-an-primitive"] === "text";
113
+ if (isTextNode) {
114
+ const nodeAttrId =
115
+ owner.node.dataAttributes["data-agent-native-node-id"] ??
116
+ owner.node.id;
117
+ scheduleBeginTextEditForScreen(owner.fileId, nodeAttrId, {
118
+ boardFileId,
119
+ });
120
+ return;
121
+ }
122
+ const childNodes = owner.node.children
123
+ .map((childId) => codeLayerOwnerByNodeIdRef.current.get(childId)?.node)
124
+ .filter((node): node is CodeLayerNode => Boolean(node));
125
+ if (
126
+ selectCodeLayerNodesForHotkey(owner.fileId, childNodes, [owner.node.id])
127
+ ) {
128
+ return;
129
+ }
130
+ }
131
+ }
132
+ const target = getOverviewEnterTarget({
133
+ activeFileId: activeFile?.id ?? activeFileId,
134
+ overviewSelectedScreenIds,
135
+ });
136
+ if (!target) return;
137
+ // Ground-truth Figma: Enter on a screen/container selects ALL of its
138
+ // direct children (not just the first) in addition to drilling in.
139
+ // rootNodeIds from the screen's own code-layer projection are exactly
140
+ // its direct children (nodes with no parent, i.e. direct children of
141
+ // <body>) — the same ids setSelectedLayerIdsState already accepts
142
+ // elsewhere (e.g. after insert/paste).
143
+ const targetProjection = buildCodeLayerProjection(
144
+ getProjectionContentForScreen(target),
145
+ );
146
+ if (targetProjection.rootNodeIds.length > 0) {
147
+ setSelectedLayerIdsState(targetProjection.rootNodeIds);
148
+ }
149
+ // Drill-in selects the screen's children and stays on the infinite canvas.
150
+ // It used to also switch to a focused single-screen view — that view no
151
+ // longer exists (the only two views are the canvas and the responsive
152
+ // interactive one), and routing this through the focused view now would
153
+ // land the user in Interact, where there is no selection at all.
154
+ setActiveFileId(target);
155
+ }
@@ -0,0 +1,168 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type { PenPath } from "@shared/pen-path";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+
5
+ import { getScreenPreviewViewport } from "@/components/design/multi-screen/frame-geometry";
6
+ import type { ElementInfo } from "@/components/design/types";
7
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
8
+ import { resolveScreenEntryZoom } from "@/pages/design-editor/overview-camera";
9
+ import {
10
+ DEFAULT_INTERACT_DEVICE_PRESET,
11
+ resolveInteractDeviceForScreen,
12
+ } from "@/pages/design-editor/responsive-interact";
13
+ import type { DesignTool, EditorMode } from "@/pages/design-editor/types";
14
+ import { FOCUSED_SCREEN_ZOOM } from "@/pages/design-editor/types";
15
+
16
+ export interface EnterSingleScreenArgs {
17
+ activeFileId: string | null;
18
+ canvasFrameGeometryById: CanvasFrameGeometryById;
19
+ clearPendingOverviewLayerSelectionTimer: () => void;
20
+ overviewScreens: OverviewScreen[];
21
+ pendingOverviewLayerSelectionRef: RefObject<string | null>;
22
+ pendingOverviewScreenSelectionRef: RefObject<string | null>;
23
+ runEditorViewTransition: (update: () => void) => void;
24
+ screenZoomByIdRef: RefObject<Map<string, number>>;
25
+ setActiveFileId: Dispatch<SetStateAction<string | null>>;
26
+ setActiveTool: Dispatch<SetStateAction<DesignTool>>;
27
+ setCreatedOverviewLayerSelection: Dispatch<
28
+ SetStateAction<{ screenId: string; layerId: string } | null>
29
+ >;
30
+ setDrawMode: Dispatch<SetStateAction<boolean>>;
31
+ setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
32
+ setInteractDeviceName: Dispatch<SetStateAction<string>>;
33
+ setInteractDeviceSize: Dispatch<
34
+ SetStateAction<{ width: number; height: number }>
35
+ >;
36
+ setMode: Dispatch<SetStateAction<EditorMode>>;
37
+ setPinMode: Dispatch<SetStateAction<boolean>>;
38
+ setScreenZoom: Dispatch<SetStateAction<number>>;
39
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
40
+ setVectorEditingState: Dispatch<
41
+ SetStateAction<{ screenId: string; nodeId: string; path: PenPath } | null>
42
+ >;
43
+ setViewMode: Dispatch<SetStateAction<"single" | "overview">>;
44
+ viewModeRef: RefObject<"single" | "overview">;
45
+ }
46
+
47
+ export interface EnterSingleScreenOptions {
48
+ mode?: EditorMode;
49
+ }
50
+
51
+ export function runEnterSingleScreen(
52
+ {
53
+ activeFileId,
54
+ canvasFrameGeometryById,
55
+ clearPendingOverviewLayerSelectionTimer,
56
+ overviewScreens,
57
+ pendingOverviewLayerSelectionRef,
58
+ pendingOverviewScreenSelectionRef,
59
+ runEditorViewTransition,
60
+ screenZoomByIdRef,
61
+ setActiveFileId,
62
+ setActiveTool,
63
+ setCreatedOverviewLayerSelection,
64
+ setDrawMode,
65
+ setHoveredElement,
66
+ setInteractDeviceName,
67
+ setInteractDeviceSize,
68
+ setMode,
69
+ setPinMode,
70
+ setScreenZoom,
71
+ setSelectedElement,
72
+ setVectorEditingState,
73
+ setViewMode,
74
+ viewModeRef,
75
+ }: EnterSingleScreenArgs,
76
+ fileId?: string | null,
77
+ options?: EnterSingleScreenOptions,
78
+ ) {
79
+ const entryMode = options?.mode ?? "interact";
80
+ const targetFileId = fileId ?? activeFileId;
81
+ const targetScreen = targetFileId
82
+ ? overviewScreens.find((screen) => screen.id === targetFileId)
83
+ : undefined;
84
+ const targetMetadataSize = targetScreen
85
+ ? {
86
+ width: targetScreen.width ?? DEFAULT_INTERACT_DEVICE_PRESET.width,
87
+ height: targetScreen.height ?? DEFAULT_INTERACT_DEVICE_PRESET.height,
88
+ }
89
+ : undefined;
90
+ const targetViewport =
91
+ targetFileId && targetMetadataSize
92
+ ? getScreenPreviewViewport(targetMetadataSize, {
93
+ width:
94
+ canvasFrameGeometryById[targetFileId]?.width ??
95
+ targetMetadataSize.width,
96
+ height:
97
+ canvasFrameGeometryById[targetFileId]?.height ??
98
+ targetMetadataSize.height,
99
+ })
100
+ : undefined;
101
+ const nextInteractDevice = resolveInteractDeviceForScreen(
102
+ targetViewport
103
+ ? {
104
+ width: targetViewport.viewportWidth,
105
+ height: targetViewport.viewportHeight,
106
+ }
107
+ : undefined,
108
+ );
109
+ if (
110
+ viewModeRef.current === "single" &&
111
+ (!fileId || fileId === activeFileId)
112
+ ) {
113
+ if (fileId && fileId === activeFileId) {
114
+ // Re-focusing the screen that's already active is a deliberate
115
+ // "reset view" affordance (e.g. re-clicking the same screen's
116
+ // Interact button) — reset to the default zoom rather than
117
+ // restoring the remembered one, mirroring the previous behavior.
118
+ setScreenZoom(FOCUSED_SCREEN_ZOOM);
119
+ }
120
+ // The early return used to swallow a requested mode change, so
121
+ // re-clicking a screen after closing Interact left it in whatever
122
+ // mode it had drifted to instead of reopening the responsive view.
123
+ setMode(entryMode);
124
+ setInteractDeviceName(nextInteractDevice.name);
125
+ setInteractDeviceSize({
126
+ width: nextInteractDevice.width,
127
+ height: nextInteractDevice.height,
128
+ });
129
+ return;
130
+ }
131
+ viewModeRef.current = "single";
132
+ pendingOverviewScreenSelectionRef.current = null;
133
+ pendingOverviewLayerSelectionRef.current = null;
134
+ clearPendingOverviewLayerSelectionTimer();
135
+ setCreatedOverviewLayerSelection(null);
136
+ // P5/vector-edit: MultiScreenCanvas (the only place the vectorEdit
137
+ // overlay renders) unmounts on leaving overview, so an active
138
+ // vector-edit session has nothing left to render into — clear it
139
+ // rather than leaving a stale/orphaned session in memory that would
140
+ // resurface if the user returns to overview later.
141
+ setVectorEditingState(null);
142
+ // Per-screen zoom memory: restore the target screen's last-remembered
143
+ // zoom (recorded by the screenZoomByIdRef effect above) instead of
144
+ // always resetting to FOCUSED_SCREEN_ZOOM, so leaving and re-entering a
145
+ // screen preserves where the user left off. Falls back to
146
+ // FOCUSED_SCREEN_ZOOM for a screen's first visit.
147
+ const restoredZoom = resolveScreenEntryZoom(
148
+ targetFileId,
149
+ screenZoomByIdRef.current,
150
+ FOCUSED_SCREEN_ZOOM,
151
+ );
152
+ runEditorViewTransition(() => {
153
+ if (fileId) setActiveFileId(fileId);
154
+ setDrawMode(false);
155
+ setPinMode(false);
156
+ setMode(entryMode);
157
+ setSelectedElement(null);
158
+ setHoveredElement(null);
159
+ setActiveTool("move");
160
+ setScreenZoom(restoredZoom);
161
+ setInteractDeviceName(nextInteractDevice.name);
162
+ setInteractDeviceSize({
163
+ width: nextInteractDevice.width,
164
+ height: nextInteractDevice.height,
165
+ });
166
+ setViewMode("single");
167
+ });
168
+ }