@agent-native/core 0.80.0 → 0.80.2

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 +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +14 -1
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/application-state/handlers.ts +1 -1
  27. package/corpus/core/src/brand-kit/index.ts +1 -1
  28. package/corpus/core/src/cli/skills.ts +18 -0
  29. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  30. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  31. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  32. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  33. package/corpus/core/src/extensions/path.ts +1 -4
  34. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  35. package/corpus/core/src/mcp/build-server.ts +2 -2
  36. package/corpus/core/src/mcp/embed-app.ts +35 -1
  37. package/corpus/core/src/org/handlers.ts +5 -5
  38. package/corpus/core/src/org/plugin.ts +2 -2
  39. package/corpus/core/src/scripts/call-agent.ts +1 -1
  40. package/corpus/core/src/scripts/db/safety.ts +1 -1
  41. package/corpus/core/src/server/auth.ts +3 -0
  42. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  43. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  44. package/corpus/core/src/server/index.ts +8 -0
  45. package/corpus/core/src/styles/agent-native.css +14 -7
  46. package/corpus/core/src/usage/store.ts +2 -2
  47. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  48. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  49. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  50. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  51. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  52. package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
  53. package/corpus/templates/analytics/app/i18n-data.ts +404 -0
  54. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
  58. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
  59. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  66. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  67. package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
  68. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  69. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  70. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  71. package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
  72. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  73. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  74. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  75. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  76. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  77. package/corpus/templates/content/shared/document-export.ts +1 -1
  78. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  79. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
  80. package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
  81. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  82. package/corpus/templates/design/actions/update-file.ts +9 -2
  83. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
  84. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
  85. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
  86. package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
  87. package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
  88. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
  89. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
  90. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
  91. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
  92. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
  93. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
  94. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
  95. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  96. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  97. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
  98. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
  99. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  100. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
  101. package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
  102. package/corpus/templates/design/app/global.css +29 -3
  103. package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
  104. package/corpus/templates/design/app/i18n-data.ts +239 -1
  105. package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
  106. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  107. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  108. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  109. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  110. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  111. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  112. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  113. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  114. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  115. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  116. package/corpus/templates/design/shared/code-layer.ts +209 -9
  117. package/corpus/templates/design/shared/html-content.ts +21 -0
  118. package/corpus/templates/design/shared/pen-path.ts +207 -0
  119. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  120. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  121. package/corpus/templates/slides/AGENTS.md +3 -2
  122. package/corpus/templates/slides/actions/import-file.ts +26 -17
  123. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  124. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  125. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  126. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  127. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  128. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  129. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  130. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  131. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  132. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  133. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  134. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  135. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  136. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  137. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  138. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  139. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  140. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  141. package/dist/application-state/handlers.js +1 -1
  142. package/dist/application-state/handlers.js.map +1 -1
  143. package/dist/brand-kit/index.d.ts +1 -1
  144. package/dist/brand-kit/index.js +1 -1
  145. package/dist/brand-kit/index.js.map +1 -1
  146. package/dist/cli/skills.d.ts.map +1 -1
  147. package/dist/cli/skills.js +18 -0
  148. package/dist/cli/skills.js.map +1 -1
  149. package/dist/client/AgentPanel.js +1 -1
  150. package/dist/client/AgentPanel.js.map +1 -1
  151. package/dist/client/MultiTabAssistantChat.js +1 -1
  152. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  153. package/dist/client/extensions/portable-extension.d.ts +1 -1
  154. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  155. package/dist/client/extensions/portable-extension.js.map +1 -1
  156. package/dist/code-agents/transcript-normalizer.js +1 -1
  157. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  158. package/dist/collab/awareness.d.ts +2 -2
  159. package/dist/collab/awareness.d.ts.map +1 -1
  160. package/dist/collab/routes.d.ts +1 -1
  161. package/dist/extensions/path.d.ts +1 -1
  162. package/dist/extensions/path.d.ts.map +1 -1
  163. package/dist/extensions/path.js.map +1 -1
  164. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  165. package/dist/integrations/a2a-continuation-processor.js +1 -1
  166. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  167. package/dist/mcp/build-server.d.ts +1 -1
  168. package/dist/mcp/build-server.d.ts.map +1 -1
  169. package/dist/mcp/build-server.js +1 -1
  170. package/dist/mcp/build-server.js.map +1 -1
  171. package/dist/mcp/embed-app.d.ts.map +1 -1
  172. package/dist/mcp/embed-app.js +35 -1
  173. package/dist/mcp/embed-app.js.map +1 -1
  174. package/dist/notifications/routes.d.ts +3 -3
  175. package/dist/observability/routes.d.ts +8 -8
  176. package/dist/org/handlers.js +5 -5
  177. package/dist/org/handlers.js.map +1 -1
  178. package/dist/org/plugin.js +2 -2
  179. package/dist/org/plugin.js.map +1 -1
  180. package/dist/resources/handlers.d.ts +1 -1
  181. package/dist/scripts/call-agent.js +1 -1
  182. package/dist/scripts/call-agent.js.map +1 -1
  183. package/dist/scripts/db/safety.js +1 -1
  184. package/dist/scripts/db/safety.js.map +1 -1
  185. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  186. package/dist/server/auth.d.ts.map +1 -1
  187. package/dist/server/auth.js +3 -0
  188. package/dist/server/auth.js.map +1 -1
  189. package/dist/server/builder-design-systems.d.ts +28 -0
  190. package/dist/server/builder-design-systems.d.ts.map +1 -0
  191. package/dist/server/builder-design-systems.js +217 -0
  192. package/dist/server/builder-design-systems.js.map +1 -0
  193. package/dist/server/core-routes-plugin.d.ts +1 -0
  194. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  195. package/dist/server/core-routes-plugin.js +20 -3
  196. package/dist/server/core-routes-plugin.js.map +1 -1
  197. package/dist/server/index.d.ts +1 -0
  198. package/dist/server/index.d.ts.map +1 -1
  199. package/dist/server/index.js +1 -0
  200. package/dist/server/index.js.map +1 -1
  201. package/dist/styles/agent-native.css +14 -7
  202. package/dist/usage/store.js +2 -2
  203. package/dist/usage/store.js.map +1 -1
  204. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  205. package/docs/content/locales/ar-SA/template-design.mdx +14 -1
  206. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  207. package/docs/content/locales/de-DE/template-design.mdx +15 -1
  208. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  209. package/docs/content/locales/es-ES/template-design.mdx +15 -1
  210. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  211. package/docs/content/locales/fr-FR/template-design.mdx +15 -1
  212. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  213. package/docs/content/locales/hi-IN/template-design.mdx +15 -1
  214. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  215. package/docs/content/locales/ja-JP/template-design.mdx +16 -1
  216. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  217. package/docs/content/locales/ko-KR/template-design.mdx +14 -1
  218. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  219. package/docs/content/locales/pt-BR/template-design.mdx +14 -1
  220. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  221. package/docs/content/locales/zh-CN/template-design.mdx +14 -1
  222. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  223. package/docs/content/locales/zh-TW/template-design.mdx +14 -1
  224. package/docs/content/skills-guide.mdx +5 -0
  225. package/docs/content/template-design.mdx +14 -1
  226. package/package.json +1 -1
  227. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  228. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -5,11 +5,14 @@ import {
5
5
  useCollaborativeDoc,
6
6
  isReconcileLeadClient,
7
7
  generateTabId,
8
+ dedupeCollabUsersByEmail,
8
9
  emailToColor,
9
10
  emailToName,
10
11
  PresenceBar,
11
12
  AgentToggleButton,
12
13
  ShareButton,
14
+ agentNativePath,
15
+ ensureEmbedAuthFetchInterceptor,
13
16
  isEmbedAuthActive,
14
17
  sendToAgentChat,
15
18
  getBrowserTabId,
@@ -22,13 +25,13 @@ import {
22
25
  useT,
23
26
  useChangeVersion,
24
27
  useAgentChatContext,
28
+ useAvatarUrl,
25
29
  type CollabUser,
26
30
  type PromptComposerSubmitOptions,
27
31
  } from "@agent-native/core/client";
28
32
  import type { TweakDefinition } from "@shared/api";
29
33
  import {
30
34
  parseCanvasFrameGeometryById,
31
- type CanvasFrameGeometry,
32
35
  type CanvasFrameGeometryById,
33
36
  } from "@shared/canvas-frames";
34
37
  import {
@@ -40,12 +43,14 @@ import {
40
43
  type CodeLayerNode,
41
44
  type CodeLayerTreeNode,
42
45
  } from "@shared/code-layer";
46
+ import { shouldUseLiveFileContent } from "@shared/html-content";
43
47
  import {
44
48
  resolveTweaksToCssVars,
45
49
  type TweakSelections,
46
50
  } from "@shared/resolve-tweaks";
47
51
  import {
48
52
  IconArrowLeft,
53
+ IconArrowUpRight,
49
54
  IconPencil,
50
55
  IconMessage,
51
56
  IconBrush,
@@ -57,6 +62,7 @@ import {
57
62
  IconViewportWide,
58
63
  IconPlus,
59
64
  IconLayoutGrid,
65
+ IconFrame,
60
66
  IconX,
61
67
  IconPin,
62
68
  IconCode,
@@ -69,12 +75,19 @@ import {
69
75
  IconTypography,
70
76
  IconHandStop,
71
77
  IconSquare,
78
+ IconLine,
79
+ IconCircle,
80
+ IconTriangle,
81
+ IconStar,
82
+ IconPhotoVideo,
72
83
  IconVectorBezier,
73
84
  IconScale,
74
85
  IconScribble,
75
- IconDiamonds,
86
+ IconHandClick,
87
+ IconTransformPoint,
76
88
  IconDownload,
77
89
  IconFileExport,
90
+ IconPlayerPlay,
78
91
  } from "@tabler/icons-react";
79
92
  import { useQueryClient } from "@tanstack/react-query";
80
93
  import {
@@ -121,11 +134,13 @@ import { VariantGrid } from "@/components/design/VariantGrid";
121
134
  import { VariantHandoffCard } from "@/components/design/VariantHandoffCard";
122
135
  import PromptPopover from "@/components/editor/PromptDialog";
123
136
  import type { UploadedFile } from "@/components/editor/PromptDialog";
137
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
124
138
  import { Button } from "@/components/ui/button";
125
139
  import {
126
140
  DropdownMenu,
127
141
  DropdownMenuContent,
128
142
  DropdownMenuItem,
143
+ DropdownMenuLabel,
129
144
  DropdownMenuRadioGroup,
130
145
  DropdownMenuRadioItem,
131
146
  DropdownMenuSeparator,
@@ -187,22 +202,60 @@ const STORED_RUN_LIVENESS_GRACE_MS = 20_000;
187
202
  const MAX_DESIGN_UNDO_STACK = 50;
188
203
  const OVERVIEW_ZOOM_THRESHOLD = 60;
189
204
  const FOCUSED_SCREEN_ZOOM = 100;
205
+ const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
190
206
  // Higher than the toolbar presets so overview zooming still feels like canvas
191
207
  // work; trackpad/pinch zooming past this commits to editing that screen.
192
208
  const OVERVIEW_EDIT_ZOOM_THRESHOLD = 250;
193
209
 
210
+ interface FileContentSaveRequest {
211
+ id: string;
212
+ content: string;
213
+ syncCollab: boolean;
214
+ }
215
+
216
+ function byteLength(value: string): number {
217
+ if (typeof TextEncoder === "undefined") return value.length;
218
+ return new TextEncoder().encode(value).length;
219
+ }
220
+
221
+ function sendFileContentSaveKeepalive(pending: FileContentSaveRequest): void {
222
+ if (typeof window === "undefined") return;
223
+ const body = JSON.stringify({
224
+ id: pending.id,
225
+ content: pending.content,
226
+ syncCollab: pending.syncCollab,
227
+ });
228
+ if (byteLength(body) > KEEPALIVE_FILE_SAVE_MAX_BYTES) return;
229
+ ensureEmbedAuthFetchInterceptor();
230
+ void fetch(agentNativePath("/_agent-native/actions/update-file"), {
231
+ method: "POST",
232
+ headers: {
233
+ "Content-Type": "application/json",
234
+ "X-Agent-Native-Frontend": "1",
235
+ },
236
+ body,
237
+ cache: "no-store",
238
+ keepalive: true,
239
+ }).catch(() => {});
240
+ }
241
+
194
242
  type EditorMode = "annotate" | "edit" | "interact";
195
243
  type DesignTool =
196
244
  | "move"
197
245
  | "frame"
198
246
  | "rect"
247
+ | "line"
248
+ | "arrow"
249
+ | "ellipse"
250
+ | "polygon"
251
+ | "star"
199
252
  | "text"
200
253
  | "pen"
201
254
  | "hand"
202
255
  | "comment"
203
256
  | "draw"
204
- | "scale"
205
- | "overview";
257
+ | "scale";
258
+ type ShapeTool = "rect" | "line" | "arrow" | "ellipse" | "polygon" | "star";
206
259
 
207
260
  interface DesignFile {
208
261
  id: string;
@@ -422,7 +475,7 @@ function applyInlineStylesToHtml(
422
475
  if (typeof window === "undefined") return null;
423
476
  try {
424
477
  const doc = new DOMParser().parseFromString(content, "text/html");
425
- const element = doc.querySelector(selector) as HTMLElement | null;
478
+ const element = queryUniqueSelector(doc, selector) as HTMLElement | null;
426
479
  if (!element) return null;
427
480
  Object.entries(styles).forEach(([property, value]) => {
428
481
  (element.style as any)[property] = value;
@@ -576,6 +629,16 @@ function primitiveLayerName(primitive: CanvasPrimitiveInsert): string {
576
629
  switch (primitive.kind) {
577
630
  case "frame":
578
631
  return "Frame";
632
+ case "line":
633
+ return "Line";
634
+ case "arrow":
635
+ return "Arrow";
636
+ case "ellipse":
637
+ return "Ellipse";
638
+ case "polygon":
639
+ return "Polygon";
640
+ case "star":
641
+ return "Star";
579
642
  case "path":
580
643
  return "Vector";
581
644
  case "text":
@@ -602,9 +665,14 @@ function appendCanvasPrimitiveToHtml(
602
665
  const nodeId = uniqueLayerId(primitive.kind);
603
666
  const layerName = primitiveLayerName(primitive);
604
667
 
605
- if (primitive.kind === "path") {
668
+ if (
669
+ primitive.kind === "path" ||
670
+ primitive.kind === "line" ||
671
+ primitive.kind === "arrow"
672
+ ) {
606
673
  const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
607
674
  const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
675
+ const markerId = `${nodeId}-arrow`;
608
676
  const points = primitive.points?.length
609
677
  ? primitive.points
610
678
  : [
@@ -615,14 +683,15 @@ function appendCanvasPrimitiveToHtml(
615
683
  const originY = Math.min(...points.map((point) => point.y));
616
684
  path.setAttribute(
617
685
  "d",
618
- points
619
- .map((point, index) => {
620
- const command = index === 0 ? "M" : "L";
621
- return `${command} ${Math.round(point.x - originX)} ${Math.round(
622
- point.y - originY,
623
- )}`;
624
- })
625
- .join(" "),
686
+ primitive.pathData ??
687
+ points
688
+ .map((point, index) => {
689
+ const command = index === 0 ? "M" : "L";
690
+ return `${command} ${Math.round(point.x - originX)} ${Math.round(
691
+ point.y - originY,
692
+ )}`;
693
+ })
694
+ .join(" "),
626
695
  );
627
696
  path.setAttribute("fill", "none");
628
697
  path.setAttribute(
@@ -632,6 +701,33 @@ function appendCanvasPrimitiveToHtml(
632
701
  path.setAttribute("stroke-width", String(primitive.strokeWidth ?? 3));
633
702
  path.setAttribute("stroke-linecap", "round");
634
703
  path.setAttribute("stroke-linejoin", "round");
704
+ if (primitive.kind === "arrow") {
705
+ const defs = doc.createElementNS("http://www.w3.org/2000/svg", "defs");
706
+ const marker = doc.createElementNS(
707
+ "http://www.w3.org/2000/svg",
708
+ "marker",
709
+ );
710
+ const arrowHead = doc.createElementNS(
711
+ "http://www.w3.org/2000/svg",
712
+ "path",
713
+ );
714
+ marker.setAttribute("id", markerId);
715
+ marker.setAttribute("markerWidth", "10");
716
+ marker.setAttribute("markerHeight", "10");
717
+ marker.setAttribute("refX", "8");
718
+ marker.setAttribute("refY", "5");
719
+ marker.setAttribute("orient", "auto");
720
+ marker.setAttribute("markerUnits", "strokeWidth");
721
+ arrowHead.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
722
+ arrowHead.setAttribute(
723
+ "fill",
724
+ primitive.stroke ?? "var(--primary, #2563eb)",
725
+ );
726
+ marker.appendChild(arrowHead);
727
+ defs.appendChild(marker);
728
+ svg.appendChild(defs);
729
+ path.setAttribute("marker-end", `url(#${markerId})`);
730
+ }
635
731
  svg.setAttribute("data-agent-native-node-id", nodeId);
636
732
  svg.setAttribute("data-agent-native-layer-name", layerName);
637
733
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
@@ -654,14 +750,55 @@ function appendCanvasPrimitiveToHtml(
654
750
  return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
655
751
  }
656
752
 
753
+ if (primitive.kind === "polygon" || primitive.kind === "star") {
754
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
755
+ const polygon = doc.createElementNS(
756
+ "http://www.w3.org/2000/svg",
757
+ "polygon",
758
+ );
759
+ polygon.setAttribute(
760
+ "points",
761
+ polygonPointsForHtmlShape(primitive.kind, width, height),
762
+ );
763
+ polygon.setAttribute("fill", primitive.fill ?? "rgba(37, 99, 235, 0.16)");
764
+ polygon.setAttribute("stroke", primitive.stroke ?? "rgb(37, 99, 235)");
765
+ polygon.setAttribute(
766
+ "stroke-width",
767
+ String(primitive.strokeWidth ?? 1.5),
768
+ );
769
+ polygon.setAttribute("stroke-linejoin", "round");
770
+ svg.setAttribute("data-agent-native-node-id", nodeId);
771
+ svg.setAttribute("data-agent-native-layer-name", layerName);
772
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
773
+ svg.setAttribute(
774
+ "style",
775
+ [
776
+ "position:absolute",
777
+ `left:${left}px`,
778
+ `top:${top}px`,
779
+ `width:${width}px`,
780
+ `height:${height}px`,
781
+ "overflow:visible",
782
+ geometry.rotation ? `transform:rotate(${geometry.rotation}deg)` : "",
783
+ ]
784
+ .filter(Boolean)
785
+ .join(";"),
786
+ );
787
+ svg.appendChild(polygon);
788
+ doc.body.appendChild(svg);
789
+ return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
790
+ }
791
+
657
792
  const element = doc.createElement("div");
658
793
  element.setAttribute("data-agent-native-node-id", nodeId);
659
794
  element.setAttribute("data-agent-native-layer-name", layerName);
660
795
  element.style.position = "absolute";
661
796
  element.style.left = `${left}px`;
662
797
  element.style.top = `${top}px`;
663
- element.style.width = `${width}px`;
664
- element.style.height = `${height}px`;
798
+ if (!(primitive.kind === "text" && primitive.autoSize)) {
799
+ element.style.width = `${width}px`;
800
+ element.style.height = `${height}px`;
801
+ }
665
802
  if (geometry.rotation) {
666
803
  element.style.transform = `rotate(${geometry.rotation}deg)`;
667
804
  }
@@ -675,12 +812,20 @@ function appendCanvasPrimitiveToHtml(
675
812
  element.style.overflow = "hidden";
676
813
  } else if (primitive.kind === "text") {
677
814
  element.textContent = primitive.text ?? "Text";
678
- element.style.display = "flex";
679
- element.style.alignItems = "center";
815
+ element.style.display = primitive.autoSize ? "inline-block" : "flex";
816
+ if (!primitive.autoSize) {
817
+ element.style.alignItems = "center";
818
+ }
680
819
  element.style.color = primitive.fill ?? "currentColor";
681
820
  element.style.fontSize = "16px";
682
821
  element.style.lineHeight = "1.2";
683
822
  element.style.whiteSpace = "pre-wrap";
823
+ } else if (primitive.kind === "ellipse") {
824
+ element.style.background = primitive.fill ?? "rgba(37, 99, 235, 0.16)";
825
+ element.style.border = `${primitive.strokeWidth ?? 1}px solid ${
826
+ primitive.stroke ?? "rgb(37, 99, 235)"
827
+ }`;
828
+ element.style.borderRadius = "9999px";
684
829
  } else {
685
830
  element.style.background = primitive.fill ?? "rgba(37, 99, 235, 0.16)";
686
831
  element.style.border = `${primitive.strokeWidth ?? 1}px solid ${
@@ -696,6 +841,45 @@ function appendCanvasPrimitiveToHtml(
696
841
  }
697
842
  }
698
843
 
844
+ function polygonPointsForHtmlShape(
845
+ kind: "polygon" | "star",
846
+ width: number,
847
+ height: number,
848
+ ): string {
849
+ const safeWidth = Math.max(1, width);
850
+ const safeHeight = Math.max(1, height);
851
+ const cx = safeWidth / 2;
852
+ const cy = safeHeight / 2;
853
+ const radius = Math.max(1, Math.min(safeWidth, safeHeight) / 2);
854
+ const points: Array<{ x: number; y: number }> = [];
855
+
856
+ if (kind === "polygon") {
857
+ for (let index = 0; index < 3; index += 1) {
858
+ const angle = -Math.PI / 2 + (index * Math.PI * 2) / 3;
859
+ points.push({
860
+ x: cx + Math.cos(angle) * radius,
861
+ y: cy + Math.sin(angle) * radius,
862
+ });
863
+ }
864
+ } else {
865
+ for (let index = 0; index < 10; index += 1) {
866
+ const angle = -Math.PI / 2 + (index * Math.PI) / 5;
867
+ const pointRadius = index % 2 === 0 ? radius : radius * 0.45;
868
+ points.push({
869
+ x: cx + Math.cos(angle) * pointRadius,
870
+ y: cy + Math.sin(angle) * pointRadius,
871
+ });
872
+ }
873
+ }
874
+
875
+ return points
876
+ .map(
877
+ (point) =>
878
+ `${Math.round(point.x * 10) / 10},${Math.round(point.y * 10) / 10}`,
879
+ )
880
+ .join(" ");
881
+ }
882
+
699
883
  function cloneHtmlLayerAtPosition(
700
884
  content: string,
701
885
  layerHtml: string,
@@ -758,6 +942,18 @@ function queryFirstSelector(
758
942
  return null;
759
943
  }
760
944
 
945
+ function queryUniqueSelector(
946
+ root: ParentNode,
947
+ selector: string,
948
+ ): Element | null {
949
+ try {
950
+ const matches = root.querySelectorAll(selector);
951
+ return matches.length === 1 ? (matches[0] ?? null) : null;
952
+ } catch {
953
+ return null;
954
+ }
955
+ }
956
+
761
957
  function insertClonedHtmlLayer(
762
958
  content: string,
763
959
  cloneHtml: string,
@@ -819,7 +1015,7 @@ function getElementOuterHtml(content: string, selector: string): string | null {
819
1015
  if (typeof window === "undefined") return null;
820
1016
  try {
821
1017
  const doc = new DOMParser().parseFromString(content, "text/html");
822
- return doc.querySelector(selector)?.outerHTML ?? null;
1018
+ return queryUniqueSelector(doc, selector)?.outerHTML ?? null;
823
1019
  } catch {
824
1020
  return null;
825
1021
  }
@@ -832,7 +1028,7 @@ function removeElementFromHtml(
832
1028
  if (typeof window === "undefined") return null;
833
1029
  try {
834
1030
  const doc = new DOMParser().parseFromString(content, "text/html");
835
- const element = doc.querySelector(selector);
1031
+ const element = queryUniqueSelector(doc, selector);
836
1032
  if (!element) return null;
837
1033
  element.remove();
838
1034
  return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
@@ -892,7 +1088,7 @@ function updateElementContentInHtml(
892
1088
  if (typeof window === "undefined") return null;
893
1089
  try {
894
1090
  const doc = new DOMParser().parseFromString(content, "text/html");
895
- const element = doc.querySelector(selector);
1091
+ const element = queryUniqueSelector(doc, selector);
896
1092
  if (!element) return null;
897
1093
  if (html !== undefined) {
898
1094
  element.innerHTML = sanitizeEditableInnerHtml(html);
@@ -959,12 +1155,15 @@ function codeLayerSelectorMatches(
959
1155
  ): boolean {
960
1156
  if (!node || !selector) return false;
961
1157
  const target = normalizeCodeLayerSelector(selector);
1158
+ const targetHasDirectPath = target.includes(" > ");
962
1159
  return codeLayerSelectorAliases(node).some((candidate) => {
963
1160
  const normalized = normalizeCodeLayerSelector(candidate);
964
1161
  return (
965
1162
  normalized === target ||
966
1163
  normalized.endsWith(` > ${target}`) ||
967
- target.endsWith(` > ${normalized}`)
1164
+ (targetHasDirectPath &&
1165
+ normalized.includes(" > ") &&
1166
+ target.endsWith(` > ${normalized}`))
968
1167
  );
969
1168
  });
970
1169
  }
@@ -985,6 +1184,7 @@ function codeLayerTreeToPanelNodes(
985
1184
  id: node.id,
986
1185
  name: node.name,
987
1186
  type: layerTypeForCodeLayer(node),
1187
+ layout: node.layout,
988
1188
  detail: node.detail,
989
1189
  badge: node.badge,
990
1190
  selectable: !locked && !hidden,
@@ -1070,6 +1270,30 @@ function resolveCodeLayerNodeFromBridge(
1070
1270
  );
1071
1271
  }
1072
1272
 
1273
+ function elementInfoExistsInContent(
1274
+ content: string,
1275
+ info: ElementInfo | null,
1276
+ ): boolean {
1277
+ if (!info) return false;
1278
+ const projection = buildCodeLayerProjection(content);
1279
+ if (
1280
+ resolveCodeLayerNodeFromBridge(
1281
+ projection,
1282
+ info.selector,
1283
+ info.sourceId ?? info.id,
1284
+ )
1285
+ ) {
1286
+ return true;
1287
+ }
1288
+ if (!info.selector || typeof window === "undefined") return false;
1289
+ try {
1290
+ const doc = new DOMParser().parseFromString(content, "text/html");
1291
+ return Boolean(queryUniqueSelector(doc, info.selector));
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ }
1296
+
1073
1297
  function collectCodeLayerAncestors(
1074
1298
  nodes: CodeLayerTreeNode[],
1075
1299
  targetId: string,
@@ -1112,6 +1336,147 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
1112
1336
  );
1113
1337
  }
1114
1338
 
1339
+ interface DesignCollaborator {
1340
+ user: CollabUser;
1341
+ image?: string;
1342
+ isCurrent?: boolean;
1343
+ }
1344
+
1345
+ function userInitial(nameOrEmail: string): string {
1346
+ const trimmed = nameOrEmail.trim();
1347
+ if (!trimmed) return "?";
1348
+ return trimmed.charAt(0).toUpperCase();
1349
+ }
1350
+
1351
+ function userColor(user: CollabUser): string {
1352
+ return user.color || emailToColor(user.email);
1353
+ }
1354
+
1355
+ function DesignCollaboratorAvatar({
1356
+ collaborator,
1357
+ className,
1358
+ }: {
1359
+ collaborator: DesignCollaborator;
1360
+ className?: string;
1361
+ }) {
1362
+ const label = collaborator.user.name || emailToName(collaborator.user.email);
1363
+ const storedAvatarUrl = useAvatarUrl(collaborator.user.email);
1364
+ const avatarUrl = storedAvatarUrl ?? collaborator.image;
1365
+
1366
+ return (
1367
+ <Avatar
1368
+ className={cn(
1369
+ "size-7 border-2 border-[var(--design-editor-panel-bg)] shadow-sm",
1370
+ className,
1371
+ )}
1372
+ >
1373
+ {avatarUrl ? <AvatarImage src={avatarUrl} alt={label} /> : null}
1374
+ <AvatarFallback
1375
+ className="text-[10px] font-semibold text-white"
1376
+ style={{ backgroundColor: userColor(collaborator.user) }}
1377
+ >
1378
+ {userInitial(label || collaborator.user.email)}
1379
+ </AvatarFallback>
1380
+ </Avatar>
1381
+ );
1382
+ }
1383
+
1384
+ function DesignCollaboratorsMenu({
1385
+ collaborators,
1386
+ followingEmail,
1387
+ label,
1388
+ onAvatarClick,
1389
+ }: {
1390
+ collaborators: DesignCollaborator[];
1391
+ followingEmail?: string | null;
1392
+ label: string;
1393
+ onAvatarClick?: (user: CollabUser | null) => void;
1394
+ }) {
1395
+ if (collaborators.length === 0) return null;
1396
+
1397
+ const visibleCollaborators = collaborators.slice(0, 3);
1398
+ const hasMultipleCollaborators = collaborators.length > 1;
1399
+ const followingLower = followingEmail?.trim().toLowerCase() ?? null;
1400
+
1401
+ return (
1402
+ <DropdownMenu>
1403
+ <DropdownMenuTrigger asChild>
1404
+ <button
1405
+ type="button"
1406
+ className="flex h-8 min-w-0 cursor-pointer items-center rounded-md pr-1 text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
1407
+ aria-label={label}
1408
+ >
1409
+ <span className="flex items-center">
1410
+ {visibleCollaborators.map((collaborator, index) => (
1411
+ <DesignCollaboratorAvatar
1412
+ key={`${collaborator.user.email}:${index}`}
1413
+ collaborator={collaborator}
1414
+ className={index === 0 ? undefined : "-ml-2"}
1415
+ />
1416
+ ))}
1417
+ </span>
1418
+ {hasMultipleCollaborators ? (
1419
+ <IconChevronDown className="ml-0.5 size-3 opacity-70" />
1420
+ ) : null}
1421
+ </button>
1422
+ </DropdownMenuTrigger>
1423
+ <DropdownMenuContent align="start" className="w-64">
1424
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
1425
+ {label}
1426
+ </DropdownMenuLabel>
1427
+ {collaborators.map((collaborator) => {
1428
+ const user = collaborator.user;
1429
+ const email = user.email.trim().toLowerCase();
1430
+ const isFollowing =
1431
+ followingLower != null && email === followingLower;
1432
+ const name = user.name || emailToName(user.email);
1433
+
1434
+ return (
1435
+ <DropdownMenuItem
1436
+ key={user.email}
1437
+ onSelect={() => {
1438
+ if (!collaborator.isCurrent) onAvatarClick?.(user);
1439
+ }}
1440
+ className="gap-2"
1441
+ >
1442
+ <DesignCollaboratorAvatar collaborator={collaborator} />
1443
+ <span className="min-w-0 flex-1">
1444
+ <span className="block truncate text-sm font-medium">
1445
+ {name}
1446
+ </span>
1447
+ <span className="block truncate text-xs text-muted-foreground">
1448
+ {user.email}
1449
+ </span>
1450
+ </span>
1451
+ {isFollowing ? (
1452
+ <IconCheck className="size-3.5 text-[var(--design-editor-accent-color)]" />
1453
+ ) : null}
1454
+ </DropdownMenuItem>
1455
+ );
1456
+ })}
1457
+ </DropdownMenuContent>
1458
+ </DropdownMenu>
1459
+ );
1460
+ }
1461
+
1462
+ function externalPreviewUrlForContent(content: string): string | null {
1463
+ const trimmed = content.trim();
1464
+ if (!/^https?:\/\//i.test(trimmed)) return null;
1465
+ try {
1466
+ const url = new URL(trimmed);
1467
+ url.hash = "";
1468
+ return url.toString();
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+
1474
+ function fullPreviewHtml(content: string): string {
1475
+ const trimmed = content.trim();
1476
+ if (/<!doctype html|<html[\s>]/i.test(trimmed)) return content;
1477
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${content}</body></html>`;
1478
+ }
1479
+
1115
1480
  type FigmaToolbarOption = {
1116
1481
  key: string;
1117
1482
  label: string;
@@ -1135,6 +1500,7 @@ function FigmaToolbarTool({
1135
1500
  options: FigmaToolbarOption[];
1136
1501
  onPrimary: () => void;
1137
1502
  }) {
1503
+ const hasOptionsMenu = options.length > 1;
1138
1504
  return (
1139
1505
  <div
1140
1506
  className={cn(
@@ -1159,45 +1525,51 @@ function FigmaToolbarTool({
1159
1525
  <TooltipContent side="top">{label}</TooltipContent>
1160
1526
  </Tooltip>
1161
1527
 
1162
- <DropdownMenu>
1163
- <DropdownMenuTrigger asChild>
1164
- <button
1165
- type="button"
1166
- className={cn(
1167
- "flex h-8 w-4 cursor-pointer items-center justify-center rounded-r-md transition-colors",
1168
- active ? "hover:bg-white/15" : "hover:bg-white/10",
1169
- )}
1170
- aria-label={`${label} options`}
1171
- >
1172
- <IconChevronDown className="size-3" />
1173
- </button>
1174
- </DropdownMenuTrigger>
1175
- <DropdownMenuContent
1176
- side="top"
1177
- align="center"
1178
- sideOffset={12}
1179
- className="w-56 rounded-2xl border-white/10 bg-neutral-950 p-2 text-neutral-50 shadow-[0_24px_70px_-24px_rgba(0,0,0,0.9)]"
1180
- >
1181
- {options.map((option) => (
1182
- <DropdownMenuItem
1183
- key={option.key}
1184
- disabled={option.disabled}
1185
- onSelect={option.onSelect}
1186
- className="h-10 rounded-lg text-sm text-neutral-100 focus:bg-white/10 focus:text-white disabled:text-neutral-500"
1187
- >
1188
- <span className="mr-2 flex size-5 items-center justify-center text-neutral-100">
1189
- {option.active ? <IconCheck className="size-4" /> : option.icon}
1190
- </span>
1191
- <span className="min-w-0 flex-1 truncate">{option.label}</span>
1192
- {option.shortcut && (
1193
- <DropdownMenuShortcut className="ml-3 text-neutral-400">
1194
- {option.shortcut}
1195
- </DropdownMenuShortcut>
1528
+ {hasOptionsMenu ? (
1529
+ <DropdownMenu>
1530
+ <DropdownMenuTrigger asChild>
1531
+ <button
1532
+ type="button"
1533
+ className={cn(
1534
+ "flex h-8 w-4 cursor-pointer items-center justify-center rounded-r-md transition-colors",
1535
+ active ? "hover:bg-white/15" : "hover:bg-white/10",
1196
1536
  )}
1197
- </DropdownMenuItem>
1198
- ))}
1199
- </DropdownMenuContent>
1200
- </DropdownMenu>
1537
+ aria-label={`${label} options`}
1538
+ >
1539
+ <IconChevronDown className="size-3" />
1540
+ </button>
1541
+ </DropdownMenuTrigger>
1542
+ <DropdownMenuContent
1543
+ side="top"
1544
+ align="center"
1545
+ sideOffset={12}
1546
+ className="w-56 rounded-2xl border-white/10 bg-neutral-950 p-2 text-neutral-50 shadow-[0_24px_70px_-24px_rgba(0,0,0,0.9)]"
1547
+ >
1548
+ {options.map((option) => (
1549
+ <DropdownMenuItem
1550
+ key={option.key}
1551
+ disabled={option.disabled}
1552
+ onSelect={option.onSelect}
1553
+ className="h-10 rounded-lg text-sm text-neutral-100 focus:bg-white/10 focus:text-white disabled:text-neutral-500"
1554
+ >
1555
+ <span className="mr-2 flex size-5 items-center justify-center text-neutral-100">
1556
+ {option.active ? (
1557
+ <IconCheck className="size-4" />
1558
+ ) : (
1559
+ option.icon
1560
+ )}
1561
+ </span>
1562
+ <span className="min-w-0 flex-1 truncate">{option.label}</span>
1563
+ {option.shortcut && (
1564
+ <DropdownMenuShortcut className="ml-3 text-neutral-400">
1565
+ {option.shortcut}
1566
+ </DropdownMenuShortcut>
1567
+ )}
1568
+ </DropdownMenuItem>
1569
+ ))}
1570
+ </DropdownMenuContent>
1571
+ </DropdownMenu>
1572
+ ) : null}
1201
1573
  </div>
1202
1574
  );
1203
1575
  }
@@ -1245,25 +1617,22 @@ function FigmaBottomToolbar({
1245
1617
  activeTool,
1246
1618
  onMove,
1247
1619
  onFrame,
1248
- onRect,
1620
+ onShape,
1249
1621
  onText,
1250
1622
  onPen,
1251
1623
  onHand,
1252
1624
  onDraw,
1253
1625
  onScale,
1254
1626
  onCommentPin,
1255
- overviewActive,
1256
1627
  onModeChange,
1257
- onScreensToggle,
1258
1628
  }: {
1259
1629
  mode: EditorMode;
1260
1630
  pinMode: boolean;
1261
1631
  drawMode: boolean;
1262
1632
  activeTool: DesignTool;
1263
- overviewActive: boolean;
1264
1633
  onMove: () => void;
1265
1634
  onFrame: () => void;
1266
- onRect: () => void;
1635
+ onShape: (tool: ShapeTool) => void;
1267
1636
  onText: () => void;
1268
1637
  onPen: () => void;
1269
1638
  onHand: () => void;
@@ -1271,9 +1640,95 @@ function FigmaBottomToolbar({
1271
1640
  onScale: () => void;
1272
1641
  onCommentPin: () => void;
1273
1642
  onModeChange: (mode: EditorMode) => void;
1274
- onScreensToggle: () => void;
1275
1643
  }) {
1276
1644
  const t = useT();
1645
+ const shapeTools = new Set<DesignTool>([
1646
+ "rect",
1647
+ "line",
1648
+ "arrow",
1649
+ "ellipse",
1650
+ "polygon",
1651
+ "star",
1652
+ ]);
1653
+ const activeShape = shapeTools.has(activeTool)
1654
+ ? (activeTool as ShapeTool)
1655
+ : "rect";
1656
+ const shapeIcon = (tool: ShapeTool, className: string) => {
1657
+ switch (tool) {
1658
+ case "line":
1659
+ return <IconLine className={className} />;
1660
+ case "arrow":
1661
+ return <IconArrowUpRight className={className} />;
1662
+ case "ellipse":
1663
+ return <IconCircle className={className} />;
1664
+ case "polygon":
1665
+ return <IconTriangle className={className} />;
1666
+ case "star":
1667
+ return <IconStar className={className} />;
1668
+ case "rect":
1669
+ default:
1670
+ return <IconSquare className={className} />;
1671
+ }
1672
+ };
1673
+ const shapeOptions: FigmaToolbarOption[] = [
1674
+ {
1675
+ key: "rect",
1676
+ label: t("designEditor.tools.rect"),
1677
+ icon: shapeIcon("rect", "size-4"),
1678
+ shortcut: "R",
1679
+ active: activeTool === "rect",
1680
+ onSelect: () => onShape("rect"),
1681
+ },
1682
+ {
1683
+ key: "line",
1684
+ label: t("designEditor.tools.line"),
1685
+ icon: shapeIcon("line", "size-4"),
1686
+ shortcut: "L",
1687
+ active: activeTool === "line",
1688
+ onSelect: () => onShape("line"),
1689
+ },
1690
+ {
1691
+ key: "arrow",
1692
+ label: t("designEditor.tools.arrow"),
1693
+ icon: shapeIcon("arrow", "size-4"),
1694
+ shortcut: "⇧L",
1695
+ active: activeTool === "arrow",
1696
+ onSelect: () => onShape("arrow"),
1697
+ },
1698
+ {
1699
+ key: "ellipse",
1700
+ label: t("designEditor.tools.ellipse"),
1701
+ icon: shapeIcon("ellipse", "size-4"),
1702
+ shortcut: "O",
1703
+ active: activeTool === "ellipse",
1704
+ onSelect: () => onShape("ellipse"),
1705
+ },
1706
+ {
1707
+ key: "polygon",
1708
+ label: t("designEditor.tools.polygon"),
1709
+ icon: shapeIcon("polygon", "size-4"),
1710
+ active: activeTool === "polygon",
1711
+ onSelect: () => onShape("polygon"),
1712
+ },
1713
+ {
1714
+ key: "star",
1715
+ label: t("designEditor.tools.star"),
1716
+ icon: shapeIcon("star", "size-4"),
1717
+ active: activeTool === "star",
1718
+ onSelect: () => onShape("star"),
1719
+ },
1720
+ {
1721
+ key: "image-video",
1722
+ label: t("designEditor.tools.imageVideo"),
1723
+ icon: <IconPhotoVideo className="size-4" />,
1724
+ shortcut: "⇧⌘K",
1725
+ disabled: true,
1726
+ onSelect: () => {},
1727
+ },
1728
+ ];
1729
+ const activeShapeOption =
1730
+ shapeOptions.find((option) => option.key === activeShape) ??
1731
+ shapeOptions[0]!;
1277
1732
  const tools: Array<{
1278
1733
  key: string;
1279
1734
  active: boolean;
@@ -1319,42 +1774,26 @@ function FigmaBottomToolbar({
1319
1774
  key: "frame",
1320
1775
  active: activeTool === "frame",
1321
1776
  label: t("designEditor.tools.frame"),
1322
- icon: <IconLayoutGrid className="size-[18px]" />,
1777
+ icon: <IconFrame className="size-[18px]" />,
1323
1778
  onClick: onFrame,
1324
1779
  options: [
1325
1780
  {
1326
1781
  key: "frame",
1327
1782
  label: t("designEditor.tools.frame"),
1328
- icon: <IconLayoutGrid className="size-4" />,
1783
+ icon: <IconFrame className="size-4" />,
1329
1784
  shortcut: "F",
1330
1785
  active: activeTool === "frame",
1331
1786
  onSelect: onFrame,
1332
1787
  },
1333
- {
1334
- key: "screens",
1335
- label: t("designEditor.modes.screens"),
1336
- icon: <IconLayoutGrid className="size-4" />,
1337
- active: overviewActive,
1338
- onSelect: onScreensToggle,
1339
- },
1340
1788
  ],
1341
1789
  },
1342
1790
  {
1343
- key: "rect",
1344
- active: activeTool === "rect",
1345
- label: t("designEditor.tools.rect"),
1346
- icon: <IconSquare className="size-[18px]" />,
1347
- onClick: onRect,
1348
- options: [
1349
- {
1350
- key: "rect",
1351
- label: t("designEditor.tools.rect"),
1352
- icon: <IconSquare className="size-4" />,
1353
- shortcut: "R",
1354
- active: activeTool === "rect",
1355
- onSelect: onRect,
1356
- },
1357
- ],
1791
+ key: "shape",
1792
+ active: shapeTools.has(activeTool),
1793
+ label: activeShapeOption.label,
1794
+ icon: shapeIcon(activeShape, "size-[18px]"),
1795
+ onClick: () => onShape(activeShape),
1796
+ options: shapeOptions,
1358
1797
  },
1359
1798
  {
1360
1799
  key: "text",
@@ -1424,7 +1863,7 @@ function FigmaBottomToolbar({
1424
1863
  ];
1425
1864
 
1426
1865
  const modes: Array<{
1427
- key: EditorMode | "screens";
1866
+ key: EditorMode;
1428
1867
  active: boolean;
1429
1868
  label: string;
1430
1869
  icon: ReactNode;
@@ -1432,32 +1871,25 @@ function FigmaBottomToolbar({
1432
1871
  }> = [
1433
1872
  {
1434
1873
  key: "annotate",
1435
- active: mode === "annotate" && !overviewActive,
1874
+ active: mode === "annotate",
1436
1875
  label: t("designEditor.modes.annotate"),
1437
1876
  icon: <IconScribble className="size-[18px]" />,
1438
1877
  onClick: () => onModeChange("annotate"),
1439
1878
  },
1440
1879
  {
1441
1880
  key: "edit",
1442
- active: mode === "edit" && !overviewActive,
1881
+ active: mode === "edit",
1443
1882
  label: t("designEditor.modes.edit"),
1444
- icon: <IconPointer className="size-[18px]" />,
1883
+ icon: <IconTransformPoint className="size-[18px]" />,
1445
1884
  onClick: () => onModeChange("edit"),
1446
1885
  },
1447
1886
  {
1448
1887
  key: "interact",
1449
- active: mode === "interact" && !overviewActive,
1888
+ active: mode === "interact",
1450
1889
  label: t("designEditor.modes.interact"),
1451
- icon: <IconDiamonds className="size-[18px]" />,
1890
+ icon: <IconHandClick className="size-[18px]" />,
1452
1891
  onClick: () => onModeChange("interact"),
1453
1892
  },
1454
- {
1455
- key: "screens",
1456
- active: overviewActive,
1457
- label: t("designEditor.modes.screens"),
1458
- icon: <IconLayoutGrid className="size-[18px]" />,
1459
- onClick: onScreensToggle,
1460
- },
1461
1893
  ];
1462
1894
 
1463
1895
  return (
@@ -2049,13 +2481,17 @@ export default function DesignEditor() {
2049
2481
  ]);
2050
2482
 
2051
2483
  // Current user info for collaborative presence
2052
- const currentUser: CollabUser | undefined = session?.email
2053
- ? {
2054
- name: emailToName(session.email),
2055
- email: session.email,
2056
- color: emailToColor(session.email),
2057
- }
2058
- : undefined;
2484
+ const currentUser: CollabUser | undefined = useMemo(
2485
+ () =>
2486
+ session?.email
2487
+ ? {
2488
+ name: session.name?.trim() || emailToName(session.email),
2489
+ email: session.email,
2490
+ color: emailToColor(session.email),
2491
+ }
2492
+ : undefined,
2493
+ [session?.email, session?.name],
2494
+ );
2059
2495
 
2060
2496
  // Data fetching
2061
2497
  useEffect(() => {
@@ -2135,14 +2571,83 @@ export default function DesignEditor() {
2135
2571
  const exportHtmlMutation = useActionMutation("export-html");
2136
2572
  const exportZipMutation = useActionMutation("export-zip");
2137
2573
  const [, setPatchProof] = useState<PatchProofState | null>(null);
2138
- const pendingFileSaveRef = useRef<{ id: string; content: string } | null>(
2139
- null,
2140
- );
2574
+ const pendingFileSaveRef = useRef<{
2575
+ id: string;
2576
+ content: string;
2577
+ syncCollab: boolean;
2578
+ } | null>(null);
2579
+ const fileSaveChainsRef = useRef<Record<string, Promise<void>>>({});
2580
+ const latestFileSaveForUnloadRef = useRef<
2581
+ Record<string, FileContentSaveRequest>
2582
+ >({});
2141
2583
  const fileSaveTimerRef = useRef<number | null>(null);
2142
2584
 
2585
+ const saveFileContent = useCallback(
2586
+ (pending: FileContentSaveRequest) => {
2587
+ latestFileSaveForUnloadRef.current[pending.id] = pending;
2588
+ const previous =
2589
+ fileSaveChainsRef.current[pending.id] ?? Promise.resolve();
2590
+ const current = previous
2591
+ .catch(() => {})
2592
+ .then(async () => {
2593
+ try {
2594
+ await updateFileMutation.mutateAsync({
2595
+ id: pending.id,
2596
+ content: pending.content,
2597
+ syncCollab: pending.syncCollab,
2598
+ } as any);
2599
+ setPatchProof((prev) =>
2600
+ prev && prev.fileId === pending.id && prev.status === "queued"
2601
+ ? { ...prev, status: "applied" }
2602
+ : prev,
2603
+ );
2604
+ } catch (error) {
2605
+ setPatchProof((prev) =>
2606
+ prev && prev.fileId === pending.id && prev.status === "queued"
2607
+ ? {
2608
+ ...prev,
2609
+ status: "failed",
2610
+ error:
2611
+ error instanceof Error
2612
+ ? error.message
2613
+ : t("common.genericError"),
2614
+ }
2615
+ : prev,
2616
+ );
2617
+ }
2618
+ });
2619
+ fileSaveChainsRef.current[pending.id] = current;
2620
+ void current.finally(() => {
2621
+ if (fileSaveChainsRef.current[pending.id] === current) {
2622
+ delete fileSaveChainsRef.current[pending.id];
2623
+ }
2624
+ });
2625
+ },
2626
+ [t, updateFileMutation],
2627
+ );
2628
+
2143
2629
  const queueFileContentSave = useCallback(
2144
- (fileId: string, content: string) => {
2145
- pendingFileSaveRef.current = { id: fileId, content };
2630
+ (
2631
+ fileId: string,
2632
+ content: string,
2633
+ options: { syncCollab?: boolean; immediate?: boolean } = {},
2634
+ ) => {
2635
+ const pending = {
2636
+ id: fileId,
2637
+ content,
2638
+ syncCollab: options.syncCollab ?? true,
2639
+ };
2640
+ latestFileSaveForUnloadRef.current[fileId] = pending;
2641
+ if (options.immediate) {
2642
+ if (fileSaveTimerRef.current) {
2643
+ window.clearTimeout(fileSaveTimerRef.current);
2644
+ fileSaveTimerRef.current = null;
2645
+ }
2646
+ pendingFileSaveRef.current = null;
2647
+ saveFileContent(pending);
2648
+ return;
2649
+ }
2650
+ pendingFileSaveRef.current = pending;
2146
2651
  if (fileSaveTimerRef.current) {
2147
2652
  window.clearTimeout(fileSaveTimerRef.current);
2148
2653
  }
@@ -2151,42 +2656,25 @@ export default function DesignEditor() {
2151
2656
  pendingFileSaveRef.current = null;
2152
2657
  fileSaveTimerRef.current = null;
2153
2658
  if (!pending) return;
2154
- updateFileMutation.mutate(
2155
- {
2156
- id: pending.id,
2157
- content: pending.content,
2158
- } as any,
2159
- {
2160
- onSuccess: () => {
2161
- setPatchProof((prev) =>
2162
- prev && prev.fileId === pending.id && prev.status === "queued"
2163
- ? { ...prev, status: "applied" }
2164
- : prev,
2165
- );
2166
- },
2167
- onError: (error) => {
2168
- setPatchProof((prev) =>
2169
- prev && prev.fileId === pending.id && prev.status === "queued"
2170
- ? {
2171
- ...prev,
2172
- status: "failed",
2173
- error:
2174
- error instanceof Error
2175
- ? error.message
2176
- : t("common.genericError"),
2177
- }
2178
- : prev,
2179
- );
2180
- },
2181
- },
2182
- );
2659
+ saveFileContent(pending);
2183
2660
  }, 400);
2184
2661
  },
2185
- [t, updateFileMutation],
2662
+ [saveFileContent],
2186
2663
  );
2187
2664
 
2188
2665
  useEffect(() => {
2666
+ const handlePageHide = () => {
2667
+ if (pendingFileSaveRef.current) {
2668
+ latestFileSaveForUnloadRef.current[pendingFileSaveRef.current.id] =
2669
+ pendingFileSaveRef.current;
2670
+ }
2671
+ Object.values(latestFileSaveForUnloadRef.current).forEach(
2672
+ sendFileContentSaveKeepalive,
2673
+ );
2674
+ };
2675
+ window.addEventListener("pagehide", handlePageHide);
2189
2676
  return () => {
2677
+ window.removeEventListener("pagehide", handlePageHide);
2190
2678
  if (fileSaveTimerRef.current) {
2191
2679
  window.clearTimeout(fileSaveTimerRef.current);
2192
2680
  }
@@ -2607,7 +3095,7 @@ export default function DesignEditor() {
2607
3095
  setPinMode(false);
2608
3096
  setMode("edit");
2609
3097
  setSelectedElement(null);
2610
- setActiveTool("overview");
3098
+ setActiveTool("move");
2611
3099
  setViewMode("overview");
2612
3100
  } else if (editorView === "single") {
2613
3101
  viewModeRef.current = "single";
@@ -2688,7 +3176,7 @@ export default function DesignEditor() {
2688
3176
  });
2689
3177
  if (nextId) {
2690
3178
  setActiveFileId(nextId);
2691
- setActiveTool("overview");
3179
+ setActiveTool("move");
2692
3180
  setViewMode("overview");
2693
3181
  if (request?.canvasPosition) {
2694
3182
  queueFrameGeometrySave({
@@ -2760,6 +3248,63 @@ export default function DesignEditor() {
2760
3248
  );
2761
3249
  }, [createFileMutation, files, id, queryClient, t]);
2762
3250
 
3251
+ const handleCreateScreenFrame = useCallback(
3252
+ (geometry: { x: number; y: number; width: number; height: number }) => {
3253
+ if (!id) return;
3254
+ const filename = nextBlankScreenFilename(files);
3255
+ const nextGeometry = {
3256
+ x: Math.round(geometry.x),
3257
+ y: Math.round(geometry.y),
3258
+ width: Math.max(64, Math.round(geometry.width)),
3259
+ height: Math.max(64, Math.round(geometry.height)),
3260
+ };
3261
+ createFileMutation.mutate(
3262
+ {
3263
+ designId: id,
3264
+ filename,
3265
+ content: blankScreenHtml(prettyScreenName(filename)),
3266
+ fileType: "html",
3267
+ } as any,
3268
+ {
3269
+ onSuccess: (result: any) => {
3270
+ const nextId = typeof result?.id === "string" ? result.id : null;
3271
+ queryClient.invalidateQueries({
3272
+ queryKey: ["action", "get-design"],
3273
+ });
3274
+ if (nextId) {
3275
+ setActiveFileId(nextId);
3276
+ setSelectedElement(null);
3277
+ setSelectedLayerIdsState([nextId]);
3278
+ setActiveTool("move");
3279
+ setMode("edit");
3280
+ setViewMode("overview");
3281
+ writeFrameGeometrySnapshot({
3282
+ ...canvasFrameGeometryById,
3283
+ [nextId]: nextGeometry,
3284
+ });
3285
+ }
3286
+ },
3287
+ onError: (error) => {
3288
+ toast.error(
3289
+ error instanceof Error
3290
+ ? error.message
3291
+ : t("designEditor.toasts.screenDuplicateError"),
3292
+ );
3293
+ },
3294
+ },
3295
+ );
3296
+ },
3297
+ [
3298
+ canvasFrameGeometryById,
3299
+ createFileMutation,
3300
+ files,
3301
+ id,
3302
+ queryClient,
3303
+ t,
3304
+ writeFrameGeometrySnapshot,
3305
+ ],
3306
+ );
3307
+
2763
3308
  // Collaborative editing for the active file
2764
3309
  const { ydoc, awareness, isSynced, activeUsers, agentActive } =
2765
3310
  useCollaborativeDoc({
@@ -2836,13 +3381,30 @@ export default function DesignEditor() {
2836
3381
  const ytext = ydoc.getText("content");
2837
3382
  const text = ytext.toString();
2838
3383
  if (text.length > 0) {
3384
+ const storedContent = activeFile?.content ?? "";
3385
+ if (
3386
+ !shouldUseLiveFileContent({
3387
+ liveContent: text,
3388
+ storedContent,
3389
+ fileType: activeFile?.fileType ?? "html",
3390
+ })
3391
+ ) {
3392
+ setCollabContent(storedContent);
3393
+ lastLocalContentRef.current = storedContent;
3394
+ setContentRenderRevision((revision) => revision + 1);
3395
+ ydoc.transact(() => {
3396
+ ytext.delete(0, ytext.length);
3397
+ ytext.insert(0, storedContent);
3398
+ }, TAB_ID);
3399
+ return;
3400
+ }
2839
3401
  // Y.Doc snapshots are a render seed, not the SQL source of truth; the
2840
3402
  // reconcile effect below advances the updatedAt watermark only after it
2841
3403
  // confirms or applies the current DB content.
2842
3404
  setCollabContent(text);
2843
3405
  setContentRenderRevision((revision) => revision + 1);
2844
3406
  }
2845
- }, [ydoc, isSynced, activeFileId]);
3407
+ }, [ydoc, isSynced, activeFileId, activeFile?.content, activeFile?.fileType]);
2846
3408
 
2847
3409
  // Keep the freshest DB `updatedAt` in a ref the observe handler can read.
2848
3410
  useEffect(() => {
@@ -2887,34 +3449,11 @@ export default function DesignEditor() {
2887
3449
  if (!isLocalEdit) {
2888
3450
  setSelectedElement((prev) => {
2889
3451
  if (!prev) return prev;
2890
- try {
2891
- const iframe = document.querySelector<HTMLIFrameElement>(
2892
- "iframe[data-design-preview-iframe]",
2893
- );
2894
- const doc = iframe?.contentDocument;
2895
- if (doc && (!prev.selector || !doc.querySelector(prev.selector))) {
2896
- return null;
2897
- }
2898
- } catch {
2899
- // iframe not accessible yet — clear defensively
2900
- return null;
2901
- }
2902
- return prev;
3452
+ return elementInfoExistsInContent(next, prev) ? prev : null;
2903
3453
  });
2904
3454
  setHoveredElement((prev) => {
2905
3455
  if (!prev) return prev;
2906
- try {
2907
- const iframe = document.querySelector<HTMLIFrameElement>(
2908
- "iframe[data-design-preview-iframe]",
2909
- );
2910
- const doc = iframe?.contentDocument;
2911
- if (doc && (!prev.selector || !doc.querySelector(prev.selector))) {
2912
- return null;
2913
- }
2914
- } catch {
2915
- return null;
2916
- }
2917
- return prev;
3456
+ return elementInfoExistsInContent(next, prev) ? prev : null;
2918
3457
  });
2919
3458
  }
2920
3459
  };
@@ -2981,6 +3520,31 @@ export default function DesignEditor() {
2981
3520
  if (!activeFile || !isSynced) return;
2982
3521
  const dbContent = activeFile.content ?? "";
2983
3522
  const dbUpdatedAt = activeFile.updatedAt ?? null;
3523
+ if (
3524
+ typeof collabContent === "string" &&
3525
+ !shouldUseLiveFileContent({
3526
+ liveContent: collabContent,
3527
+ storedContent: dbContent,
3528
+ fileType: activeFile.fileType,
3529
+ })
3530
+ ) {
3531
+ clearStaleAgentCollabRecovery();
3532
+ setCollabContent(dbContent);
3533
+ lastLocalContentRef.current = dbContent;
3534
+ if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
3535
+ setContentRenderRevision((revision) => revision + 1);
3536
+
3537
+ if (isLeadClient && ydoc) {
3538
+ const ytext = ydoc.getText("content");
3539
+ if (ytext.toString() !== dbContent) {
3540
+ ydoc.transact(() => {
3541
+ ytext.delete(0, ytext.length);
3542
+ ytext.insert(0, dbContent);
3543
+ }, TAB_ID);
3544
+ }
3545
+ }
3546
+ return;
3547
+ }
2984
3548
 
2985
3549
  // Already reflecting this exact content (our own echo or Yjs already
2986
3550
  // delivered it) — just advance the watermark and stop.
@@ -3174,6 +3738,29 @@ export default function DesignEditor() {
3174
3738
  [followingEmail, stopFollowing],
3175
3739
  );
3176
3740
 
3741
+ const designCollaborators = useMemo<DesignCollaborator[]>(() => {
3742
+ const currentEmail = currentUser?.email.trim().toLowerCase() ?? null;
3743
+ const humans = dedupeCollabUsersByEmail([
3744
+ ...(currentUser ? [currentUser] : []),
3745
+ ...activeUsers,
3746
+ ]).filter((user) => user.email.trim().toLowerCase() !== "agent@system");
3747
+ const otherHumans = humans.filter(
3748
+ (user) => user.email.trim().toLowerCase() !== currentEmail,
3749
+ );
3750
+ const collaborators = otherHumans.map((user) => ({ user }));
3751
+
3752
+ if (!currentUser) return collaborators;
3753
+
3754
+ return [
3755
+ {
3756
+ user: currentUser,
3757
+ image: session?.image,
3758
+ isCurrent: true,
3759
+ },
3760
+ ...collaborators,
3761
+ ];
3762
+ }, [activeUsers, currentUser, session?.image]);
3763
+
3177
3764
  // Resolve the content to render: prefer collab content, fall back to DB
3178
3765
  const activeContent = collabContent ?? activeFile?.content ?? "";
3179
3766
  const pageStyles = useMemo(
@@ -3248,11 +3835,38 @@ export default function DesignEditor() {
3248
3835
  const applyLocalContentUpdate = useCallback(
3249
3836
  (
3250
3837
  nextContent: string,
3251
- options: { refreshPreview?: boolean; skipPreview?: boolean } = {},
3838
+ options: {
3839
+ refreshPreview?: boolean;
3840
+ skipPreview?: boolean;
3841
+ immediateSave?: boolean;
3842
+ } = {},
3252
3843
  ) => {
3253
3844
  if (!activeFile) return;
3254
3845
  setCollabContent(nextContent);
3255
3846
  lastLocalContentRef.current = nextContent;
3847
+ if (id) {
3848
+ const optimisticUpdatedAt = new Date().toISOString();
3849
+ queryClient.setQueryData(
3850
+ ["action", "get-design", { id }],
3851
+ (old: any) => {
3852
+ if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
3853
+ return old;
3854
+ }
3855
+ return {
3856
+ ...old,
3857
+ files: old.files.map((file: DesignFile) =>
3858
+ file.id === activeFile.id
3859
+ ? {
3860
+ ...file,
3861
+ content: nextContent,
3862
+ updatedAt: optimisticUpdatedAt,
3863
+ }
3864
+ : file,
3865
+ ),
3866
+ };
3867
+ },
3868
+ );
3869
+ }
3256
3870
  const forceRefresh = options.refreshPreview === true;
3257
3871
  const replacedPreview = options.skipPreview
3258
3872
  ? true
@@ -3271,9 +3885,20 @@ export default function DesignEditor() {
3271
3885
  }, LOCAL_EDIT_ORIGIN);
3272
3886
  }
3273
3887
  }
3274
- queueFileContentSave(activeFile.id, nextContent);
3888
+ queueFileContentSave(activeFile.id, nextContent, {
3889
+ syncCollab: !(ydoc && isSynced),
3890
+ immediate: options.immediateSave,
3891
+ });
3275
3892
  },
3276
- [activeFile, isSynced, queueFileContentSave, replacePreviewContent, ydoc],
3893
+ [
3894
+ activeFile,
3895
+ id,
3896
+ isSynced,
3897
+ queryClient,
3898
+ queueFileContentSave,
3899
+ replacePreviewContent,
3900
+ ydoc,
3901
+ ],
3277
3902
  );
3278
3903
 
3279
3904
  const applyFileContentUpdate = useCallback(
@@ -3297,21 +3922,13 @@ export default function DesignEditor() {
3297
3922
  ),
3298
3923
  };
3299
3924
  });
3300
- updateFileMutation.mutate({ id: fileId, content: nextContent } as any, {
3301
- onError: () => {
3302
- queryClient.invalidateQueries({
3303
- queryKey: ["action", "get-design"],
3304
- });
3305
- },
3925
+ saveFileContent({
3926
+ id: fileId,
3927
+ content: nextContent,
3928
+ syncCollab: true,
3306
3929
  });
3307
3930
  },
3308
- [
3309
- activeFile?.id,
3310
- applyLocalContentUpdate,
3311
- id,
3312
- queryClient,
3313
- updateFileMutation,
3314
- ],
3931
+ [activeFile?.id, applyLocalContentUpdate, id, queryClient, saveFileContent],
3315
3932
  );
3316
3933
 
3317
3934
  const handleCreatePrimitive = useCallback(
@@ -3319,7 +3936,11 @@ export default function DesignEditor() {
3319
3936
  const targetFile = files.find((file) => file.id === screenId);
3320
3937
  if (!targetFile) return false;
3321
3938
  const baseContent =
3322
- targetFile.id === activeFile?.id ? activeContent : targetFile.content;
3939
+ targetFile.id === activeFile?.id
3940
+ ? ydoc && isSynced
3941
+ ? ydoc.getText("content").toString()
3942
+ : (collabContentRef.current ?? activeContent)
3943
+ : targetFile.content;
3323
3944
  const nextContent = appendCanvasPrimitiveToHtml(baseContent, primitive);
3324
3945
  if (!nextContent) {
3325
3946
  toast.error(t("designEditor.toasts.primitiveInsertFailed"));
@@ -3327,7 +3948,7 @@ export default function DesignEditor() {
3327
3948
  }
3328
3949
 
3329
3950
  if (targetFile.id === activeFile?.id) {
3330
- applyLocalContentUpdate(nextContent);
3951
+ applyLocalContentUpdate(nextContent, { immediateSave: true });
3331
3952
  } else {
3332
3953
  queryClient.setQueryData(
3333
3954
  ["action", "get-design", { id }],
@@ -3345,16 +3966,11 @@ export default function DesignEditor() {
3345
3966
  };
3346
3967
  },
3347
3968
  );
3348
- updateFileMutation.mutate(
3349
- { id: targetFile.id, content: nextContent } as any,
3350
- {
3351
- onError: () => {
3352
- queryClient.invalidateQueries({
3353
- queryKey: ["action", "get-design"],
3354
- });
3355
- },
3356
- },
3357
- );
3969
+ saveFileContent({
3970
+ id: targetFile.id,
3971
+ content: nextContent,
3972
+ syncCollab: true,
3973
+ });
3358
3974
  }
3359
3975
 
3360
3976
  return true;
@@ -3365,9 +3981,11 @@ export default function DesignEditor() {
3365
3981
  applyLocalContentUpdate,
3366
3982
  files,
3367
3983
  id,
3984
+ isSynced,
3368
3985
  queryClient,
3986
+ saveFileContent,
3369
3987
  t,
3370
- updateFileMutation,
3988
+ ydoc,
3371
3989
  ],
3372
3990
  );
3373
3991
 
@@ -3396,8 +4014,8 @@ export default function DesignEditor() {
3396
4014
  setSelectedElement(null);
3397
4015
  }, []);
3398
4016
 
3399
- const handleRectTool = useCallback(() => {
3400
- setActiveTool("rect");
4017
+ const handleShapeTool = useCallback((tool: ShapeTool) => {
4018
+ setActiveTool(tool);
3401
4019
  setViewMode("overview");
3402
4020
  setMode("edit");
3403
4021
  setDrawMode(false);
@@ -3405,6 +4023,10 @@ export default function DesignEditor() {
3405
4023
  setSelectedElement(null);
3406
4024
  }, []);
3407
4025
 
4026
+ const handleRectTool = useCallback(() => {
4027
+ handleShapeTool("rect");
4028
+ }, [handleShapeTool]);
4029
+
3408
4030
  const handlePenTool = useCallback(() => {
3409
4031
  setActiveTool("pen");
3410
4032
  setViewMode("overview");
@@ -3908,7 +4530,9 @@ export default function DesignEditor() {
3908
4530
  }, LOCAL_EDIT_ORIGIN);
3909
4531
  }
3910
4532
  }
3911
- queueFileContentSave(activeFile.id, resolvedNextContent);
4533
+ queueFileContentSave(activeFile.id, resolvedNextContent, {
4534
+ syncCollab: !(ydoc && isSynced),
4535
+ });
3912
4536
  if (resolvedNode) setSelectedLayerIdsState([resolvedNode.id]);
3913
4537
  setSelectedElement((prev) => {
3914
4538
  if (options.elementInfo) return options.elementInfo;
@@ -4030,16 +4654,31 @@ export default function DesignEditor() {
4030
4654
  );
4031
4655
  return false;
4032
4656
  }
4033
- applyLocalContentUpdate(patch.content, { refreshPreview: false });
4034
- if (targetNode) setSelectedLayerIdsState([targetNode.id]);
4657
+ const movedNode =
4658
+ (patch.result.after?.nodeId
4659
+ ? patch.projection.nodes.find(
4660
+ (node) => node.id === patch.result.after?.nodeId,
4661
+ )
4662
+ : null) ??
4663
+ resolveCodeLayerNodeFromBridge(
4664
+ patch.projection,
4665
+ selector,
4666
+ details?.sourceId ??
4667
+ elementInfo?.sourceId ??
4668
+ (targetNode
4669
+ ? bridgeSourceIdForCodeLayerNode(targetNode)
4670
+ : undefined),
4671
+ );
4672
+ applyLocalContentUpdate(patch.content, { skipPreview: true });
4673
+ if (movedNode) setSelectedLayerIdsState([movedNode.id]);
4035
4674
  if (elementInfo) {
4036
4675
  setSelectedElement({
4037
4676
  ...elementInfo,
4038
- sourceId: targetNode
4039
- ? bridgeSourceIdForCodeLayerNode(targetNode)
4677
+ sourceId: movedNode
4678
+ ? bridgeSourceIdForCodeLayerNode(movedNode)
4040
4679
  : elementInfo.sourceId,
4041
- selector: targetNode
4042
- ? preferredCodeLayerSelector(targetNode)
4680
+ selector: movedNode
4681
+ ? preferredCodeLayerSelector(movedNode)
4043
4682
  : elementInfo.selector,
4044
4683
  });
4045
4684
  }
@@ -4433,7 +5072,9 @@ export default function DesignEditor() {
4433
5072
  if (ydoc && activeFile) {
4434
5073
  const next = ydoc.getText("content").toString();
4435
5074
  lastLocalContentRef.current = next;
4436
- queueFileContentSave(activeFile.id, next);
5075
+ queueFileContentSave(activeFile.id, next, {
5076
+ syncCollab: !(ydoc && isSynced),
5077
+ });
4437
5078
  if (!replacePreviewContent(next)) {
4438
5079
  setContentRenderRevision((revision) => revision + 1);
4439
5080
  }
@@ -4472,6 +5113,7 @@ export default function DesignEditor() {
4472
5113
  }, [
4473
5114
  ydoc,
4474
5115
  activeFile,
5116
+ isSynced,
4475
5117
  queueFileContentSave,
4476
5118
  replacePreviewContent,
4477
5119
  syncUndoRedoState,
@@ -4486,7 +5128,9 @@ export default function DesignEditor() {
4486
5128
  if (ydoc && activeFile) {
4487
5129
  const next = ydoc.getText("content").toString();
4488
5130
  lastLocalContentRef.current = next;
4489
- queueFileContentSave(activeFile.id, next);
5131
+ queueFileContentSave(activeFile.id, next, {
5132
+ syncCollab: !(ydoc && isSynced),
5133
+ });
4490
5134
  if (!replacePreviewContent(next)) {
4491
5135
  setContentRenderRevision((revision) => revision + 1);
4492
5136
  }
@@ -4525,6 +5169,7 @@ export default function DesignEditor() {
4525
5169
  }, [
4526
5170
  ydoc,
4527
5171
  activeFile,
5172
+ isSynced,
4528
5173
  queueFileContentSave,
4529
5174
  replacePreviewContent,
4530
5175
  syncUndoRedoState,
@@ -4575,7 +5220,7 @@ export default function DesignEditor() {
4575
5220
  setPinMode(false);
4576
5221
  setMode("edit");
4577
5222
  setSelectedElement(null);
4578
- setActiveTool("overview");
5223
+ setActiveTool("move");
4579
5224
  setViewMode("overview");
4580
5225
  });
4581
5226
  }, [runEditorViewTransition]);
@@ -4767,7 +5412,7 @@ export default function DesignEditor() {
4767
5412
  setDrawMode(false);
4768
5413
  setPinMode(false);
4769
5414
  setMode("edit");
4770
- setActiveTool("overview");
5415
+ setActiveTool("move");
4771
5416
  setViewMode("overview");
4772
5417
  setOverviewSelectAllRequest((request) => request + 1);
4773
5418
  }, [files.length]);
@@ -4813,7 +5458,7 @@ export default function DesignEditor() {
4813
5458
  onZoomReset: () => setZoom(100),
4814
5459
  onZoomToFit: () => {
4815
5460
  setViewMode("overview");
4816
- setActiveTool("overview");
5461
+ setActiveTool("move");
4817
5462
  setZoom(100);
4818
5463
  },
4819
5464
  onZoomToSelection: () => {
@@ -5514,6 +6159,32 @@ ${serializedHtml}
5514
6159
  });
5515
6160
  }, [designDataJson, files]);
5516
6161
 
6162
+ const activeScreenPreviewUrl = useMemo(() => {
6163
+ if (builderPreviewUrl) return builderPreviewUrl;
6164
+ const screen = overviewScreens.find((item) => item.id === activeFile?.id);
6165
+ return (
6166
+ screen?.url ||
6167
+ screen?.previewUrl ||
6168
+ externalPreviewUrlForContent(activeContent)
6169
+ );
6170
+ }, [activeContent, activeFile?.id, builderPreviewUrl, overviewScreens]);
6171
+
6172
+ const handleOpenDesignPreview = useCallback(() => {
6173
+ if (activeScreenPreviewUrl) {
6174
+ window.open(activeScreenPreviewUrl, "_blank", "noopener,noreferrer");
6175
+ return;
6176
+ }
6177
+
6178
+ const content = activeContent.trim();
6179
+ if (!content) return;
6180
+
6181
+ const blobUrl = URL.createObjectURL(
6182
+ new Blob([fullPreviewHtml(activeContent)], { type: "text/html" }),
6183
+ );
6184
+ window.open(blobUrl, "_blank", "noopener,noreferrer");
6185
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
6186
+ }, [activeContent, activeScreenPreviewUrl]);
6187
+
5517
6188
  const activeLayerId =
5518
6189
  selectedLayerIds[selectedLayerIds.length - 1] ??
5519
6190
  selectedElementLayerId ??
@@ -5571,7 +6242,9 @@ ${serializedHtml}
5571
6242
  moved = true;
5572
6243
  }
5573
6244
  if (!moved || nextContent === sourceContent) return;
5574
- applyFileContentUpdate(targetOwner.fileId, nextContent);
6245
+ applyFileContentUpdate(targetOwner.fileId, nextContent, {
6246
+ refreshPreview: targetOwner.fileId === activeFile?.id,
6247
+ });
5575
6248
  },
5576
6249
  [
5577
6250
  activeContent,
@@ -5808,16 +6481,60 @@ ${serializedHtml}
5808
6481
 
5809
6482
  const deviceFrameIcon =
5810
6483
  deviceFrame === "desktop" ? (
5811
- <IconDeviceDesktop className="w-3.5 h-3.5" />
6484
+ <IconDeviceDesktop className="size-3" />
5812
6485
  ) : deviceFrame === "tablet" ? (
5813
- <IconDeviceTablet className="w-3.5 h-3.5" />
6486
+ <IconDeviceTablet className="size-3" />
5814
6487
  ) : deviceFrame === "mobile" ? (
5815
- <IconDeviceMobile className="w-3.5 h-3.5" />
6488
+ <IconDeviceMobile className="size-3" />
5816
6489
  ) : (
5817
- <IconViewportWide className="w-3.5 h-3.5" />
6490
+ <IconViewportWide className="size-3" />
5818
6491
  );
5819
6492
  const questionFlowActive = pendingQuestionsVisible;
5820
6493
 
6494
+ const deviceFrameControl = (
6495
+ <DropdownMenu>
6496
+ <Tooltip>
6497
+ <TooltipTrigger asChild>
6498
+ <DropdownMenuTrigger asChild>
6499
+ <Button
6500
+ variant="ghost"
6501
+ size="sm"
6502
+ className="h-7 shrink-0 gap-0.5 px-1.5 cursor-pointer text-muted-foreground hover:text-foreground"
6503
+ disabled={viewMode === "overview"}
6504
+ >
6505
+ {deviceFrameIcon}
6506
+ <IconChevronDown className="size-2.5 opacity-60" />
6507
+ </Button>
6508
+ </DropdownMenuTrigger>
6509
+ </TooltipTrigger>
6510
+ <TooltipContent>{t("designEditor.devicePreview")}</TooltipContent>
6511
+ </Tooltip>
6512
+ <DropdownMenuContent align="end" className="w-44">
6513
+ <DropdownMenuRadioGroup
6514
+ value={deviceFrame}
6515
+ onValueChange={(v) => setDeviceFrame(v as DeviceFrameType)}
6516
+ >
6517
+ <DropdownMenuRadioItem value="none">
6518
+ <IconViewportWide className="mr-2 h-4 w-4" />
6519
+ {t("designEditor.devices.responsive")}
6520
+ </DropdownMenuRadioItem>
6521
+ <DropdownMenuRadioItem value="desktop">
6522
+ <IconDeviceDesktop className="mr-2 h-4 w-4" />
6523
+ {t("designEditor.devices.desktop")}
6524
+ </DropdownMenuRadioItem>
6525
+ <DropdownMenuRadioItem value="tablet">
6526
+ <IconDeviceTablet className="mr-2 h-4 w-4" />
6527
+ {t("designEditor.devices.tablet")}
6528
+ </DropdownMenuRadioItem>
6529
+ <DropdownMenuRadioItem value="mobile">
6530
+ <IconDeviceMobile className="mr-2 h-4 w-4" />
6531
+ {t("designEditor.devices.mobile")}
6532
+ </DropdownMenuRadioItem>
6533
+ </DropdownMenuRadioGroup>
6534
+ </DropdownMenuContent>
6535
+ </DropdownMenu>
6536
+ );
6537
+
5821
6538
  const projectMenu = (
5822
6539
  <DropdownMenu>
5823
6540
  <DropdownMenuTrigger asChild>
@@ -5966,7 +6683,7 @@ ${serializedHtml}
5966
6683
  setTitleEditing(false);
5967
6684
  }
5968
6685
  }}
5969
- className="h-7 min-w-0 flex-1 text-sm"
6686
+ className="-mx-1 h-7 min-w-0 flex-1 border-transparent bg-[var(--design-editor-panel-raised-bg)] px-1 py-0 text-[13px] font-medium text-foreground shadow-none ring-offset-0 focus-visible:border-[var(--design-editor-control-border)] focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)] focus-visible:ring-offset-0"
5970
6687
  />
5971
6688
  ) : (
5972
6689
  <Tooltip>
@@ -5977,7 +6694,7 @@ ${serializedHtml}
5977
6694
  setTitleDraft(design.title);
5978
6695
  setTitleEditing(true);
5979
6696
  }}
5980
- className="-mx-1 min-w-0 flex-1 cursor-text truncate rounded px-1 text-left text-sm font-medium text-foreground/90 hover:bg-accent/50"
6697
+ className="-mx-1 min-w-0 flex-1 cursor-text truncate rounded px-1 text-left text-[13px] font-medium text-foreground/90 hover:bg-accent/50"
5981
6698
  >
5982
6699
  {design.title}
5983
6700
  </button>
@@ -5986,111 +6703,84 @@ ${serializedHtml}
5986
6703
  </Tooltip>
5987
6704
  );
5988
6705
 
5989
- const rightSidebarActions = (
5990
- <div className="shrink-0 border-b border-border bg-[var(--design-editor-panel-bg)] p-2">
5991
- <div className="flex flex-wrap items-center justify-end gap-1">
5992
- <DropdownMenu>
5993
- <Tooltip>
5994
- <TooltipTrigger asChild>
5995
- <DropdownMenuTrigger asChild>
5996
- <Button
5997
- variant="ghost"
5998
- size="sm"
5999
- className="h-7 gap-1 px-2 cursor-pointer"
6000
- disabled={viewMode === "overview"}
6001
- >
6002
- {deviceFrameIcon}
6003
- <IconChevronDown className="w-3 h-3 opacity-60" />
6004
- </Button>
6005
- </DropdownMenuTrigger>
6006
- </TooltipTrigger>
6007
- <TooltipContent>{t("designEditor.devicePreview")}</TooltipContent>
6008
- </Tooltip>
6009
- <DropdownMenuContent align="end" className="w-44">
6010
- <DropdownMenuRadioGroup
6011
- value={deviceFrame}
6012
- onValueChange={(v) => setDeviceFrame(v as DeviceFrameType)}
6706
+ const zoomControl = (
6707
+ <DropdownMenu>
6708
+ <Tooltip>
6709
+ <TooltipTrigger asChild>
6710
+ <DropdownMenuTrigger asChild>
6711
+ <Button
6712
+ variant="ghost"
6713
+ size="sm"
6714
+ className="h-6 gap-0.5 px-1 text-[10px] tabular-nums text-muted-foreground cursor-pointer hover:text-foreground"
6013
6715
  >
6014
- <DropdownMenuRadioItem value="none">
6015
- <IconViewportWide className="mr-2 h-4 w-4" />
6016
- {t("designEditor.devices.responsive")}
6017
- </DropdownMenuRadioItem>
6018
- <DropdownMenuRadioItem value="desktop">
6019
- <IconDeviceDesktop className="mr-2 h-4 w-4" />
6020
- {t("designEditor.devices.desktop")}
6021
- </DropdownMenuRadioItem>
6022
- <DropdownMenuRadioItem value="tablet">
6023
- <IconDeviceTablet className="mr-2 h-4 w-4" />
6024
- {t("designEditor.devices.tablet")}
6025
- </DropdownMenuRadioItem>
6026
- <DropdownMenuRadioItem value="mobile">
6027
- <IconDeviceMobile className="mr-2 h-4 w-4" />
6028
- {t("designEditor.devices.mobile")}
6029
- </DropdownMenuRadioItem>
6030
- </DropdownMenuRadioGroup>
6031
- </DropdownMenuContent>
6032
- </DropdownMenu>
6716
+ {zoomLabel}
6717
+ <IconChevronDown className="size-2.5 opacity-60" />
6718
+ </Button>
6719
+ </DropdownMenuTrigger>
6720
+ </TooltipTrigger>
6721
+ <TooltipContent>{t("designEditor.zoom")}</TooltipContent>
6722
+ </Tooltip>
6723
+ <DropdownMenuContent align="end" className="w-40">
6724
+ <DropdownMenuItem onClick={handleZoomOut}>
6725
+ <IconZoomOut className="mr-2 h-4 w-4" />
6726
+ {t("designEditor.zoomOut")}
6727
+ </DropdownMenuItem>
6728
+ <DropdownMenuItem onClick={handleZoomIn}>
6729
+ <IconZoomIn className="mr-2 h-4 w-4" />
6730
+ {t("designEditor.zoomIn")}
6731
+ </DropdownMenuItem>
6732
+ <DropdownMenuSeparator />
6733
+ {ZOOM_PRESETS.map((preset) => (
6734
+ <DropdownMenuItem
6735
+ key={preset}
6736
+ onClick={() => setZoom(preset)}
6737
+ className="justify-between"
6738
+ >
6739
+ <span>{preset}%</span>
6740
+ {Math.round(zoom) === preset && <IconCheck className="h-4 w-4" />}
6741
+ </DropdownMenuItem>
6742
+ ))}
6743
+ </DropdownMenuContent>
6744
+ </DropdownMenu>
6745
+ );
6033
6746
 
6034
- <DropdownMenu>
6747
+ const rightSidebarActions = (
6748
+ <div className="shrink-0 border-b border-border bg-[var(--design-editor-panel-bg)] px-2 py-1.5">
6749
+ <div className="flex min-h-8 items-center justify-between gap-2">
6750
+ <DesignCollaboratorsMenu
6751
+ collaborators={designCollaborators}
6752
+ followingEmail={followingEmail}
6753
+ label={t("designEditor.collaborators")}
6754
+ onAvatarClick={handleAvatarClick}
6755
+ />
6756
+
6757
+ <div className="ml-auto flex shrink-0 items-center gap-1">
6035
6758
  <Tooltip>
6036
6759
  <TooltipTrigger asChild>
6037
- <DropdownMenuTrigger asChild>
6038
- <Button
6039
- variant="ghost"
6040
- size="sm"
6041
- className="h-7 gap-1 px-2 text-xs tabular-nums text-muted-foreground cursor-pointer"
6042
- >
6043
- {zoomLabel}
6044
- <IconChevronDown className="w-3 h-3 opacity-60" />
6045
- </Button>
6046
- </DropdownMenuTrigger>
6760
+ <Button
6761
+ variant="ghost"
6762
+ size="icon"
6763
+ className="size-8 cursor-pointer rounded-md text-foreground hover:bg-accent hover:text-foreground"
6764
+ onClick={handleOpenDesignPreview}
6765
+ disabled={!activeScreenPreviewUrl && !activeContent.trim()}
6766
+ aria-label={t("designEditor.designPreview")}
6767
+ >
6768
+ <IconPlayerPlay className="size-5" />
6769
+ </Button>
6047
6770
  </TooltipTrigger>
6048
- <TooltipContent>{t("designEditor.zoom")}</TooltipContent>
6771
+ <TooltipContent>{t("designEditor.designPreview")}</TooltipContent>
6049
6772
  </Tooltip>
6050
- <DropdownMenuContent align="end" className="w-40">
6051
- <DropdownMenuItem onClick={handleZoomOut}>
6052
- <IconZoomOut className="mr-2 h-4 w-4" />
6053
- {t("designEditor.zoomOut")}
6054
- </DropdownMenuItem>
6055
- <DropdownMenuItem onClick={handleZoomIn}>
6056
- <IconZoomIn className="mr-2 h-4 w-4" />
6057
- {t("designEditor.zoomIn")}
6058
- </DropdownMenuItem>
6059
- <DropdownMenuSeparator />
6060
- {ZOOM_PRESETS.map((preset) => (
6061
- <DropdownMenuItem
6062
- key={preset}
6063
- onClick={() => setZoom(preset)}
6064
- className="justify-between"
6065
- >
6066
- <span>{preset}%</span>
6067
- {Math.round(zoom) === preset && (
6068
- <IconCheck className="h-4 w-4" />
6069
- )}
6070
- </DropdownMenuItem>
6071
- ))}
6072
- </DropdownMenuContent>
6073
- </DropdownMenu>
6074
6773
 
6075
- <div className="mx-1 h-5 w-px bg-border" />
6076
-
6077
- <PresenceBar
6078
- activeUsers={activeUsers}
6079
- agentActive={agentActive}
6080
- currentUserEmail={session?.email}
6081
- onAvatarClick={handleAvatarClick}
6082
- followingEmail={followingEmail}
6083
- />
6084
-
6085
- <ShareButton
6086
- resourceType="design"
6087
- resourceId={id}
6088
- resourceTitle={design.title}
6089
- hideTriggerIcon
6090
- triggerClassName="h-7 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-2 text-xs !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
6091
- />
6774
+ <ShareButton
6775
+ resourceType="design"
6776
+ resourceId={id}
6777
+ resourceTitle={design.title}
6778
+ hideTriggerIcon
6779
+ triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
6780
+ />
6092
6781
 
6093
- <AgentToggleButton />
6782
+ <AgentToggleButton />
6783
+ </div>
6094
6784
  </div>
6095
6785
  </div>
6096
6786
  );
@@ -6304,7 +6994,9 @@ ${serializedHtml}
6304
6994
  >
6305
6995
  <TabsList className="pointer-events-auto h-8">
6306
6996
  <TabsTrigger value="edit" className="h-6 gap-1 px-2 text-xs">
6307
- {mode === "edit" && <IconPencil className="h-3 w-3" />}
6997
+ {mode === "edit" && (
6998
+ <IconTransformPoint className="h-3 w-3" />
6999
+ )}
6308
7000
  {t("designEditor.modes.edit")}
6309
7001
  </TabsTrigger>
6310
7002
  <TabsTrigger
@@ -6312,7 +7004,9 @@ ${serializedHtml}
6312
7004
  className="h-6 gap-1 px-2 text-xs"
6313
7005
  disabled={!activeFile || viewMode === "overview"}
6314
7006
  >
6315
- {mode === "interact" && <IconPointer className="h-3 w-3" />}
7007
+ {mode === "interact" && (
7008
+ <IconHandClick className="h-3 w-3" />
7009
+ )}
6316
7010
  {t("designEditor.modes.interact")}
6317
7011
  </TabsTrigger>
6318
7012
  <TabsTrigger
@@ -6328,31 +7022,6 @@ ${serializedHtml}
6328
7022
  </div>
6329
7023
  )}
6330
7024
  <div className="ml-auto flex shrink-0 items-center gap-1 pl-2">
6331
- {/* Overview / single-screen toggle. Clicking Overview shows every
6332
- file in the design as a Figma-style pannable lineup. */}
6333
- <Tooltip>
6334
- <TooltipTrigger asChild>
6335
- <Button
6336
- variant={viewMode === "overview" ? "secondary" : "ghost"}
6337
- size="icon"
6338
- className="h-7 w-7 cursor-pointer"
6339
- onClick={handleViewModeToggle}
6340
- aria-label={
6341
- viewMode === "overview"
6342
- ? t("designEditor.returnToCurrentScreen")
6343
- : t("designEditor.openScreenOverview")
6344
- }
6345
- >
6346
- <IconLayoutGrid className="w-3.5 h-3.5" />
6347
- </Button>
6348
- </TooltipTrigger>
6349
- <TooltipContent>
6350
- {viewMode === "overview"
6351
- ? t("designEditor.currentScreen")
6352
- : t("designEditor.screenOverview")}
6353
- </TooltipContent>
6354
- </Tooltip>
6355
-
6356
7025
  {!embedded && (
6357
7026
  <>
6358
7027
  {/* Device preview — collapsed into a single menu. */}
@@ -6488,9 +7157,10 @@ ${serializedHtml}
6488
7157
  className="relative flex min-h-0 shrink-0 flex-col bg-[var(--design-editor-panel-bg)]"
6489
7158
  style={{ width: leftSidebarWidth }}
6490
7159
  >
6491
- <div className="flex h-11 shrink-0 items-center gap-2 border-b border-border px-2">
7160
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-border px-2">
6492
7161
  {projectMenu}
6493
7162
  {projectTitleControl}
7163
+ {deviceFrameControl}
6494
7164
  </div>
6495
7165
  <div className="min-h-0 flex-1">
6496
7166
  <LayersPanel
@@ -6617,10 +7287,9 @@ ${serializedHtml}
6617
7287
  pinMode={pinMode}
6618
7288
  drawMode={drawMode}
6619
7289
  activeTool={activeTool}
6620
- overviewActive={viewMode === "overview"}
6621
7290
  onMove={handleMoveTool}
6622
7291
  onFrame={handleFrameTool}
6623
- onRect={handleRectTool}
7292
+ onShape={handleShapeTool}
6624
7293
  onText={handleTextTool}
6625
7294
  onPen={handlePenTool}
6626
7295
  onHand={handleHandTool}
@@ -6628,7 +7297,6 @@ ${serializedHtml}
6628
7297
  onScale={handleScaleTool}
6629
7298
  onCommentPin={handlePinToolToggle}
6630
7299
  onModeChange={handleModeChange}
6631
- onScreensToggle={handleViewModeToggle}
6632
7300
  />
6633
7301
  )}
6634
7302
 
@@ -6672,7 +7340,7 @@ ${serializedHtml}
6672
7340
  onSelectAll={handleSelectAllFrames}
6673
7341
  onZoomToFit={() => {
6674
7342
  setViewMode("overview");
6675
- setActiveTool("overview");
7343
+ setActiveTool("move");
6676
7344
  setZoom(100);
6677
7345
  }}
6678
7346
  onZoomToSelection={() => setZoom(150)}
@@ -6721,12 +7389,13 @@ ${serializedHtml}
6721
7389
  onGeometryChange={queueFrameGeometrySave}
6722
7390
  onGeometryCommit={handleGeometryCommit}
6723
7391
  onCreatePrimitive={handleCreatePrimitive}
7392
+ onCreateScreenFrame={handleCreateScreenFrame}
6724
7393
  onDeleteSelection={handleDeleteOverviewSelection}
6725
7394
  onPick={(id) => {
6726
7395
  setSelectedElement(null);
6727
7396
  setSelectedLayerIdsState([id]);
6728
7397
  setActiveFileId(id);
6729
- setActiveTool("overview");
7398
+ setActiveTool("move");
6730
7399
  setMode("edit");
6731
7400
  }}
6732
7401
  onEdit={enterSingleScreen}
@@ -6935,6 +7604,7 @@ ${serializedHtml}
6935
7604
  selectedElement={selectedElement}
6936
7605
  pageStyles={pageStyles}
6937
7606
  zoom={zoom}
7607
+ headerTrailing={zoomControl}
6938
7608
  width={rightSidebarWidth}
6939
7609
  activeTab={activeInspectorTab}
6940
7610
  onActiveTabChange={setActiveInspectorTab}