@agent-native/core 0.80.11 → 0.81.0

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 (225) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +47 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  6. package/corpus/core/src/client/chat/index.ts +1 -0
  7. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  8. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  10. package/corpus/core/src/client/index.ts +1 -0
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  12. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  13. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  14. package/corpus/core/src/db/client.ts +29 -2
  15. package/corpus/core/src/provider-api/index.ts +29 -0
  16. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  17. package/corpus/core/src/styles/agent-native.css +54 -0
  18. package/corpus/core/src/usage/store.ts +2 -2
  19. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  20. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  21. package/corpus/templates/analytics/app/global.css +8 -0
  22. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  23. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  24. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  25. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  26. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  27. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  28. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  33. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  34. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  35. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  36. package/corpus/templates/assets/app/global.css +15 -0
  37. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  38. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  39. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  40. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  42. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  43. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  45. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  46. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  47. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  48. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  49. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  50. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  51. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  52. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  55. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  56. package/corpus/templates/design/AGENTS.md +27 -0
  57. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  58. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  59. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  60. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  61. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  62. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  63. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  64. package/corpus/templates/design/actions/export-zip.ts +23 -8
  65. package/corpus/templates/design/actions/generate-design.ts +53 -1
  66. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  67. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  68. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  69. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  70. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  71. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  72. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  73. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  74. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  75. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  76. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  77. package/corpus/templates/design/actions/navigate.ts +18 -3
  78. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  79. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  80. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  81. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  82. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  83. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  84. package/corpus/templates/design/actions/view-screen.ts +1 -1
  85. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  86. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  87. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  88. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  89. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  90. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  91. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  92. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  93. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  94. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  95. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  96. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  97. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  98. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  99. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  100. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  101. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  102. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  103. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  104. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  105. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  106. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  107. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  108. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  109. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  110. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  111. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  112. package/corpus/templates/design/app/global.css +7 -0
  113. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  114. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  115. package/corpus/templates/design/app/i18n-data.ts +358 -0
  116. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  117. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  118. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  119. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  120. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  143. package/corpus/templates/design/e2e/helpers.ts +168 -65
  144. package/corpus/templates/design/package.json +3 -1
  145. package/corpus/templates/design/server/db/schema.ts +23 -0
  146. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  147. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  148. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  149. package/corpus/templates/design/server/plugins/db.ts +39 -0
  150. package/corpus/templates/design/server/register-secrets.ts +38 -0
  151. package/corpus/templates/design/shared/board-file.ts +228 -0
  152. package/corpus/templates/design/shared/board-objects.ts +288 -0
  153. package/corpus/templates/design/shared/code-layer.ts +52 -0
  154. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  155. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  156. package/corpus/templates/design/tsconfig.json +2 -1
  157. package/corpus/templates/slides/AGENTS.md +9 -0
  158. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  159. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  160. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  161. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  162. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  163. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  164. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  166. package/corpus/templates/slides/package.json +1 -0
  167. package/corpus/templates/slides/vite.config.ts +1 -0
  168. package/dist/cli/design-connect.d.ts +25 -0
  169. package/dist/cli/design-connect.d.ts.map +1 -1
  170. package/dist/cli/design-connect.js +357 -18
  171. package/dist/cli/design-connect.js.map +1 -1
  172. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  173. package/dist/client/ConnectBuilderCard.js +1 -0
  174. package/dist/client/ConnectBuilderCard.js.map +1 -1
  175. package/dist/client/chat/index.d.ts +1 -1
  176. package/dist/client/chat/index.d.ts.map +1 -1
  177. package/dist/client/chat/index.js +1 -1
  178. package/dist/client/chat/index.js.map +1 -1
  179. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  180. package/dist/client/chat/run-recovery.js +5 -5
  181. package/dist/client/chat/run-recovery.js.map +1 -1
  182. package/dist/client/chat-view-transition.d.ts +6 -0
  183. package/dist/client/chat-view-transition.d.ts.map +1 -1
  184. package/dist/client/chat-view-transition.js +23 -2
  185. package/dist/client/chat-view-transition.js.map +1 -1
  186. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  187. package/dist/client/composer/PromptComposer.js +3 -2
  188. package/dist/client/composer/PromptComposer.js.map +1 -1
  189. package/dist/client/index.d.ts +1 -1
  190. package/dist/client/index.d.ts.map +1 -1
  191. package/dist/client/index.js +1 -1
  192. package/dist/client/index.js.map +1 -1
  193. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  194. package/dist/client/org/OrgSwitcher.js +11 -4
  195. package/dist/client/org/OrgSwitcher.js.map +1 -1
  196. package/dist/client/sharing/ShareButton.js +3 -3
  197. package/dist/client/sharing/ShareButton.js.map +1 -1
  198. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  199. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  200. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  201. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  202. package/dist/collab/awareness.d.ts +2 -2
  203. package/dist/collab/awareness.d.ts.map +1 -1
  204. package/dist/collab/routes.d.ts +2 -2
  205. package/dist/db/client.d.ts +1 -0
  206. package/dist/db/client.d.ts.map +1 -1
  207. package/dist/db/client.js +28 -2
  208. package/dist/db/client.js.map +1 -1
  209. package/dist/notifications/routes.d.ts +3 -3
  210. package/dist/observability/routes.d.ts +2 -2
  211. package/dist/progress/routes.d.ts +1 -1
  212. package/dist/provider-api/index.d.ts +5 -4
  213. package/dist/provider-api/index.d.ts.map +1 -1
  214. package/dist/provider-api/index.js +28 -0
  215. package/dist/provider-api/index.js.map +1 -1
  216. package/dist/resources/handlers.d.ts +3 -3
  217. package/dist/server/core-routes-plugin.d.ts +25 -0
  218. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  219. package/dist/server/core-routes-plugin.js +43 -18
  220. package/dist/server/core-routes-plugin.js.map +1 -1
  221. package/dist/server/transcribe-voice.d.ts +1 -1
  222. package/dist/styles/agent-native.css +54 -0
  223. package/dist/usage/store.js +2 -2
  224. package/dist/usage/store.js.map +1 -1
  225. package/package.json +1 -1
