@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,114 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type { CodeLayerNode } from "@shared/code-layer";
3
+ import { buildCodeLayerProjection } from "@shared/code-layer";
4
+ import type { RefObject } from "react";
5
+
6
+ import { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
7
+ import type { FrameGeometry } from "@/components/design/multi-screen/types";
8
+ import type { DesignHotkeyDistributeAxis } from "@/hooks/useDesignHotkeys";
9
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
10
+ import {
11
+ cloneCanvasFrameGeometry,
12
+ getCanvasFrameGeometry,
13
+ } from "@/pages/design-editor/design-data-geometry-utils";
14
+ import type { AlignableRect } from "@/pages/design-editor/layout-operations";
15
+ import { computeDistributedPositions } from "@/pages/design-editor/layout-operations";
16
+ import type { DesignFile } from "@/pages/design-editor/types";
17
+
18
+ export interface DistributeSelectionArgs {
19
+ activeFile: DesignFile;
20
+ boardFileId: string | undefined;
21
+ boardFrameGeometry: FrameGeometry | undefined;
22
+ canEditDesign: boolean;
23
+ commitNodePositions: (
24
+ baseContent: string,
25
+ positions: ReadonlyMap<string, { x: number; y: number }>,
26
+ ) => boolean;
27
+ designDataJsonRef: RefObject<Record<string, unknown>>;
28
+ getActiveFileSelectedNodeIds: (content: string) => string[];
29
+ getFreshActiveContent: () => string;
30
+ handleGeometryCommit: (
31
+ before: CanvasFrameGeometryById,
32
+ after: CanvasFrameGeometryById,
33
+ options?: { source?: "pointer" | "keyboard" },
34
+ ) => void;
35
+ overviewScreens: OverviewScreen[];
36
+ overviewSelectedScreenIds: string[];
37
+ rectFromCodeLayerNode: (node: CodeLayerNode) => AlignableRect;
38
+ viewModeRef: RefObject<"single" | "overview">;
39
+ }
40
+
41
+ export function runDistributeSelection(
42
+ {
43
+ activeFile,
44
+ boardFileId,
45
+ boardFrameGeometry,
46
+ canEditDesign,
47
+ commitNodePositions,
48
+ designDataJsonRef,
49
+ getActiveFileSelectedNodeIds,
50
+ getFreshActiveContent,
51
+ handleGeometryCommit,
52
+ overviewScreens,
53
+ overviewSelectedScreenIds,
54
+ rectFromCodeLayerNode,
55
+ viewModeRef,
56
+ }: DistributeSelectionArgs,
57
+ axis: DesignHotkeyDistributeAxis,
58
+ ) {
59
+ if (!canEditDesign) return;
60
+
61
+ if (viewModeRef.current === "overview") {
62
+ if (overviewSelectedScreenIds.length < 3) return;
63
+ const before = getCanvasFrameGeometry(designDataJsonRef.current);
64
+ const screenRects: AlignableRect[] = [];
65
+ overviewSelectedScreenIds.forEach((screenId) => {
66
+ const screenIndex = overviewScreens.findIndex(
67
+ (screen) => screen.id === screenId,
68
+ );
69
+ const screen =
70
+ screenIndex >= 0 ? overviewScreens[screenIndex] : undefined;
71
+ const fallbackGeometry =
72
+ screenIndex >= 0
73
+ ? getInitialFrameGeometry(screenIndex, {
74
+ width: screen?.width ?? 1280,
75
+ height: screen?.height ?? 2560,
76
+ })
77
+ : boardFileId === screenId
78
+ ? boardFrameGeometry
79
+ : undefined;
80
+ if (!fallbackGeometry) return;
81
+ const geometry = { ...fallbackGeometry, ...before[screenId] };
82
+ screenRects.push({
83
+ id: screenId,
84
+ x: geometry.x,
85
+ y: geometry.y,
86
+ width: geometry.width,
87
+ height: geometry.height,
88
+ });
89
+ });
90
+ if (screenRects.length < 3) return;
91
+ const positions = computeDistributedPositions(screenRects, axis);
92
+ if (positions.size === 0) return;
93
+ const after = cloneCanvasFrameGeometry(before);
94
+ positions.forEach((position, screenId) => {
95
+ after[screenId] = { ...after[screenId]!, ...position };
96
+ });
97
+ handleGeometryCommit(before, after);
98
+ return;
99
+ }
100
+
101
+ if (!activeFile) return;
102
+ const baseContent = getFreshActiveContent();
103
+ const nodeIds = getActiveFileSelectedNodeIds(baseContent);
104
+ if (nodeIds.length < 3) return;
105
+ const projection = buildCodeLayerProjection(baseContent);
106
+ const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
107
+ const selectedNodes = nodeIds
108
+ .map((nodeId) => nodesById.get(nodeId))
109
+ .filter((node): node is CodeLayerNode => Boolean(node));
110
+ if (selectedNodes.length < 3) return;
111
+ const selectedRects = selectedNodes.map(rectFromCodeLayerNode);
112
+ const positions = computeDistributedPositions(selectedRects, axis);
113
+ commitNodePositions(baseContent, positions);
114
+ }
@@ -0,0 +1,116 @@
1
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import { normalizeDesignSourceType } from "@shared/source-mode";
3
+ import type { Dispatch, RefObject, SetStateAction } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
7
+ import type { RasterPdfPage } from "@/pages/design-editor/export-capture";
8
+ import {
9
+ PDF_MIN_PRINT_RASTER_SCALE,
10
+ createMultiPageRasterPdf,
11
+ } from "@/pages/design-editor/export-capture";
12
+ import {
13
+ PngCaptureError,
14
+ cropCanvasToRect,
15
+ renderExportDocumentCanvas,
16
+ } from "@/pages/design-editor/png-export-render";
17
+
18
+ export interface DownloadAllScreensPdfArgs {
19
+ activeCanvasSourceType: "inline" | "localhost" | "fusion";
20
+ canEditDesign: boolean;
21
+ canvasFrameGeometryById: CanvasFrameGeometryById;
22
+ fallbackExportName: (extension: string, suffix?: string) => string;
23
+ overviewScreens: OverviewScreen[];
24
+ pngExportingRef: RefObject<boolean>;
25
+ setPngExporting: Dispatch<SetStateAction<boolean>>;
26
+ showRasterCaptureError: (error: unknown, format?: "png" | "pdf") => void;
27
+ t: (key: string, options?: Record<string, unknown>) => string;
28
+ triggerBlobDownload: (blob: Blob, filename: string) => void;
29
+ }
30
+
31
+ export async function runDownloadAllScreensPdf({
32
+ activeCanvasSourceType,
33
+ canEditDesign,
34
+ canvasFrameGeometryById,
35
+ fallbackExportName,
36
+ overviewScreens,
37
+ pngExportingRef,
38
+ setPngExporting,
39
+ showRasterCaptureError,
40
+ t,
41
+ triggerBlobDownload,
42
+ }: DownloadAllScreensPdfArgs) {
43
+ if (pngExportingRef.current) return;
44
+ if (overviewScreens.length < 2) return;
45
+ pngExportingRef.current = true;
46
+ setPngExporting(true);
47
+ try {
48
+ const html2canvas = (await import("html2canvas")).default;
49
+ const pages: RasterPdfPage[] = [];
50
+ for (const screen of overviewScreens) {
51
+ const iframe = document.querySelector<HTMLIFrameElement>(
52
+ `iframe[data-design-preview-iframe][data-screen-iframe-id="${CSS.escape(screen.id)}"]`,
53
+ );
54
+ if (!iframe) throw new PngCaptureError("no-preview");
55
+ let doc: Document | null = null;
56
+ try {
57
+ doc = iframe.contentDocument;
58
+ if (!doc?.documentElement) doc = null;
59
+ } catch {
60
+ doc = null;
61
+ }
62
+ if (!doc) {
63
+ const sourceType =
64
+ normalizeDesignSourceType(iframe.dataset.designSourceType) ??
65
+ activeCanvasSourceType;
66
+ if (sourceType !== "inline") {
67
+ throw new PngCaptureError("external-preview");
68
+ }
69
+ if (!canEditDesign) {
70
+ throw new PngCaptureError("read-only-preview");
71
+ }
72
+ throw new PngCaptureError("no-preview");
73
+ }
74
+ const geometry = canvasFrameGeometryById[screen.id] ?? {};
75
+ const pageWidth = Math.max(
76
+ 1,
77
+ geometry.width ?? screen.width ?? iframe.clientWidth,
78
+ );
79
+ const pageHeight = Math.max(
80
+ 1,
81
+ geometry.height ?? screen.height ?? iframe.clientHeight,
82
+ );
83
+ const rendered = await renderExportDocumentCanvas({
84
+ doc,
85
+ iframe,
86
+ // Same print-quality floor as the single-page path: a 1x capture
87
+ // stretched to fill a fixed physical page size reads as blurry.
88
+ exportScale: PDF_MIN_PRINT_RASTER_SCALE,
89
+ render: html2canvas,
90
+ });
91
+ const view = doc.defaultView;
92
+ const viewportCanvas = cropCanvasToRect(
93
+ rendered.canvas,
94
+ {
95
+ x: view?.scrollX ?? 0,
96
+ y: view?.scrollY ?? 0,
97
+ width: Math.max(1, iframe.clientWidth),
98
+ height: Math.max(1, iframe.clientHeight),
99
+ },
100
+ rendered.scale,
101
+ );
102
+ const dataUrl = (viewportCanvas ?? rendered.canvas).toDataURL(
103
+ "image/png",
104
+ );
105
+ pages.push({ dataUrl, width: pageWidth, height: pageHeight });
106
+ }
107
+ const pdf = await createMultiPageRasterPdf(pages);
108
+ triggerBlobDownload(pdf, fallbackExportName("pdf", "all-screens"));
109
+ toast.success(t("designEditor.toasts.pdfAllScreensDownloaded"));
110
+ } catch (error) {
111
+ showRasterCaptureError(error, "pdf");
112
+ } finally {
113
+ pngExportingRef.current = false;
114
+ setPngExporting(false);
115
+ }
116
+ }
@@ -0,0 +1,103 @@
1
+ import type { Dispatch, RefObject, SetStateAction } from "react";
2
+ import { toast } from "sonner";
3
+
4
+ import type { ExportSettingsValue } from "@/components/design/inspector";
5
+ import type { ElementInfo } from "@/components/design/types";
6
+ import {
7
+ PDF_MIN_PRINT_RASTER_SCALE,
8
+ createSinglePageRasterPdf,
9
+ } from "@/pages/design-editor/export-capture";
10
+ import type { PngCaptureScope } from "@/pages/design-editor/png-export-render";
11
+ import { resolveExportCropRect } from "@/pages/design-editor/png-export-render";
12
+
13
+ export interface DownloadPdfArgs {
14
+ fallbackExportName: (extension: string, suffix?: string) => string;
15
+ pngExportingRef: RefObject<boolean>;
16
+ renderPngBlob: (arg0: {
17
+ scope: PngCaptureScope;
18
+ settings?: Partial<ExportSettingsValue>;
19
+ format?: "png" | "jpg" | "webp";
20
+ }) => Promise<Blob>;
21
+ resolvePngCaptureTarget: (scope: PngCaptureScope) => {
22
+ cropSelection: ElementInfo | readonly ElementInfo[] | null;
23
+ doc: Document;
24
+ iframe: HTMLIFrameElement;
25
+ };
26
+ setPngExporting: Dispatch<SetStateAction<boolean>>;
27
+ showRasterCaptureError: (error: unknown, format?: "png" | "pdf") => void;
28
+ t: (key: string, options?: Record<string, unknown>) => string;
29
+ triggerBlobDownload: (blob: Blob, filename: string) => void;
30
+ }
31
+
32
+ export async function runDownloadPdf(
33
+ {
34
+ fallbackExportName,
35
+ pngExportingRef,
36
+ renderPngBlob,
37
+ resolvePngCaptureTarget,
38
+ setPngExporting,
39
+ showRasterCaptureError,
40
+ t,
41
+ triggerBlobDownload,
42
+ }: DownloadPdfArgs,
43
+ settings?: Partial<ExportSettingsValue>,
44
+ ) {
45
+ if (pngExportingRef.current) return;
46
+ pngExportingRef.current = true;
47
+ setPngExporting(true);
48
+ try {
49
+ const { cropSelection, doc, iframe } = resolvePngCaptureTarget("document");
50
+ const crop = resolveExportCropRect(doc, cropSelection);
51
+ const pageWidth = Math.max(
52
+ 1,
53
+ crop?.width ??
54
+ Math.max(
55
+ doc.documentElement.scrollWidth,
56
+ doc.body?.scrollWidth ?? 0,
57
+ iframe.clientWidth,
58
+ ),
59
+ );
60
+ const pageHeight = Math.max(
61
+ 1,
62
+ crop?.height ??
63
+ Math.max(
64
+ doc.documentElement.scrollHeight,
65
+ doc.body?.scrollHeight ?? 0,
66
+ iframe.clientHeight,
67
+ ),
68
+ );
69
+ // Force a print-quality raster floor: the PDF page renders at a fixed
70
+ // physical size (see createSinglePageRasterPdf), so a 1x capture —
71
+ // the export panel's ordinary default — embeds only ~96 DPI, which
72
+ // looks visibly soft once printed. Still honor an explicit higher
73
+ // user-selected scale (3x/4x).
74
+ const pdfScale = Math.max(
75
+ PDF_MIN_PRINT_RASTER_SCALE,
76
+ settings?.scale ?? PDF_MIN_PRINT_RASTER_SCALE,
77
+ );
78
+ const png = await renderPngBlob({
79
+ scope: "document",
80
+ settings: { ...settings, scale: pdfScale },
81
+ format: "png",
82
+ });
83
+ const dataUrl = await new Promise<string>((resolve, reject) => {
84
+ const reader = new FileReader();
85
+ reader.onerror = () =>
86
+ reject(reader.error ?? new Error("PDF read failed"));
87
+ reader.onload = () => resolve(String(reader.result));
88
+ reader.readAsDataURL(png);
89
+ });
90
+ const pdf = await createSinglePageRasterPdf({
91
+ dataUrl,
92
+ width: pageWidth,
93
+ height: pageHeight,
94
+ });
95
+ triggerBlobDownload(pdf, fallbackExportName("pdf", settings?.suffix));
96
+ toast.success(t("designEditor.toasts.pdfDownloaded"));
97
+ } catch (error) {
98
+ showRasterCaptureError(error, "pdf");
99
+ } finally {
100
+ pngExportingRef.current = false;
101
+ setPngExporting(false);
102
+ }
103
+ }
@@ -0,0 +1,159 @@
1
+ import type { Dispatch, SetStateAction } from "react";
2
+ import { toast } from "sonner";
3
+
4
+ import type { ExportSettingsValue } from "@/components/design/inspector";
5
+ import type { ElementInfo } from "@/components/design/types";
6
+ import {
7
+ buildStaticForeignObjectSvg,
8
+ stripNonStaticXmlAttributes,
9
+ waitForExportReady,
10
+ } from "@/pages/design-editor/export-capture";
11
+ import {
12
+ removeEditorChromeOverlays,
13
+ resolveExportCropRect,
14
+ sanitizeSerializedXmlForSvg,
15
+ } from "@/pages/design-editor/png-export-render";
16
+ import type { DesignData } from "@/pages/design-editor/types";
17
+
18
+ export interface DownloadSvgArgs {
19
+ design: DesignData | null;
20
+ fallbackExportName: (extension: string, suffix?: string) => string;
21
+ selectedElement: ElementInfo | null;
22
+ setSvgExporting: Dispatch<SetStateAction<boolean>>;
23
+ t: (key: string, options?: Record<string, unknown>) => string;
24
+ triggerBlobDownload: (blob: Blob, filename: string) => void;
25
+ }
26
+
27
+ export async function runDownloadSvg(
28
+ {
29
+ design,
30
+ fallbackExportName,
31
+ selectedElement,
32
+ setSvgExporting,
33
+ t,
34
+ triggerBlobDownload,
35
+ }: DownloadSvgArgs,
36
+ settings?: Partial<ExportSettingsValue>,
37
+ ) {
38
+ const iframe = document.querySelector<HTMLIFrameElement>(
39
+ "iframe[data-design-preview-iframe]",
40
+ );
41
+ const doc = iframe?.contentDocument;
42
+ if (!doc?.documentElement) {
43
+ toast.error(t("designEditor.toasts.openScreenSvg"));
44
+ return;
45
+ }
46
+
47
+ setSvgExporting(true);
48
+ try {
49
+ // Same readiness race as the PNG path (see waitForExportReady):
50
+ // without this, an SVG downloaded right after a screen loads inlines
51
+ // whatever partial/empty CSSOM exists at that instant, producing an
52
+ // unstyled export for no reason a user could tell from the UI.
53
+ await waitForExportReady(doc);
54
+ const width = Math.max(
55
+ doc.documentElement.scrollWidth,
56
+ doc.body?.scrollWidth ?? 0,
57
+ iframe?.clientWidth ?? 0,
58
+ );
59
+ const height = Math.max(
60
+ doc.documentElement.scrollHeight,
61
+ doc.body?.scrollHeight ?? 0,
62
+ iframe?.clientHeight ?? 0,
63
+ );
64
+ const clone = doc.documentElement.cloneNode(true) as HTMLElement;
65
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
66
+ const stylesheetLinks = Array.from(
67
+ doc.querySelectorAll<HTMLLinkElement>('link[rel~="stylesheet"]'),
68
+ );
69
+ const clonedStylesheetLinks = Array.from(
70
+ clone.querySelectorAll<HTMLLinkElement>('link[rel~="stylesheet"]'),
71
+ );
72
+ const stylesheets = Array.from(doc.styleSheets);
73
+
74
+ stylesheetLinks.forEach((link, index) => {
75
+ const sheet = stylesheets.find(
76
+ (candidate) =>
77
+ (candidate as StyleSheet & { ownerNode?: Node | null }).ownerNode ===
78
+ link,
79
+ ) as CSSStyleSheet | undefined;
80
+ let cssText = "";
81
+ try {
82
+ cssText = Array.from(sheet?.cssRules ?? [])
83
+ .map((rule) => rule.cssText)
84
+ .join("\n");
85
+ } catch {
86
+ // Cross-origin stylesheets cannot be read. Leave the original link in
87
+ // place instead of failing the whole export.
88
+ return;
89
+ }
90
+ if (!cssText.trim()) return;
91
+ const style = doc.createElement("style");
92
+ style.setAttribute(
93
+ "data-agent-native-inlined-stylesheet",
94
+ link.getAttribute("href") ?? "",
95
+ );
96
+ style.textContent = cssText;
97
+ clonedStylesheetLinks[index]?.replaceWith(style);
98
+ });
99
+ clone.querySelectorAll("script").forEach((node) => node.remove());
100
+ // Strip the editor's selection outline / handles so the SVG shows only
101
+ // the design, not the editor chrome.
102
+ removeEditorChromeOverlays(clone);
103
+ // The live DOM can contain Alpine/Vue-style directive attributes such
104
+ // as `@click`, `:class`, and `x-bind:class`. They are valid HTML but
105
+ // invalid/unbound XML names, and the old serializer emitted a download
106
+ // that xmllint/Figma rejected at the first directive. This is a static
107
+ // snapshot (scripts are removed just above), so strip executable and
108
+ // otherwise XML-unsafe attributes from the clone before serialization.
109
+ stripNonStaticXmlAttributes(clone);
110
+ clone.style.width = `${width}px`;
111
+ clone.style.minHeight = `${height}px`;
112
+
113
+ const body = clone.querySelector("body") as HTMLElement | null;
114
+ if (body) {
115
+ body.style.margin = body.style.margin || "0";
116
+ body.style.width = `${width}px`;
117
+ body.style.minHeight = `${height}px`;
118
+ }
119
+
120
+ const serializedHtml = sanitizeSerializedXmlForSvg(
121
+ new XMLSerializer().serializeToString(clone),
122
+ );
123
+ const safeTitle =
124
+ design?.title
125
+ ?.replace(/&/g, "&amp;")
126
+ .replace(/"/g, "&quot;")
127
+ .replace(/</g, "&lt;")
128
+ .replace(/>/g, "&gt;") || t("designEditor.designExport");
129
+ const exportScale = Math.max(0.1, Math.min(4, settings?.scale ?? 1));
130
+ // When an element is selected, crop to just that frame by narrowing the
131
+ // SVG viewBox to its document-space rect. The foreignObject still holds
132
+ // the full document so layout and inherited styles stay intact; the
133
+ // viewBox clips the visible region to the selection.
134
+ const cropRect = resolveExportCropRect(doc, selectedElement);
135
+ const svg = buildStaticForeignObjectSvg({
136
+ documentWidth: width,
137
+ documentHeight: height,
138
+ cropRect,
139
+ scale: exportScale,
140
+ safeTitle,
141
+ serializedHtml,
142
+ });
143
+
144
+ triggerBlobDownload(
145
+ new Blob([svg], { type: "image/svg+xml;charset=utf-8" }),
146
+ fallbackExportName("svg", settings?.suffix),
147
+ );
148
+ toast.success(t("designEditor.toasts.svgDownloaded"));
149
+ } catch (error) {
150
+ console.error("SVG export failed:", error);
151
+ toast.error(
152
+ error instanceof Error
153
+ ? error.message
154
+ : t("designEditor.toasts.svgExportError"),
155
+ );
156
+ } finally {
157
+ setSvgExporting(false);
158
+ }
159
+ }
@@ -0,0 +1,201 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { RefObject } from "react";
5
+ import { toast } from "sonner";
6
+
7
+ import { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
8
+ import type { FrameGeometry } from "@/components/design/multi-screen/types";
9
+ import {
10
+ nextDuplicatedFilename,
11
+ normalizedDesignFileType,
12
+ reassignDuplicatedNodeIds,
13
+ } from "@/pages/design-editor/canvas-primitive-insert";
14
+ import type { DesignDataOperation } from "@/pages/design-editor/data-operations";
15
+ import { applyDesignDataOperations } from "@/pages/design-editor/data-operations";
16
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
17
+ import {
18
+ getCanvasFrameGeometry,
19
+ getDesignDataRecord,
20
+ } from "@/pages/design-editor/design-data-geometry-utils";
21
+ import type { FileCreationHistoryEntry } from "@/pages/design-editor/history";
22
+ import type { DesignFile } from "@/pages/design-editor/types";
23
+
24
+ export interface DuplicateScreenArgs {
25
+ canEditDesign: boolean;
26
+ createFileAsync: ReturnType<
27
+ typeof useActionMutation<undefined, undefined, "create-file">
28
+ >["mutateAsync"];
29
+ designDataJsonRef: RefObject<Record<string, unknown>>;
30
+ files: DesignFile[];
31
+ focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
32
+ id: string | undefined;
33
+ liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
34
+ optimisticallyInsertCreatedFile: (args: {
35
+ fileId: string;
36
+ filename: string;
37
+ fileType: DesignFile["fileType"];
38
+ content: string;
39
+ result?: Record<string, unknown> | null;
40
+ }) => void;
41
+ overviewScreens: OverviewScreen[];
42
+ queryClient: QueryClient;
43
+ recordFileCreationHistoryEntry: (entry: FileCreationHistoryEntry) => void;
44
+ t: (key: string, options?: Record<string, unknown>) => string;
45
+ updateDesignAsync: ReturnType<
46
+ typeof useActionMutation<undefined, undefined, "update-design">
47
+ >["mutateAsync"];
48
+ writeFrameGeometrySnapshot: (
49
+ geometryById: CanvasFrameGeometryById,
50
+ options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
51
+ ) => void;
52
+ }
53
+
54
+ export function runDuplicateScreen(
55
+ {
56
+ canEditDesign,
57
+ createFileAsync,
58
+ designDataJsonRef,
59
+ files,
60
+ focusCreatedScreen,
61
+ id,
62
+ liveFrameGeometryRef,
63
+ optimisticallyInsertCreatedFile,
64
+ overviewScreens,
65
+ queryClient,
66
+ recordFileCreationHistoryEntry,
67
+ t,
68
+ updateDesignAsync,
69
+ writeFrameGeometrySnapshot,
70
+ }: DuplicateScreenArgs,
71
+ screenId: string,
72
+ request?: {
73
+ canvasPosition?: { x: number; y: number };
74
+ },
75
+ ) {
76
+ if (!id || !canEditDesign) return;
77
+ const source = files.find((file) => file.id === screenId);
78
+ if (!source) return;
79
+ const filename = nextDuplicatedFilename(files, source.filename);
80
+ const content = reassignDuplicatedNodeIds(source.content);
81
+ const fileType = normalizedDesignFileType(source.fileType);
82
+ const sourceOverviewScreen = overviewScreens.find(
83
+ (screen) => screen.id === screenId,
84
+ );
85
+ const fallbackGeometry = getInitialFrameGeometry(overviewScreens.length, {
86
+ width: sourceOverviewScreen?.width ?? 1280,
87
+ height: sourceOverviewScreen?.height ?? 2560,
88
+ });
89
+ const createdGeometry: FrameGeometry = request?.canvasPosition
90
+ ? {
91
+ ...fallbackGeometry,
92
+ ...liveFrameGeometryRef.current[screenId],
93
+ x: request.canvasPosition.x,
94
+ y: request.canvasPosition.y,
95
+ }
96
+ : fallbackGeometry;
97
+
98
+ // Per-call mutate callbacks, not a promise, would silently strand every
99
+ // duplicate but the last: a second mutate() detaches the observer from
100
+ // the first mutation, so only the newest call's onSuccess ever runs.
101
+ void createFileAsync({
102
+ designId: id,
103
+ filename,
104
+ content,
105
+ fileType,
106
+ } as any)
107
+ .then((result: any) => {
108
+ const nextId = typeof result?.id === "string" ? result.id : null;
109
+ // Refetch only when there is no created id to insert optimistically:
110
+ // a whole-design refetch re-downloads every screen's HTML, which is
111
+ // what made adding a frame feel slow.
112
+ if (!nextId) {
113
+ queryClient.invalidateQueries({
114
+ queryKey: ["action", "get-design"],
115
+ });
116
+ } else {
117
+ optimisticallyInsertCreatedFile({
118
+ fileId: nextId,
119
+ filename,
120
+ fileType,
121
+ content,
122
+ result,
123
+ });
124
+ // Optimistic geometry keeps frame, selection, and camera agreeing
125
+ // before the refetch. Base it on the map writeFrameGeometrySnapshot
126
+ // diffs against, or a sibling duplicate's placement is deleted.
127
+ writeFrameGeometrySnapshot({
128
+ ...getCanvasFrameGeometry(designDataJsonRef.current),
129
+ [nextId]: createdGeometry,
130
+ });
131
+ focusCreatedScreen(nextId, createdGeometry);
132
+ recordFileCreationHistoryEntry({
133
+ filename,
134
+ content,
135
+ fileType,
136
+ geometry: createdGeometry,
137
+ });
138
+ // A duplicated localhost/fusion screen stays URL-backed only if its
139
+ // metadata comes along, and the carry must be path-addressed or it
140
+ // replaces a peer's metadata for every other screen.
141
+ const sourceMetadataById = getDesignDataRecord(
142
+ designDataJsonRef.current,
143
+ "screenMetadata",
144
+ );
145
+ const sourceMetadata = getDesignDataRecord(
146
+ sourceMetadataById,
147
+ screenId,
148
+ );
149
+ const sourceType = sourceMetadata.sourceType;
150
+ if (sourceType === "localhost" || sourceType === "fusion") {
151
+ const dataOperations: DesignDataOperation[] = [
152
+ {
153
+ op: "set",
154
+ path: ["screenMetadata", nextId],
155
+ value: { ...sourceMetadata },
156
+ },
157
+ ];
158
+ const sourceLocalhostScreensById = getDesignDataRecord(
159
+ designDataJsonRef.current,
160
+ "localhostScreens",
161
+ );
162
+ const sourceLocalhostScreen = getDesignDataRecord(
163
+ sourceLocalhostScreensById,
164
+ screenId,
165
+ );
166
+ if (Object.keys(sourceLocalhostScreen).length > 0) {
167
+ dataOperations.push({
168
+ op: "set",
169
+ path: ["localhostScreens", nextId],
170
+ value: { ...sourceLocalhostScreen },
171
+ });
172
+ }
173
+ const nextData = applyDesignDataOperations(
174
+ designDataJsonRef.current,
175
+ dataOperations,
176
+ );
177
+ designDataJsonRef.current = nextData;
178
+ queryClient.setQueryData(
179
+ ["action", "get-design", { id }],
180
+ (old: any) => {
181
+ if (!old || typeof old !== "object") return old;
182
+ return { ...old, data: JSON.stringify(nextData) };
183
+ },
184
+ );
185
+ void updateDesignAsync({ id, dataOperations } as any).catch(() => {
186
+ queryClient.invalidateQueries({
187
+ queryKey: ["action", "get-design"],
188
+ });
189
+ });
190
+ }
191
+ }
192
+ toast.success(t("designEditor.toasts.screenDuplicated"));
193
+ })
194
+ .catch((error: unknown) => {
195
+ toast.error(
196
+ error instanceof Error
197
+ ? error.message
198
+ : t("designEditor.toasts.screenDuplicateError"),
199
+ );
200
+ });
201
+ }