@agent-native/core 0.92.9 → 0.92.11

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 (345) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +181 -15
  5. package/corpus/core/src/agent/run-manager.ts +31 -4
  6. package/corpus/core/src/agent/run-store.ts +80 -0
  7. package/corpus/core/src/cli/design-connect.ts +433 -151
  8. package/corpus/core/src/cli/skills.ts +14 -0
  9. package/corpus/core/src/client/AssistantChat.tsx +25 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
  11. package/corpus/core/src/client/agent-chat.ts +165 -1
  12. package/corpus/core/src/client/chat/index.ts +9 -1
  13. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
  14. package/corpus/core/src/client/index.ts +5 -0
  15. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  16. package/corpus/core/src/collab/awareness-store.ts +23 -12
  17. package/corpus/core/src/collab/client.ts +16 -5
  18. package/corpus/core/src/collab/index.ts +4 -0
  19. package/corpus/core/src/collab/recent-edits.ts +65 -2
  20. package/corpus/core/src/deploy/build.ts +133 -5
  21. package/corpus/core/src/index.browser.ts +1 -0
  22. package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
  23. package/corpus/core/src/server/builder-design-systems.ts +30 -4
  24. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
  25. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
  26. package/corpus/templates/clips/AGENTS.md +4 -0
  27. package/corpus/templates/clips/actions/create-recording.ts +2 -0
  28. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
  29. package/corpus/templates/clips/actions/request-transcript.ts +102 -33
  30. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
  31. package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
  32. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +48 -4
  33. package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
  34. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  42. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  43. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  44. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  45. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
  46. package/corpus/templates/clips/app/routes/record.tsx +17 -4
  47. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  48. package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
  57. package/corpus/templates/clips/desktop/src/app.tsx +12 -2
  58. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
  60. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
  61. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
  62. package/corpus/templates/clips/desktop/src/styles.css +6 -3
  63. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
  64. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  65. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
  66. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
  67. package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
  68. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
  69. package/corpus/templates/clips/shared/recording-core.ts +12 -0
  70. package/corpus/templates/content/AGENTS.md +7 -6
  71. package/corpus/templates/content/actions/_document-flush.ts +132 -17
  72. package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
  73. package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
  74. package/corpus/templates/content/actions/link-notion-page.ts +2 -0
  75. package/corpus/templates/content/actions/pull-document.ts +5 -7
  76. package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
  77. package/corpus/templates/content/actions/push-notion-page.ts +10 -0
  78. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
  79. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
  80. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
  81. package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
  82. package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
  83. package/corpus/templates/content/app/i18n-data.ts +18 -0
  84. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
  85. package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
  86. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
  87. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
  88. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
  90. package/corpus/templates/design/AGENTS.md +45 -10
  91. package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
  92. package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
  93. package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
  94. package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
  95. package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
  96. package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
  97. package/corpus/templates/design/actions/generate-design.ts +79 -12
  98. package/corpus/templates/design/actions/generate-screens.ts +57 -12
  99. package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
  100. package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
  101. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
  102. package/corpus/templates/design/actions/import-design-source.ts +4 -31
  103. package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
  104. package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
  105. package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
  106. package/corpus/templates/design/actions/list-design-components.ts +104 -0
  107. package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
  108. package/corpus/templates/design/actions/open-component-source.ts +2 -4
  109. package/corpus/templates/design/actions/present-design-variants.ts +412 -16
  110. package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
  111. package/corpus/templates/design/actions/run-design-audit.ts +25 -3
  112. package/corpus/templates/design/actions/show-design-questions.ts +10 -0
  113. package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
  114. package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
  115. package/corpus/templates/design/actions/write-local-file.ts +25 -1
  116. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
  117. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
  118. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
  119. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
  120. package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
  121. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
  122. package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
  123. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
  124. package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
  125. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
  126. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
  127. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
  128. package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
  129. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
  130. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
  131. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
  132. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
  133. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
  134. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
  135. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
  136. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
  137. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
  138. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
  139. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
  140. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
  141. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
  142. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
  143. package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
  144. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
  145. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
  146. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
  147. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
  148. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
  149. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
  150. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
  151. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
  152. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
  153. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
  154. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
  155. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
  156. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
  157. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
  158. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
  159. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
  160. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
  161. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
  162. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
  163. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
  164. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
  165. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
  166. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
  167. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
  168. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
  169. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
  170. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
  171. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
  172. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
  173. package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
  174. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
  178. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
  179. package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
  180. package/corpus/templates/design/app/components/design/types.ts +33 -0
  181. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
  182. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
  183. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
  184. package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
  185. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
  186. package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
  187. package/corpus/templates/design/app/i18n-data.ts +1217 -10
  188. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
  189. package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
  190. package/corpus/templates/design/app/lib/design-import.ts +24 -0
  191. package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
  192. package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
  193. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
  194. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
  195. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
  196. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
  197. package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
  198. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
  199. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
  200. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
  201. package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
  202. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
  203. package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
  240. package/corpus/templates/design/e2e/global-setup.ts +3 -6
  241. package/corpus/templates/design/e2e/helpers.ts +27 -6
  242. package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
  243. package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
  244. package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
  245. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
  246. package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
  247. package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
  248. package/corpus/templates/design/server/plugins/db.ts +41 -0
  249. package/corpus/templates/design/server/source-workspace.ts +64 -0
  250. package/corpus/templates/design/shared/board-file.ts +13 -2
  251. package/corpus/templates/design/shared/canvas-math.ts +231 -33
  252. package/corpus/templates/design/shared/code-layer.ts +122 -8
  253. package/corpus/templates/design/shared/component-library.ts +161 -0
  254. package/corpus/templates/design/shared/db-conflict.ts +27 -0
  255. package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
  256. package/corpus/templates/design/shared/figma-url.ts +149 -0
  257. package/corpus/templates/design/shared/pen-path.ts +16 -7
  258. package/corpus/templates/design/shared/source-mode.ts +1 -0
  259. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
  260. package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
  261. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
  262. package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
  263. package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
  264. package/dist/agent/production-agent.d.ts +79 -6
  265. package/dist/agent/production-agent.d.ts.map +1 -1
  266. package/dist/agent/production-agent.js +161 -14
  267. package/dist/agent/production-agent.js.map +1 -1
  268. package/dist/agent/run-manager.d.ts.map +1 -1
  269. package/dist/agent/run-manager.js +29 -5
  270. package/dist/agent/run-manager.js.map +1 -1
  271. package/dist/agent/run-store.d.ts +46 -0
  272. package/dist/agent/run-store.d.ts.map +1 -1
  273. package/dist/agent/run-store.js +61 -0
  274. package/dist/agent/run-store.js.map +1 -1
  275. package/dist/cli/design-connect.d.ts +6 -0
  276. package/dist/cli/design-connect.d.ts.map +1 -1
  277. package/dist/cli/design-connect.js +336 -139
  278. package/dist/cli/design-connect.js.map +1 -1
  279. package/dist/cli/skills.d.ts.map +1 -1
  280. package/dist/cli/skills.js +14 -0
  281. package/dist/cli/skills.js.map +1 -1
  282. package/dist/client/AssistantChat.d.ts +2 -0
  283. package/dist/client/AssistantChat.d.ts.map +1 -1
  284. package/dist/client/AssistantChat.js +19 -3
  285. package/dist/client/AssistantChat.js.map +1 -1
  286. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  287. package/dist/client/MultiTabAssistantChat.js +38 -18
  288. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  289. package/dist/client/agent-chat.d.ts +60 -0
  290. package/dist/client/agent-chat.d.ts.map +1 -1
  291. package/dist/client/agent-chat.js +122 -1
  292. package/dist/client/agent-chat.js.map +1 -1
  293. package/dist/client/chat/index.d.ts +1 -1
  294. package/dist/client/chat/index.d.ts.map +1 -1
  295. package/dist/client/chat/index.js +1 -1
  296. package/dist/client/chat/index.js.map +1 -1
  297. package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
  298. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  299. package/dist/client/extensions/EmbeddedExtension.js +36 -2
  300. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  301. package/dist/client/index.d.ts +1 -1
  302. package/dist/client/index.d.ts.map +1 -1
  303. package/dist/client/index.js +1 -1
  304. package/dist/client/index.js.map +1 -1
  305. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  306. package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
  307. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  308. package/dist/collab/awareness-store.d.ts +5 -0
  309. package/dist/collab/awareness-store.d.ts.map +1 -1
  310. package/dist/collab/awareness-store.js +19 -12
  311. package/dist/collab/awareness-store.js.map +1 -1
  312. package/dist/collab/awareness.d.ts +2 -2
  313. package/dist/collab/awareness.d.ts.map +1 -1
  314. package/dist/collab/client.d.ts.map +1 -1
  315. package/dist/collab/client.js +13 -5
  316. package/dist/collab/client.js.map +1 -1
  317. package/dist/collab/index.d.ts +1 -0
  318. package/dist/collab/index.d.ts.map +1 -1
  319. package/dist/collab/index.js +1 -0
  320. package/dist/collab/index.js.map +1 -1
  321. package/dist/collab/recent-edits.d.ts +3 -1
  322. package/dist/collab/recent-edits.d.ts.map +1 -1
  323. package/dist/collab/recent-edits.js +57 -2
  324. package/dist/collab/recent-edits.js.map +1 -1
  325. package/dist/collab/routes.d.ts +1 -1
  326. package/dist/deploy/build.d.ts +24 -0
  327. package/dist/deploy/build.d.ts.map +1 -1
  328. package/dist/deploy/build.js +102 -5
  329. package/dist/deploy/build.js.map +1 -1
  330. package/dist/index.browser.d.ts +1 -1
  331. package/dist/index.browser.d.ts.map +1 -1
  332. package/dist/index.browser.js +1 -1
  333. package/dist/index.browser.js.map +1 -1
  334. package/dist/notifications/routes.d.ts +1 -1
  335. package/dist/observability/routes.d.ts +5 -5
  336. package/dist/progress/routes.d.ts +1 -1
  337. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  338. package/dist/server/agent-chat-plugin.js +87 -10
  339. package/dist/server/agent-chat-plugin.js.map +1 -1
  340. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  341. package/dist/server/builder-design-systems.d.ts +15 -0
  342. package/dist/server/builder-design-systems.d.ts.map +1 -1
  343. package/dist/server/builder-design-systems.js +9 -4
  344. package/dist/server/builder-design-systems.js.map +1 -1
  345. package/package.json +1 -1