@@ -10,7 +10,7 @@ import {
10
10
  emailToColor,
11
11
  emailToName,
12
12
  PresenceBar,
13
- AgentToggleButton,
13
+ AgentChatSurface,
14
14
  ShareButton,
15
15
  agentNativePath,
16
16
  appBasePath,
@@ -33,6 +33,7 @@ import {
33
33
  type PromptComposerSubmitOptions,
34
34
  } from "@agent-native/core/client";
35
35
  import type { TweakDefinition } from "@shared/api";
36
+ import { isBoardFile } from "@shared/board-file";
36
37
  import {
37
38
  parseCanvasFrameGeometryById,
38
39
  type CanvasFrameGeometry,
@@ -57,6 +58,7 @@ import {
57
58
  hasCapability,
58
59
  } from "@shared/design-source-capabilities";
59
60
  import { shouldUseLiveFileContent } from "@shared/html-content";
61
+ import type { MotionTrack } from "@shared/motion-timeline";
60
62
  import {
61
63
  resolveTweaksToCssVars,
62
64
  type TweakSelections,
@@ -81,11 +83,14 @@ import {
81
83
  IconFrame,
82
84
  IconX,
83
85
  IconPin,
86
+ IconAssembly,
84
87
  IconCode,
85
88
  IconArchive,
89
+ IconFile,
86
90
  IconPhoto,
87
91
  IconRefresh,
88
92
  IconChevronDown,
93
+ IconChevronRight,
89
94
  IconCheck,
90
95
  IconPointer,
91
96
  IconTypography,
@@ -139,7 +144,11 @@ import {
139
144
  type MotionTrackWire,
140
145
  } from "@/components/design/DesignCanvas";
141
146
  import { DesignEditorSkeleton } from "@/components/design/DesignEditorSkeleton";
142
- import type { DesignExtensionSlotContext } from "@/components/design/DesignExtensionsPanel";
147
+ import {
148
+ AssetLibraryPanel,
149
+ DesignExtensionsPanel,
150
+ type DesignExtensionSlotContext,
151
+ } from "@/components/design/DesignExtensionsPanel";
143
152
  import {
144
153
  EditPanel,
145
154
  type InspectCodeData,
@@ -152,6 +161,10 @@ import {
152
161
  type LayersPanelMoveIntent,
153
162
  type LayersPanelNode,
154
163
  } from "@/components/design/LayersPanel";
164
+ import {
165
+ LocalhostWriteConsentDialog,
166
+ type LocalhostWriteConsentPayload,
167
+ } from "@/components/design/LocalhostWriteConsentDialog";
155
168
  import { LocalSourceEditBanner } from "@/components/design/LocalSourceEditBanner";
156
169
  import {
157
170
  MotionDock,
@@ -161,9 +174,11 @@ import {
161
174
  MultiScreenCanvas,
162
175
  OVERVIEW_FRAME_WIDTH,
163
176
  type CanvasPrimitiveInsert,
177
+ type FrameGeometry,
164
178
  } from "@/components/design/MultiScreenCanvas";
165
179
  import { QuestionFlow } from "@/components/design/QuestionFlow";
166
180
  import type { ReviewPanelProps } from "@/components/design/ReviewPanel";
181
+ import { TokensPanel } from "@/components/design/TokensPanel";
167
182
  import type { ElementInfo, DeviceFrameType } from "@/components/design/types";
168
183
  import {
169
184
  DEVICE_FRAME_VIEWPORTS,
@@ -197,6 +212,11 @@ import {
197
212
  DropdownMenuTrigger,
198
213
  } from "@/components/ui/dropdown-menu";
199
214
  import { Input } from "@/components/ui/input";
215
+ import {
216
+ Popover,
217
+ PopoverContent,
218
+ PopoverTrigger,
219
+ } from "@/components/ui/popover";
200
220
  import { Spinner } from "@/components/ui/spinner";
201
221
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
202
222
  import { Textarea } from "@/components/ui/textarea";
@@ -244,6 +264,12 @@ const AUTO_RETRY_DELAY_MS = 1200;
244
264
  const STORED_RUN_LIVENESS_GRACE_MS = 20_000;
245
265
  const MAX_DESIGN_UNDO_STACK = 50;
246
266
  const OVERVIEW_ZOOM_THRESHOLD = 60;
267
+ const MOTION_DOCK_TRANSITION_MS = 200;
268
+ const MOTION_AUTOSAVE_DELAY_MS = 500;
269
+ /** Extensions that the localhost bridge allows to be written back to source. */
270
+ const LOCALHOST_WRITE_EXTENSIONS = new Set([".html", ".htm", ".css"]);
271
+ const NO_LOCALHOST_WRITE_CONTENT_MESSAGE =
272
+ "No content to write. Open the screen first." /* i18n-ignore */;
247
273
  const FOCUSED_SCREEN_ZOOM = 100;
248
274
  const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
249
275
  const UNSUPPORTED_HTML2CANVAS_COLOR_RE =
@@ -341,13 +367,16 @@ export function getOverviewEnterTarget(args: {
341
367
 
342
368
  export function getSidebarCodeLayerSelectionState(args: {
343
369
  currentViewMode: "single" | "overview";
370
+ ownerFileId?: string | null;
344
371
  overviewSelectedScreenIds: string[];
345
372
  }) {
346
- const { currentViewMode, overviewSelectedScreenIds } = args;
373
+ const { currentViewMode, ownerFileId, overviewSelectedScreenIds } = args;
347
374
  return {
348
375
  viewMode: currentViewMode,
349
376
  overviewSelectedScreenIds:
350
- currentViewMode === "overview" ? [] : overviewSelectedScreenIds,
377
+ currentViewMode === "overview" && ownerFileId
378
+ ? [ownerFileId]
379
+ : overviewSelectedScreenIds,
351
380
  };
352
381
  }
353
382
 
@@ -459,6 +488,21 @@ export function getLayerMoveIterationOrder<T>(
459
488
  return placement === "after" ? [...orderedIds].reverse() : [...orderedIds];
460
489
  }
461
490
 
491
+ export function removeUndoRedoOrderKind<T extends string>(
492
+ order: readonly T[],
493
+ kind: T,
494
+ ): T[] {
495
+ return order.filter((entry) => entry !== kind);
496
+ }
497
+
498
+ type UndoRedoOrderKind = "content" | "file-content" | "geometry";
499
+
500
+ function isContentUndoRedoOrderKind(
501
+ kind: UndoRedoOrderKind | undefined,
502
+ ): kind is "content" | "file-content" {
503
+ return kind === "content" || kind === "file-content";
504
+ }
505
+
462
506
  function resolveZoomUpdate(update: SetStateAction<number>, current: number) {
463
507
  return typeof update === "function" ? update(current) : update;
464
508
  }
@@ -734,6 +778,83 @@ interface CodingHandoffResult {
734
778
  fileCount?: number;
735
779
  }
736
780
 
781
+ export interface MotionTimelineRow {
782
+ id: string | null;
783
+ designId: string;
784
+ sourceRef: string | null;
785
+ filePath: string | null;
786
+ tracks: unknown;
787
+ durationMs: number;
788
+ defaultEase: string;
789
+ compiledHash: string | null;
790
+ cssHash?: string | null;
791
+ source?: "stored" | "recovered-css" | "stored-css-drift";
792
+ createdAt: string | null;
793
+ updatedAt: string | null;
794
+ }
795
+
796
+ interface MotionTimelineQueryResult {
797
+ designId: string;
798
+ timelines: MotionTimelineRow[];
799
+ count: number;
800
+ }
801
+
802
+ function isMotionTrack(value: unknown): value is MotionTrack {
803
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
804
+ const candidate = value as {
805
+ targetNodeId?: unknown;
806
+ property?: unknown;
807
+ keyframes?: unknown;
808
+ };
809
+ return (
810
+ typeof candidate.targetNodeId === "string" &&
811
+ typeof candidate.property === "string" &&
812
+ Array.isArray(candidate.keyframes)
813
+ );
814
+ }
815
+
816
+ function normalizeMotionTracks(value: unknown): MotionTrack[] {
817
+ return Array.isArray(value) ? value.filter(isMotionTrack) : [];
818
+ }
819
+
820
+ function labelForMotionTrack(
821
+ track: MotionTrack,
822
+ projection: CodeLayerProjection,
823
+ ): string {
824
+ const node = projection.nodes.find(
825
+ (candidate) =>
826
+ candidate.dataAttributes["data-agent-native-node-id"] ===
827
+ track.targetNodeId || candidate.id === track.targetNodeId,
828
+ );
829
+ return node?.layerName || node?.tag || track.targetNodeId;
830
+ }
831
+
832
+ export function hydrateMotionDockTracks(
833
+ tracks: unknown,
834
+ projection: CodeLayerProjection,
835
+ ): MotionDockTrack[] {
836
+ return normalizeMotionTracks(tracks).map((track) => ({
837
+ ...track,
838
+ label: labelForMotionTrack(track, projection),
839
+ }));
840
+ }
841
+
842
+ export function motionTimelineFingerprint(
843
+ fileId: string,
844
+ timeline: MotionTimelineRow | null | undefined,
845
+ ): string {
846
+ if (!timeline) return `${fileId}:empty`;
847
+ return [
848
+ fileId,
849
+ timeline.id ?? "css",
850
+ timeline.updatedAt ?? "no-updated-at",
851
+ timeline.compiledHash ?? "no-compiled-hash",
852
+ timeline.cssHash ?? "no-css-hash",
853
+ timeline.source ?? "stored",
854
+ JSON.stringify(timeline.tracks),
855
+ ].join(":");
856
+ }
857
+
737
858
  function buildSignInHrefForDesignIntent(intent: PostAuthDesignIntent): string {
738
859
  const base = agentNativePath("/_agent-native/sign-in");
739
860
  if (typeof window === "undefined") return base;
@@ -894,6 +1015,7 @@ function designEditorCommandFromSearchParams(
894
1015
  ): DesignEditorCommand | null {
895
1016
  const editorView = searchParams.get("view");
896
1017
  const inspector = searchParams.get("inspector");
1018
+ const leftPanel = normalizeDesignLeftPanel(searchParams.get("panel"));
897
1019
  const screen =
898
1020
  searchParams.get("screen") ??
899
1021
  searchParams.get("fileId") ??
@@ -907,6 +1029,7 @@ function designEditorCommandFromSearchParams(
907
1029
  inspector !== "design" &&
908
1030
  inspector !== "tweaks" &&
909
1031
  inspector !== "extensions" &&
1032
+ !leftPanel &&
910
1033
  !screen &&
911
1034
  !tool
912
1035
  ) {
@@ -926,6 +1049,7 @@ function designEditorCommandFromSearchParams(
926
1049
  ) {
927
1050
  command.inspectorTab = inspector;
928
1051
  }
1052
+ if (leftPanel) command.leftPanel = leftPanel;
929
1053
  if (screen) command.screen = screen;
930
1054
  if (Number.isFinite(zoom)) {
931
1055
  command.zoom = zoom;
@@ -1433,6 +1557,10 @@ function appendCanvasPrimitiveToHtml(
1433
1557
  const element = doc.createElement("div");
1434
1558
  element.setAttribute("data-agent-native-node-id", nodeId);
1435
1559
  element.setAttribute("data-agent-native-layer-name", layerName);
1560
+ // Kind marker so the layers panel shows a shape/text/frame icon for this
1561
+ // primitive (rectangle/ellipse/text/frame) instead of the generic code
1562
+ // glyph. Read by treeTypeForNode in shared/code-layer.ts.
1563
+ element.setAttribute("data-an-primitive", primitive.kind);
1436
1564
  element.style.position = "absolute";
1437
1565
  element.style.left = `${left}px`;
1438
1566
  element.style.top = `${top}px`;
@@ -2054,6 +2182,36 @@ function elementInfoFromCodeLayerNode(node: CodeLayerNode): ElementInfo {
2054
2182
  };
2055
2183
  }
2056
2184
 
2185
+ function camelCaseCssProperty(property: string): string {
2186
+ return property.replace(/-([a-z])/g, (_, letter: string) =>
2187
+ letter.toUpperCase(),
2188
+ );
2189
+ }
2190
+
2191
+ function cssStyleAliases(
2192
+ styles: Record<string, string>,
2193
+ ): Record<string, string> {
2194
+ const result: Record<string, string> = {};
2195
+ for (const [property, value] of Object.entries(styles)) {
2196
+ result[property] = value;
2197
+ if (property.includes("-")) {
2198
+ result[camelCaseCssProperty(property)] = value;
2199
+ }
2200
+ }
2201
+ return result;
2202
+ }
2203
+
2204
+ function refreshedComputedStyles(
2205
+ info: ElementInfo,
2206
+ sourceStyles: Record<string, string>,
2207
+ sourceClasses: readonly string[],
2208
+ ): Record<string, string> {
2209
+ const sourceWithAliases = cssStyleAliases(sourceStyles);
2210
+ return sourceClasses.length > 0
2211
+ ? { ...info.computedStyles, ...sourceWithAliases }
2212
+ : sourceWithAliases;
2213
+ }
2214
+
2057
2215
  function codeLayerNodeMatchesBridgeTarget(
2058
2216
  node: CodeLayerNode,
2059
2217
  selector?: string,
@@ -2143,7 +2301,7 @@ function canonicalElementInfoForCodeLayerNode(
2143
2301
  ...info,
2144
2302
  sourceId: bridgeSourceIdForCodeLayerNode(node),
2145
2303
  selector: preferredCodeLayerSelector(node),
2146
- classes: node.classes.length > 0 ? node.classes : info.classes,
2304
+ classes: node.classes,
2147
2305
  confidence: node.confidence,
2148
2306
  editCapabilities: info.editCapabilities?.some((capability) =>
2149
2307
  capability.kind.startsWith("deterministic"),
@@ -2188,27 +2346,52 @@ function codeLayerPatchMessage(
2188
2346
  : message;
2189
2347
  }
2190
2348
 
2191
- function elementInfoExistsInContent(
2349
+ export function refreshElementInfoFromContent(
2192
2350
  content: string,
2193
2351
  info: ElementInfo | null,
2194
- ): boolean {
2195
- if (!info) return false;
2352
+ ): ElementInfo | null {
2353
+ if (!info) return null;
2196
2354
  const projection = buildCodeLayerProjection(content);
2197
- if (
2355
+ const node =
2356
+ resolveCodeLayerNodeFromElementInfo(projection, info) ??
2198
2357
  resolveCodeLayerNodeFromBridge(
2199
2358
  projection,
2200
2359
  info.selector,
2201
2360
  info.sourceId ?? info.id,
2202
- )
2203
- ) {
2204
- return true;
2361
+ );
2362
+ if (node) {
2363
+ const sourceInfo = elementInfoFromCodeLayerNode(node);
2364
+ return {
2365
+ ...canonicalElementInfoForCodeLayerNode(info, node),
2366
+ computedStyles: refreshedComputedStyles(
2367
+ info,
2368
+ sourceInfo.computedStyles,
2369
+ sourceInfo.classes,
2370
+ ),
2371
+ textContent: sourceInfo.textContent,
2372
+ isFlexChild: sourceInfo.isFlexChild,
2373
+ isFlexContainer: sourceInfo.isFlexContainer,
2374
+ parentDisplay: sourceInfo.parentDisplay,
2375
+ };
2205
2376
  }
2206
- if (!info.selector || typeof window === "undefined") return false;
2377
+ if (!info.selector || typeof window === "undefined") return null;
2207
2378
  try {
2208
2379
  const doc = new DOMParser().parseFromString(content, "text/html");
2209
- return Boolean(queryUniqueSelector(doc, info.selector));
2380
+ const element = queryUniqueSelector(doc, info.selector);
2381
+ if (!element) return null;
2382
+ const classes = Array.from(element.classList);
2383
+ return {
2384
+ ...info,
2385
+ classes,
2386
+ computedStyles: refreshedComputedStyles(
2387
+ info,
2388
+ parseInlineStyleAttribute(element.getAttribute("style")),
2389
+ classes,
2390
+ ),
2391
+ textContent: element.textContent?.slice(0, 200) ?? info.textContent,
2392
+ };
2210
2393
  } catch {
2211
- return false;
2394
+ return null;
2212
2395
  }
2213
2396
  }
2214
2397
 
@@ -2300,6 +2483,41 @@ function findCodeLayerNodeInProjection(
2300
2483
  return fallbackMatches.length === 1 ? (fallbackMatches[0] ?? null) : null;
2301
2484
  }
2302
2485
 
2486
+ export function findMovedCodeLayerNodeInProjection(
2487
+ projection: CodeLayerProjection,
2488
+ previousNode: CodeLayerNode,
2489
+ movedNodeId?: string | null,
2490
+ ): CodeLayerNode | null {
2491
+ if (movedNodeId) {
2492
+ const movedMatch = projection.nodes.find(
2493
+ (node) =>
2494
+ node.id === movedNodeId ||
2495
+ node.dataAttributes["data-agent-native-node-id"] === movedNodeId ||
2496
+ node.dataAttributes["data-code-layer-id"] === movedNodeId ||
2497
+ node.dataAttributes["data-layer-id"] === movedNodeId ||
2498
+ node.dataAttributes["data-builder-id"] === movedNodeId ||
2499
+ node.dataAttributes["data-loc"] === movedNodeId ||
2500
+ node.attributes.id === movedNodeId,
2501
+ );
2502
+ if (movedMatch) return movedMatch;
2503
+ }
2504
+ return findCodeLayerNodeInProjection(projection, previousNode);
2505
+ }
2506
+
2507
+ export function parseInlineStyleAttribute(
2508
+ style: string | null | undefined,
2509
+ ): Record<string, string> {
2510
+ const result: Record<string, string> = {};
2511
+ for (const declaration of (style ?? "").split(";")) {
2512
+ const separator = declaration.indexOf(":");
2513
+ if (separator <= 0) continue;
2514
+ const property = declaration.slice(0, separator).trim();
2515
+ const value = declaration.slice(separator + 1).trim();
2516
+ if (property && value) result[property] = value;
2517
+ }
2518
+ return result;
2519
+ }
2520
+
2303
2521
  function AgentNativeMenuMark({ className }: { className?: string }) {
2304
2522
  return (
2305
2523
  <svg
@@ -2326,6 +2544,110 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
2326
2544
  );
2327
2545
  }
2328
2546
 
2547
+ type DesignLeftPanel = "file" | "agent" | "assets" | "tools" | "tokens";
2548
+
2549
+ function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
2550
+ if (value === "extensions") return "tools";
2551
+ return value === "file" ||
2552
+ value === "agent" ||
2553
+ value === "assets" ||
2554
+ value === "tools" ||
2555
+ value === "tokens"
2556
+ ? value
2557
+ : undefined;
2558
+ }
2559
+
2560
+ function DesignWorkspaceRail({
2561
+ activePanel,
2562
+ projectMenu,
2563
+ onPanelChange,
2564
+ }: {
2565
+ activePanel: DesignLeftPanel;
2566
+ projectMenu: ReactNode;
2567
+ onPanelChange: (panel: DesignLeftPanel) => void;
2568
+ }) {
2569
+ const t = useT();
2570
+ const items: Array<{
2571
+ panel: DesignLeftPanel;
2572
+ label: string;
2573
+ icon: ReactNode;
2574
+ }> = [
2575
+ {
2576
+ panel: "file",
2577
+ label: t("designEditor.leftRail.file"),
2578
+ icon: <IconFile className="size-[15px]" />,
2579
+ },
2580
+ {
2581
+ panel: "agent",
2582
+ label: t("designEditor.leftRail.agent"),
2583
+ icon: <IconMessage className="size-[15px]" />,
2584
+ },
2585
+ {
2586
+ panel: "assets",
2587
+ label: t("designEditor.leftRail.assets"),
2588
+ icon: <IconPhoto className="size-[15px]" />,
2589
+ },
2590
+ {
2591
+ panel: "tools",
2592
+ label: t("designEditor.leftRail.tools"),
2593
+ icon: <IconTerminal2 className="size-[15px]" />,
2594
+ },
2595
+ {
2596
+ panel: "tokens",
2597
+ label: t("designEditor.leftRail.tokens"),
2598
+ icon: <IconAssembly className="size-[15px]" />,
2599
+ },
2600
+ ];
2601
+
2602
+ return (
2603
+ <nav
2604
+ aria-label={t("designEditor.leftRail.label")}
2605
+ className="flex w-[52px] shrink-0 flex-col items-center border-r border-[var(--design-editor-panel-divider-color)] bg-[var(--design-editor-panel-bg)] py-3"
2606
+ >
2607
+ <div className="mb-3 flex h-8 items-center justify-center">
2608
+ {projectMenu}
2609
+ </div>
2610
+ <div className="mb-5 h-px w-8 bg-border/70" />
2611
+ <div className="flex min-h-0 flex-1 flex-col items-center gap-4">
2612
+ {items.map((item) => {
2613
+ const active = item.panel === activePanel;
2614
+ return (
2615
+ <Tooltip key={item.panel}>
2616
+ <TooltipTrigger asChild>
2617
+ <button
2618
+ type="button"
2619
+ aria-label={item.label}
2620
+ aria-current={active ? "page" : undefined}
2621
+ onClick={() => onPanelChange(item.panel)}
2622
+ className={cn(
2623
+ "group flex w-12 cursor-pointer flex-col items-center justify-start gap-1.5 rounded-none text-[10px] font-medium leading-none text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
2624
+ active && "text-foreground",
2625
+ )}
2626
+ >
2627
+ <span
2628
+ className={cn(
2629
+ "flex size-8 items-center justify-center rounded-lg transition-colors",
2630
+ active
2631
+ ? "bg-[var(--design-editor-selection-color)] text-[var(--design-editor-accent-color)]"
2632
+ : "text-muted-foreground group-hover:bg-[var(--design-editor-layer-hover-color)] group-hover:text-foreground",
2633
+ )}
2634
+ >
2635
+ {item.icon}
2636
+ </span>
2637
+ <span className="max-w-full truncate leading-none">
2638
+ {item.label}
2639
+ </span>
2640
+ </button>
2641
+ </TooltipTrigger>
2642
+ <TooltipContent side="right">{item.label}</TooltipContent>
2643
+ </Tooltip>
2644
+ );
2645
+ })}
2646
+ </div>
2647
+ </nav>
2648
+ );
2649
+ }
2650
+
2329
2651
  interface DesignCollaborator {
2330
2652
  user: CollabUser;
2331
2653
  image?: string;
@@ -2524,7 +2846,13 @@ function DesignToolbarTool({
2524
2846
  ? "bg-[var(--design-editor-accent-color)] text-white"
2525
2847
  : "hover:bg-white/10 hover:text-white",
2526
2848
  )}
2527
- onClick={onPrimary}
2849
+ onClick={(event) => {
2850
+ if (event.detail === 0) onPrimary();
2851
+ }}
2852
+ onPointerDown={(event) => {
2853
+ if (event.button !== 0) return;
2854
+ onPrimary();
2855
+ }}
2528
2856
  aria-label={label}
2529
2857
  aria-pressed={active}
2530
2858
  >
@@ -3141,6 +3469,10 @@ export default function DesignEditor() {
3141
3469
  const appStateVersion = useChangeVersion("app-state");
3142
3470
  const browserTabId = getBrowserTabId();
3143
3471
  const embedded = isEmbedAuthActive();
3472
+ const designChatScope = useMemo(
3473
+ () => (id ? ({ type: "design" as const, id } as const) : null),
3474
+ [id],
3475
+ );
3144
3476
 
3145
3477
  const isBuilderDesignEmbed = useMemo(() => {
3146
3478
  if (typeof window === "undefined") return false;
@@ -3185,6 +3517,8 @@ export default function DesignEditor() {
3185
3517
  const [contentRenderRevision, setContentRenderRevision] = useState(0);
3186
3518
  const [activeInspectorTab, setActiveInspectorTab] =
3187
3519
  useState<InspectorTab>("design");
3520
+ const [activeLeftPanel, setActiveLeftPanel] =
3521
+ useState<DesignLeftPanel>("file");
3188
3522
  const [leftSidebarWidth, setLeftSidebarWidth] = useState(256);
3189
3523
  const [rightSidebarWidth, setRightSidebarWidth] = useState(256);
3190
3524
  const [layersSearchQuery, setLayersSearchQuery] = useState("");
@@ -3195,7 +3529,79 @@ export default function DesignEditor() {
3195
3529
  const [overviewSelectedScreenIds, setOverviewSelectedScreenIds] = useState<
3196
3530
  string[]
3197
3531
  >([]);
3532
+ const [createdOverviewLayerSelection, setCreatedOverviewLayerSelection] =
3533
+ useState<{ screenId: string; layerId: string } | null>(null);
3198
3534
  const pendingOverviewScreenSelectionRef = useRef<string | null>(null);
3535
+ const pendingOverviewLayerSelectionRef = useRef<string | null>(null);
3536
+ // Tracks the nodeId of the most recently created TEXT primitive across one
3537
+ // handleCreatePrimitive → handlePrimitiveCreated round-trip. Cleared after
3538
+ // use. Lets handlePrimitiveCreated trigger begin-text-edit without needing
3539
+ // the primitive kind in its signature.
3540
+ const pendingTextEditNodeIdRef = useRef<string | null>(null);
3541
+ const pendingOverviewLayerSelectionClearTimerRef = useRef<number | null>(
3542
+ null,
3543
+ );
3544
+
3545
+ useEffect(() => {
3546
+ const focusAgentComposer = () => {
3547
+ requestAnimationFrame(() => {
3548
+ const panel = document.querySelector("[data-design-agent-panel]");
3549
+ const prosemirror = panel?.querySelector(
3550
+ ".ProseMirror",
3551
+ ) as HTMLElement | null;
3552
+ if (prosemirror) {
3553
+ prosemirror.focus();
3554
+ return;
3555
+ }
3556
+ const textarea = panel?.querySelector("textarea") as HTMLElement | null;
3557
+ textarea?.focus();
3558
+ });
3559
+ };
3560
+ const openAgentPanel = () => {
3561
+ setActiveLeftPanel("agent");
3562
+ focusAgentComposer();
3563
+ };
3564
+ const toggleAgentPanel = () =>
3565
+ setActiveLeftPanel((current) => {
3566
+ const next = current === "agent" ? "file" : "agent";
3567
+ if (next === "agent") focusAgentComposer();
3568
+ return next;
3569
+ });
3570
+ window.addEventListener("agent-panel:open", openAgentPanel);
3571
+ window.addEventListener("agent-panel:toggle", toggleAgentPanel);
3572
+ return () => {
3573
+ window.removeEventListener("agent-panel:open", openAgentPanel);
3574
+ window.removeEventListener("agent-panel:toggle", toggleAgentPanel);
3575
+ };
3576
+ }, []);
3577
+
3578
+ const clearPendingOverviewLayerSelectionTimer = useCallback(() => {
3579
+ if (pendingOverviewLayerSelectionClearTimerRef.current === null) return;
3580
+ window.clearTimeout(pendingOverviewLayerSelectionClearTimerRef.current);
3581
+ pendingOverviewLayerSelectionClearTimerRef.current = null;
3582
+ }, []);
3583
+ const schedulePendingOverviewLayerSelectionClear = useCallback(
3584
+ (layerId: string) => {
3585
+ clearPendingOverviewLayerSelectionTimer();
3586
+ pendingOverviewLayerSelectionClearTimerRef.current = window.setTimeout(
3587
+ () => {
3588
+ if (pendingOverviewLayerSelectionRef.current === layerId) {
3589
+ pendingOverviewLayerSelectionRef.current = null;
3590
+ }
3591
+ setCreatedOverviewLayerSelection((current) =>
3592
+ current?.layerId === layerId ? null : current,
3593
+ );
3594
+ pendingOverviewLayerSelectionClearTimerRef.current = null;
3595
+ },
3596
+ 1800,
3597
+ );
3598
+ },
3599
+ [clearPendingOverviewLayerSelectionTimer],
3600
+ );
3601
+ useEffect(
3602
+ () => clearPendingOverviewLayerSelectionTimer,
3603
+ [clearPendingOverviewLayerSelectionTimer],
3604
+ );
3199
3605
  const [lockedLayerIds, setLockedLayerIds] = useState<Set<string>>(
3200
3606
  () => new Set(),
3201
3607
  );
@@ -3220,10 +3626,63 @@ export default function DesignEditor() {
3220
3626
 
3221
3627
  // ── Motion dock state (§6.3) ────────────────────────────────────────────────
3222
3628
  // The MotionDock is mounted below the canvas and shown when motionDockOpen.
3223
- // Tracks and durationMs are local state; "Write to CSS" calls applyMotionEdit.
3629
+ // Tracks and durationMs are local state; edits autosave via applyMotionEdit.
3224
3630
  const [motionDockOpen, setMotionDockOpen] = useState(false);
3631
+ const [motionDockMounted, setMotionDockMounted] = useState(false);
3632
+ const motionDockUnmountTimerRef = useRef<number | null>(null);
3633
+ const [motionTimelineId, setMotionTimelineId] = useState<string | null>(null);
3225
3634
  const [motionTracks, setMotionTracks] = useState<MotionDockTrack[]>([]);
3226
3635
  const [motionDurationMs, setMotionDurationMs] = useState(1000);
3636
+ const [motionTracksDirty, setMotionTracksDirty] = useState(false);
3637
+ const [motionAutosaveRevision, setMotionAutosaveRevision] = useState(0);
3638
+ const [motionHydrationFingerprint, setMotionHydrationFingerprint] = useState<
3639
+ string | null
3640
+ >(null);
3641
+ const motionAutosaveRevisionRef = useRef(0);
3642
+ const motionAutosaveFailedRevisionRef = useRef<number | null>(null);
3643
+ const motionAutosaveTimerRef = useRef<number | null>(null);
3644
+ const lastScheduledMotionAutosaveRevisionRef = useRef(0);
3645
+ const previousMotionFileIdRef = useRef<string | null>(null);
3646
+ const clearMotionDockUnmountTimer = useCallback(() => {
3647
+ if (motionDockUnmountTimerRef.current === null) return;
3648
+ window.clearTimeout(motionDockUnmountTimerRef.current);
3649
+ motionDockUnmountTimerRef.current = null;
3650
+ }, []);
3651
+ const clearMotionAutosaveTimer = useCallback(() => {
3652
+ if (motionAutosaveTimerRef.current === null) return;
3653
+ window.clearTimeout(motionAutosaveTimerRef.current);
3654
+ motionAutosaveTimerRef.current = null;
3655
+ }, []);
3656
+ const setMotionDockOpenAnimated = useCallback(
3657
+ (open: boolean) => {
3658
+ clearMotionDockUnmountTimer();
3659
+ if (open) {
3660
+ setMotionDockMounted(true);
3661
+ if (typeof window === "undefined") {
3662
+ setMotionDockOpen(true);
3663
+ return;
3664
+ }
3665
+ window.requestAnimationFrame(() => setMotionDockOpen(true));
3666
+ return;
3667
+ }
3668
+
3669
+ setMotionDockOpen(false);
3670
+ if (typeof window === "undefined") {
3671
+ setMotionDockMounted(false);
3672
+ return;
3673
+ }
3674
+ motionDockUnmountTimerRef.current = window.setTimeout(() => {
3675
+ setMotionDockMounted(false);
3676
+ motionDockUnmountTimerRef.current = null;
3677
+ }, MOTION_DOCK_TRANSITION_MS);
3678
+ },
3679
+ [clearMotionDockUnmountTimer],
3680
+ );
3681
+ useEffect(
3682
+ () => () => clearMotionDockUnmountTimer(),
3683
+ [clearMotionDockUnmountTimer],
3684
+ );
3685
+ useEffect(() => () => clearMotionAutosaveTimer(), [clearMotionAutosaveTimer]);
3227
3686
  const [shaderFillPreview, setShaderFillPreview] = useState<{
3228
3687
  selector?: string;
3229
3688
  nodeId?: string;
@@ -3302,9 +3761,7 @@ export default function DesignEditor() {
3302
3761
  }, [isBuilderDesignEmbed]);
3303
3762
 
3304
3763
  const focusDesignInspectorForSelection = useCallback(() => {
3305
- setActiveInspectorTab((current) =>
3306
- current === "extensions" ? current : "design",
3307
- );
3764
+ setActiveInspectorTab("design");
3308
3765
  }, []);
3309
3766
 
3310
3767
  useEffect(() => {
@@ -3313,9 +3770,7 @@ export default function DesignEditor() {
3313
3770
 
3314
3771
  useEffect(() => {
3315
3772
  if (hasSelectedElement) return;
3316
- setActiveInspectorTab((current) =>
3317
- current === "extensions" ? current : "tweaks",
3318
- );
3773
+ setActiveInspectorTab("tweaks");
3319
3774
  }, [hasSelectedElement]);
3320
3775
 
3321
3776
  const startSidebarResize = useCallback(
@@ -3378,8 +3833,8 @@ export default function DesignEditor() {
3378
3833
  const suppressContentHistoryRef = useRef(false);
3379
3834
  const geometryUndoStackRef = useRef<GeometryHistoryEntry[]>([]);
3380
3835
  const geometryRedoStackRef = useRef<GeometryHistoryEntry[]>([]);
3381
- const historyOrderRef = useRef<Array<"content" | "geometry">>([]);
3382
- const redoOrderRef = useRef<Array<"content" | "geometry">>([]);
3836
+ const historyOrderRef = useRef<UndoRedoOrderKind[]>([]);
3837
+ const redoOrderRef = useRef<UndoRedoOrderKind[]>([]);
3383
3838
  const syncUndoRedoState = useCallback(() => {
3384
3839
  const undoManager = undoManagerRef.current;
3385
3840
  setCanUndo(
@@ -3393,6 +3848,23 @@ export default function DesignEditor() {
3393
3848
  geometryRedoStackRef.current.length > 0,
3394
3849
  );
3395
3850
  }, []);
3851
+ const recordContentHistoryEntry = useCallback(
3852
+ (entry: ContentHistoryEntry) => {
3853
+ if (entry.before === entry.after) return;
3854
+ contentUndoStackRef.current = [
3855
+ ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
3856
+ entry,
3857
+ ];
3858
+ contentRedoStackRef.current = [];
3859
+ historyOrderRef.current = [
3860
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
3861
+ "file-content",
3862
+ ];
3863
+ redoOrderRef.current = [];
3864
+ syncUndoRedoState();
3865
+ },
3866
+ [syncUndoRedoState],
3867
+ );
3396
3868
  const clearLocalUndoRedoStacks = useCallback(() => {
3397
3869
  contentUndoStackRef.current = [];
3398
3870
  contentRedoStackRef.current = [];
@@ -3408,6 +3880,18 @@ export default function DesignEditor() {
3408
3880
  // Dismissible localhost-source banner (reset per session).
3409
3881
  const [localSourceBannerDismissed, setLocalSourceBannerDismissed] =
3410
3882
  useState(false);
3883
+ // Localhost write-consent dialog state. When the agent wants to write a local
3884
+ // file and no valid grant exists for the active connection, we show the dialog
3885
+ // with a pending payload; the user clicks "Allow writes" to mint a grant.
3886
+ const [localhostWriteConsentOpen, setLocalhostWriteConsentOpen] =
3887
+ useState(false);
3888
+ const [localhostWriteConsentPayload, setLocalhostWriteConsentPayload] =
3889
+ useState<LocalhostWriteConsentPayload | null>(null);
3890
+ // Active localhost connection id for the consent dialog.
3891
+ const [localhostConsentConnectionId, setLocalhostConsentConnectionId] =
3892
+ useState<string>("");
3893
+ // Tracks whether an "Apply to source" write is in progress.
3894
+ const [applyToSourcePending, setApplyToSourcePending] = useState(false);
3411
3895
  // Shared visual-editor annotate overlays. drawMode owns the send toolbar,
3412
3896
  // while pinMode temporarily routes canvas clicks to comment pins that queue
3413
3897
  // into the same agent submission.
@@ -3819,6 +4303,8 @@ export default function DesignEditor() {
3819
4303
  const exportHtmlMutation = useActionMutation("export-html");
3820
4304
  const exportZipMutation = useActionMutation("export-zip");
3821
4305
  const applyMotionEditMutation = useActionMutation("apply-motion-edit");
4306
+ const applyMotionEdit = applyMotionEditMutation.mutate;
4307
+ const motionAutosavePending = applyMotionEditMutation.isPending;
3822
4308
  // §6.4 breakpoint mutations — wired to MultiScreenCanvas + affordance
3823
4309
  const addBreakpointMutation = useActionMutation("add-breakpoint");
3824
4310
  const setActiveBreakpointMutation = useActionMutation(
@@ -3832,12 +4318,28 @@ export default function DesignEditor() {
3832
4318
  "open-component-source",
3833
4319
  );
3834
4320
 
4321
+ // Board file migration — lazy, idempotent, triggers on design open when
4322
+ // designs.data.boardFileId is absent.
4323
+ const migrateBoardObjectsMutation = useActionMutation(
4324
+ "migrate-board-objects-to-file",
4325
+ );
4326
+
3835
4327
  // §6.6 — "Make it real" migration flow (migrate-inline-design-to-app).
3836
4328
  // The mutation stays unconditional; the dialog gates on isSignedIn.
3837
4329
  const migrateMutation = useActionMutation("migrate-inline-design-to-app");
3838
4330
 
3839
4331
  // Dialog open/close state for the "Make this a real app" flow.
3840
4332
  const [makeRealDialogOpen, setMakeRealDialogOpen] = useState(false);
4333
+ const [publishWaitlistPopoverOpen, setPublishWaitlistPopoverOpen] =
4334
+ useState(false);
4335
+ const [publishWaitlistPopoverView, setPublishWaitlistPopoverView] = useState<
4336
+ "actions" | "waitlist"
4337
+ >("actions");
4338
+ const [publishWaitlistJoined, setPublishWaitlistJoined] = useState(false);
4339
+ const [joiningPublishWaitlist, setJoiningPublishWaitlist] = useState(false);
4340
+ const [publishWaitlistError, setPublishWaitlistError] = useState<
4341
+ string | null
4342
+ >(null);
3841
4343
 
3842
4344
  // Result payload returned by migrate-inline-design-to-app on success.
3843
4345
  // `null` = not yet migrated; populated once the Builder agent accepts the job.
@@ -3878,6 +4380,16 @@ export default function DesignEditor() {
3878
4380
  const fileSaveTimersRef = useRef<Record<string, number>>({});
3879
4381
  const postAuthSaveRef = useRef<string | null>(null);
3880
4382
 
4383
+ const cancelQueuedFileContentSave = useCallback((fileId: string) => {
4384
+ const timer = fileSaveTimersRef.current[fileId];
4385
+ if (timer) {
4386
+ window.clearTimeout(timer);
4387
+ delete fileSaveTimersRef.current[fileId];
4388
+ }
4389
+ delete pendingFileSavesRef.current[fileId];
4390
+ delete latestFileSaveForUnloadRef.current[fileId];
4391
+ }, []);
4392
+
3881
4393
  const saveFileContent = useCallback(
3882
4394
  (pending: FileContentSaveRequest) => {
3883
4395
  if (!canEditDesignRef.current) return;
@@ -4258,6 +4770,39 @@ export default function DesignEditor() {
4258
4770
  () => getCanvasFrameGeometry(designDataJson),
4259
4771
  [designDataJson],
4260
4772
  );
4773
+
4774
+ // ── Board file ─────────────────────────────────────────────────────────────
4775
+ // The board is a reserved design_file (filename "__board__.html") whose id is
4776
+ // stored in designs.data.boardFileId. On design open, if boardFileId is absent,
4777
+ // we trigger migrate-board-objects-to-file (lazy + idempotent) which creates
4778
+ // the board file and migrates any legacy boardObjects.
4779
+ const boardFileId = useMemo(() => {
4780
+ const raw = (designDataJson as Record<string, unknown>).boardFileId;
4781
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
4782
+ }, [designDataJson]);
4783
+
4784
+ // Trigger migration on design open when boardFileId is absent.
4785
+ const migrateBoardTriggeredRef = useRef<string | null>(null);
4786
+ useEffect(() => {
4787
+ if (!id || !canEditDesign) return;
4788
+ if (boardFileId) return; // already migrated
4789
+ if (migrateBoardTriggeredRef.current === id) return;
4790
+ migrateBoardTriggeredRef.current = id;
4791
+ migrateBoardObjectsMutation.mutate({ designId: id } as any, {
4792
+ onSuccess: () => {
4793
+ void queryClient.invalidateQueries({
4794
+ queryKey: ["action", "get-design", { id }],
4795
+ });
4796
+ },
4797
+ });
4798
+ }, [
4799
+ boardFileId,
4800
+ canEditDesign,
4801
+ id,
4802
+ migrateBoardObjectsMutation,
4803
+ queryClient,
4804
+ ]);
4805
+
4261
4806
  const overviewScreens = useMemo(() => {
4262
4807
  const metadataByFileId = getDesignDataRecord(
4263
4808
  designDataJson,
@@ -4295,45 +4840,82 @@ export default function DesignEditor() {
4295
4840
  ? breakpointSet.breakpoints.map((bp) => bp.widthPx)
4296
4841
  : undefined;
4297
4842
 
4298
- return files.map((file) => {
4299
- const metadata = getDesignDataRecord(metadataByFileId, file.id);
4300
- const stringValue = (key: string) =>
4301
- typeof metadata[key] === "string"
4302
- ? (metadata[key] as string)
4303
- : undefined;
4304
- const numberValue = (key: string) =>
4305
- typeof metadata[key] === "number" && Number.isFinite(metadata[key])
4306
- ? (metadata[key] as number)
4307
- : undefined;
4308
- return {
4309
- id: file.id,
4310
- filename: file.filename,
4311
- content: file.content,
4312
- updatedAt: file.updatedAt,
4313
- sourceType: stringValue("sourceType"),
4314
- source: stringValue("source"),
4315
- sourceFile: stringValue("sourceFile"),
4316
- lod: stringValue("lod"),
4317
- previewState: stringValue("previewState"),
4318
- status: stringValue("status"),
4319
- title: stringValue("title"),
4320
- width: numberValue("width"),
4321
- height: numberValue("height"),
4322
- url: stringValue("url"),
4323
- previewUrl: stringValue("previewUrl"),
4324
- // Breakpoint preview widths (§6.4). When non-empty, MultiScreenCanvas
4325
- // renders one iframe per width to the right of the primary frame.
4326
- breakpointWidths: bpWidths,
4327
- // Active breakpoint width tracked in component state; shared across all
4328
- // screens (a design has one active breakpoint set at a time in v1).
4329
- activeBreakpointWidth: bpWidths?.includes(
4330
- activeBreakpointWidthState ?? -1,
4331
- )
4332
- ? activeBreakpointWidthState
4333
- : undefined,
4334
- };
4335
- });
4336
- }, [designDataJson, files, activeBreakpointWidthState]);
4843
+ // Exclude the board file it is rendered by its own DesignCanvas instance
4844
+ // in MultiScreenCanvas and must not appear as a screen frame.
4845
+ return files
4846
+ .filter((file) => !isBoardFile(file.filename))
4847
+ .map((file) => {
4848
+ const metadata = getDesignDataRecord(metadataByFileId, file.id);
4849
+ const stringValue = (key: string) =>
4850
+ typeof metadata[key] === "string"
4851
+ ? (metadata[key] as string)
4852
+ : undefined;
4853
+ const numberValue = (key: string) =>
4854
+ typeof metadata[key] === "number" && Number.isFinite(metadata[key])
4855
+ ? (metadata[key] as number)
4856
+ : undefined;
4857
+ return {
4858
+ id: file.id,
4859
+ filename: file.filename,
4860
+ content: file.content,
4861
+ updatedAt: file.updatedAt,
4862
+ sourceType: stringValue("sourceType"),
4863
+ source: stringValue("source"),
4864
+ sourceFile: stringValue("sourceFile"),
4865
+ connectionId: stringValue("connectionId"),
4866
+ lod: stringValue("lod"),
4867
+ previewState: stringValue("previewState"),
4868
+ status: stringValue("status"),
4869
+ title: stringValue("title"),
4870
+ width: numberValue("width"),
4871
+ height: numberValue("height"),
4872
+ url: stringValue("url"),
4873
+ previewUrl: stringValue("previewUrl"),
4874
+ // Breakpoint preview widths (§6.4). When non-empty, MultiScreenCanvas
4875
+ // renders one iframe per width to the right of the primary frame.
4876
+ breakpointWidths: bpWidths,
4877
+ // Active breakpoint width tracked in component state; shared across all
4878
+ // screens (a design has one active breakpoint set at a time in v1).
4879
+ activeBreakpointWidth: bpWidths?.includes(
4880
+ activeBreakpointWidthState ?? -1,
4881
+ )
4882
+ ? activeBreakpointWidthState
4883
+ : undefined,
4884
+ };
4885
+ });
4886
+ }, [designDataJson, files, activeBreakpointWidthState, boardFileId]);
4887
+
4888
+ // The board file's current HTML content — sourced from the files array (which
4889
+ // includes pending local writes). undefined when boardFileId is not yet set.
4890
+ const boardFileContent = useMemo(() => {
4891
+ if (!boardFileId) return undefined;
4892
+ const boardFile = files.find((file) => file.id === boardFileId);
4893
+ return typeof boardFile?.content === "string" ? boardFile.content : "";
4894
+ }, [boardFileId, files]);
4895
+
4896
+ // Logical canvas-space bounding box of the board iframe. The board covers the
4897
+ // full canvas surface, so we compute the union of all screen frame geometries.
4898
+ // x:0/y:0 since screen geometries use canvas-space coords that start at origin.
4899
+ const boardFrameGeometry = useMemo((): FrameGeometry | undefined => {
4900
+ if (!boardFileId) return undefined;
4901
+ const entries = Object.values(canvasFrameGeometryById);
4902
+ if (entries.length === 0) return { x: 0, y: 0, width: 1920, height: 1080 };
4903
+ let maxRight = 0;
4904
+ let maxBottom = 0;
4905
+ for (const geo of entries) {
4906
+ const right = (geo.x ?? 0) + (geo.width ?? 0);
4907
+ const bottom = (geo.y ?? 0) + (geo.height ?? 0);
4908
+ if (right > maxRight) maxRight = right;
4909
+ if (bottom > maxBottom) maxBottom = bottom;
4910
+ }
4911
+ return {
4912
+ x: 0,
4913
+ y: 0,
4914
+ width: Math.min(Math.max(maxRight, 1920), 16384),
4915
+ height: Math.min(Math.max(maxBottom, 1080), 16384),
4916
+ };
4917
+ }, [boardFileId, canvasFrameGeometryById]);
4918
+
4337
4919
  const queueFrameGeometrySave = useCallback(
4338
4920
  (geometryById: CanvasFrameGeometryById) => {
4339
4921
  if (!id || !canEditDesignRef.current) return;
@@ -4426,6 +5008,8 @@ export default function DesignEditor() {
4426
5008
  },
4427
5009
  ];
4428
5010
  geometryRedoStackRef.current = [];
5011
+ contentRedoStackRef.current = [];
5012
+ undoManagerRef.current?.clear(false, true);
4429
5013
  historyOrderRef.current = [
4430
5014
  ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
4431
5015
  "geometry",
@@ -4435,11 +5019,12 @@ export default function DesignEditor() {
4435
5019
  beforeSnapshot,
4436
5020
  afterSnapshot,
4437
5021
  );
4438
- if (resizedFrameIds.length > 0) {
4439
- writeFrameGeometrySnapshot(afterSnapshot, {
4440
- syncViewportFrameIds: resizedFrameIds,
4441
- });
4442
- }
5022
+ writeFrameGeometrySnapshot(
5023
+ afterSnapshot,
5024
+ resizedFrameIds.length > 0
5025
+ ? { syncViewportFrameIds: resizedFrameIds }
5026
+ : undefined,
5027
+ );
4443
5028
  syncUndoRedoState();
4444
5029
  },
4445
5030
  [syncUndoRedoState, writeFrameGeometrySnapshot],
@@ -4553,6 +5138,7 @@ export default function DesignEditor() {
4553
5138
  return;
4554
5139
  }
4555
5140
 
5141
+ const shouldSkipQuestions = pending.skipQuestions === true;
4556
5142
  const context = [
4557
5143
  sourceContext,
4558
5144
  `Design id: "${id}"`,
@@ -4563,18 +5149,26 @@ export default function DesignEditor() {
4563
5149
  : "",
4564
5150
  fileContext,
4565
5151
  "",
4566
- ...designIntakeQuestionDirectives(id, pendingDesignSystemId),
5152
+ ...(shouldSkipQuestions
5153
+ ? designGenerationDirectives(id, pendingDesignSystemId)
5154
+ : designIntakeQuestionDirectives(id, pendingDesignSystemId)),
4567
5155
  ].join("\n");
4568
5156
 
4569
5157
  clearGenerationCompleteTimer();
4570
5158
  setGenerationIssue(null);
4571
- const runTabId = agentSubmit(`Create design: ${prompt}`, context, {
4572
- model: pending.model,
4573
- engine: pending.engine,
4574
- effort: pending.effort,
4575
- newTab: true,
4576
- images,
4577
- });
5159
+ const runTabId = agentSubmit(
5160
+ shouldSkipQuestions
5161
+ ? `Generate design for "${design.title}": ${prompt}`
5162
+ : `Create design: ${prompt}`,
5163
+ context,
5164
+ {
5165
+ model: pending.model,
5166
+ engine: pending.engine,
5167
+ effort: pending.effort,
5168
+ newTab: true,
5169
+ images,
5170
+ },
5171
+ );
4578
5172
  setGenerationChatTabId(runTabId);
4579
5173
  patchPendingGeneration(id, {
4580
5174
  runTabId,
@@ -4609,6 +5203,25 @@ export default function DesignEditor() {
4609
5203
  }, [files, activeFileId]);
4610
5204
 
4611
5205
  const activeFile = files.find((f) => f.id === activeFileId) ?? files[0];
5206
+ const motionTimelineQueryParams =
5207
+ id && activeFile?.id
5208
+ ? { designId: id, sourceRef: activeFile.id }
5209
+ : { designId: "", sourceRef: "" };
5210
+ const { data: motionTimelineResult } =
5211
+ useActionQuery<MotionTimelineQueryResult>(
5212
+ "get-motion-timeline",
5213
+ motionTimelineQueryParams,
5214
+ {
5215
+ enabled: Boolean(id && activeFile?.id),
5216
+ refetchOnMount: "always",
5217
+ },
5218
+ );
5219
+ useEffect(() => {
5220
+ if (activeFile && !embedded) return;
5221
+ clearMotionDockUnmountTimer();
5222
+ setMotionDockOpen(false);
5223
+ setMotionDockMounted(false);
5224
+ }, [activeFile, clearMotionDockUnmountTimer, embedded]);
4612
5225
  useEffect(() => {
4613
5226
  if (!reviewFileId || reviewFileId === activeFile?.id) return;
4614
5227
  setReviewFileId(null);
@@ -4722,16 +5335,18 @@ export default function DesignEditor() {
4722
5335
  if (target && !targetFile) return false;
4723
5336
 
4724
5337
  const inspectorTab =
4725
- command.inspectorTab === "design" ||
4726
- command.inspectorTab === "tweaks" ||
4727
- command.inspectorTab === "extensions"
5338
+ command.inspectorTab === "design" || command.inspectorTab === "tweaks"
4728
5339
  ? command.inspectorTab
4729
- : command.inspector === "design" ||
4730
- command.inspector === "tweaks" ||
4731
- command.inspector === "extensions"
5340
+ : command.inspector === "design" || command.inspector === "tweaks"
4732
5341
  ? command.inspector
4733
5342
  : undefined;
4734
5343
  if (inspectorTab) setActiveInspectorTab(inspectorTab);
5344
+ const leftPanel =
5345
+ normalizeDesignLeftPanel(command.leftPanel) ??
5346
+ normalizeDesignLeftPanel(command.panel) ??
5347
+ normalizeDesignLeftPanel(command.inspectorTab) ??
5348
+ normalizeDesignLeftPanel(command.inspector);
5349
+ if (leftPanel) setActiveLeftPanel(leftPanel);
4735
5350
 
4736
5351
  const commandTool = normalizeDesignTool(command.tool);
4737
5352
  const effectiveCommandTool =
@@ -4888,21 +5503,59 @@ export default function DesignEditor() {
4888
5503
  const handleAddScreen = useCallback(() => {
4889
5504
  if (!id || !canEditDesign) return;
4890
5505
  const filename = nextBlankScreenFilename(files);
5506
+ const content = blankScreenHtml(prettyScreenName(filename));
4891
5507
  createFileMutation.mutate(
4892
5508
  {
4893
5509
  designId: id,
4894
5510
  filename,
4895
- content: blankScreenHtml(prettyScreenName(filename)),
5511
+ content,
4896
5512
  fileType: "html",
4897
5513
  } as any,
4898
5514
  {
4899
5515
  onSuccess: (result: any) => {
4900
5516
  const nextId = typeof result?.id === "string" ? result.id : null;
4901
- queryClient.invalidateQueries({
4902
- queryKey: ["action", "get-design"],
4903
- });
4904
5517
  if (nextId) {
5518
+ const now = new Date().toISOString();
5519
+ queryClient.setQueryData(
5520
+ ["action", "get-design", { id }],
5521
+ (old: any) => {
5522
+ if (
5523
+ !old ||
5524
+ typeof old !== "object" ||
5525
+ !Array.isArray(old.files)
5526
+ ) {
5527
+ return old;
5528
+ }
5529
+ const optimisticFile: DesignFile = {
5530
+ id: nextId,
5531
+ filename,
5532
+ fileType: "html",
5533
+ content,
5534
+ createdAt:
5535
+ typeof result?.createdAt === "string"
5536
+ ? result.createdAt
5537
+ : now,
5538
+ updatedAt:
5539
+ typeof result?.updatedAt === "string"
5540
+ ? result.updatedAt
5541
+ : now,
5542
+ };
5543
+ return {
5544
+ ...old,
5545
+ files: old.files.some(
5546
+ (file: DesignFile) => file.id === nextId,
5547
+ )
5548
+ ? old.files.map((file: DesignFile) =>
5549
+ file.id === nextId ? optimisticFile : file,
5550
+ )
5551
+ : [...old.files, optimisticFile],
5552
+ };
5553
+ },
5554
+ );
4905
5555
  pendingOverviewScreenSelectionRef.current = nextId;
5556
+ pendingOverviewLayerSelectionRef.current = null;
5557
+ clearPendingOverviewLayerSelectionTimer();
5558
+ setCreatedOverviewLayerSelection(null);
4906
5559
  setActiveFileId(nextId);
4907
5560
  setSelectedElement(null);
4908
5561
  setSelectedLayerIdsState([nextId]);
@@ -4912,6 +5565,9 @@ export default function DesignEditor() {
4912
5565
  viewModeRef.current = "overview";
4913
5566
  setViewMode("overview");
4914
5567
  }
5568
+ queryClient.invalidateQueries({
5569
+ queryKey: ["action", "get-design"],
5570
+ });
4915
5571
  },
4916
5572
  onError: (error) => {
4917
5573
  toast.error(
@@ -4922,12 +5578,21 @@ export default function DesignEditor() {
4922
5578
  },
4923
5579
  },
4924
5580
  );
4925
- }, [canEditDesign, createFileMutation, files, id, queryClient, t]);
5581
+ }, [
5582
+ canEditDesign,
5583
+ clearPendingOverviewLayerSelectionTimer,
5584
+ createFileMutation,
5585
+ files,
5586
+ id,
5587
+ queryClient,
5588
+ t,
5589
+ ]);
4926
5590
 
4927
5591
  const handleCreateScreenFrame = useCallback(
4928
5592
  (geometry: { x: number; y: number; width: number; height: number }) => {
4929
5593
  if (!id || !canEditDesign) return;
4930
5594
  const filename = nextBlankScreenFilename(files);
5595
+ const content = blankScreenHtml(prettyScreenName(filename));
4931
5596
  const nextGeometry = {
4932
5597
  x: Math.round(geometry.x),
4933
5598
  y: Math.round(geometry.y),
@@ -4938,17 +5603,54 @@ export default function DesignEditor() {
4938
5603
  {
4939
5604
  designId: id,
4940
5605
  filename,
4941
- content: blankScreenHtml(prettyScreenName(filename)),
5606
+ content,
4942
5607
  fileType: "html",
4943
5608
  } as any,
4944
5609
  {
4945
5610
  onSuccess: (result: any) => {
4946
5611
  const nextId = typeof result?.id === "string" ? result.id : null;
4947
- queryClient.invalidateQueries({
4948
- queryKey: ["action", "get-design"],
4949
- });
4950
5612
  if (nextId) {
5613
+ const now = new Date().toISOString();
5614
+ queryClient.setQueryData(
5615
+ ["action", "get-design", { id }],
5616
+ (old: any) => {
5617
+ if (
5618
+ !old ||
5619
+ typeof old !== "object" ||
5620
+ !Array.isArray(old.files)
5621
+ ) {
5622
+ return old;
5623
+ }
5624
+ const optimisticFile: DesignFile = {
5625
+ id: nextId,
5626
+ filename,
5627
+ fileType: "html",
5628
+ content,
5629
+ createdAt:
5630
+ typeof result?.createdAt === "string"
5631
+ ? result.createdAt
5632
+ : now,
5633
+ updatedAt:
5634
+ typeof result?.updatedAt === "string"
5635
+ ? result.updatedAt
5636
+ : now,
5637
+ };
5638
+ return {
5639
+ ...old,
5640
+ files: old.files.some(
5641
+ (file: DesignFile) => file.id === nextId,
5642
+ )
5643
+ ? old.files.map((file: DesignFile) =>
5644
+ file.id === nextId ? optimisticFile : file,
5645
+ )
5646
+ : [...old.files, optimisticFile],
5647
+ };
5648
+ },
5649
+ );
4951
5650
  pendingOverviewScreenSelectionRef.current = nextId;
5651
+ pendingOverviewLayerSelectionRef.current = null;
5652
+ clearPendingOverviewLayerSelectionTimer();
5653
+ setCreatedOverviewLayerSelection(null);
4952
5654
  setActiveFileId(nextId);
4953
5655
  setSelectedElement(null);
4954
5656
  setSelectedLayerIdsState([nextId]);
@@ -4962,6 +5664,9 @@ export default function DesignEditor() {
4962
5664
  [nextId]: nextGeometry,
4963
5665
  });
4964
5666
  }
5667
+ queryClient.invalidateQueries({
5668
+ queryKey: ["action", "get-design"],
5669
+ });
4965
5670
  },
4966
5671
  onError: (error) => {
4967
5672
  toast.error(
@@ -4976,6 +5681,7 @@ export default function DesignEditor() {
4976
5681
  [
4977
5682
  canEditDesign,
4978
5683
  canvasFrameGeometryById,
5684
+ clearPendingOverviewLayerSelectionTimer,
4979
5685
  createFileMutation,
4980
5686
  files,
4981
5687
  id,
@@ -5203,11 +5909,11 @@ export default function DesignEditor() {
5203
5909
  if (!isLocalEdit) {
5204
5910
  setSelectedElement((prev) => {
5205
5911
  if (!prev) return prev;
5206
- return elementInfoExistsInContent(next, prev) ? prev : null;
5912
+ return refreshElementInfoFromContent(next, prev);
5207
5913
  });
5208
5914
  setHoveredElement((prev) => {
5209
5915
  if (!prev) return prev;
5210
- return elementInfoExistsInContent(next, prev) ? prev : null;
5916
+ return refreshElementInfoFromContent(next, prev);
5211
5917
  });
5212
5918
  }
5213
5919
  };
@@ -5225,6 +5931,14 @@ export default function DesignEditor() {
5225
5931
  if (!ydoc || !isSynced) {
5226
5932
  undoManagerRef.current?.destroy();
5227
5933
  undoManagerRef.current = null;
5934
+ historyOrderRef.current = removeUndoRedoOrderKind(
5935
+ historyOrderRef.current,
5936
+ "content",
5937
+ );
5938
+ redoOrderRef.current = removeUndoRedoOrderKind(
5939
+ redoOrderRef.current,
5940
+ "content",
5941
+ );
5228
5942
  syncUndoRedoState();
5229
5943
  return;
5230
5944
  }
@@ -5258,7 +5972,15 @@ export default function DesignEditor() {
5258
5972
  um.off("stack-cleared", syncState);
5259
5973
  um.destroy();
5260
5974
  undoManagerRef.current = null;
5261
- syncUndoRedoState();
5975
+ historyOrderRef.current = removeUndoRedoOrderKind(
5976
+ historyOrderRef.current,
5977
+ "content",
5978
+ );
5979
+ redoOrderRef.current = removeUndoRedoOrderKind(
5980
+ redoOrderRef.current,
5981
+ "content",
5982
+ );
5983
+ syncUndoRedoState();
5262
5984
  };
5263
5985
  }, [ydoc, isSynced, syncUndoRedoState]);
5264
5986
 
@@ -5642,6 +6364,56 @@ export default function DesignEditor() {
5642
6364
  () => buildCodeLayerProjection(activeContent),
5643
6365
  [activeContent],
5644
6366
  );
6367
+ const activeMotionTimeline = motionTimelineResult?.timelines?.[0] ?? null;
6368
+ const activeMotionHydrationFingerprint = activeFile?.id
6369
+ ? motionTimelineFingerprint(activeFile.id, activeMotionTimeline)
6370
+ : null;
6371
+ const activeMotionProjectionFingerprint = activeMotionHydrationFingerprint
6372
+ ? activeMotionHydrationFingerprint
6373
+ : null;
6374
+
6375
+ useEffect(() => {
6376
+ const fileId = activeFile?.id ?? null;
6377
+ if (previousMotionFileIdRef.current === fileId) return;
6378
+ previousMotionFileIdRef.current = fileId;
6379
+ clearMotionAutosaveTimer();
6380
+ motionAutosaveRevisionRef.current = 0;
6381
+ motionAutosaveFailedRevisionRef.current = null;
6382
+ lastScheduledMotionAutosaveRevisionRef.current = 0;
6383
+ setMotionTimelineId(null);
6384
+ setMotionTracks([]);
6385
+ setMotionDurationMs(1000);
6386
+ setMotionTracksDirty(false);
6387
+ setMotionAutosaveRevision(0);
6388
+ setMotionHydrationFingerprint(null);
6389
+ }, [activeFile?.id, clearMotionAutosaveTimer]);
6390
+
6391
+ useEffect(() => {
6392
+ if (!activeFile?.id || !activeMotionProjectionFingerprint) return;
6393
+ if (motionTracksDirty) return;
6394
+ if (motionHydrationFingerprint === activeMotionProjectionFingerprint)
6395
+ return;
6396
+
6397
+ const hydratedTracks = activeMotionTimeline
6398
+ ? hydrateMotionDockTracks(
6399
+ activeMotionTimeline.tracks,
6400
+ activeCodeLayerProjection,
6401
+ )
6402
+ : [];
6403
+
6404
+ setMotionTimelineId(activeMotionTimeline?.id ?? null);
6405
+ setMotionTracks(hydratedTracks);
6406
+ setMotionDurationMs(activeMotionTimeline?.durationMs ?? 1000);
6407
+ setMotionHydrationFingerprint(activeMotionProjectionFingerprint);
6408
+ }, [
6409
+ activeCodeLayerProjection,
6410
+ activeFile?.id,
6411
+ activeMotionProjectionFingerprint,
6412
+ activeMotionTimeline,
6413
+ motionHydrationFingerprint,
6414
+ motionTracksDirty,
6415
+ ]);
6416
+
5645
6417
  const selectedCodeLayerNode = useMemo(() => {
5646
6418
  if (!selectedElement) return null;
5647
6419
  return resolveCodeLayerNodeFromElementInfo(
@@ -5789,11 +6561,37 @@ export default function DesignEditor() {
5789
6561
  return { nodeId, label };
5790
6562
  }, [selectedCodeLayerNode, selectedElement?.tagName]);
5791
6563
 
6564
+ const markMotionTracksDirty = useCallback(() => {
6565
+ setMotionTracksDirty(true);
6566
+ setMotionAutosaveRevision((revision) => {
6567
+ const next = revision + 1;
6568
+ motionAutosaveRevisionRef.current = next;
6569
+ motionAutosaveFailedRevisionRef.current = null;
6570
+ return next;
6571
+ });
6572
+ }, []);
6573
+
6574
+ const handleMotionTracksChange = useCallback(
6575
+ (tracks: MotionDockTrack[]) => {
6576
+ setMotionTracks(tracks);
6577
+ markMotionTracksDirty();
6578
+ },
6579
+ [markMotionTracksDirty],
6580
+ );
6581
+
6582
+ const handleMotionDurationChange = useCallback(
6583
+ (durationMs: number) => {
6584
+ setMotionDurationMs(durationMs);
6585
+ markMotionTracksDirty();
6586
+ },
6587
+ [markMotionTracksDirty],
6588
+ );
6589
+
5792
6590
  // Serialisable subset of the dock's tracks for the DesignCanvas motion-preview
5793
6591
  // bridge. Strips the UI-only `label` field. Only populated while the dock is
5794
6592
  // open so a closed dock never leaves preview overrides on the canvas; an empty
5795
6593
  // array makes DesignCanvas send `motion-preview-clear`. Scrubbing previews
5796
- // these tracks live in the iframe it never writes (that is "Write to CSS").
6594
+ // these tracks live in the iframe; autosave only runs for track/duration edits.
5797
6595
  const motionTracksWire = useMemo<MotionTrackWire[]>(() => {
5798
6596
  if (!motionDockOpen || motionTracks.length === 0) return [];
5799
6597
  return motionTracks.map(({ label: _label, ...track }) => track);
@@ -5934,6 +6732,8 @@ export default function DesignEditor() {
5934
6732
  refreshPreview?: boolean;
5935
6733
  skipPreview?: boolean;
5936
6734
  immediateSave?: boolean;
6735
+ persist?: boolean;
6736
+ updatedAt?: string;
5937
6737
  } = {},
5938
6738
  ) => {
5939
6739
  if (!activeFile || !canEditDesignRef.current) return;
@@ -5950,27 +6750,21 @@ export default function DesignEditor() {
5950
6750
  !yjsHistoryAvailable &&
5951
6751
  previousContent !== nextContent
5952
6752
  ) {
5953
- contentUndoStackRef.current = [
5954
- ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
5955
- {
5956
- fileId: activeFile.id,
5957
- before: previousContent,
5958
- after: nextContent,
5959
- },
5960
- ];
5961
- contentRedoStackRef.current = [];
5962
- historyOrderRef.current = [
5963
- ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
5964
- "content",
5965
- ];
5966
- redoOrderRef.current = [];
5967
- syncUndoRedoState();
6753
+ recordContentHistoryEntry({
6754
+ fileId: activeFile.id,
6755
+ before: previousContent,
6756
+ after: nextContent,
6757
+ });
6758
+ }
6759
+ if (options.updatedAt) {
6760
+ clearPendingLocalFileContent(activeFile.id);
6761
+ } else {
6762
+ markPendingLocalFileContent(
6763
+ activeFile.id,
6764
+ nextContent,
6765
+ activeFile.updatedAt,
6766
+ );
5968
6767
  }
5969
- markPendingLocalFileContent(
5970
- activeFile.id,
5971
- nextContent,
5972
- activeFile.updatedAt,
5973
- );
5974
6768
  setCollabContent(nextContent);
5975
6769
  setCollabContentFileId(activeFile.id);
5976
6770
  lastLocalContentRef.current = nextContent;
@@ -5991,7 +6785,13 @@ export default function DesignEditor() {
5991
6785
  // from a client-clock timestamp can, under clock skew, make a
5992
6786
  // later server-authored agent edit look "older" and get
5993
6787
  // dropped by the watermark gate (agent edit silently lost).
5994
- { ...file, content: nextContent }
6788
+ {
6789
+ ...file,
6790
+ content: nextContent,
6791
+ ...(options.updatedAt
6792
+ ? { updatedAt: options.updatedAt }
6793
+ : {}),
6794
+ }
5995
6795
  : file,
5996
6796
  ),
5997
6797
  };
@@ -6016,19 +6816,26 @@ export default function DesignEditor() {
6016
6816
  }, LOCAL_EDIT_ORIGIN);
6017
6817
  }
6018
6818
  }
6019
- queueFileContentSave(activeFile.id, nextContent, {
6020
- syncCollab: !(ydoc && isSynced),
6021
- immediate: options.immediateSave,
6022
- });
6819
+ if (options.persist === false) {
6820
+ cancelQueuedFileContentSave(activeFile.id);
6821
+ } else {
6822
+ queueFileContentSave(activeFile.id, nextContent, {
6823
+ syncCollab: !(ydoc && isSynced),
6824
+ immediate: options.immediateSave,
6825
+ });
6826
+ }
6023
6827
  },
6024
6828
  [
6025
6829
  activeFile,
6830
+ cancelQueuedFileContentSave,
6831
+ clearPendingLocalFileContent,
6026
6832
  id,
6027
6833
  isSynced,
6028
6834
  markPendingLocalFileContent,
6029
6835
  queryClient,
6030
6836
  queueFileContentSave,
6031
6837
  replacePreviewContent,
6838
+ recordContentHistoryEntry,
6032
6839
  syncUndoRedoState,
6033
6840
  ydoc,
6034
6841
  ],
@@ -6038,7 +6845,12 @@ export default function DesignEditor() {
6038
6845
  (
6039
6846
  fileId: string,
6040
6847
  nextContent: string,
6041
- options: { refreshPreview?: boolean; skipPreview?: boolean } = {},
6848
+ options: {
6849
+ refreshPreview?: boolean;
6850
+ skipPreview?: boolean;
6851
+ persist?: boolean;
6852
+ updatedAt?: string;
6853
+ } = {},
6042
6854
  ) => {
6043
6855
  if (!canEditDesignRef.current) return;
6044
6856
  if (fileId === activeFile?.id) {
@@ -6046,7 +6858,15 @@ export default function DesignEditor() {
6046
6858
  return;
6047
6859
  }
6048
6860
  const previousFile = files.find((file) => file.id === fileId);
6049
- markPendingLocalFileContent(fileId, nextContent, previousFile?.updatedAt);
6861
+ if (options.updatedAt) {
6862
+ clearPendingLocalFileContent(fileId);
6863
+ } else {
6864
+ markPendingLocalFileContent(
6865
+ fileId,
6866
+ nextContent,
6867
+ previousFile?.updatedAt,
6868
+ );
6869
+ }
6050
6870
  queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
6051
6871
  if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
6052
6872
  return old;
@@ -6054,19 +6874,33 @@ export default function DesignEditor() {
6054
6874
  return {
6055
6875
  ...old,
6056
6876
  files: old.files.map((file: DesignFile) =>
6057
- file.id === fileId ? { ...file, content: nextContent } : file,
6877
+ file.id === fileId
6878
+ ? {
6879
+ ...file,
6880
+ content: nextContent,
6881
+ ...(options.updatedAt
6882
+ ? { updatedAt: options.updatedAt }
6883
+ : {}),
6884
+ }
6885
+ : file,
6058
6886
  ),
6059
6887
  };
6060
6888
  });
6061
- saveFileContent({
6062
- id: fileId,
6063
- content: nextContent,
6064
- syncCollab: true,
6065
- });
6889
+ if (options.persist === false) {
6890
+ cancelQueuedFileContentSave(fileId);
6891
+ } else {
6892
+ saveFileContent({
6893
+ id: fileId,
6894
+ content: nextContent,
6895
+ syncCollab: true,
6896
+ });
6897
+ }
6066
6898
  },
6067
6899
  [
6068
6900
  activeFile?.id,
6069
6901
  applyLocalContentUpdate,
6902
+ cancelQueuedFileContentSave,
6903
+ clearPendingLocalFileContent,
6070
6904
  files,
6071
6905
  id,
6072
6906
  markPendingLocalFileContent,
@@ -6075,10 +6909,105 @@ export default function DesignEditor() {
6075
6909
  ],
6076
6910
  );
6077
6911
 
6912
+ useEffect(() => {
6913
+ if (!id || !activeFile?.id || !motionTracksDirty) return;
6914
+ if (motionTracks.length === 0) return;
6915
+ if (motionAutosavePending) return;
6916
+ if (motionAutosaveFailedRevisionRef.current === motionAutosaveRevision)
6917
+ return;
6918
+ if (
6919
+ lastScheduledMotionAutosaveRevisionRef.current ===
6920
+ motionAutosaveRevision &&
6921
+ motionAutosaveTimerRef.current !== null
6922
+ ) {
6923
+ return;
6924
+ }
6925
+
6926
+ const revisionAtSchedule = motionAutosaveRevision;
6927
+ lastScheduledMotionAutosaveRevisionRef.current = revisionAtSchedule;
6928
+ clearMotionAutosaveTimer();
6929
+ motionAutosaveTimerRef.current = window.setTimeout(() => {
6930
+ motionAutosaveTimerRef.current = null;
6931
+ if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
6932
+ applyMotionEdit(
6933
+ {
6934
+ designId: id,
6935
+ fileId: activeFile.id,
6936
+ timelineId: motionTimelineId ?? undefined,
6937
+ sourceRef: activeFile.id,
6938
+ tracks: motionTracks.map(({ label: _label, ...track }) => track),
6939
+ durationMs: motionDurationMs,
6940
+ currentContent: getFreshActiveFileContent({
6941
+ activeContent,
6942
+ latestContent: latestActiveContentRef.current,
6943
+ lastLocalContent: lastLocalContentRef.current,
6944
+ }),
6945
+ includeContent: true,
6946
+ },
6947
+ {
6948
+ onSuccess: (result) => {
6949
+ const response = result as {
6950
+ fileId?: unknown;
6951
+ timelineId?: unknown;
6952
+ patchedContent?: unknown;
6953
+ };
6954
+ if (typeof response.timelineId === "string") {
6955
+ setMotionTimelineId(response.timelineId);
6956
+ }
6957
+ if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
6958
+ setMotionTracksDirty(false);
6959
+ setMotionHydrationFingerprint(null);
6960
+ lastScheduledMotionAutosaveRevisionRef.current = 0;
6961
+ }
6962
+ if (
6963
+ typeof response.fileId === "string" &&
6964
+ typeof response.patchedContent === "string"
6965
+ ) {
6966
+ applyFileContentUpdate(response.fileId, response.patchedContent, {
6967
+ refreshPreview: response.fileId === activeFile.id,
6968
+ });
6969
+ }
6970
+ void queryClient.invalidateQueries({
6971
+ queryKey: ["action", "get-motion-timeline"],
6972
+ });
6973
+ },
6974
+ onError: (error) => {
6975
+ if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
6976
+ motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
6977
+ lastScheduledMotionAutosaveRevisionRef.current = 0;
6978
+ }
6979
+ toast.error(
6980
+ error instanceof Error
6981
+ ? error.message
6982
+ : // i18n-ignore: fallback toast for motion autosave failure.
6983
+ "Motion changes could not be saved.",
6984
+ );
6985
+ },
6986
+ },
6987
+ );
6988
+ }, MOTION_AUTOSAVE_DELAY_MS);
6989
+ }, [
6990
+ activeFile?.id,
6991
+ activeContent,
6992
+ applyFileContentUpdate,
6993
+ applyMotionEdit,
6994
+ clearMotionAutosaveTimer,
6995
+ id,
6996
+ motionAutosaveRevision,
6997
+ motionAutosavePending,
6998
+ motionDurationMs,
6999
+ motionTimelineId,
7000
+ motionTracks,
7001
+ motionTracksDirty,
7002
+ queryClient,
7003
+ ]);
7004
+
6078
7005
  const handleComponentPropApplied = useCallback(
6079
- (fileId: string, nextContent: string) => {
7006
+ (fileId: string, nextContent: string, updatedAt?: string) => {
6080
7007
  applyFileContentUpdate(fileId, nextContent, {
6081
7008
  refreshPreview: fileId === activeFile?.id,
7009
+ persist: false,
7010
+ updatedAt,
6082
7011
  });
6083
7012
  },
6084
7013
  [activeFile?.id, applyFileContentUpdate],
@@ -6098,6 +7027,7 @@ export default function DesignEditor() {
6098
7027
  ) {
6099
7028
  applyFileContentUpdate(result.fileId, result.patchedContent, {
6100
7029
  refreshPreview: result.fileId === activeFile?.id,
7030
+ persist: false,
6101
7031
  });
6102
7032
  }
6103
7033
  void handleRunDesignAudit();
@@ -6142,14 +7072,29 @@ export default function DesignEditor() {
6142
7072
  if (!canEditDesign) return false;
6143
7073
  const targetFile = files.find((file) => file.id === screenId);
6144
7074
  if (!targetFile) return false;
7075
+ const pendingContent = pendingLocalFileContentsRef.current.get(
7076
+ targetFile.id,
7077
+ )?.content;
7078
+ const storedContent = targetFile.content ?? "";
6145
7079
  const baseContent =
6146
- targetFile.id === activeFile?.id
6147
- ? ydoc && isSynced
6148
- ? ydoc.getText("content").toString()
6149
- : ((collabContentFileIdRef.current === activeFile.id
6150
- ? collabContentRef.current
6151
- : null) ?? activeContent)
6152
- : targetFile.content;
7080
+ pendingContent ??
7081
+ (targetFile.id === activeFile?.id
7082
+ ? (() => {
7083
+ const liveContent =
7084
+ ydoc && isSynced
7085
+ ? ydoc.getText("content").toString()
7086
+ : ((collabContentFileIdRef.current === activeFile.id
7087
+ ? collabContentRef.current
7088
+ : null) ?? activeContent);
7089
+ return shouldUseLiveFileContent({
7090
+ liveContent,
7091
+ storedContent,
7092
+ fileType: targetFile.fileType,
7093
+ })
7094
+ ? liveContent
7095
+ : storedContent;
7096
+ })()
7097
+ : storedContent);
6153
7098
  const nextContent = appendCanvasPrimitiveToHtml(baseContent, primitive);
6154
7099
  if (!nextContent) {
6155
7100
  toast.error(t("designEditor.toasts.primitiveInsertFailed"));
@@ -6166,6 +7111,11 @@ export default function DesignEditor() {
6166
7111
  if (targetFile.id === activeFile?.id) {
6167
7112
  applyLocalContentUpdate(nextContent, { immediateSave: true });
6168
7113
  } else {
7114
+ recordContentHistoryEntry({
7115
+ fileId: targetFile.id,
7116
+ before: baseContent,
7117
+ after: nextContent,
7118
+ });
6169
7119
  queryClient.setQueryData(
6170
7120
  ["action", "get-design", { id }],
6171
7121
  (old: any) => {
@@ -6189,7 +7139,21 @@ export default function DesignEditor() {
6189
7139
  });
6190
7140
  }
6191
7141
 
6192
- return projectedNodeId ?? primitive.nodeId ?? true;
7142
+ const result = projectedNodeId ?? primitive.nodeId ?? true;
7143
+
7144
+ // Record the nodeId when a TEXT primitive is created so the next
7145
+ // handlePrimitiveCreated (or handleBoardDrawPrimitive) can immediately
7146
+ // enter text-edit mode — fixing the "click to add text should let me
7147
+ // type immediately" bug. The ref is read once and cleared.
7148
+ if (primitive.kind === "text") {
7149
+ const textNodeId =
7150
+ typeof result === "string" ? result : (primitive.nodeId ?? null);
7151
+ pendingTextEditNodeIdRef.current = textNodeId;
7152
+ } else {
7153
+ pendingTextEditNodeIdRef.current = null;
7154
+ }
7155
+
7156
+ return result;
6193
7157
  },
6194
7158
  [
6195
7159
  activeContent,
@@ -6200,6 +7164,7 @@ export default function DesignEditor() {
6200
7164
  id,
6201
7165
  isSynced,
6202
7166
  queryClient,
7167
+ recordContentHistoryEntry,
6203
7168
  saveFileContent,
6204
7169
  t,
6205
7170
  ydoc,
@@ -6213,25 +7178,112 @@ export default function DesignEditor() {
6213
7178
  // new primitive selected, matching Figma behaviour. We activate the
6214
7179
  // target screen (so the layers panel shows its content) and select the
6215
7180
  // new node, but do NOT switch to single/full view.
6216
- pendingOverviewScreenSelectionRef.current = null;
6217
- setActiveFileId(screenId);
6218
- setSelectedElement(null);
6219
- setHoveredElement(null);
6220
- setSelectedLayerIdsState([nodeId]);
6221
- setOverviewSelectedScreenIds([]);
6222
- setActiveTool("move");
6223
- setMode("edit");
7181
+ pendingOverviewScreenSelectionRef.current = screenId;
7182
+ pendingOverviewLayerSelectionRef.current = nodeId;
7183
+ clearPendingOverviewLayerSelectionTimer();
7184
+ flushSync(() => {
7185
+ setCreatedOverviewLayerSelection({ screenId, layerId: nodeId });
7186
+ setActiveFileId(screenId);
7187
+ setSelectedElement(null);
7188
+ setHoveredElement(null);
7189
+ setSelectedLayerIdsState([nodeId]);
7190
+ setOverviewSelectedScreenIds([screenId]);
7191
+ setActiveTool("move");
7192
+ setMode("edit");
7193
+ });
6224
7194
  // viewMode stays at "overview" — no setViewMode("single") call here.
7195
+
7196
+ // Immediately enter text-editing for newly created TEXT primitives so the
7197
+ // user can start typing without a second click (fixes auto-size flow too).
7198
+ // We read-and-clear the ref set by handleCreatePrimitive just above. The
7199
+ // 50 ms delay gives React a tick to commit the new content into the active
7200
+ // screen's iframe before the bridge tries to find the element by nodeId.
7201
+ const textNodeId = pendingTextEditNodeIdRef.current;
7202
+ pendingTextEditNodeIdRef.current = null;
7203
+ if (textNodeId) {
7204
+ setTimeout(() => {
7205
+ (window as any).__designCanvasBeginTextEdit?.(textNodeId);
7206
+ }, 50);
7207
+ }
6225
7208
  },
6226
- [],
7209
+ [clearPendingOverviewLayerSelectionTimer],
6227
7210
  );
6228
7211
 
6229
- const handleOverviewScreenSelectionChange = useCallback((ids: string[]) => {
6230
- const pendingId = pendingOverviewScreenSelectionRef.current;
6231
- if (pendingId && ids.length === 0) return;
6232
- if (pendingId) pendingOverviewScreenSelectionRef.current = null;
6233
- setOverviewSelectedScreenIds(ids);
6234
- }, []);
7212
+ /**
7213
+ * Called by MultiScreenCanvas when a draft primitive is committed in empty
7214
+ * canvas space (outside all screen frames). Appends the primitive to the
7215
+ * board file content via the shared handleCreatePrimitive path so the bridge
7216
+ * engine handles persistence identically to in-screen elements.
7217
+ */
7218
+ const handleBoardDrawPrimitive = useCallback(
7219
+ (primitive: CanvasPrimitiveInsert) => {
7220
+ if (!boardFileId || !canEditDesign) return false;
7221
+ const result = handleCreatePrimitive(boardFileId, primitive);
7222
+ if (!result) return false;
7223
+
7224
+ // For TEXT primitives drawn on the board surface, immediately enter
7225
+ // text-editing mode via begin-text-edit. The board DesignCanvas uses
7226
+ // registerRuntimeBridge=false so window.__designCanvasBeginTextEdit is
7227
+ // not set; instead we broadcast the message to all preview iframes — the
7228
+ // one that contains the new nodeId will handle it, others ignore it.
7229
+ // The 50 ms delay lets the board iframe receive its content update first.
7230
+ if (primitive.kind === "text") {
7231
+ const textNodeId =
7232
+ pendingTextEditNodeIdRef.current ?? primitive.nodeId ?? null;
7233
+ pendingTextEditNodeIdRef.current = null;
7234
+ if (textNodeId) {
7235
+ setTimeout(() => {
7236
+ document
7237
+ .querySelectorAll<HTMLIFrameElement>(
7238
+ "iframe[data-design-preview-iframe]",
7239
+ )
7240
+ .forEach((iframe) => {
7241
+ iframe.contentWindow?.postMessage(
7242
+ { type: "begin-text-edit", nodeId: textNodeId },
7243
+ "*",
7244
+ );
7245
+ });
7246
+ }, 50);
7247
+ }
7248
+ }
7249
+
7250
+ return result;
7251
+ },
7252
+ [boardFileId, canEditDesign, handleCreatePrimitive],
7253
+ );
7254
+
7255
+ const handleOverviewScreenSelectionChange = useCallback(
7256
+ (ids: string[]) => {
7257
+ const pendingId = pendingOverviewScreenSelectionRef.current;
7258
+ const fileIds = new Set(files.map((file) => file.id));
7259
+ if (pendingId && ids.length === 0) return;
7260
+ if (pendingId && ids.includes(pendingId)) {
7261
+ setOverviewSelectedScreenIds(
7262
+ ids.filter(
7263
+ (layerId) => fileIds.has(layerId) || layerId === pendingId,
7264
+ ),
7265
+ );
7266
+ if (fileIds.has(pendingId)) {
7267
+ pendingOverviewScreenSelectionRef.current = null;
7268
+ }
7269
+ return;
7270
+ }
7271
+ if (pendingId) {
7272
+ pendingOverviewScreenSelectionRef.current = null;
7273
+ pendingOverviewLayerSelectionRef.current = null;
7274
+ clearPendingOverviewLayerSelectionTimer();
7275
+ setCreatedOverviewLayerSelection(null);
7276
+ }
7277
+ setOverviewSelectedScreenIds(
7278
+ ids.filter((layerId) => fileIds.has(layerId)),
7279
+ );
7280
+ },
7281
+ [clearPendingOverviewLayerSelectionTimer, files],
7282
+ );
7283
+
7284
+ const shouldPreserveBlockedOverviewLayerSelectionRef = useRef<
7285
+ (screenId: string) => boolean
7286
+ >(() => false);
6235
7287
 
6236
7288
  const handleMoveTool = useCallback(() => {
6237
7289
  if (!canEditDesign) return;
@@ -6243,36 +7295,42 @@ export default function DesignEditor() {
6243
7295
 
6244
7296
  const handleFrameTool = useCallback(() => {
6245
7297
  if (!canEditDesign) return;
6246
- setActiveTool("frame");
6247
- setMode("edit");
6248
- setDrawMode(false);
6249
- setPinMode(false);
6250
- setSelectedElement(null);
6251
- viewModeRef.current = "overview";
6252
- setViewMode("overview");
7298
+ flushSync(() => {
7299
+ setActiveTool("frame");
7300
+ setMode("edit");
7301
+ setDrawMode(false);
7302
+ setPinMode(false);
7303
+ setSelectedElement(null);
7304
+ viewModeRef.current = "overview";
7305
+ setViewMode("overview");
7306
+ });
6253
7307
  }, [canEditDesign]);
6254
7308
 
6255
7309
  const handleTextTool = useCallback(() => {
6256
7310
  if (!canEditDesign) return;
6257
- setActiveTool("text");
6258
- viewModeRef.current = "overview";
6259
- setViewMode("overview");
6260
- setMode("edit");
6261
- setDrawMode(false);
6262
- setPinMode(false);
6263
- setSelectedElement(null);
6264
- }, [canEditDesign]);
6265
-
6266
- const handleShapeTool = useCallback(
6267
- (tool: ShapeTool) => {
6268
- if (!canEditDesign) return;
6269
- setActiveTool(tool);
7311
+ flushSync(() => {
7312
+ setActiveTool("text");
6270
7313
  viewModeRef.current = "overview";
6271
7314
  setViewMode("overview");
6272
7315
  setMode("edit");
6273
7316
  setDrawMode(false);
6274
7317
  setPinMode(false);
6275
7318
  setSelectedElement(null);
7319
+ });
7320
+ }, [canEditDesign]);
7321
+
7322
+ const handleShapeTool = useCallback(
7323
+ (tool: ShapeTool) => {
7324
+ if (!canEditDesign) return;
7325
+ flushSync(() => {
7326
+ setActiveTool(tool);
7327
+ viewModeRef.current = "overview";
7328
+ setViewMode("overview");
7329
+ setMode("edit");
7330
+ setDrawMode(false);
7331
+ setPinMode(false);
7332
+ setSelectedElement(null);
7333
+ });
6276
7334
  },
6277
7335
  [canEditDesign],
6278
7336
  );
@@ -6283,13 +7341,15 @@ export default function DesignEditor() {
6283
7341
 
6284
7342
  const handlePenTool = useCallback(() => {
6285
7343
  if (!canEditDesign) return;
6286
- setActiveTool("pen");
6287
- viewModeRef.current = "overview";
6288
- setViewMode("overview");
6289
- setMode("edit");
6290
- setDrawMode(false);
6291
- setPinMode(false);
6292
- setSelectedElement(null);
7344
+ flushSync(() => {
7345
+ setActiveTool("pen");
7346
+ viewModeRef.current = "overview";
7347
+ setViewMode("overview");
7348
+ setMode("edit");
7349
+ setDrawMode(false);
7350
+ setPinMode(false);
7351
+ setSelectedElement(null);
7352
+ });
6293
7353
  }, [canEditDesign]);
6294
7354
 
6295
7355
  const handleHandTool = useCallback(() => {
@@ -6456,6 +7516,7 @@ export default function DesignEditor() {
6456
7516
  mode,
6457
7517
  activeTool,
6458
7518
  inspectorTab: activeInspectorTab,
7519
+ leftPanel: activeLeftPanel,
6459
7520
  // §8 DesignNavigationState additions — dock + breakpoint context
6460
7521
  dock: { kind: "motion" as const, open: motionDockOpen },
6461
7522
  motion: {
@@ -6523,6 +7584,7 @@ export default function DesignEditor() {
6523
7584
  mode: selection.mode,
6524
7585
  activeTool: selection.activeTool,
6525
7586
  inspectorTab: selection.inspectorTab,
7587
+ leftPanel: selection.leftPanel,
6526
7588
  dock: selection.dock,
6527
7589
  motion: selection.motion,
6528
7590
  breakpoint: selection.breakpoint,
@@ -6563,6 +7625,7 @@ export default function DesignEditor() {
6563
7625
  mode,
6564
7626
  activeTool,
6565
7627
  activeInspectorTab,
7628
+ activeLeftPanel,
6566
7629
  overviewSelectedScreenIds,
6567
7630
  viewMode,
6568
7631
  zoom,
@@ -6617,6 +7680,8 @@ export default function DesignEditor() {
6617
7680
  designTitle: design?.title ?? null,
6618
7681
  activeFileId: activeFile?.id ?? null,
6619
7682
  activeFilename: activeFile?.filename ?? null,
7683
+ activeFileUpdatedAt: activeFile?.updatedAt ?? null,
7684
+ activeContent,
6620
7685
  viewMode,
6621
7686
  zoom,
6622
7687
  screens: files.map((file) => ({
@@ -6638,12 +7703,22 @@ export default function DesignEditor() {
6638
7703
  });
6639
7704
  },
6640
7705
  onShaderFillPreviewClear: () => setShaderFillPreview(null),
7706
+ onShaderFillApplied: (fileId, content, updatedAt) => {
7707
+ applyFileContentUpdate(fileId, content, {
7708
+ refreshPreview: fileId === activeFile?.id,
7709
+ persist: false,
7710
+ updatedAt,
7711
+ });
7712
+ },
6641
7713
  }),
6642
7714
  [
7715
+ activeContent,
6643
7716
  activeFile?.filename,
6644
7717
  activeFile?.fileType,
6645
7718
  activeFile?.id,
7719
+ activeFile?.updatedAt,
6646
7720
  activeTool,
7721
+ applyFileContentUpdate,
6647
7722
  design?.title,
6648
7723
  files,
6649
7724
  id,
@@ -6660,6 +7735,23 @@ export default function DesignEditor() {
6660
7735
 
6661
7736
  const handleScreenElementSelect = useCallback(
6662
7737
  (screenId: string, info: ElementInfo) => {
7738
+ const pendingLayerId = pendingOverviewLayerSelectionRef.current;
7739
+ const pendingScreenId = pendingOverviewScreenSelectionRef.current;
7740
+ const createdSelection = createdOverviewLayerSelection;
7741
+ if (createdSelection?.screenId === screenId) {
7742
+ return;
7743
+ }
7744
+ if (
7745
+ (pendingLayerId || createdSelection?.layerId) &&
7746
+ (!pendingScreenId || pendingScreenId === screenId) &&
7747
+ isScreenRootElementInfo(info)
7748
+ ) {
7749
+ return;
7750
+ }
7751
+ pendingOverviewScreenSelectionRef.current = null;
7752
+ pendingOverviewLayerSelectionRef.current = null;
7753
+ clearPendingOverviewLayerSelectionTimer();
7754
+ setCreatedOverviewLayerSelection(null);
6663
7755
  const projection = getCodeLayerProjectionForScreen(screenId);
6664
7756
  const canonical = projection
6665
7757
  ? canonicalizeElementInfoFromProjection(projection, info)
@@ -6667,6 +7759,14 @@ export default function DesignEditor() {
6667
7759
  const node = projection
6668
7760
  ? resolveCodeLayerNodeFromElementInfo(projection, canonical)
6669
7761
  : null;
7762
+ if (
7763
+ shouldPreserveBlockedOverviewLayerSelectionRef.current(screenId) &&
7764
+ (isScreenRootElementInfo(canonical) ||
7765
+ !node ||
7766
+ selectedLayerIdsState.includes(node.id))
7767
+ ) {
7768
+ return;
7769
+ }
6670
7770
  setActiveFileId(screenId);
6671
7771
  setSelectedElement(canonical);
6672
7772
  setHoveredElement(null);
@@ -6679,21 +7779,49 @@ export default function DesignEditor() {
6679
7779
  setMode("edit");
6680
7780
  focusDesignInspectorForSelection();
6681
7781
  },
6682
- [focusDesignInspectorForSelection, getCodeLayerProjectionForScreen],
7782
+ [
7783
+ clearPendingOverviewLayerSelectionTimer,
7784
+ createdOverviewLayerSelection,
7785
+ focusDesignInspectorForSelection,
7786
+ getCodeLayerProjectionForScreen,
7787
+ selectedLayerIdsState,
7788
+ ],
6683
7789
  );
6684
7790
 
6685
- const handleScreenElementClear = useCallback((screenId: string) => {
6686
- setActiveFileId(screenId);
6687
- setSelectedElement(null);
6688
- setHoveredElement(null);
6689
- setHoveredElementScreenId(null);
6690
- setSelectedLayerIdsState([]);
6691
- if (viewModeRef.current === "overview") {
6692
- setOverviewSelectedScreenIds([]);
6693
- }
6694
- setActiveTool("move");
6695
- setMode("edit");
6696
- }, []);
7791
+ const handleScreenElementClear = useCallback(
7792
+ (screenId: string) => {
7793
+ const pendingLayerId = pendingOverviewLayerSelectionRef.current;
7794
+ const pendingScreenId = pendingOverviewScreenSelectionRef.current;
7795
+ const createdSelection = createdOverviewLayerSelection;
7796
+ if (createdSelection?.screenId === screenId) {
7797
+ return;
7798
+ }
7799
+ if (
7800
+ (pendingLayerId || createdSelection?.layerId) &&
7801
+ (!pendingScreenId || pendingScreenId === screenId)
7802
+ ) {
7803
+ return;
7804
+ }
7805
+ if (shouldPreserveBlockedOverviewLayerSelectionRef.current(screenId)) {
7806
+ return;
7807
+ }
7808
+ pendingOverviewScreenSelectionRef.current = null;
7809
+ pendingOverviewLayerSelectionRef.current = null;
7810
+ clearPendingOverviewLayerSelectionTimer();
7811
+ setCreatedOverviewLayerSelection(null);
7812
+ setActiveFileId(screenId);
7813
+ setSelectedElement(null);
7814
+ setHoveredElement(null);
7815
+ setHoveredElementScreenId(null);
7816
+ setSelectedLayerIdsState([]);
7817
+ if (viewModeRef.current === "overview") {
7818
+ setOverviewSelectedScreenIds([]);
7819
+ }
7820
+ setActiveTool("move");
7821
+ setMode("edit");
7822
+ },
7823
+ [clearPendingOverviewLayerSelectionTimer, createdOverviewLayerSelection],
7824
+ );
6697
7825
 
6698
7826
  const handleElementSelect = useCallback(
6699
7827
  (info: ElementInfo) => {
@@ -6721,6 +7849,10 @@ export default function DesignEditor() {
6721
7849
 
6722
7850
  const handleScreenElementDblClickText = useCallback(
6723
7851
  (screenId: string, info: ElementInfo) => {
7852
+ pendingOverviewScreenSelectionRef.current = null;
7853
+ pendingOverviewLayerSelectionRef.current = null;
7854
+ clearPendingOverviewLayerSelectionTimer();
7855
+ setCreatedOverviewLayerSelection(null);
6724
7856
  const projection = getCodeLayerProjectionForScreen(screenId);
6725
7857
  const canonical = projection
6726
7858
  ? canonicalizeElementInfoFromProjection(projection, info)
@@ -6739,7 +7871,12 @@ export default function DesignEditor() {
6739
7871
  setMode("edit");
6740
7872
  focusDesignInspectorForSelection();
6741
7873
  },
6742
- [focusDesignInspectorForSelection, getCodeLayerProjectionForScreen],
7874
+ [
7875
+ clearPendingOverviewLayerSelectionTimer,
7876
+ createdOverviewLayerSelection,
7877
+ focusDesignInspectorForSelection,
7878
+ getCodeLayerProjectionForScreen,
7879
+ ],
6743
7880
  );
6744
7881
 
6745
7882
  const handleElementDblClickText = useCallback(
@@ -6944,7 +8081,9 @@ export default function DesignEditor() {
6944
8081
  createdAt: Date.now(),
6945
8082
  });
6946
8083
  const sendStyleChange = (window as any).__designCanvasSendStyle;
6947
- if (!options.runtimeApplied && typeof sendStyleChange === "function") {
8084
+ const runtimeStyleApplied =
8085
+ !options.runtimeApplied && typeof sendStyleChange === "function";
8086
+ if (runtimeStyleApplied) {
6948
8087
  const selectorCandidates = targetNode
6949
8088
  ? codeLayerSelectorAliases(targetNode)
6950
8089
  : selector
@@ -7095,6 +8234,12 @@ export default function DesignEditor() {
7095
8234
  queueFileContentSave(activeFile.id, resolvedNextContent, {
7096
8235
  syncCollab: !(ydoc && isSynced),
7097
8236
  });
8237
+ if (
8238
+ !runtimeStyleApplied &&
8239
+ !replacePreviewContent(resolvedNextContent, selector)
8240
+ ) {
8241
+ setContentRenderRevision((revision) => revision + 1);
8242
+ }
7098
8243
  if (resolvedNode) setSelectedLayerIdsState([resolvedNode.id]);
7099
8244
  setSelectedElement((prev) => {
7100
8245
  if (options.elementInfo) return options.elementInfo;
@@ -7122,6 +8267,7 @@ export default function DesignEditor() {
7122
8267
  activeBreakpointWidthState,
7123
8268
  canEditDesign,
7124
8269
  queueFileContentSave,
8270
+ replacePreviewContent,
7125
8271
  selectedElement,
7126
8272
  t,
7127
8273
  ydoc,
@@ -7585,6 +8731,28 @@ export default function DesignEditor() {
7585
8731
  return false;
7586
8732
  }
7587
8733
  applyFileContentUpdate(screenId, patch.content, { skipPreview: true });
8734
+ const movedNode =
8735
+ (patch.result.after?.nodeId
8736
+ ? patch.projection.nodes.find(
8737
+ (node) => node.id === patch.result.after?.nodeId,
8738
+ )
8739
+ : null) ??
8740
+ resolveCodeLayerNodeFromBridge(
8741
+ patch.projection,
8742
+ selector,
8743
+ details?.sourceId ??
8744
+ elementInfo?.sourceId ??
8745
+ (targetNode
8746
+ ? bridgeSourceIdForCodeLayerNode(targetNode)
8747
+ : undefined),
8748
+ );
8749
+ if (movedNode) {
8750
+ setActiveFileId(screenId);
8751
+ setSelectedLayerIdsState([movedNode.id]);
8752
+ setSelectedElement(elementInfoFromCodeLayerNode(movedNode));
8753
+ } else {
8754
+ setSelectedElement(null);
8755
+ }
7588
8756
  return true;
7589
8757
  },
7590
8758
  [
@@ -8319,12 +9487,14 @@ export default function DesignEditor() {
8319
9487
  *
8320
9488
  * The bridge in the source screen's iframe posts phase:"end" with the
8321
9489
  * selector / sourceNodeId of the dragged element. MultiScreenCanvas maps
8322
- * the board point to a target screen and calls this handler. We resolve
8323
- * both screens' content, identify the node by its data-agent-native-node-id
8324
- * (falling back to a projection lookup by selector when only the selector is
8325
- * available), call moveNodeBetweenDocuments to move it into the target
8326
- * screen's <body>, persist both files, switch the active screen to the
8327
- * target, and select the moved node keeping viewMode "overview" throughout.
9490
+ * the board point to a target screen, optionally runs a hit-test in the
9491
+ * target iframe to resolve an anchorNodeId and placement, then calls this
9492
+ * handler. We resolve both screens' content, identify the node by its
9493
+ * data-agent-native-node-id (falling back to a projection lookup by selector
9494
+ * when only the selector is available), call moveNodeBetweenDocuments with
9495
+ * the anchor from the hit-test (or top-level "inside" fallback), persist
9496
+ * both files, switch the active screen to the target, and select the moved
9497
+ * node — keeping viewMode "overview" throughout.
8328
9498
  */
8329
9499
  const handleCrossScreenElementDrop = useCallback(
8330
9500
  ({
@@ -8332,11 +9502,15 @@ export default function DesignEditor() {
8332
9502
  sourceNodeId,
8333
9503
  sourceScreenId,
8334
9504
  targetScreenId,
9505
+ targetAnchorNodeId,
9506
+ targetAnchorPlacement,
8335
9507
  }: {
8336
9508
  sourceSelector: string;
8337
9509
  sourceNodeId?: string;
8338
9510
  sourceScreenId: string;
8339
9511
  targetScreenId: string;
9512
+ targetAnchorNodeId?: string;
9513
+ targetAnchorPlacement?: "before" | "after" | "inside";
8340
9514
  }) => {
8341
9515
  if (!canEditDesign) return;
8342
9516
  if (sourceScreenId === targetScreenId) return;
@@ -8366,9 +9540,16 @@ export default function DesignEditor() {
8366
9540
  sourceNodeId ??
8367
9541
  sourceSelector;
8368
9542
 
9543
+ // Use hit-test anchor when the canvas supplied one; fall back to
9544
+ // top-level body append ("inside" with no anchor = existing behaviour).
8369
9545
  const result = moveNodeBetweenDocuments(sourceContent, destContent, {
8370
9546
  nodeId: nodeAttrId,
8371
- placement: "inside",
9547
+ ...(targetAnchorNodeId
9548
+ ? {
9549
+ anchorNodeId: targetAnchorNodeId,
9550
+ placement: targetAnchorPlacement ?? "inside",
9551
+ }
9552
+ : { placement: "inside" }),
8372
9553
  });
8373
9554
  if (result.status !== "applied") {
8374
9555
  toast.error(
@@ -8602,11 +9783,11 @@ export default function DesignEditor() {
8602
9783
  // Clear stale selection if the undo removed the selected element.
8603
9784
  setSelectedElement((prev) => {
8604
9785
  if (!prev) return prev;
8605
- return elementInfoExistsInContent(next, prev) ? prev : null;
9786
+ return refreshElementInfoFromContent(next, prev);
8606
9787
  });
8607
9788
  setHoveredElement((prev) => {
8608
9789
  if (!prev) return prev;
8609
- return elementInfoExistsInContent(next, prev) ? prev : null;
9790
+ return refreshElementInfoFromContent(next, prev);
8610
9791
  });
8611
9792
  }
8612
9793
  redoOrderRef.current = [
@@ -8616,34 +9797,49 @@ export default function DesignEditor() {
8616
9797
  return true;
8617
9798
  }
8618
9799
 
8619
- if (!activeFile) return false;
8620
- const entry = contentUndoStackRef.current.pop();
8621
- if (!entry || entry.fileId !== activeFile.id) return false;
9800
+ const entry =
9801
+ contentUndoStackRef.current[contentUndoStackRef.current.length - 1];
9802
+ if (!entry) return false;
9803
+ if (
9804
+ entry.fileId !== activeFile?.id &&
9805
+ !files.some((file) => file.id === entry.fileId)
9806
+ ) {
9807
+ return false;
9808
+ }
9809
+ contentUndoStackRef.current.pop();
8622
9810
  contentRedoStackRef.current = [
8623
9811
  ...contentRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
8624
9812
  entry,
8625
9813
  ];
8626
9814
  redoOrderRef.current = [
8627
9815
  ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
8628
- "content",
9816
+ "file-content",
8629
9817
  ];
8630
9818
  suppressContentHistoryRef.current = true;
8631
9819
  try {
8632
- applyLocalContentUpdate(entry.before, {
8633
- refreshPreview: false,
8634
- immediateSave: true,
8635
- });
9820
+ if (entry.fileId === activeFile?.id) {
9821
+ applyLocalContentUpdate(entry.before, {
9822
+ refreshPreview: false,
9823
+ immediateSave: true,
9824
+ });
9825
+ } else {
9826
+ applyFileContentUpdate(entry.fileId, entry.before, {
9827
+ refreshPreview: false,
9828
+ });
9829
+ }
8636
9830
  } finally {
8637
9831
  suppressContentHistoryRef.current = false;
8638
9832
  }
8639
- setSelectedElement((prev) => {
8640
- if (!prev) return prev;
8641
- return elementInfoExistsInContent(entry.before, prev) ? prev : null;
8642
- });
8643
- setHoveredElement((prev) => {
8644
- if (!prev) return prev;
8645
- return elementInfoExistsInContent(entry.before, prev) ? prev : null;
8646
- });
9833
+ if (entry.fileId === activeFile?.id) {
9834
+ setSelectedElement((prev) => {
9835
+ if (!prev) return prev;
9836
+ return refreshElementInfoFromContent(entry.before, prev);
9837
+ });
9838
+ setHoveredElement((prev) => {
9839
+ if (!prev) return prev;
9840
+ return refreshElementInfoFromContent(entry.before, prev);
9841
+ });
9842
+ }
8647
9843
  return true;
8648
9844
  };
8649
9845
  const undoGeometry = () => {
@@ -8670,7 +9866,7 @@ export default function DesignEditor() {
8670
9866
  const didUndo =
8671
9867
  preferred === "geometry"
8672
9868
  ? undoGeometry() || undoContent()
8673
- : preferred === "content"
9869
+ : isContentUndoRedoOrderKind(preferred)
8674
9870
  ? undoContent() || undoGeometry()
8675
9871
  : undoContent() || undoGeometry();
8676
9872
  if (didUndo) {
@@ -8679,8 +9875,10 @@ export default function DesignEditor() {
8679
9875
  }, [
8680
9876
  ydoc,
8681
9877
  activeFile,
9878
+ applyFileContentUpdate,
8682
9879
  applyLocalContentUpdate,
8683
9880
  canEditDesign,
9881
+ files,
8684
9882
  isSynced,
8685
9883
  markPendingLocalFileContent,
8686
9884
  queueFileContentSave,
@@ -8712,11 +9910,11 @@ export default function DesignEditor() {
8712
9910
  // Clear stale selection if the redo removed the selected element.
8713
9911
  setSelectedElement((prev) => {
8714
9912
  if (!prev) return prev;
8715
- return elementInfoExistsInContent(next, prev) ? prev : null;
9913
+ return refreshElementInfoFromContent(next, prev);
8716
9914
  });
8717
9915
  setHoveredElement((prev) => {
8718
9916
  if (!prev) return prev;
8719
- return elementInfoExistsInContent(next, prev) ? prev : null;
9917
+ return refreshElementInfoFromContent(next, prev);
8720
9918
  });
8721
9919
  }
8722
9920
  historyOrderRef.current = [
@@ -8726,34 +9924,49 @@ export default function DesignEditor() {
8726
9924
  return true;
8727
9925
  }
8728
9926
 
8729
- if (!activeFile) return false;
8730
- const entry = contentRedoStackRef.current.pop();
8731
- if (!entry || entry.fileId !== activeFile.id) return false;
9927
+ const entry =
9928
+ contentRedoStackRef.current[contentRedoStackRef.current.length - 1];
9929
+ if (!entry) return false;
9930
+ if (
9931
+ entry.fileId !== activeFile?.id &&
9932
+ !files.some((file) => file.id === entry.fileId)
9933
+ ) {
9934
+ return false;
9935
+ }
9936
+ contentRedoStackRef.current.pop();
8732
9937
  contentUndoStackRef.current = [
8733
9938
  ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
8734
9939
  entry,
8735
9940
  ];
8736
9941
  historyOrderRef.current = [
8737
9942
  ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
8738
- "content",
9943
+ "file-content",
8739
9944
  ];
8740
9945
  suppressContentHistoryRef.current = true;
8741
9946
  try {
8742
- applyLocalContentUpdate(entry.after, {
8743
- refreshPreview: false,
8744
- immediateSave: true,
8745
- });
9947
+ if (entry.fileId === activeFile?.id) {
9948
+ applyLocalContentUpdate(entry.after, {
9949
+ refreshPreview: false,
9950
+ immediateSave: true,
9951
+ });
9952
+ } else {
9953
+ applyFileContentUpdate(entry.fileId, entry.after, {
9954
+ refreshPreview: false,
9955
+ });
9956
+ }
8746
9957
  } finally {
8747
9958
  suppressContentHistoryRef.current = false;
8748
9959
  }
8749
- setSelectedElement((prev) => {
8750
- if (!prev) return prev;
8751
- return elementInfoExistsInContent(entry.after, prev) ? prev : null;
8752
- });
8753
- setHoveredElement((prev) => {
8754
- if (!prev) return prev;
8755
- return elementInfoExistsInContent(entry.after, prev) ? prev : null;
8756
- });
9960
+ if (entry.fileId === activeFile?.id) {
9961
+ setSelectedElement((prev) => {
9962
+ if (!prev) return prev;
9963
+ return refreshElementInfoFromContent(entry.after, prev);
9964
+ });
9965
+ setHoveredElement((prev) => {
9966
+ if (!prev) return prev;
9967
+ return refreshElementInfoFromContent(entry.after, prev);
9968
+ });
9969
+ }
8757
9970
  return true;
8758
9971
  };
8759
9972
  const redoGeometry = () => {
@@ -8780,7 +9993,7 @@ export default function DesignEditor() {
8780
9993
  const didRedo =
8781
9994
  preferred === "geometry"
8782
9995
  ? redoGeometry() || redoContent()
8783
- : preferred === "content"
9996
+ : isContentUndoRedoOrderKind(preferred)
8784
9997
  ? redoContent() || redoGeometry()
8785
9998
  : redoContent() || redoGeometry();
8786
9999
  if (didRedo) {
@@ -8789,8 +10002,10 @@ export default function DesignEditor() {
8789
10002
  }, [
8790
10003
  ydoc,
8791
10004
  activeFile,
10005
+ applyFileContentUpdate,
8792
10006
  applyLocalContentUpdate,
8793
10007
  canEditDesign,
10008
+ files,
8794
10009
  isSynced,
8795
10010
  markPendingLocalFileContent,
8796
10011
  queueFileContentSave,
@@ -8865,6 +10080,10 @@ export default function DesignEditor() {
8865
10080
  const enterOverviewFromZoom = useCallback(() => {
8866
10081
  if (viewModeRef.current === "overview") return;
8867
10082
  viewModeRef.current = "overview";
10083
+ pendingOverviewScreenSelectionRef.current = null;
10084
+ pendingOverviewLayerSelectionRef.current = null;
10085
+ clearPendingOverviewLayerSelectionTimer();
10086
+ setCreatedOverviewLayerSelection(null);
8868
10087
  runEditorViewTransition(() => {
8869
10088
  setDrawMode(false);
8870
10089
  setPinMode(false);
@@ -8874,7 +10093,7 @@ export default function DesignEditor() {
8874
10093
  setActiveTool("move");
8875
10094
  setViewMode("overview");
8876
10095
  });
8877
- }, [runEditorViewTransition]);
10096
+ }, [clearPendingOverviewLayerSelectionTimer, runEditorViewTransition]);
8878
10097
 
8879
10098
  const enterSingleScreen = useCallback(
8880
10099
  (fileId?: string | null) => {
@@ -8888,6 +10107,10 @@ export default function DesignEditor() {
8888
10107
  return;
8889
10108
  }
8890
10109
  viewModeRef.current = "single";
10110
+ pendingOverviewScreenSelectionRef.current = null;
10111
+ pendingOverviewLayerSelectionRef.current = null;
10112
+ clearPendingOverviewLayerSelectionTimer();
10113
+ setCreatedOverviewLayerSelection(null);
8891
10114
  runEditorViewTransition(() => {
8892
10115
  if (fileId) setActiveFileId(fileId);
8893
10116
  setDrawMode(false);
@@ -8900,7 +10123,11 @@ export default function DesignEditor() {
8900
10123
  setViewMode("single");
8901
10124
  });
8902
10125
  },
8903
- [activeFileId, runEditorViewTransition],
10126
+ [
10127
+ activeFileId,
10128
+ clearPendingOverviewLayerSelectionTimer,
10129
+ runEditorViewTransition,
10130
+ ],
8904
10131
  );
8905
10132
 
8906
10133
  useEffect(() => {
@@ -8971,14 +10198,22 @@ export default function DesignEditor() {
8971
10198
 
8972
10199
  const handleSidebarScreenSelect = useCallback(
8973
10200
  (screenId: string) => {
10201
+ pendingOverviewScreenSelectionRef.current = null;
10202
+ pendingOverviewLayerSelectionRef.current = null;
10203
+ clearPendingOverviewLayerSelectionTimer();
10204
+ setCreatedOverviewLayerSelection(null);
8974
10205
  setOverviewSelectedScreenIds([]);
8975
10206
  setSelectedLayerIdsState([]);
8976
10207
  enterSingleScreen(screenId);
8977
10208
  },
8978
- [enterSingleScreen],
10209
+ [clearPendingOverviewLayerSelectionTimer, enterSingleScreen],
8979
10210
  );
8980
10211
 
8981
10212
  const handleSidebarScreenOverview = useCallback(() => {
10213
+ pendingOverviewScreenSelectionRef.current = null;
10214
+ pendingOverviewLayerSelectionRef.current = null;
10215
+ clearPendingOverviewLayerSelectionTimer();
10216
+ setCreatedOverviewLayerSelection(null);
8982
10217
  setOverviewSelectedScreenIds([]);
8983
10218
  setSelectedLayerIdsState([]);
8984
10219
  if (viewModeRef.current === "overview") {
@@ -8991,7 +10226,7 @@ export default function DesignEditor() {
8991
10226
  return;
8992
10227
  }
8993
10228
  enterOverviewFromZoom();
8994
- }, [enterOverviewFromZoom]);
10229
+ }, [clearPendingOverviewLayerSelectionTimer, enterOverviewFromZoom]);
8995
10230
 
8996
10231
  const handlePinToolToggle = useCallback(() => {
8997
10232
  if (!activeFile || !canEditDesign) return;
@@ -9806,11 +11041,11 @@ ${serializedHtml}
9806
11041
  ? "Preparing agent prompt..."
9807
11042
  : codingHandoffPreviewFallback);
9808
11043
  const shareExportTab = (
9809
- <div className="space-y-5">
9810
- <div className="text-sm font-semibold text-muted-foreground">
11044
+ <div className="space-y-3">
11045
+ <div className="text-[11px] font-semibold uppercase text-muted-foreground">
9811
11046
  {"Format" /* i18n-ignore share export section label */}
9812
11047
  </div>
9813
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
11048
+ <div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
9814
11049
  {shareExportOptions.map((option) => {
9815
11050
  const selected = option.value === shareExportFormat;
9816
11051
  const ExportIcon = option.Icon;
@@ -9820,45 +11055,47 @@ ${serializedHtml}
9820
11055
  type="button"
9821
11056
  onClick={() => setShareExportFormat(option.value)}
9822
11057
  className={cn(
9823
- "relative min-h-32 rounded-xl border bg-background p-4 text-left transition-colors hover:bg-accent/35",
11058
+ "relative flex min-h-[76px] items-start gap-2.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] p-2.5 text-left transition-colors hover:bg-[var(--design-editor-panel-raised-bg)]",
9824
11059
  selected
9825
- ? "border-primary bg-primary/5 ring-1 ring-primary"
9826
- : "border-border",
11060
+ ? "bg-[var(--design-editor-panel-raised-bg)] ring-1 ring-[var(--design-editor-accent-color)]"
11061
+ : "",
9827
11062
  )}
9828
11063
  >
9829
- <span className="mb-5 inline-flex size-12 items-center justify-center rounded-xl bg-muted text-muted-foreground">
9830
- <ExportIcon className="size-5" strokeWidth={1.75} />
11064
+ <span className="inline-flex size-7 shrink-0 items-center justify-center rounded-md bg-[var(--design-editor-panel-raised-bg)] text-muted-foreground">
11065
+ <ExportIcon className="size-3.5" strokeWidth={1.75} />
9831
11066
  </span>
9832
- <span className="block text-base font-semibold text-foreground">
9833
- {option.title}{" "}
9834
- <span className="text-sm font-medium text-muted-foreground">
9835
- {option.extension}
11067
+ <span className="min-w-0 flex-1">
11068
+ <span className="block truncate text-[12px] font-semibold text-foreground">
11069
+ {option.title}{" "}
11070
+ <span className="text-[11px] font-medium text-muted-foreground">
11071
+ {option.extension}
11072
+ </span>
11073
+ </span>
11074
+ <span className="mt-0.5 block text-[11px] leading-4 text-muted-foreground">
11075
+ {option.description}
9836
11076
  </span>
9837
- </span>
9838
- <span className="mt-1 block text-sm leading-5 text-muted-foreground">
9839
- {option.description}
9840
11077
  </span>
9841
11078
  <span
9842
11079
  aria-hidden
9843
11080
  className={cn(
9844
- "absolute right-4 top-4 inline-flex size-6 items-center justify-center rounded-full border",
11081
+ "absolute right-2.5 top-2.5 inline-flex size-4 items-center justify-center rounded-full border",
9845
11082
  selected
9846
- ? "border-primary bg-primary text-primary-foreground"
9847
- : "border-border bg-background",
11083
+ ? "border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)] text-[var(--design-editor-accent-contrast-color)]"
11084
+ : "border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)]",
9848
11085
  )}
9849
11086
  >
9850
- {selected ? <IconCheck className="size-4" /> : null}
11087
+ {selected ? <IconCheck className="size-3" /> : null}
9851
11088
  </span>
9852
11089
  </button>
9853
11090
  );
9854
11091
  })}
9855
11092
  </div>
9856
- <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
11093
+ <div className="flex flex-wrap items-center justify-between gap-2 border-t border-[var(--design-editor-panel-divider-color)] pt-3">
9857
11094
  <div className="min-w-0">
9858
- <div className="text-sm font-medium text-foreground">
11095
+ <div className="text-[12px] font-medium text-foreground">
9859
11096
  {selectedShareExportOption.title}
9860
11097
  </div>
9861
- <div className="text-xs text-muted-foreground">
11098
+ <div className="text-[11px] text-muted-foreground">
9862
11099
  {selectedShareExportOption.description}
9863
11100
  </div>
9864
11101
  </div>
@@ -9866,34 +11103,34 @@ ${serializedHtml}
9866
11103
  type="button"
9867
11104
  onClick={selectedShareExportOption.onDownload}
9868
11105
  disabled={selectedShareExportOption.disabled}
9869
- className="h-10 gap-2 rounded-lg px-4"
11106
+ className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
9870
11107
  >
9871
- <IconDownload className="size-4" />
11108
+ <IconDownload className="size-3.5" />
9872
11109
  {"Download" /* i18n-ignore share export action */}
9873
11110
  </Button>
9874
11111
  </div>
9875
11112
  </div>
9876
11113
  );
9877
11114
  const shareSendToTab = (
9878
- <div className="space-y-5">
9879
- <div className="overflow-hidden rounded-xl border border-neutral-800 bg-neutral-950 shadow-sm">
9880
- <div className="flex h-11 items-center border-b border-neutral-800 px-4">
11115
+ <div className="space-y-3">
11116
+ <div className="overflow-hidden rounded-md border border-neutral-800 bg-neutral-950 shadow-sm">
11117
+ <div className="flex h-8 items-center border-b border-neutral-800 px-3">
9881
11118
  <div className="flex items-center gap-2">
9882
- <span className="size-3 rounded-full bg-red-500" />
9883
- <span className="size-3 rounded-full bg-yellow-400" />
9884
- <span className="size-3 rounded-full bg-green-500" />
11119
+ <span className="size-2.5 rounded-full bg-red-500" />
11120
+ <span className="size-2.5 rounded-full bg-yellow-400" />
11121
+ <span className="size-2.5 rounded-full bg-green-500" />
9885
11122
  </div>
9886
- <div className="min-w-0 flex-1 truncate text-center text-sm font-medium text-neutral-400">
11123
+ <div className="min-w-0 flex-1 truncate text-center text-[12px] font-medium text-neutral-400">
9887
11124
  {"Your agent" /* i18n-ignore terminal title */}
9888
11125
  </div>
9889
- <IconTerminal2 className="size-4 text-neutral-500" />
11126
+ <IconTerminal2 className="size-3.5 text-neutral-500" />
9890
11127
  </div>
9891
- <pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words px-5 py-4 font-mono text-sm leading-6 text-neutral-100">
11128
+ <pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words px-3 py-3 font-mono text-[12px] leading-5 text-neutral-100">
9892
11129
  {`> ${codingHandoffPreviewText}`}
9893
11130
  </pre>
9894
11131
  </div>
9895
11132
 
9896
- <div className="flex flex-wrap items-center gap-3">
11133
+ <div className="flex flex-wrap items-center gap-2">
9897
11134
  <Button
9898
11135
  type="button"
9899
11136
  onClick={handleSendToPrimaryAction}
@@ -9902,12 +11139,12 @@ ${serializedHtml}
9902
11139
  ? !activeFile || codingHandoffLoading
9903
11140
  : codingHandoffLoading
9904
11141
  }
9905
- className="h-10 gap-2 rounded-lg px-4"
11142
+ className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
9906
11143
  >
9907
11144
  {downloadZipInstead ? (
9908
- <IconArchive className="size-4" />
11145
+ <IconArchive className="size-3.5" />
9909
11146
  ) : (
9910
- <IconClipboard className="size-4" />
11147
+ <IconClipboard className="size-3.5" />
9911
11148
  )}
9912
11149
  {
9913
11150
  downloadZipInstead
@@ -9917,20 +11154,20 @@ ${serializedHtml}
9917
11154
  </Button>
9918
11155
  </div>
9919
11156
 
9920
- <div className="space-y-4">
9921
- <div className="flex items-start gap-3">
11157
+ <div className="space-y-3">
11158
+ <div className="flex items-start gap-2.5">
9922
11159
  <Checkbox
9923
11160
  checked={downloadZipInstead}
9924
11161
  onCheckedChange={(checked) =>
9925
11162
  setDownloadZipInstead(checked === true)
9926
11163
  }
9927
- className="mt-1"
11164
+ className="mt-0.5"
9928
11165
  />
9929
11166
  <div className="min-w-0">
9930
- <div className="text-sm font-medium text-foreground">
11167
+ <div className="text-[12px] font-medium text-foreground">
9931
11168
  {"Download zip instead" /* i18n-ignore share send option */}
9932
11169
  </div>
9933
- <div className="mt-0.5 text-sm leading-5 text-muted-foreground">
11170
+ <div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
9934
11171
  {
9935
11172
  "For agents without the Design connector, drop the bundle into your agent's chat manually." /* i18n-ignore share send option description */
9936
11173
  }
@@ -9938,8 +11175,8 @@ ${serializedHtml}
9938
11175
  </div>
9939
11176
  </div>
9940
11177
 
9941
- <div className="space-y-2">
9942
- <label className="text-sm font-medium text-foreground">
11178
+ <div className="space-y-1.5">
11179
+ <label className="text-[12px] font-medium text-foreground">
9943
11180
  {
9944
11181
  "Give the agent more detail on what to implement" /* i18n-ignore share send detail label */
9945
11182
  }{" "}
@@ -9951,12 +11188,17 @@ ${serializedHtml}
9951
11188
  value={codingHandoffDetail}
9952
11189
  onChange={(event) => setCodingHandoffDetail(event.target.value)}
9953
11190
  placeholder={activeFile?.filename ?? "Add implementation notes..."}
9954
- className="min-h-24 resize-none rounded-lg bg-background"
11191
+ className="min-h-20 resize-none rounded-md bg-[var(--design-editor-control-bg)] text-[12px]"
9955
11192
  />
9956
11193
  </div>
9957
11194
  </div>
9958
11195
  </div>
9959
11196
  );
11197
+ const designSharePopoverClassName =
11198
+ "z-[100010] !w-[min(620px,calc(100vw-32px))] !p-3 " +
11199
+ "[&_[role=tablist]]:!inline-flex [&_[role=tablist]]:!w-fit [&_[role=tablist]]:!self-start [&_[role=tablist]]:justify-start [&_[role=tablist]]:gap-0.5 [&_[role=tablist]]:rounded-none [&_[role=tablist]]:bg-transparent [&_[role=tablist]]:p-0 " +
11200
+ "[&_[role=tab]]:!h-6 [&_[role=tab]]:!flex-none [&_[role=tab]]:rounded-md [&_[role=tab]]:px-2 [&_[role=tab]]:text-[11px] [&_[role=tab]]:font-semibold [&_[role=tab]]:shadow-none [&_[role=tab]]:ring-0 " +
11201
+ "[&_[role=tab]:hover]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab]:hover]:text-foreground [&_[role=tab][aria-selected=true]]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab][aria-selected=true]]:text-foreground [&_[role=tab][aria-selected=true]]:ring-0";
9960
11202
  const designShareTabs = {
9961
11203
  shareLabel: "Share link" /* i18n-ignore share tab label */,
9962
11204
  defaultValue: "share",
@@ -10066,6 +11308,23 @@ ${serializedHtml}
10066
11308
  });
10067
11309
  return state;
10068
11310
  }, [codeLayerModelsByFile, hiddenLayerIds, lockedLayerIds]);
11311
+ useEffect(() => {
11312
+ shouldPreserveBlockedOverviewLayerSelectionRef.current = (
11313
+ screenId: string,
11314
+ ) => {
11315
+ if (viewModeRef.current !== "overview") return false;
11316
+ return selectedLayerIdsState.some((layerId) => {
11317
+ const owner = codeLayerOwnerByNodeId.get(layerId);
11318
+ if (!owner || owner.fileId !== screenId) return false;
11319
+ return (
11320
+ effectiveCodeLayerState.lockedIds.has(screenId) ||
11321
+ effectiveCodeLayerState.hiddenIds.has(screenId) ||
11322
+ effectiveCodeLayerState.lockedIds.has(layerId) ||
11323
+ effectiveCodeLayerState.hiddenIds.has(layerId)
11324
+ );
11325
+ });
11326
+ };
11327
+ }, [codeLayerOwnerByNodeId, effectiveCodeLayerState, selectedLayerIdsState]);
10069
11328
  useEffect(() => {
10070
11329
  const fileIds = new Set(files.map((file) => file.id));
10071
11330
  const allCodeLayerNodes = codeLayerModelsByFile.flatMap(
@@ -10171,25 +11430,9 @@ ${serializedHtml}
10171
11430
 
10172
11431
  const layerPanelFiles = useMemo<LayersPanelFile[]>(
10173
11432
  () =>
10174
- files.map((file) => ({
10175
- id: file.id,
10176
- name: prettyScreenName(file.filename),
10177
- filename: file.filename,
10178
- fileType: file.fileType,
10179
- detail: file.filename,
10180
- locked: lockedLayerIds.has(file.id),
10181
- hidden: hiddenLayerIds.has(file.id),
10182
- lockable: true,
10183
- hideable: true,
10184
- renamable: true,
10185
- })),
10186
- [files, hiddenLayerIds, lockedLayerIds],
10187
- );
10188
- const overviewLayerPanelFiles = useMemo<LayersPanelFile[]>(
10189
- () =>
10190
- files.map((file) => {
10191
- const model = codeLayerModelByFileId.get(file.id);
10192
- return {
11433
+ files
11434
+ .filter((file) => !isBoardFile(file.filename))
11435
+ .map((file) => ({
10193
11436
  id: file.id,
10194
11437
  name: prettyScreenName(file.filename),
10195
11438
  filename: file.filename,
@@ -10200,16 +11443,54 @@ ${serializedHtml}
10200
11443
  lockable: true,
10201
11444
  hideable: true,
10202
11445
  renamable: true,
10203
- layers: codeLayerTreeToPanelNodes(
10204
- model?.tree ?? [],
10205
- lockedLayerIds,
10206
- hiddenLayerIds,
10207
- ),
10208
- };
10209
- }),
11446
+ })),
11447
+ [files, hiddenLayerIds, lockedLayerIds],
11448
+ );
11449
+ const overviewLayerPanelFiles = useMemo<LayersPanelFile[]>(
11450
+ () =>
11451
+ files
11452
+ .filter((file) => !isBoardFile(file.filename))
11453
+ .map((file) => {
11454
+ const model = codeLayerModelByFileId.get(file.id);
11455
+ return {
11456
+ id: file.id,
11457
+ name: prettyScreenName(file.filename),
11458
+ filename: file.filename,
11459
+ fileType: file.fileType,
11460
+ detail: file.filename,
11461
+ locked: lockedLayerIds.has(file.id),
11462
+ hidden: hiddenLayerIds.has(file.id),
11463
+ lockable: true,
11464
+ hideable: true,
11465
+ renamable: true,
11466
+ layers: codeLayerTreeToPanelNodes(
11467
+ model?.tree ?? [],
11468
+ lockedLayerIds,
11469
+ hiddenLayerIds,
11470
+ ),
11471
+ };
11472
+ }),
10210
11473
  [codeLayerModelByFileId, files, hiddenLayerIds, lockedLayerIds],
10211
11474
  );
10212
11475
 
11476
+ // Board objects shown as top-level peer rows in the layers panel, right
11477
+ // alongside the screen frames. Derived from the same code-layer model that
11478
+ // feeds codeLayerOwnerByNodeId so a layer-row click resolves to the board
11479
+ // file (sets it active + selects the element). buildCodeLayerProjection was
11480
+ // the wrong source here: it produced different node ids that the owner map
11481
+ // could not route, and returned no roots for the migrated board fragments.
11482
+ const boardElements = useMemo<LayersPanelNode[] | undefined>(() => {
11483
+ if (!boardFileId) return undefined;
11484
+ const model = codeLayerModelByFileId.get(boardFileId);
11485
+ if (!model?.tree?.length) return undefined;
11486
+ const nodes = codeLayerTreeToPanelNodes(
11487
+ model.tree,
11488
+ lockedLayerIds,
11489
+ hiddenLayerIds,
11490
+ );
11491
+ return nodes.length > 0 ? nodes : undefined;
11492
+ }, [boardFileId, codeLayerModelByFileId, lockedLayerIds, hiddenLayerIds]);
11493
+
10213
11494
  const activeLayerPanelNodes = useMemo<LayersPanelNode[]>(
10214
11495
  () => activeCodeLayerPanelNodes,
10215
11496
  [activeCodeLayerPanelNodes],
@@ -10223,6 +11504,18 @@ ${serializedHtml}
10223
11504
  ).map((node) => node.id),
10224
11505
  );
10225
11506
  const fileIds = new Set(files.map((file) => file.id));
11507
+ const pendingOverviewScreenId = pendingOverviewScreenSelectionRef.current;
11508
+ const pendingOverviewLayerId = pendingOverviewLayerSelectionRef.current;
11509
+ if (pendingOverviewScreenId) {
11510
+ validIds.add(pendingOverviewScreenId);
11511
+ fileIds.add(pendingOverviewScreenId);
11512
+ }
11513
+ if (pendingOverviewLayerId) {
11514
+ validIds.add(pendingOverviewLayerId);
11515
+ }
11516
+ if (createdOverviewLayerSelection) {
11517
+ validIds.add(createdOverviewLayerSelection.layerId);
11518
+ }
10226
11519
  if (selectedElementLayerId) validIds.add(selectedElementLayerId);
10227
11520
  files.forEach((file) => validIds.add(file.id));
10228
11521
  const selectedStateIds = selectedLayerIdsState.filter((layerId) =>
@@ -10235,11 +11528,13 @@ ${serializedHtml}
10235
11528
  viewMode === "overview" &&
10236
11529
  selectedStateIds.some((layerId) => fileIds.has(layerId));
10237
11530
  const baseSelection =
10238
- viewMode === "overview" && !hasOverviewCodeLayerSelection
10239
- ? overviewSelectedScreenIds.length > 0 || !hasOverviewFileSelection
10240
- ? overviewSelectedScreenIds
10241
- : selectedLayerIdsState
10242
- : selectedLayerIdsState;
11531
+ viewMode === "overview" && createdOverviewLayerSelection
11532
+ ? [createdOverviewLayerSelection.layerId]
11533
+ : viewMode === "overview" && !hasOverviewCodeLayerSelection
11534
+ ? overviewSelectedScreenIds.length > 0 || !hasOverviewFileSelection
11535
+ ? overviewSelectedScreenIds
11536
+ : selectedLayerIdsState
11537
+ : selectedLayerIdsState;
10243
11538
  const filtered = baseSelection.filter((layerId) => validIds.has(layerId));
10244
11539
  if (selectedElementLayerId && !filtered.includes(selectedElementLayerId)) {
10245
11540
  if (filtered.length > 1) return [...filtered, selectedElementLayerId];
@@ -10249,6 +11544,7 @@ ${serializedHtml}
10249
11544
  }, [
10250
11545
  activeCodeLayerProjection.nodes,
10251
11546
  codeLayerModelsByFile,
11547
+ createdOverviewLayerSelection,
10252
11548
  files,
10253
11549
  overviewSelectedScreenIds,
10254
11550
  selectedElementLayerId,
@@ -10261,6 +11557,59 @@ ${serializedHtml}
10261
11557
  selectedLayerIdsRef.current = selectedLayerIds;
10262
11558
  }, [selectedLayerIds]);
10263
11559
 
11560
+ const layerPanelSelectedIds = useMemo(
11561
+ () =>
11562
+ viewMode === "overview" && createdOverviewLayerSelection
11563
+ ? [createdOverviewLayerSelection.layerId]
11564
+ : selectedLayerIds,
11565
+ [createdOverviewLayerSelection, selectedLayerIds, viewMode],
11566
+ );
11567
+
11568
+ const layerPanelExpandedIds = useMemo(() => {
11569
+ if (viewMode !== "overview" || !createdOverviewLayerSelection) {
11570
+ return expandedLayerIds;
11571
+ }
11572
+ const next = new Set(expandedLayerIds);
11573
+ next.add(createdOverviewLayerSelection.screenId);
11574
+ return Array.from(next);
11575
+ }, [createdOverviewLayerSelection, expandedLayerIds, viewMode]);
11576
+
11577
+ useEffect(() => {
11578
+ const pendingLayerId = pendingOverviewLayerSelectionRef.current;
11579
+ if (!pendingLayerId) return;
11580
+ if (!selectedLayerIdsState.includes(pendingLayerId)) {
11581
+ pendingOverviewLayerSelectionRef.current = null;
11582
+ clearPendingOverviewLayerSelectionTimer();
11583
+ return;
11584
+ }
11585
+ const owner = codeLayerOwnerByNodeId.get(pendingLayerId);
11586
+ if (!owner) return;
11587
+ schedulePendingOverviewLayerSelectionClear(pendingLayerId);
11588
+ setActiveFileId(owner.fileId);
11589
+ setSelectedElement(elementInfoFromCodeLayerNode(owner.node));
11590
+ setExpandedLayerIds((current) => {
11591
+ const next = new Set(current);
11592
+ next.add(owner.fileId);
11593
+ collectCodeLayerAncestors(owner.tree, pendingLayerId).forEach((id) =>
11594
+ next.add(id),
11595
+ );
11596
+ return next.size === current.length ? current : Array.from(next);
11597
+ });
11598
+ }, [
11599
+ clearPendingOverviewLayerSelectionTimer,
11600
+ codeLayerOwnerByNodeId,
11601
+ schedulePendingOverviewLayerSelectionClear,
11602
+ selectedLayerIdsState,
11603
+ ]);
11604
+
11605
+ useEffect(() => {
11606
+ const pendingScreenId = pendingOverviewScreenSelectionRef.current;
11607
+ if (!pendingScreenId) return;
11608
+ if (files.some((file) => file.id === pendingScreenId)) {
11609
+ pendingOverviewScreenSelectionRef.current = null;
11610
+ }
11611
+ }, [files]);
11612
+
10264
11613
  useEffect(() => {
10265
11614
  setSelectedLayerIdsState((current) => {
10266
11615
  if (!selectedElementLayerId) {
@@ -10286,7 +11635,26 @@ ${serializedHtml}
10286
11635
  ancestorIds.forEach((ancestorId) => next.add(ancestorId));
10287
11636
  return next.size === current.length ? current : Array.from(next);
10288
11637
  });
10289
- }, [activeCodeLayerTree, codeLayerOwnerByNodeId, selectedElementLayerId]);
11638
+ }, [activeCodeLayerTree, codeLayerOwnerByNodeId, selectedElementLayerId]);
11639
+
11640
+ useEffect(() => {
11641
+ const selectedCodeLayerIds = selectedLayerIds.filter((layerId) =>
11642
+ codeLayerOwnerByNodeId.has(layerId),
11643
+ );
11644
+ if (selectedCodeLayerIds.length === 0) return;
11645
+ setExpandedLayerIds((current) => {
11646
+ const next = new Set(current);
11647
+ selectedCodeLayerIds.forEach((layerId) => {
11648
+ const owner = codeLayerOwnerByNodeId.get(layerId);
11649
+ if (!owner) return;
11650
+ next.add(owner.fileId);
11651
+ collectCodeLayerAncestors(owner.tree, layerId).forEach((ancestorId) =>
11652
+ next.add(ancestorId),
11653
+ );
11654
+ });
11655
+ return next.size === current.length ? current : Array.from(next);
11656
+ });
11657
+ }, [codeLayerOwnerByNodeId, selectedLayerIds]);
10290
11658
 
10291
11659
  useEffect(() => {
10292
11660
  if (!selectedElementLayerId) return;
@@ -10381,6 +11749,8 @@ ${serializedHtml}
10381
11749
  return defaultMatch?.id ?? "auto";
10382
11750
  }, [activeBreakpointWidthState, statesPanelBreakpoints]);
10383
11751
 
11752
+ const publishDesignTitle = design?.title?.trim() || "Untitled design";
11753
+
10384
11754
  const handleOpenDesignPreview = useCallback(() => {
10385
11755
  if (activeScreenPreviewUrl) {
10386
11756
  window.open(activeScreenPreviewUrl, "_blank", "noopener,noreferrer");
@@ -10397,6 +11767,53 @@ ${serializedHtml}
10397
11767
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
10398
11768
  }, [activeContent, activeScreenPreviewUrl]);
10399
11769
 
11770
+ const handleJoinPublishWaitlist = useCallback(async () => {
11771
+ if (!isSignedIn) {
11772
+ handleSignInToSave();
11773
+ return;
11774
+ }
11775
+
11776
+ setJoiningPublishWaitlist(true);
11777
+ setPublishWaitlistError(null);
11778
+
11779
+ try {
11780
+ const res = await fetch(
11781
+ new URL(
11782
+ agentNativePath("/_agent-native/builder/branch-waitlist"),
11783
+ window.location.origin,
11784
+ ).href,
11785
+ {
11786
+ method: "POST",
11787
+ headers: { "Content-Type": "application/json" },
11788
+ body: JSON.stringify({
11789
+ pageUrl: window.location.href,
11790
+ prompt: `Publish design "${publishDesignTitle}" as an app.`,
11791
+ source: "design_editor_publish_app_menu",
11792
+ useCase: "design_publish_app",
11793
+ }),
11794
+ },
11795
+ );
11796
+ const data = await res.json().catch(() => ({}));
11797
+ if (!res.ok) {
11798
+ throw new Error(
11799
+ typeof data?.error === "string"
11800
+ ? data.error
11801
+ : `Request failed (${res.status})`,
11802
+ );
11803
+ }
11804
+
11805
+ setPublishWaitlistJoined(true);
11806
+ } catch (err) {
11807
+ setPublishWaitlistError(
11808
+ err instanceof Error
11809
+ ? err.message
11810
+ : "Couldn't join the waitlist. Please try again.",
11811
+ );
11812
+ } finally {
11813
+ setJoiningPublishWaitlist(false);
11814
+ }
11815
+ }, [handleSignInToSave, isSignedIn, publishDesignTitle]);
11816
+
10400
11817
  const activeLayerId =
10401
11818
  selectedLayerIds[selectedLayerIds.length - 1] ??
10402
11819
  selectedElementLayerId ??
@@ -10430,6 +11847,198 @@ ${serializedHtml}
10430
11847
  source: activeOverviewScreen?.source,
10431
11848
  })
10432
11849
  : undefined;
11850
+ // Connection id for the active localhost screen — needed to mint write grants.
11851
+ const activeLocalhostConnectionId = activeScreenIsLocalSource
11852
+ ? ((activeOverviewScreen as { connectionId?: string } | undefined)
11853
+ ?.connectionId ?? "")
11854
+ : "";
11855
+
11856
+ /**
11857
+ * Request consent to write a local file for the active localhost screen.
11858
+ * If no valid grant exists, opens the consent dialog; once granted the
11859
+ * caller should proceed to call write-local-file via the action surface.
11860
+ *
11861
+ * Only works when the active screen is localhost-backed and the current user
11862
+ * has editor access. For non-localhost screens use the normal Ask-AI path.
11863
+ *
11864
+ * The files parameter is for display in the consent dialog only; the actual
11865
+ * write must be performed by the caller via the write-local-file action.
11866
+ */
11867
+ const requestLocalhostWrite = useCallback(
11868
+ (opts: {
11869
+ files: string[];
11870
+ onGranted: LocalhostWriteConsentPayload["onGranted"];
11871
+ onCancel?: () => void;
11872
+ }) => {
11873
+ if (!id || !canEditDesign || !activeLocalhostConnectionId) return;
11874
+
11875
+ const rootPath =
11876
+ activeScreenRouteSourceFile ?? activeLocalhostConnectionId;
11877
+
11878
+ setLocalhostConsentConnectionId(activeLocalhostConnectionId);
11879
+ setLocalhostWriteConsentPayload({
11880
+ rootPath,
11881
+ files: opts.files,
11882
+ onGranted: opts.onGranted,
11883
+ onCancel: opts.onCancel ?? (() => {}),
11884
+ });
11885
+ setLocalhostWriteConsentOpen(true);
11886
+ },
11887
+ [
11888
+ activeLocalhostConnectionId,
11889
+ activeScreenRouteSourceFile,
11890
+ canEditDesign,
11891
+ id,
11892
+ ],
11893
+ );
11894
+ // requestLocalhostWrite is consumed via the component instance or by
11895
+ // connected inspector components; not all render paths call it directly.
11896
+ void requestLocalhostWrite;
11897
+
11898
+ /**
11899
+ * Derive a relative file path from the active localhost screen.
11900
+ * Prefers `sourceFile` (the build-output relative path recorded at connect
11901
+ * time) over the URL pathname so the path maps to the actual file on disk.
11902
+ * Returns undefined when no usable path can be determined.
11903
+ */
11904
+ const activeLocalhostRelPath = useMemo<string | undefined>(() => {
11905
+ if (!activeScreenIsLocalSource) return undefined;
11906
+ // Prefer the explicit sourceFile (e.g. "src/index.html").
11907
+ const sf = activeScreenRouteSourceFile;
11908
+ if (sf?.trim()) return sf.trim();
11909
+ // Fall back to URL pathname (e.g. "/page.html" → "page.html").
11910
+ const url = activeOverviewScreen?.url;
11911
+ if (!url) return undefined;
11912
+ try {
11913
+ const pathname = new URL(url).pathname.replace(/^\//, "");
11914
+ return pathname || undefined;
11915
+ } catch {
11916
+ return undefined;
11917
+ }
11918
+ }, [
11919
+ activeScreenIsLocalSource,
11920
+ activeScreenRouteSourceFile,
11921
+ activeOverviewScreen?.url,
11922
+ ]);
11923
+
11924
+ /** True when the active localhost screen maps to an HTML/CSS file we can write. */
11925
+ const activeLocalhostRouteIsWritable =
11926
+ activeScreenIsLocalSource &&
11927
+ Boolean(activeLocalhostRelPath) &&
11928
+ LOCALHOST_WRITE_EXTENSIONS.has(
11929
+ (activeLocalhostRelPath?.match(/\.[^.]+$/) ?? [])[0]?.toLowerCase() ?? "",
11930
+ );
11931
+
11932
+ /**
11933
+ * Strip editor-only node-id attributes from HTML source so they are not
11934
+ * written back to the user's local file.
11935
+ *
11936
+ * Kept attributes (intentional user content):
11937
+ * - data-agent-native-layer-name (human-readable display name)
11938
+ * - data-screen="…" (prototype navigation)
11939
+ * - any other data-* not listed below
11940
+ *
11941
+ * Stripped attributes (editor plumbing only):
11942
+ * - data-agent-native-node-id (stable selection id stamped by the editor)
11943
+ * - data-code-layer-id (alternative layer id for localhost components)
11944
+ */
11945
+ function stripEditorOnlyAttributes(html: string): string {
11946
+ if (typeof window === "undefined") return html;
11947
+ try {
11948
+ const doc = new DOMParser().parseFromString(html, "text/html");
11949
+ const STRIP_ATTRS = [
11950
+ "data-agent-native-node-id",
11951
+ "data-code-layer-id",
11952
+ ] as const;
11953
+ for (const attr of STRIP_ATTRS) {
11954
+ doc.querySelectorAll(`[${attr}]`).forEach((el) => {
11955
+ el.removeAttribute(attr);
11956
+ });
11957
+ }
11958
+ // Serialise back. Use outerHTML of <html> to preserve doctype-less
11959
+ // fragments; for full documents prefer innerHTML wrapping.
11960
+ const doctype = doc.doctype
11961
+ ? new XMLSerializer().serializeToString(doc.doctype) + "\n"
11962
+ : "";
11963
+ const htmlEl = doc.documentElement;
11964
+ return doctype + htmlEl.outerHTML;
11965
+ } catch {
11966
+ // If DOMParser fails (e.g. malformed HTML) fall back to the raw content.
11967
+ return html;
11968
+ }
11969
+ }
11970
+
11971
+ /**
11972
+ * "Apply to source" — write the current editor content back to the local
11973
+ * file via the bridge. Opens the consent dialog if no grant exists yet, then
11974
+ * calls write-local-file with a clean version of the editor content (editor-
11975
+ * only attribute stamps stripped).
11976
+ *
11977
+ * Only operates on HTML/CSS routes (gated by activeLocalhostRouteIsWritable).
11978
+ * For non-HTML routes (React/JSX/TS), keep routing to the agent chat instead.
11979
+ */
11980
+ const handleApplyToSource = useCallback(() => {
11981
+ if (
11982
+ !id ||
11983
+ !canEditDesign ||
11984
+ !activeLocalhostConnectionId ||
11985
+ !activeLocalhostRelPath
11986
+ )
11987
+ return;
11988
+ const relPath = activeLocalhostRelPath;
11989
+ const connectionId = activeLocalhostConnectionId;
11990
+ // Snapshot current editor content at call time.
11991
+ const rawContent = latestActiveContentRef.current;
11992
+ if (!rawContent) {
11993
+ toast.error(NO_LOCALHOST_WRITE_CONTENT_MESSAGE);
11994
+ return;
11995
+ }
11996
+ // Strip editor-only attributes before writing so the on-disk file stays
11997
+ // clean. Only strip for HTML routes; CSS files have no DOM attributes.
11998
+ const ext = (relPath.match(/\.[^.]+$/) ?? [])[0]?.toLowerCase() ?? "";
11999
+ const content =
12000
+ ext === ".html" || ext === ".htm"
12001
+ ? stripEditorOnlyAttributes(rawContent)
12002
+ : rawContent;
12003
+
12004
+ requestLocalhostWrite({
12005
+ files: [relPath],
12006
+ onGranted: ({ bridgeToken, rootPath: grantedRootPath, grantId }) => {
12007
+ void (async () => {
12008
+ setApplyToSourcePending(true);
12009
+ try {
12010
+ await callAction("write-local-file", {
12011
+ designId: id,
12012
+ connectionId,
12013
+ relPath,
12014
+ content,
12015
+ });
12016
+ toast.success(
12017
+ `Written to ${relPath} (grant ${grantId.slice(0, 6)}…, root ${grantedRootPath})`,
12018
+ );
12019
+ } catch (err) {
12020
+ toast.error(
12021
+ `Write failed: ${err instanceof Error ? err.message : String(err)}`,
12022
+ );
12023
+ } finally {
12024
+ setApplyToSourcePending(false);
12025
+ }
12026
+ // Suppress unused-var warning; bridgeToken is checked by the bridge
12027
+ // internally via the X-Bridge-Token header (set in write-local-file).
12028
+ void bridgeToken;
12029
+ })();
12030
+ },
12031
+ onCancel: () => {
12032
+ setApplyToSourcePending(false);
12033
+ },
12034
+ });
12035
+ }, [
12036
+ id,
12037
+ canEditDesign,
12038
+ activeLocalhostConnectionId,
12039
+ activeLocalhostRelPath,
12040
+ requestLocalhostWrite,
12041
+ ]);
10433
12042
 
10434
12043
  // canGroup: 2+ DOM-node layers selected in the active screen (not file rows).
10435
12044
  const fileIdSet = new Set(files.map((f) => f.id));
@@ -10574,7 +12183,11 @@ ${serializedHtml}
10574
12183
  // Group by source file so multiple nodes from the same source are
10575
12184
  // applied sequentially against the running source content.
10576
12185
  const sourceContentMap = new Map<string, string>();
10577
- for (const { draggedId, sourceFileId } of crossFileDrags) {
12186
+ const movedNodeIdByDraggedId = new Map<string, string>();
12187
+ for (const { draggedId, sourceFileId } of getLayerMoveIterationOrder(
12188
+ crossFileDrags,
12189
+ intent.placement,
12190
+ )) {
10578
12191
  const srcFile = files.find((f) => f.id === sourceFileId);
10579
12192
  if (!srcFile) continue;
10580
12193
  const currentSourceContent = getLayerMoveSourceContent({
@@ -10617,6 +12230,7 @@ ${serializedHtml}
10617
12230
  }
10618
12231
  sourceContentMap.set(sourceFileId, result.sourceHtml);
10619
12232
  nextDestContent = result.destHtml;
12233
+ movedNodeIdByDraggedId.set(draggedId, result.movedNodeId ?? nodeAttrId);
10620
12234
  moved = true;
10621
12235
  }
10622
12236
 
@@ -10630,12 +12244,16 @@ ${serializedHtml}
10630
12244
  ? buildCodeLayerTree(finalDestProjection)
10631
12245
  : [];
10632
12246
  const movedNodesAfterMove = movedIdOrder
10633
- .map((draggedId) => movedNodeSnapshots.get(draggedId))
10634
- .map((node) =>
10635
- node && finalDestProjection
10636
- ? findCodeLayerNodeInProjection(finalDestProjection, node)
10637
- : null,
10638
- )
12247
+ .map((draggedId) => {
12248
+ const node = movedNodeSnapshots.get(draggedId);
12249
+ return node && finalDestProjection
12250
+ ? findMovedCodeLayerNodeInProjection(
12251
+ finalDestProjection,
12252
+ node,
12253
+ movedNodeIdByDraggedId.get(draggedId),
12254
+ )
12255
+ : null;
12256
+ })
10639
12257
  .filter((node): node is CodeLayerNode => Boolean(node));
10640
12258
 
10641
12259
  if (movedNodesAfterMove.length > 0) {
@@ -10707,6 +12325,10 @@ ${serializedHtml}
10707
12325
  },
10708
12326
  ) => {
10709
12327
  const nextLayerIds = ids.filter((layerId) => !layerId.startsWith("__"));
12328
+ pendingOverviewScreenSelectionRef.current = null;
12329
+ pendingOverviewLayerSelectionRef.current = null;
12330
+ clearPendingOverviewLayerSelectionTimer();
12331
+ setCreatedOverviewLayerSelection(null);
10710
12332
  if (intent.additive && !intent.range) {
10711
12333
  const currentLayerIds = (
10712
12334
  intent.currentSelectedIds && intent.currentSelectedIds.length > 0
@@ -10723,14 +12345,7 @@ ${serializedHtml}
10723
12345
  fileIds,
10724
12346
  layerIds: additiveLayerIds,
10725
12347
  });
10726
- const toggledScreen =
10727
- getOverviewScreenIdsFromLayerSelection({
10728
- fileIds,
10729
- layerIds: [intent.id],
10730
- }).length > 0;
10731
- if (toggledScreen || selectedScreenIds.length > 0) {
10732
- setOverviewSelectedScreenIds(selectedScreenIds);
10733
- }
12348
+ setOverviewSelectedScreenIds(selectedScreenIds);
10734
12349
  }
10735
12350
  setSelectedElement(null);
10736
12351
  focusDesignInspectorForSelection();
@@ -10746,11 +12361,16 @@ ${serializedHtml}
10746
12361
  }
10747
12362
  const codeLayerOwner = codeLayerOwnerByNodeId.get(selectedId);
10748
12363
  if (codeLayerOwner) {
12364
+ if (viewModeRef.current === "overview") {
12365
+ pendingOverviewScreenSelectionRef.current = codeLayerOwner.fileId;
12366
+ pendingOverviewLayerSelectionRef.current = selectedId;
12367
+ }
10749
12368
  if (codeLayerOwner.fileId !== activeFile?.id) {
10750
12369
  setActiveFileId(codeLayerOwner.fileId);
10751
12370
  }
10752
12371
  const nextSelectionState = getSidebarCodeLayerSelectionState({
10753
12372
  currentViewMode: viewModeRef.current,
12373
+ ownerFileId: codeLayerOwner.fileId,
10754
12374
  overviewSelectedScreenIds,
10755
12375
  });
10756
12376
  viewModeRef.current = nextSelectionState.viewMode;
@@ -10795,6 +12415,7 @@ ${serializedHtml}
10795
12415
  },
10796
12416
  [
10797
12417
  activeFile?.id,
12418
+ clearPendingOverviewLayerSelectionTimer,
10798
12419
  codeLayerOwnerByNodeId,
10799
12420
  effectiveCodeLayerState,
10800
12421
  files,
@@ -11001,6 +12622,51 @@ ${serializedHtml}
11001
12622
 
11002
12623
  const zoomLabel = `${Math.round(zoom)}%`;
11003
12624
 
12625
+ const handleTokensApplied = useCallback(
12626
+ (resolvedCssVars: Record<string, string>) => {
12627
+ if (!canEditDesign || !id) return;
12628
+ setTweakSelections((prev) => ({
12629
+ ...prev,
12630
+ ...resolvedCssVars,
12631
+ }));
12632
+ queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
12633
+ if (!old || typeof old !== "object") return old;
12634
+ let currentData: Record<string, unknown> = {};
12635
+ if (typeof old.data === "string" && old.data) {
12636
+ try {
12637
+ const parsed = JSON.parse(old.data);
12638
+ if (
12639
+ parsed &&
12640
+ typeof parsed === "object" &&
12641
+ !Array.isArray(parsed)
12642
+ ) {
12643
+ currentData = parsed;
12644
+ }
12645
+ } catch {
12646
+ currentData = {};
12647
+ }
12648
+ }
12649
+ const currentSelections =
12650
+ currentData.tweakSelections &&
12651
+ typeof currentData.tweakSelections === "object" &&
12652
+ !Array.isArray(currentData.tweakSelections)
12653
+ ? currentData.tweakSelections
12654
+ : {};
12655
+ return {
12656
+ ...old,
12657
+ data: JSON.stringify({
12658
+ ...currentData,
12659
+ tweakSelections: {
12660
+ ...currentSelections,
12661
+ ...resolvedCssVars,
12662
+ },
12663
+ }),
12664
+ };
12665
+ });
12666
+ },
12667
+ [canEditDesign, id, queryClient],
12668
+ );
12669
+
11004
12670
  // Hooks must not be called conditionally; keep navigate as an effect so the
11005
12671
  // render phase stays pure. This branch is unreachable in practice because the
11006
12672
  // design.$id.tsx route always supplies an id param.
@@ -11065,7 +12731,7 @@ ${serializedHtml}
11065
12731
  <Button
11066
12732
  variant="ghost"
11067
12733
  size="sm"
11068
- className="h-8 shrink-0 cursor-pointer gap-0.5 rounded-md px-0 text-muted-foreground hover:bg-accent hover:text-foreground"
12734
+ className="ml-1 h-8 shrink-0 cursor-pointer gap-0.5 rounded-md px-0 text-muted-foreground hover:bg-accent hover:text-foreground"
11069
12735
  aria-label={t("designEditor.devicePreview")}
11070
12736
  >
11071
12737
  {deviceFrameIcon}
@@ -11107,10 +12773,10 @@ ${serializedHtml}
11107
12773
  <Button
11108
12774
  variant="ghost"
11109
12775
  size="icon"
11110
- className="size-9 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-accent hover:text-foreground [&_svg]:size-[calc(var(--spacing)*6.4)]"
12776
+ className="size-8 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-accent hover:text-foreground [&_svg]:size-[calc(var(--spacing)*5.5)]"
11111
12777
  aria-label={t("designEditor.more")}
11112
12778
  >
11113
- <AgentNativeMenuMark className="size-[calc(var(--spacing)*6.4)] text-foreground dark:text-white" />
12779
+ <AgentNativeMenuMark className="size-[calc(var(--spacing)*5.5)] text-foreground dark:text-white" />
11114
12780
  </Button>
11115
12781
  </DropdownMenuTrigger>
11116
12782
  <DropdownMenuContent
@@ -11385,43 +13051,124 @@ ${serializedHtml}
11385
13051
  </div>
11386
13052
 
11387
13053
  <div className="flex shrink-0 items-center gap-1">
11388
- <Tooltip>
11389
- <TooltipTrigger asChild>
11390
- <Button
11391
- variant="ghost"
11392
- size="icon"
11393
- className="size-8 cursor-pointer rounded-md text-foreground hover:bg-accent hover:text-foreground"
11394
- onClick={handleOpenDesignPreview}
11395
- disabled={!activeScreenPreviewUrl && !activeContent.trim()}
11396
- aria-label={t("designEditor.designPreview")}
11397
- >
11398
- <IconPlayerPlay className="size-5" />
11399
- </Button>
11400
- </TooltipTrigger>
11401
- <TooltipContent>{t("designEditor.designPreview")}</TooltipContent>
11402
- </Tooltip>
11403
-
11404
- {/* §6.6 — "Make this a real app" shortcut button (signed-in only).
11405
- Surfaces the migration CTA without requiring the project menu. */}
11406
- {isSignedIn && (
13054
+ <Popover
13055
+ open={publishWaitlistPopoverOpen}
13056
+ onOpenChange={(open) => {
13057
+ setPublishWaitlistPopoverOpen(open);
13058
+ setPublishWaitlistPopoverView("actions");
13059
+ if (open) {
13060
+ setPublishWaitlistError(null);
13061
+ }
13062
+ }}
13063
+ >
11407
13064
  <Tooltip>
11408
13065
  <TooltipTrigger asChild>
11409
- <Button
11410
- variant="ghost"
11411
- size="icon"
11412
- className="size-8 cursor-pointer rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
11413
- onClick={handleOpenMakeReal}
11414
- disabled={migrateMutation.isPending}
11415
- aria-label={"Make this a real app" /* i18n-ignore */}
11416
- >
11417
- <IconRocket className="size-4" />
11418
- </Button>
13066
+ <PopoverTrigger asChild>
13067
+ <Button
13068
+ variant="ghost"
13069
+ size="sm"
13070
+ className="h-8 cursor-pointer gap-1 rounded-md px-2 text-foreground hover:bg-accent hover:text-foreground"
13071
+ aria-label={"Preview or publish app" /* i18n-ignore */}
13072
+ >
13073
+ <IconPlayerPlay className="size-5" />
13074
+ <IconChevronDown className="size-3 opacity-70" />
13075
+ </Button>
13076
+ </PopoverTrigger>
11419
13077
  </TooltipTrigger>
11420
13078
  <TooltipContent>
11421
- {"Make this a real app" /* i18n-ignore */}
13079
+ {"Preview or publish app" /* i18n-ignore */}
11422
13080
  </TooltipContent>
11423
13081
  </Tooltip>
11424
- )}
13082
+ <PopoverContent
13083
+ align="end"
13084
+ sideOffset={8}
13085
+ className="z-[100010] w-72 space-y-3 p-3"
13086
+ >
13087
+ {publishWaitlistPopoverView === "actions" ? (
13088
+ <div className="space-y-1">
13089
+ <Button
13090
+ variant="ghost"
13091
+ className="h-9 w-full justify-start gap-2 px-2 text-sm"
13092
+ onClick={() => {
13093
+ handleOpenDesignPreview();
13094
+ setPublishWaitlistPopoverOpen(false);
13095
+ }}
13096
+ disabled={!activeScreenPreviewUrl && !activeContent.trim()}
13097
+ >
13098
+ <IconPlayerPlay className="size-4" />
13099
+ {t("designEditor.designPreview")}
13100
+ </Button>
13101
+ <Button
13102
+ variant="ghost"
13103
+ className="h-9 w-full justify-start gap-2 px-2 text-sm"
13104
+ onClick={() => setPublishWaitlistPopoverView("waitlist")}
13105
+ >
13106
+ <IconArrowUpRight className="size-4" />
13107
+ {"Publish app" /* i18n-ignore */}
13108
+ </Button>
13109
+ </div>
13110
+ ) : (
13111
+ <>
13112
+ <div className="space-y-1">
13113
+ <p className="text-sm font-medium text-foreground">
13114
+ {
13115
+ publishWaitlistJoined
13116
+ ? "You're on the waitlist" /* i18n-ignore */
13117
+ : "Publish app" /* i18n-ignore */
13118
+ }
13119
+ </p>
13120
+ <p className="text-xs leading-5 text-muted-foreground">
13121
+ {
13122
+ publishWaitlistJoined
13123
+ ? "We'll follow up when app publishing is ready for your workspace." /* i18n-ignore */
13124
+ : isSignedIn
13125
+ ? "Publish directly from Design is opening soon. Want early access?" /* i18n-ignore */
13126
+ : "Publish directly from Design is opening soon. Sign in to join the waitlist." /* i18n-ignore */
13127
+ }
13128
+ </p>
13129
+ </div>
13130
+ {publishWaitlistError ? (
13131
+ <p role="alert" className="text-xs text-destructive">
13132
+ {publishWaitlistError}
13133
+ </p>
13134
+ ) : null}
13135
+ <div className="flex justify-end gap-2">
13136
+ <Button
13137
+ variant="outline"
13138
+ size="sm"
13139
+ className="h-8 cursor-pointer"
13140
+ onClick={() => setPublishWaitlistPopoverOpen(false)}
13141
+ >
13142
+ {
13143
+ publishWaitlistJoined
13144
+ ? "Done" /* i18n-ignore */
13145
+ : "Not now" /* i18n-ignore */
13146
+ }
13147
+ </Button>
13148
+ {!publishWaitlistJoined && (
13149
+ <Button
13150
+ size="sm"
13151
+ className="h-8 cursor-pointer"
13152
+ onClick={() => void handleJoinPublishWaitlist()}
13153
+ disabled={joiningPublishWaitlist}
13154
+ >
13155
+ {joiningPublishWaitlist ? (
13156
+ <>
13157
+ <Spinner className="mr-1.5 size-3.5" />
13158
+ {"Joining" /* i18n-ignore */}
13159
+ </>
13160
+ ) : isSignedIn ? (
13161
+ "Add me to waitlist" /* i18n-ignore */
13162
+ ) : (
13163
+ "Sign in to join" /* i18n-ignore */
13164
+ )}
13165
+ </Button>
13166
+ )}
13167
+ </div>
13168
+ </>
13169
+ )}
13170
+ </PopoverContent>
13171
+ </Popover>
11425
13172
 
11426
13173
  {isSignedIn ? (
11427
13174
  <ShareButton
@@ -11434,19 +13181,43 @@ ${serializedHtml}
11434
13181
  shareUrlLabel={t("designEditor.shareEditorLink")}
11435
13182
  shareUrlDescription={t("designEditor.shareEditorLinkDescription")}
11436
13183
  shareTabs={designShareTabs}
11437
- popoverClassName="z-[100010] w-[min(860px,92vw)] p-6"
13184
+ popoverClassName={designSharePopoverClassName}
11438
13185
  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)]"
11439
13186
  />
11440
13187
  ) : (
11441
13188
  signedOutPersistenceActions
11442
13189
  )}
11443
-
11444
- {isSignedIn && <AgentToggleButton />}
11445
13190
  </div>
11446
13191
  </div>
11447
13192
  </div>
11448
13193
  );
11449
13194
 
13195
+ const leftContentWidth = Math.max(leftSidebarWidth, 320);
13196
+ const motionDockLauncherHidden = motionDockMounted || motionDockOpen;
13197
+ const motionDockLauncher =
13198
+ !embedded && activeFile ? (
13199
+ <div
13200
+ aria-hidden={motionDockLauncherHidden ? true : undefined}
13201
+ className={cn(
13202
+ "overflow-hidden border-t border-[var(--design-editor-panel-divider-color)] transition-[max-height,opacity,border-color] duration-200 ease-out",
13203
+ motionDockLauncherHidden
13204
+ ? "max-h-0 border-transparent opacity-0"
13205
+ : "max-h-9 opacity-100",
13206
+ )}
13207
+ >
13208
+ <button
13209
+ type="button"
13210
+ aria-label={"Expand motion dock" /* i18n-ignore */}
13211
+ className="flex h-8 w-full cursor-pointer items-center gap-2 bg-[var(--design-editor-panel-bg)] px-3 text-left text-[11px] font-medium uppercase tracking-wide text-muted-foreground outline-none transition-colors hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[var(--design-editor-accent-color)] disabled:cursor-default"
13212
+ disabled={motionDockLauncherHidden}
13213
+ onClick={() => setMotionDockOpenAnimated(true)}
13214
+ >
13215
+ <IconChevronRight className="size-3.5 shrink-0" />
13216
+ <span className="min-w-0 flex-1 truncate">Motion</span>
13217
+ </button>
13218
+ </div>
13219
+ ) : null;
13220
+
11450
13221
  return (
11451
13222
  // h-full not flex-1: the parent <main> uses overflow-y-auto, not flex,
11452
13223
  // so flex-1 on the child doesn't resolve to the available height. h-full
@@ -11834,14 +13605,12 @@ ${serializedHtml}
11834
13605
  "designEditor.shareEditorLinkDescription",
11835
13606
  )}
11836
13607
  shareTabs={designShareTabs}
11837
- popoverClassName="z-[100010] w-[min(860px,92vw)] p-6"
13608
+ popoverClassName={designSharePopoverClassName}
11838
13609
  triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 !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)]"
11839
13610
  />
11840
13611
  ) : !embedded ? (
11841
13612
  signedOutPersistenceActions
11842
13613
  ) : null}
11843
-
11844
- {!embedded && isSignedIn && <AgentToggleButton />}
11845
13614
  </div>
11846
13615
  </div>
11847
13616
  </header>
@@ -11849,42 +13618,130 @@ ${serializedHtml}
11849
13618
  {/* Main canvas area */}
11850
13619
  <div className="flex-1 flex overflow-hidden relative">
11851
13620
  {!embedded ? (
11852
- <div
11853
- className="relative flex min-h-0 shrink-0 flex-col border-r border-[var(--design-editor-panel-divider-color)] bg-[var(--design-editor-panel-bg)]"
11854
- style={{ width: leftSidebarWidth }}
11855
- >
11856
- <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-border px-2">
11857
- {projectMenu}
11858
- {projectTitleControl}
11859
- </div>
11860
- <div className="min-h-0 flex-1">
11861
- <LayersPanel
11862
- screens={layerPanelFiles}
11863
- activeScreenId={activeFileId ?? undefined}
11864
- screenOverviewActive={viewMode === "overview"}
11865
- files={
11866
- viewMode === "overview" ? overviewLayerPanelFiles : undefined
11867
- }
11868
- layers={
11869
- viewMode === "overview" ? undefined : activeLayerPanelNodes
11870
- }
11871
- selectedIds={selectedLayerIds}
11872
- expandedIds={expandedLayerIds}
11873
- searchQuery={layersSearchQuery}
11874
- onScreenSelect={handleSidebarScreenSelect}
11875
- onScreenOverview={handleSidebarScreenOverview}
11876
- onAddScreen={handleAddScreen}
11877
- onSearchQueryChange={setLayersSearchQuery}
11878
- onExpandedIdsChange={setExpandedLayerIds}
11879
- onSelectionChange={handleLayerSelectionChange}
11880
- onRename={handleLayerRename}
11881
- onToggleLocked={handleToggleLayerLocked}
11882
- onToggleHidden={handleToggleLayerHidden}
11883
- onHoverLayer={handleLayerHover}
11884
- onLeaveLayer={handleLayerLeave}
11885
- onMoveLayer={handleLayerMove}
11886
- canMoveLayer={canMoveLayer}
11887
- />
13621
+ <div className="relative flex min-h-0 shrink-0 border-r border-[var(--design-editor-panel-divider-color)] bg-[var(--design-editor-panel-bg)]">
13622
+ <DesignWorkspaceRail
13623
+ activePanel={activeLeftPanel}
13624
+ projectMenu={projectMenu}
13625
+ onPanelChange={setActiveLeftPanel}
13626
+ />
13627
+ <div
13628
+ className="flex min-h-0 shrink-0 flex-col bg-[var(--design-editor-panel-bg)] transition-[width] duration-150 ease-out"
13629
+ style={{ width: leftContentWidth }}
13630
+ >
13631
+ <div
13632
+ className={cn(
13633
+ "min-h-0 flex-1 flex-col overflow-hidden",
13634
+ activeLeftPanel === "file" ? "flex" : "hidden",
13635
+ )}
13636
+ >
13637
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-border px-3">
13638
+ {projectTitleControl}
13639
+ </div>
13640
+ <div className="min-h-0 flex-1">
13641
+ <LayersPanel
13642
+ screens={layerPanelFiles}
13643
+ activeScreenId={activeFileId ?? undefined}
13644
+ screenOverviewActive={viewMode === "overview"}
13645
+ files={
13646
+ viewMode === "overview"
13647
+ ? overviewLayerPanelFiles
13648
+ : undefined
13649
+ }
13650
+ layers={
13651
+ viewMode === "overview"
13652
+ ? undefined
13653
+ : activeLayerPanelNodes
13654
+ }
13655
+ selectedIds={layerPanelSelectedIds}
13656
+ expandedIds={layerPanelExpandedIds}
13657
+ searchQuery={layersSearchQuery}
13658
+ footer={motionDockLauncher}
13659
+ onScreenSelect={handleSidebarScreenSelect}
13660
+ onScreenOverview={handleSidebarScreenOverview}
13661
+ onAddScreen={handleAddScreen}
13662
+ onSearchQueryChange={setLayersSearchQuery}
13663
+ onExpandedIdsChange={setExpandedLayerIds}
13664
+ onSelectionChange={handleLayerSelectionChange}
13665
+ onRename={handleLayerRename}
13666
+ onToggleLocked={handleToggleLayerLocked}
13667
+ onToggleHidden={handleToggleLayerHidden}
13668
+ onHoverLayer={handleLayerHover}
13669
+ onLeaveLayer={handleLayerLeave}
13670
+ onMoveLayer={handleLayerMove}
13671
+ canMoveLayer={canMoveLayer}
13672
+ boardElements={
13673
+ viewMode === "overview" ? boardElements : undefined
13674
+ }
13675
+ />
13676
+ </div>
13677
+ </div>
13678
+ <div
13679
+ data-design-agent-panel
13680
+ className={cn(
13681
+ "min-h-0 flex-1 flex-col overflow-hidden",
13682
+ activeLeftPanel === "agent" ? "flex" : "hidden",
13683
+ )}
13684
+ >
13685
+ <AgentChatSurface
13686
+ mode="panel"
13687
+ className="min-h-0 flex-1 border-0 bg-transparent shadow-none"
13688
+ emptyStateText={t("chat.emptyState")}
13689
+ suggestions={[
13690
+ t("chat.suggestionLandingPage"),
13691
+ t("chat.suggestionBrandMatch"),
13692
+ t("chat.suggestionMobile"),
13693
+ ]}
13694
+ scope={designChatScope}
13695
+ browserTabId={browserTabId}
13696
+ />
13697
+ </div>
13698
+ <div
13699
+ className={cn(
13700
+ "min-h-0 flex-1 flex-col overflow-hidden",
13701
+ activeLeftPanel === "assets" ? "flex" : "hidden",
13702
+ )}
13703
+ >
13704
+ <div className="flex min-h-8 shrink-0 items-center border-b border-border/60 px-3">
13705
+ <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
13706
+ {t("designEditor.leftRail.assets")}
13707
+ </h3>
13708
+ </div>
13709
+ <AssetLibraryPanel context={designExtensionContext} />
13710
+ </div>
13711
+ <div
13712
+ className={cn(
13713
+ "min-h-0 flex-1 flex-col overflow-hidden",
13714
+ activeLeftPanel === "tools" ? "flex" : "hidden",
13715
+ )}
13716
+ >
13717
+ <DesignExtensionsPanel
13718
+ context={designExtensionContext}
13719
+ hideAssetLibrary
13720
+ title={t("designEditor.leftRail.tools")}
13721
+ />
13722
+ </div>
13723
+ <div
13724
+ className={cn(
13725
+ "min-h-0 flex-1 flex-col overflow-hidden",
13726
+ activeLeftPanel === "tokens" ? "flex" : "hidden",
13727
+ )}
13728
+ >
13729
+ {id ? (
13730
+ <>
13731
+ <div className="flex min-h-8 shrink-0 items-center border-b border-border/60 px-3">
13732
+ <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
13733
+ {t("designEditor.tokens.title")}
13734
+ </h3>
13735
+ </div>
13736
+ <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
13737
+ <TokensPanel
13738
+ designId={id}
13739
+ onTokensApplied={handleTokensApplied}
13740
+ />
13741
+ </div>
13742
+ </>
13743
+ ) : null}
13744
+ </div>
11888
13745
  </div>
11889
13746
  <div
11890
13747
  role="separator"
@@ -12022,9 +13879,34 @@ ${serializedHtml}
12022
13879
  onDismiss={() => setLocalSourceBannerDismissed(true)}
12023
13880
  />
12024
13881
  )}
13882
+ {/* "Apply to source" affordance: write the current editor
13883
+ content back to the local HTML/CSS file via the bridge.
13884
+ Only shown for localhost-backed screens where the route
13885
+ maps to an .html/.htm/.css file and the user has editor
13886
+ access. Opens the consent dialog on first use. */}
13887
+ {activeLocalhostRouteIsWritable && canEditDesign && id && (
13888
+ <div className="flex items-center gap-2 border-b border-border/50 bg-muted/30 px-3 py-1.5">
13889
+ <Button
13890
+ size="sm"
13891
+ variant="outline"
13892
+ className="h-6 gap-1.5 px-2 text-[11px]"
13893
+ disabled={applyToSourcePending}
13894
+ onClick={handleApplyToSource}
13895
+ >
13896
+ <IconDeviceFloppy className="size-3 shrink-0" />
13897
+ {applyToSourcePending
13898
+ ? t("designEditor.writingToSource")
13899
+ : activeLocalhostRelPath
13900
+ ? t("designEditor.applyToSourcePath", {
13901
+ path: activeLocalhostRelPath,
13902
+ })
13903
+ : t("designEditor.applyToSource")}
13904
+ </Button>
13905
+ </div>
13906
+ )}
12025
13907
  <div
12026
13908
  ref={canvasContainerRef}
12027
- className="relative mx-1 min-w-0 flex-1 overflow-hidden rounded-xl bg-[var(--design-editor-canvas-bg)]"
13909
+ className="relative min-w-0 flex-1 overflow-hidden bg-[var(--design-editor-canvas-bg)]"
12028
13910
  onPointerMove={handleCanvasPointerMove}
12029
13911
  >
12030
13912
  {/* Transparent shield that blocks pointer events reaching the
@@ -12074,11 +13956,87 @@ ${serializedHtml}
12074
13956
  onPrimitiveCreated={handlePrimitiveCreated}
12075
13957
  onPrimitiveReparent={handleOverviewPrimitiveReparent}
12076
13958
  onCrossScreenElementDrop={handleCrossScreenElementDrop}
13959
+ boardFileId={boardFileId}
13960
+ boardFileContent={boardFileContent}
13961
+ boardFrameGeometry={boardFrameGeometry}
13962
+ onBoardDrawPrimitive={
13963
+ canEditDesign ? handleBoardDrawPrimitive : undefined
13964
+ }
13965
+ boardEditMode={canEditDesign}
13966
+ onBoardElementSelect={
13967
+ boardFileId
13968
+ ? (info) =>
13969
+ handleScreenElementSelect(boardFileId, info)
13970
+ : undefined
13971
+ }
13972
+ onBoardElementHover={
13973
+ boardFileId
13974
+ ? (info) =>
13975
+ handleScreenElementHover(boardFileId, info)
13976
+ : undefined
13977
+ }
13978
+ onBoardVisualStyleChange={
13979
+ boardFileId
13980
+ ? (selector, styles, info) =>
13981
+ handleScreenVisualStyleChange(
13982
+ boardFileId,
13983
+ selector,
13984
+ styles,
13985
+ info,
13986
+ )
13987
+ : undefined
13988
+ }
13989
+ onBoardVisualStructureChange={
13990
+ boardFileId
13991
+ ? (
13992
+ selector,
13993
+ anchorSelector,
13994
+ placement,
13995
+ info,
13996
+ details,
13997
+ ) =>
13998
+ handleScreenVisualStructureChange(
13999
+ boardFileId,
14000
+ selector,
14001
+ anchorSelector,
14002
+ placement,
14003
+ info,
14004
+ details,
14005
+ )
14006
+ : undefined
14007
+ }
14008
+ onBoardVisualDuplicateChange={
14009
+ boardFileId
14010
+ ? (selector, cloneHtml, info, details) =>
14011
+ handleScreenVisualDuplicateChange(
14012
+ boardFileId,
14013
+ selector,
14014
+ cloneHtml,
14015
+ info,
14016
+ details,
14017
+ )
14018
+ : undefined
14019
+ }
14020
+ onBoardTextContentChange={
14021
+ boardFileId
14022
+ ? (selector, value, info, details) =>
14023
+ handleScreenTextContentChange(
14024
+ boardFileId,
14025
+ selector,
14026
+ value,
14027
+ info,
14028
+ details,
14029
+ )
14030
+ : undefined
14031
+ }
12077
14032
  onCreateScreenFrame={handleCreateScreenFrame}
12078
14033
  onDeleteSelection={handleDeleteOverviewSelection}
12079
- onSelectionChange={setOverviewSelectedScreenIds}
14034
+ onSelectionChange={handleOverviewScreenSelectionChange}
12080
14035
  onPick={(id) => {
12081
14036
  pendingOverviewScreenSelectionRef.current = null;
14037
+ pendingOverviewLayerSelectionRef.current = null;
14038
+ clearPendingOverviewLayerSelectionTimer();
14039
+ setCreatedOverviewLayerSelection(null);
12082
14040
  setSelectedElement(null);
12083
14041
  setHoveredElement(null);
12084
14042
  setSelectedLayerIdsState([id]);
@@ -12183,7 +14141,7 @@ ${serializedHtml}
12183
14141
  editorChromeScaleY={overviewCanvasZoom / 100}
12184
14142
  editMode={mode === "edit"}
12185
14143
  interactMode={false}
12186
- readOnly={!canEditDesign}
14144
+ readOnly={!screenIsActive || !canEditDesign}
12187
14145
  scaleMode={screenIsActive && activeTool === "scale"}
12188
14146
  clearSelectionRequest={
12189
14147
  overviewClearSelectionRequest
@@ -12471,53 +14429,10 @@ ${serializedHtml}
12471
14429
  onActiveTabChange={setActiveInspectorTab}
12472
14430
  tweaks={tweaks}
12473
14431
  tweakValues={tweakSelections}
12474
- extensionContext={designExtensionContext}
12475
14432
  readOnly={initialGenerationReadOnly}
14433
+ activeContent={activeContent}
14434
+ activeFileUpdatedAt={activeFile?.updatedAt ?? null}
12476
14435
  onComponentPropApplied={handleComponentPropApplied}
12477
- onTokensApplied={(resolvedCssVars) => {
12478
- if (!canEditDesign || !id) return;
12479
- setTweakSelections((prev) => ({
12480
- ...prev,
12481
- ...resolvedCssVars,
12482
- }));
12483
- queryClient.setQueryData(
12484
- ["action", "get-design", { id }],
12485
- (old: any) => {
12486
- if (!old || typeof old !== "object") return old;
12487
- let currentData: Record<string, unknown> = {};
12488
- if (typeof old.data === "string" && old.data) {
12489
- try {
12490
- const parsed = JSON.parse(old.data);
12491
- if (
12492
- parsed &&
12493
- typeof parsed === "object" &&
12494
- !Array.isArray(parsed)
12495
- ) {
12496
- currentData = parsed;
12497
- }
12498
- } catch {
12499
- currentData = {};
12500
- }
12501
- }
12502
- const currentSelections =
12503
- currentData.tweakSelections &&
12504
- typeof currentData.tweakSelections === "object" &&
12505
- !Array.isArray(currentData.tweakSelections)
12506
- ? currentData.tweakSelections
12507
- : {};
12508
- return {
12509
- ...old,
12510
- data: JSON.stringify({
12511
- ...currentData,
12512
- tweakSelections: {
12513
- ...currentSelections,
12514
- ...resolvedCssVars,
12515
- },
12516
- }),
12517
- };
12518
- },
12519
- );
12520
- }}
12521
14436
  onTweakChange={(tweakId, value) =>
12522
14437
  setTweakSelections((prev) => {
12523
14438
  if (!canEditDesign) return prev;
@@ -12599,52 +14514,21 @@ ${serializedHtml}
12599
14514
  ) : null}
12600
14515
  </div>
12601
14516
 
12602
- {/* Motion dock (§6.3) — collapsible bottom dock; visible when activeFile
12603
- is open and the user has opened it. Canvas remains visible above.
14517
+ {/* Motion dock (§6.3) — bottom timeline mounted while opening, open, or
14518
+ closing. Canvas remains visible above.
12604
14519
  Preview-only scrubbing fires a motion-preview postMessage to the
12605
- canvas iframe (via canvasIframeRef); "Write to CSS" fires
12606
- apply-motion-edit. */}
12607
- {!embedded && activeFile ? (
14520
+ canvas iframe; track/duration edits autosave through apply-motion-edit. */}
14521
+ {!embedded && activeFile && motionDockMounted ? (
12608
14522
  <MotionDock
12609
14523
  tracks={motionTracks}
12610
14524
  durationMs={motionDurationMs}
12611
14525
  open={motionDockOpen}
12612
- onOpenChange={setMotionDockOpen}
12613
- onTracksChange={setMotionTracks}
12614
- onDurationChange={setMotionDurationMs}
14526
+ onOpenChange={setMotionDockOpenAnimated}
14527
+ onTracksChange={handleMotionTracksChange}
14528
+ onDurationChange={handleMotionDurationChange}
12615
14529
  canvasIframeRef={canvasIframeRef}
12616
14530
  selectedTarget={motionSelectedTarget}
12617
- onApply={(tracks, durationMs) => {
12618
- if (!id) return;
12619
- applyMotionEditMutation.mutate(
12620
- {
12621
- designId: id,
12622
- fileId: activeFile.id,
12623
- tracks: tracks.map(({ label: _label, ...t }) => t),
12624
- durationMs,
12625
- includeContent: true,
12626
- },
12627
- {
12628
- onSuccess: (result) => {
12629
- const response = result as {
12630
- fileId?: unknown;
12631
- patchedContent?: unknown;
12632
- };
12633
- if (
12634
- typeof response.fileId === "string" &&
12635
- typeof response.patchedContent === "string"
12636
- ) {
12637
- applyFileContentUpdate(
12638
- response.fileId,
12639
- response.patchedContent,
12640
- { refreshPreview: response.fileId === activeFile.id },
12641
- );
12642
- }
12643
- },
12644
- },
12645
- );
12646
- }}
12647
- applying={applyMotionEditMutation.isPending}
14531
+ applying={motionAutosavePending}
12648
14532
  />
12649
14533
  ) : null}
12650
14534
 
@@ -12910,6 +14794,25 @@ ${serializedHtml}
12910
14794
  )}
12911
14795
  </DialogContent>
12912
14796
  </Dialog>
14797
+
14798
+ {/* Localhost write-consent dialog: shown when the agent or editor wants to
14799
+ persist an edit to a local HTML/CSS source file and no valid grant
14800
+ exists for the active connection yet. */}
14801
+ {id && activeLocalhostConnectionId && (
14802
+ <LocalhostWriteConsentDialog
14803
+ open={localhostWriteConsentOpen}
14804
+ onOpenChange={(next) => {
14805
+ if (!next) {
14806
+ localhostWriteConsentPayload?.onCancel();
14807
+ setLocalhostWriteConsentPayload(null);
14808
+ }
14809
+ setLocalhostWriteConsentOpen(next);
14810
+ }}
14811
+ designId={id}
14812
+ connectionId={localhostConsentConnectionId}
14813
+ payload={localhostWriteConsentPayload}
14814
+ />
14815
+ )}
12913
14816
  </div>
12914
14817
  );
12915
14818
  }