@@ -733,6 +733,92 @@ export function dropDescendantsOfSelectedAncestors(
733
733
  });
734
734
  }
735
735
 
736
+ // BUG-LAYERS-MULTISELECT — Figma-parity multi-select: Cmd/Ctrl+Click toggles
737
+ // one row's membership in the selection; Shift+Click selects the visible
738
+ // range between the anchor row (the last row selected via a PLAIN click —
739
+ // Shift+Click never moves the anchor, so consecutive range clicks keep
740
+ // pivoting from the same row, matching Figma) and the clicked row; a plain
741
+ // click replaces the selection with just the clicked row. Extracted out of
742
+ // the row click handler (`selectNode` below) as a pure function so the
743
+ // range/toggle computation itself — anchor fallback when the anchor row
744
+ // scrolled out of view/was deleted, additive range-merge, and dropping a
745
+ // selected descendant whose ancestor is also selected — is unit-testable
746
+ // without mounting the panel.
747
+ export function computeLayerMultiSelectIds(args: {
748
+ id: string;
749
+ additive: boolean;
750
+ range: boolean;
751
+ currentSelectedIds: readonly string[];
752
+ anchor: string | null;
753
+ selectableVisibleIds: readonly string[];
754
+ visibleRows: readonly FlatLayerRow[];
755
+ // Source for the stale-anchor fallback search below. Defaults to
756
+ // `currentSelectedIds`. The real panel passes its own `selectedIds` prop
757
+ // here instead — pointer clicks pass a `currentSelectedIds` freshly
758
+ // re-read from the DOM (readSelectedIdsFromTree), which can transiently
759
+ // differ from the panel's own selection state, and the fallback has always
760
+ // pivoted off the latter.
761
+ anchorFallbackSelectedIds?: readonly string[];
762
+ }): { nextIds: string[]; nextAnchor: string | null } {
763
+ const {
764
+ id,
765
+ additive,
766
+ range,
767
+ currentSelectedIds,
768
+ anchor,
769
+ selectableVisibleIds,
770
+ visibleRows,
771
+ anchorFallbackSelectedIds = currentSelectedIds,
772
+ } = args;
773
+ const currentSelectedIdSet = new Set(currentSelectedIds);
774
+ let nextIds: string[];
775
+ // Only advance the anchor on plain clicks; Shift+clicks extend from the
776
+ // existing anchor so the pivot stays fixed across consecutive range
777
+ // clicks (returning `anchor` unchanged, including when it was never set).
778
+ // The one exception is the stale-anchor fallback just below, which DOES
779
+ // move the anchor even on a range click — it's re-pivoting onto a
780
+ // still-valid row, not starting a fresh selection.
781
+ let nextAnchor = range ? anchor : id;
782
+ if (range && anchor) {
783
+ let effectiveAnchor = anchor;
784
+ if (selectableVisibleIds.indexOf(effectiveAnchor) < 0) {
785
+ // Stale anchor (deleted / filtered / collapsed out of view): pivot from
786
+ // the last selected layer that is still visible & selectable, matching
787
+ // Figma's behavior instead of dropping the range to a single select.
788
+ const fallback = [...anchorFallbackSelectedIds]
789
+ .reverse()
790
+ .find((sid) => selectableVisibleIds.includes(sid));
791
+ if (fallback) {
792
+ effectiveAnchor = fallback;
793
+ nextAnchor = fallback;
794
+ }
795
+ }
796
+ const from = selectableVisibleIds.indexOf(effectiveAnchor);
797
+ const to = selectableVisibleIds.indexOf(id);
798
+ if (from >= 0 && to >= 0) {
799
+ const [start, end] = from < to ? [from, to] : [to, from];
800
+ const rangeIds = selectableVisibleIds.slice(start, end + 1);
801
+ const merged = additive
802
+ ? Array.from(new Set([...currentSelectedIds, ...rangeIds]))
803
+ : rangeIds;
804
+ // A range that spans an expanded parent and some of its children would
805
+ // otherwise co-select both. Normalize so a selected descendant whose
806
+ // ancestor is also selected gets dropped — the ancestor selection
807
+ // already implies it.
808
+ nextIds = dropDescendantsOfSelectedAncestors(merged, visibleRows);
809
+ } else {
810
+ nextIds = [id];
811
+ }
812
+ } else if (additive) {
813
+ nextIds = currentSelectedIdSet.has(id)
814
+ ? currentSelectedIds.filter((selectedId) => selectedId !== id)
815
+ : [...currentSelectedIds, id];
816
+ } else {
817
+ nextIds = [id];
818
+ }
819
+ return { nextIds, nextAnchor };
820
+ }
821
+
736
822
  export function getDraggedLayerIdsForRows(args: {
737
823
  selectedIds: readonly string[];
738
824
  nodeId: string;
@@ -745,12 +831,21 @@ export function getDraggedLayerIdsForRows(args: {
745
831
  // extract them from the parent being dragged. Optional only for
746
832
  // call-site/back-compat convenience; always pass it in the real panel.
747
833
  ancestorIdMap?: ReadonlyMap<string, string[]>;
834
+ // Full-tree node lookup used to keep locked layers out of a multi-layer
835
+ // drag payload. Figma lets a locked row remain selected alongside unlocked
836
+ // rows, but dragging one of the unlocked rows must not silently move the
837
+ // locked selection too. Optional for pure-helper/back-compat callers; the
838
+ // real panel always passes it from the same full roots used above.
839
+ nodeById?: ReadonlyMap<string, LayersPanelNode>;
748
840
  }): string[] {
749
841
  const rawDraggedIds = args.selectedIds.includes(args.nodeId)
750
842
  ? getTreeOrderedLayerIds(args.selectedIds, args.visibleRows)
751
843
  : [args.nodeId];
752
- const draggedIdSet = new Set(rawDraggedIds);
753
- return rawDraggedIds.filter((id) => {
844
+ const movableDraggedIds = rawDraggedIds.filter(
845
+ (id) => args.nodeById?.get(id)?.locked !== true,
846
+ );
847
+ const draggedIdSet = new Set(movableDraggedIds);
848
+ return movableDraggedIds.filter((id) => {
754
849
  const ancestorIds =
755
850
  args.ancestorIdMap?.get(id) ??
756
851
  args.visibleRows.find((row) => row.node.id === id)?.ancestorIds;
@@ -758,6 +853,21 @@ export function getDraggedLayerIdsForRows(args: {
758
853
  });
759
854
  }
760
855
 
856
+ /** Builds an id-to-node lookup over the full tree. Kept separate from the
857
+ * ancestor map because drag-start needs both node state (locked) and ancestry,
858
+ * while other callers only need one or the other. */
859
+ export function buildLayerNodeMap(
860
+ nodes: readonly LayersPanelNode[],
861
+ ): Map<string, LayersPanelNode> {
862
+ const map = new Map<string, LayersPanelNode>();
863
+ const visit = (node: LayersPanelNode) => {
864
+ map.set(node.id, node);
865
+ node.children?.forEach(visit);
866
+ };
867
+ nodes.forEach(visit);
868
+ return map;
869
+ }
870
+
761
871
  // Row context-menu actions (copy/duplicate/delete/group/reorder) operate on
762
872
  // the whole current selection when the right-clicked row is already part of
763
873
  // it (matching Figma), or on just that row otherwise (right-clicking an
@@ -1010,57 +1120,25 @@ function LayersPanelImpl(
1010
1120
  ) => {
1011
1121
  const currentSelectedIds =
1012
1122
  options.currentSelectedIds ?? selectedIdsRef.current;
1013
- const currentSelectedIdSet = new Set(currentSelectedIds);
1014
- let nextIds: string[];
1015
- if (options.range && lastSelectionAnchorRef.current) {
1016
- let anchor = lastSelectionAnchorRef.current;
1017
- if (selectableVisibleIds.indexOf(anchor) < 0) {
1018
- // Stale anchor (deleted / filtered / collapsed out of view): pivot from
1019
- // the last selected layer that is still visible & selectable, matching
1020
- // Figma's behavior instead of dropping the range to a single select.
1021
- const fallback = [...selectedIds]
1022
- .reverse()
1023
- .find((sid) => selectableVisibleIds.includes(sid));
1024
- if (fallback) {
1025
- anchor = fallback;
1026
- lastSelectionAnchorRef.current = fallback;
1027
- }
1028
- }
1029
- const from = selectableVisibleIds.indexOf(anchor);
1030
- const to = selectableVisibleIds.indexOf(id);
1031
- if (from >= 0 && to >= 0) {
1032
- const [start, end] = from < to ? [from, to] : [to, from];
1033
- const rangeIds = selectableVisibleIds.slice(start, end + 1);
1034
- const merged = options.additive
1035
- ? Array.from(new Set([...currentSelectedIds, ...rangeIds]))
1036
- : rangeIds;
1037
- // A range that spans an expanded parent and some of its children
1038
- // would otherwise co-select both. Normalize so a selected
1039
- // descendant whose ancestor is also selected gets dropped — the
1040
- // ancestor selection already implies it.
1041
- nextIds = dropDescendantsOfSelectedAncestors(
1042
- merged,
1043
- visibleRowsRef.current,
1044
- );
1045
- } else {
1046
- nextIds = [id];
1047
- }
1048
- } else if (options.additive) {
1049
- nextIds = currentSelectedIdSet.has(id)
1050
- ? currentSelectedIds.filter((selectedId) => selectedId !== id)
1051
- : [...currentSelectedIds, id];
1052
- } else {
1053
- nextIds = [id];
1054
- }
1055
- // Only advance the anchor on plain clicks; Shift+clicks extend from the
1056
- // existing anchor so the pivot stays fixed across consecutive range clicks.
1057
- if (!options.range) {
1058
- lastSelectionAnchorRef.current = id;
1059
- }
1123
+ // NOTE: the stale-anchor fallback intentionally searches the PANEL'S
1124
+ // OWN `selectedIds` prop, not `currentSelectedIds` (which pointer
1125
+ // clicks pass in freshly re-read from the DOM via readSelectedIdsFromTree
1126
+ // and can transiently differ) — matches the pre-extraction behavior.
1127
+ const { nextIds, nextAnchor } = computeLayerMultiSelectIds({
1128
+ id,
1129
+ additive: options.additive,
1130
+ range: options.range,
1131
+ currentSelectedIds,
1132
+ anchor: lastSelectionAnchorRef.current,
1133
+ selectableVisibleIds,
1134
+ visibleRows: visibleRowsRef.current,
1135
+ anchorFallbackSelectedIds: selectedIds,
1136
+ });
1137
+ lastSelectionAnchorRef.current = nextAnchor;
1060
1138
  lastPanelSelectionSignatureRef.current = nextIds.join("\0");
1061
1139
  onSelectionChange(nextIds, { id, selectedIds: nextIds, ...options });
1062
1140
  },
1063
- [onSelectionChange, selectableVisibleIds],
1141
+ [onSelectionChange, selectableVisibleIds, selectedIds],
1064
1142
  );
1065
1143
 
1066
1144
  const commitRename = useCallback(
@@ -1606,6 +1684,7 @@ const LayerRow = memo(function LayerRow({
1606
1684
  onFlipHorizontal,
1607
1685
  onFlipVertical,
1608
1686
  }: LayerRowProps) {
1687
+ const t = useT();
1609
1688
  const { node, depth, hasChildren, canAcceptChildren } = row;
1610
1689
  const isComponentLayer = layerNodeIsComponent(node);
1611
1690
  const selectable = node.selectable !== false;
@@ -1734,12 +1813,20 @@ const LayerRow = memo(function LayerRow({
1734
1813
  // descendant nested inside a currently-collapsed dragged ancestor is
1735
1814
  // still correctly recognized as a descendant and excluded from the drag
1736
1815
  // payload (see L13 / buildAncestorIdMap).
1737
- const ancestorIdMap = buildAncestorIdMap(rootsRef.current);
1816
+ const roots = rootsRef.current;
1817
+ const ancestorIdMap = buildAncestorIdMap(roots);
1818
+ const nodeById = buildLayerNodeMap(roots);
1819
+ // Search/collapse only changes which rows are painted; it must not change
1820
+ // the structural order of an existing multi-selection's drag payload.
1821
+ // Flatten the complete tree for ordering, while the visible rows remain
1822
+ // the interaction surface that actually started the gesture.
1823
+ const allRows = flattenRows(roots, new Set(), true);
1738
1824
  const draggedIds = getDraggedLayerIdsForRows({
1739
1825
  selectedIds: selectedIdsRef.current,
1740
1826
  nodeId: node.id,
1741
- visibleRows: visibleRowsRef.current,
1827
+ visibleRows: allRows,
1742
1828
  ancestorIdMap,
1829
+ nodeById,
1743
1830
  });
1744
1831
  event.dataTransfer.effectAllowed = "move";
1745
1832
  event.dataTransfer.setData("application/x-design-layer-id", node.id);
@@ -1747,6 +1834,27 @@ const LayerRow = memo(function LayerRow({
1747
1834
  "application/x-design-layer-ids",
1748
1835
  JSON.stringify(draggedIds),
1749
1836
  );
1837
+ // Figma parity: dragging 2+ selected layers shows a small "N layers"
1838
+ // count badge following the cursor instead of the browser's default
1839
+ // drag image — a screenshot of just the ONE row that received this
1840
+ // native dragstart event, even though every selected row moves together.
1841
+ // setDragImage requires the image element to be attached to the DOM at
1842
+ // the moment it's called, but not after — build an offscreen node here,
1843
+ // wire it up, and detach it on the next frame. Single-layer drags are
1844
+ // unaffected (kept exactly as before: the browser's own row snapshot).
1845
+ if (draggedIds.length > 1) {
1846
+ const ghost = document.createElement("div");
1847
+ ghost.textContent = t("layersPanel.dragGhostCount", {
1848
+ count: draggedIds.length,
1849
+ });
1850
+ ghost.className =
1851
+ "fixed left-[-9999px] top-[-9999px] pointer-events-none select-none whitespace-nowrap rounded-full border border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)] px-2.5 py-1 text-[11px] font-medium leading-none text-foreground shadow-[0_4px_16px_rgba(0,0,0,0.16),0_0_0_0.5px_rgba(0,0,0,0.08)]";
1852
+ document.body.appendChild(ghost);
1853
+ event.dataTransfer.setDragImage(ghost, -12, -12);
1854
+ requestAnimationFrame(() => {
1855
+ ghost.remove();
1856
+ });
1857
+ }
1750
1858
  // Store drag state at module level so handleDragOver can read it.
1751
1859
  // dataTransfer.getData() returns "" during dragover per the HTML spec.
1752
1860
  activeDragState = { sourceId: node.id, draggedIds };
@@ -2435,7 +2543,11 @@ function LayerGlyph({
2435
2543
  case "board-element":
2436
2544
  case "shape":
2437
2545
  case "rectangle":
2438
- return <RectangleLayerGlyph className={common} />;
2546
+ return shapeLayerUsesLayoutGlyph(node) ? (
2547
+ <LayoutLayerGlyph node={node} className={common} />
2548
+ ) : (
2549
+ <RectangleLayerGlyph className={common} />
2550
+ );
2439
2551
  case "vector":
2440
2552
  return <VectorLayerGlyph className={common} />;
2441
2553
  case "line":
@@ -2462,6 +2574,21 @@ function LayerGlyph({
2462
2574
  }
2463
2575
  }
2464
2576
 
2577
+ /**
2578
+ * A canvas rectangle starts as a shape, but nest-on-drop can promote it to a
2579
+ * flex/grid container. Once promoted, show the same auto-layout glyph as a
2580
+ * frame so the Layers tree reflects the layer's real layout behavior instead
2581
+ * of continuing to advertise it as a leaf rectangle.
2582
+ */
2583
+ export function shapeLayerUsesLayoutGlyph(
2584
+ node: Pick<LayersPanelNode, "type" | "layout">,
2585
+ ): boolean {
2586
+ return (
2587
+ ["board-element", "shape", "rectangle"].includes(node.type ?? "") &&
2588
+ Boolean(node.layout?.isFlexContainer || node.layout?.isGridContainer)
2589
+ );
2590
+ }
2591
+
2465
2592
  function layerNodeTagName(
2466
2593
  node: Pick<LayersPanelNode, "tagName" | "detail">,
2467
2594
  ): string | null {
@@ -21,7 +21,6 @@ export interface LocalhostWriteConsentPayload {
21
21
  /** Pending callback to invoke after user grants consent. */
22
22
  onGranted: (grant: {
23
23
  grantId: string;
24
- bridgeToken: string;
25
24
  rootPath: string;
26
25
  grantedUntil: string;
27
26
  }) => void;
@@ -58,7 +57,6 @@ export function LocalhostWriteConsentDialog({
58
57
  try {
59
58
  const result = await callAction<{
60
59
  grantId: string;
61
- bridgeToken: string;
62
60
  rootPath: string;
63
61
  grantedUntil: string;
64
62
  }>("grant-localhost-write-consent", {
@@ -139,6 +139,21 @@ function formatMsTick(ms: number): string {
139
139
  return `${s}s`;
140
140
  }
141
141
 
142
+ /**
143
+ * Whether committing on this key should also suppress the blur handler's own
144
+ * commit. Enter/Escape both trigger an explicit `.blur()` call right after
145
+ * committing so the field visually defocuses; without this guard, that
146
+ * synchronous blur re-invokes the (still stale, same-render) commit callback
147
+ * a second time in the same tick — a harmless-looking no-op for identical
148
+ * values, but it double-fires side effects like `onPlayheadChange` /
149
+ * `onDurationChange` and the preview postMessage. Mirrors
150
+ * `propInputKeyRequiresBlurGuard` in edit-panel/panel-primitives.tsx (the
151
+ * same bug class, fixed there for style-property fields).
152
+ */
153
+ export function motionFieldKeyRequiresBlurGuard(key: string): boolean {
154
+ return key === "Enter" || key === "Escape";
155
+ }
156
+
142
157
  /** Human label for an ease value: preset name when recognised, else raw. */
143
158
  function easeLabel(
144
159
  ease: MotionEase | undefined,
@@ -330,6 +345,12 @@ export function MotionDock({
330
345
 
331
346
  // Current-time field draft (null = displaying the live playhead).
332
347
  const [timeDraft, setTimeDraft] = useState<string | null>(null);
348
+ // See motionFieldKeyRequiresBlurGuard: set right before an Enter-triggered
349
+ // `.blur()` so the blur handler that fires in the same synchronous tick
350
+ // skips its own commit instead of double-invoking commitTimeDraft.
351
+ const skipNextTimeBlurCommitRef = useRef(false);
352
+ // Same guard for the duration field's Enter -> blur -> onBlur-commit path.
353
+ const skipNextDurationBlurCommitRef = useRef(false);
333
354
  /** Internal high-frequency playhead update — does NOT notify the parent. */
334
355
  const setPlayheadLocal = useCallback(
335
356
  (next: number) => {
@@ -750,6 +771,16 @@ export function MotionDock({
750
771
  timeDraft,
751
772
  ]);
752
773
 
774
+ // ── Duration field ────────────────────────────────────────────────────────
775
+ const commitDurationDraft = useCallback(() => {
776
+ const ms = parseInt(durationInput, 10);
777
+ if (!isNaN(ms) && ms >= 50) {
778
+ onDurationChange?.(ms);
779
+ } else {
780
+ setDurationInput(String(durationMs));
781
+ }
782
+ }, [durationInput, durationMs, onDurationChange]);
783
+
753
784
  // ── Space plays/pauses while the dock has focus (Figma parity) ───────────
754
785
  const handleDockKeyDown = useCallback(
755
786
  (e: ReactKeyboardEvent<HTMLDivElement>) => {
@@ -947,11 +978,32 @@ export function MotionDock({
947
978
  setTimeDraft(String(Math.round(playhead * durationMs)))
948
979
  }
949
980
  onChange={(e) => setTimeDraft(e.target.value)}
950
- onBlur={commitTimeDraft}
981
+ onBlur={() => {
982
+ if (skipNextTimeBlurCommitRef.current) {
983
+ skipNextTimeBlurCommitRef.current = false;
984
+ return;
985
+ }
986
+ commitTimeDraft();
987
+ }}
951
988
  onKeyDown={(e) => {
952
989
  if (e.key === "Enter") {
953
990
  e.preventDefault();
954
991
  commitTimeDraft();
992
+ // Without this, the blur triggered below re-enters
993
+ // commitTimeDraft() a second time in the same
994
+ // synchronous tick (see motionFieldKeyRequiresBlurGuard).
995
+ skipNextTimeBlurCommitRef.current =
996
+ motionFieldKeyRequiresBlurGuard(e.key);
997
+ (e.target as HTMLInputElement).blur();
998
+ return;
999
+ }
1000
+ if (e.key === "Escape") {
1001
+ e.preventDefault();
1002
+ // Cancel the in-progress edit — revert to displaying the
1003
+ // live playhead instead of committing the draft.
1004
+ setTimeDraft(null);
1005
+ skipNextTimeBlurCommitRef.current =
1006
+ motionFieldKeyRequiresBlurGuard(e.key);
955
1007
  (e.target as HTMLInputElement).blur();
956
1008
  }
957
1009
  }}
@@ -973,11 +1025,31 @@ export function MotionDock({
973
1025
  value={durationInput}
974
1026
  onChange={(e) => setDurationInput(e.target.value)}
975
1027
  onBlur={() => {
976
- const ms = parseInt(durationInput, 10);
977
- if (!isNaN(ms) && ms >= 50) {
978
- onDurationChange?.(ms);
979
- } else {
1028
+ if (skipNextDurationBlurCommitRef.current) {
1029
+ skipNextDurationBlurCommitRef.current = false;
1030
+ return;
1031
+ }
1032
+ commitDurationDraft();
1033
+ }}
1034
+ onKeyDown={(e) => {
1035
+ if (e.key === "Enter") {
1036
+ e.preventDefault();
1037
+ commitDurationDraft();
1038
+ // Same double-commit guard as the current-time field —
1039
+ // see motionFieldKeyRequiresBlurGuard.
1040
+ skipNextDurationBlurCommitRef.current =
1041
+ motionFieldKeyRequiresBlurGuard(e.key);
1042
+ (e.target as HTMLInputElement).blur();
1043
+ return;
1044
+ }
1045
+ if (e.key === "Escape") {
1046
+ e.preventDefault();
1047
+ // Cancel the in-progress edit — revert to the last
1048
+ // committed duration instead of persisting the draft.
980
1049
  setDurationInput(String(durationMs));
1050
+ skipNextDurationBlurCommitRef.current =
1051
+ motionFieldKeyRequiresBlurGuard(e.key);
1052
+ (e.target as HTMLInputElement).blur();
981
1053
  }
982
1054
  }}
983
1055
  className="h-5 w-16 px-1 !text-[11px] md:!text-[11px]"
@@ -2106,6 +2178,68 @@ interface CurveEditorProps {
2106
2178
  onChange: (value: [number, number, number, number]) => void;
2107
2179
  }
2108
2180
 
2181
+ /** Format a bezier control-point axis value for display (2 decimal places). */
2182
+ export function formatCurveAxisValue(n: number): string {
2183
+ return String(Math.round(n * 100) / 100);
2184
+ }
2185
+
2186
+ interface CurveNumberFieldProps {
2187
+ label: string;
2188
+ value: number;
2189
+ onChange: (n: number) => void;
2190
+ }
2191
+
2192
+ /**
2193
+ * One x1/y1/x2/y2 control-point number field in the bezier CurveEditor.
2194
+ * Keeps its own draft string instead of being fully controlled by
2195
+ * `formatCurveAxisValue(value)` so a user can type an in-progress value
2196
+ * ("-", "0.", "1.5") without the field snapping back to the last-committed,
2197
+ * rounded value on every keystroke. A prior version bound `value` directly to
2198
+ * the rounded prop: typing "0" then "." called onChange(0) (parseFloat("0.")
2199
+ * is a finite 0), which re-rendered the field back to "0" and silently
2200
+ * dropped the "." the instant it was typed — decimals (and a bare "-" before
2201
+ * a negative number) could never be entered. Mirrors the `focusedRef` resync
2202
+ * guard in edit-panel/panel-primitives.tsx's LengthField (same "mid-edit prop
2203
+ * stomp" bug class fixed there for style-property fields).
2204
+ */
2205
+ function CurveNumberField({ label, value, onChange }: CurveNumberFieldProps) {
2206
+ const [draft, setDraft] = useState(() => formatCurveAxisValue(value));
2207
+ const focusedRef = useRef(false);
2208
+
2209
+ useEffect(() => {
2210
+ if (!focusedRef.current) setDraft(formatCurveAxisValue(value));
2211
+ }, [value]);
2212
+
2213
+ return (
2214
+ <label className="flex flex-col items-stretch gap-0.5">
2215
+ <span className="text-center text-[9px] text-muted-foreground">
2216
+ {label}
2217
+ </span>
2218
+ <Input
2219
+ type="number"
2220
+ step={0.01}
2221
+ value={draft}
2222
+ onFocus={() => {
2223
+ focusedRef.current = true;
2224
+ }}
2225
+ onChange={(e) => {
2226
+ setDraft(e.target.value);
2227
+ const n = parseFloat(e.target.value);
2228
+ if (Number.isFinite(n)) onChange(n);
2229
+ }}
2230
+ onBlur={() => {
2231
+ focusedRef.current = false;
2232
+ // Snap back to the canonical rounded string — clears any
2233
+ // unparsed/partial trailing input ("-", "1.") left over from typing.
2234
+ setDraft(formatCurveAxisValue(value));
2235
+ }}
2236
+ className="h-5 px-1 !text-[10px] md:!text-[10px]"
2237
+ aria-label={label}
2238
+ />
2239
+ </label>
2240
+ );
2241
+ }
2242
+
2109
2243
  function CurveEditor({ value, onChange }: CurveEditorProps) {
2110
2244
  const t = useT();
2111
2245
  const [x1, y1, x2, y2] = value;
@@ -2151,22 +2285,11 @@ function CurveEditor({ value, onChange }: CurveEditorProps) {
2151
2285
  fieldValue: number,
2152
2286
  apply: (n: number) => [number, number, number, number],
2153
2287
  ) => (
2154
- <label className="flex flex-col items-stretch gap-0.5">
2155
- <span className="text-center text-[9px] text-muted-foreground">
2156
- {label}
2157
- </span>
2158
- <Input
2159
- type="number"
2160
- step={0.01}
2161
- value={String(Math.round(fieldValue * 100) / 100)}
2162
- onChange={(e) => {
2163
- const n = parseFloat(e.target.value);
2164
- if (Number.isFinite(n)) onChange(apply(n));
2165
- }}
2166
- className="h-5 px-1 !text-[10px] md:!text-[10px]"
2167
- aria-label={label}
2168
- />
2169
- </label>
2288
+ <CurveNumberField
2289
+ label={label}
2290
+ value={fieldValue}
2291
+ onChange={(n) => onChange(apply(n))}
2292
+ />
2170
2293
  );
2171
2294
 
2172
2295
  return (