@agent-native/core 0.80.10 → 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 (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Board object types shared between the frontend, actions, and agent skills.
3
+ *
4
+ * Board objects are persisted canvas primitives stored inside designs.data under
5
+ * the "boardObjects" key. Unlike code-layer nodes (which live inside screen HTML
6
+ * files), board objects float on the infinite canvas surface and are not bound
7
+ * to any screen iframe.
8
+ *
9
+ * The kind set mirrors DraftPrimitiveKind in MultiScreenCanvas.tsx exactly so
10
+ * the same shape-rendering logic (DraftPrimitiveContent / canvasPrimitiveReactStyle)
11
+ * can be reused for the visual output in BoardObjectLayer.
12
+ */
13
+
14
+ export type CanvasPrimitiveKindLike =
15
+ | "frame"
16
+ | "rectangle"
17
+ | "ellipse"
18
+ | "polygon"
19
+ | "star"
20
+ | "line"
21
+ | "arrow"
22
+ | "text"
23
+ | "path";
24
+
25
+ export interface BoardObjectEntry {
26
+ id: string;
27
+ kind: CanvasPrimitiveKindLike;
28
+ /**
29
+ * Bounding-box geometry in canvas units (same coordinate space as
30
+ * FrameGeometry — offset from the canvas origin before SURFACE_PADDING is
31
+ * applied). z is the stacking order; rotation is in degrees (clockwise).
32
+ */
33
+ geometry: {
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ rotation?: number;
39
+ z?: number;
40
+ };
41
+ /** CSS fill colour (background). Defaults to theme primary tint when absent. */
42
+ fill?: string;
43
+ /** CSS stroke colour (border). Defaults to theme primary stroke when absent. */
44
+ stroke?: string;
45
+ /** Stroke width in pixels. */
46
+ strokeWidth?: number;
47
+ /** Text content — only meaningful when kind === "text". */
48
+ text?: string;
49
+ /**
50
+ * SVG path data string — only meaningful when kind is "path", "line", or "arrow".
51
+ * Falls back to a pointsToPath conversion when absent and `points` is set.
52
+ */
53
+ pathData?: string;
54
+ /**
55
+ * Raw point list for polyline/polygon kinds without explicit pathData.
56
+ * Each entry is a canvas-space {x, y} point.
57
+ */
58
+ points?: Array<{ x: number; y: number }>;
59
+ /**
60
+ * When true the text box grows horizontally to fit its content
61
+ * (single-line auto-size mode).
62
+ */
63
+ autoSize?: boolean;
64
+ /** Human-readable label shown in the layers panel. */
65
+ name?: string;
66
+ /** ISO 8601 creation timestamp. */
67
+ createdAt: string;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Valid kind set for runtime validation
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const VALID_KINDS = new Set<string>([
75
+ "frame",
76
+ "rectangle",
77
+ "ellipse",
78
+ "polygon",
79
+ "star",
80
+ "line",
81
+ "arrow",
82
+ "text",
83
+ "path",
84
+ ]);
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // parseBoardObjects
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Parse the boardObjects map from an unknown value (e.g. from designs.data JSON).
92
+ *
93
+ * Accepts:
94
+ * - A Record<string, BoardObjectEntry> already deserialized from JSON.
95
+ * - A JSON string that encodes such a record.
96
+ * - null / undefined / any other value → returns an empty record.
97
+ *
98
+ * Individual entries that are missing the required `id`, `kind`, or `geometry`
99
+ * fields are silently dropped so a partially-corrupt payload never prevents
100
+ * the canvas from rendering.
101
+ */
102
+ export function parseBoardObjects(
103
+ value: unknown,
104
+ ): Record<string, BoardObjectEntry> {
105
+ let raw: unknown = value;
106
+
107
+ // Attempt to parse JSON strings.
108
+ if (typeof raw === "string") {
109
+ try {
110
+ raw = JSON.parse(raw);
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+
116
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
117
+ return {};
118
+ }
119
+
120
+ const result: Record<string, BoardObjectEntry> = {};
121
+ for (const [key, entry] of Object.entries(raw as Record<string, unknown>)) {
122
+ if (!isValidEntry(entry)) continue;
123
+ result[key] = entry as BoardObjectEntry;
124
+ }
125
+ return result;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // draftToBoardObjectEntry
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Minimal structural type matching DraftPrimitive in MultiScreenCanvas.tsx.
134
+ * Defined here so shared/board-objects.ts stays free of React/frontend imports.
135
+ */
136
+ export interface DraftPrimitiveLike {
137
+ id: string;
138
+ kind: CanvasPrimitiveKindLike;
139
+ geometry: {
140
+ x: number;
141
+ y: number;
142
+ width: number;
143
+ height: number;
144
+ rotation?: number;
145
+ z?: number;
146
+ };
147
+ points?: Array<{ x: number; y: number }>;
148
+ pathData?: string;
149
+ text?: string;
150
+ fill?: string;
151
+ stroke?: string;
152
+ strokeWidth?: number;
153
+ autoSize?: boolean;
154
+ }
155
+
156
+ /**
157
+ * Convert a DraftPrimitive (in-progress canvas shape) to a BoardObjectEntry
158
+ * ready for persistence.
159
+ *
160
+ * Called by MultiScreenCanvas when a drawn primitive is outside all frames
161
+ * (single-screen designs always absorb into the frame instead).
162
+ */
163
+ export function draftToBoardObjectEntry(
164
+ draft: DraftPrimitiveLike,
165
+ ): BoardObjectEntry {
166
+ const entry: BoardObjectEntry = {
167
+ id: draft.id,
168
+ kind: draft.kind,
169
+ geometry: { ...draft.geometry },
170
+ createdAt: new Date().toISOString(),
171
+ };
172
+ if (draft.fill !== undefined) entry.fill = draft.fill;
173
+ if (draft.stroke !== undefined) entry.stroke = draft.stroke;
174
+ if (draft.strokeWidth !== undefined) entry.strokeWidth = draft.strokeWidth;
175
+ if (draft.text !== undefined) entry.text = draft.text;
176
+ if (draft.pathData !== undefined) entry.pathData = draft.pathData;
177
+ if (draft.points !== undefined) entry.points = draft.points;
178
+ if (draft.autoSize !== undefined) entry.autoSize = draft.autoSize;
179
+ return entry;
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Board-object resize helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /** Geometry shape for resize calculations (same as BoardObjectEntry.geometry). */
187
+ export interface BoardObjectGeometry {
188
+ x: number;
189
+ y: number;
190
+ width: number;
191
+ height: number;
192
+ rotation?: number;
193
+ z?: number;
194
+ }
195
+
196
+ /**
197
+ * Apply a resize delta to a board object's geometry given the resize handle
198
+ * (one of "nw", "ne", "se", "sw") and the mouse delta (dx, dy) in canvas units.
199
+ *
200
+ * - "nw": move top-left corner → adjusts x, y, width, height.
201
+ * - "ne": move top-right corner → adjusts y, width, height.
202
+ * - "se": move bottom-right corner → adjusts width, height.
203
+ * - "sw": move bottom-left corner → adjusts x, width, height.
204
+ *
205
+ * Width and height are clamped to a minimum of 4 pixels.
206
+ */
207
+ export function applyBoardObjectResize(
208
+ origin: BoardObjectGeometry,
209
+ handle: string,
210
+ dx: number,
211
+ dy: number,
212
+ ): BoardObjectGeometry {
213
+ const MIN_SIZE = 4;
214
+ let { x, y, width, height } = origin;
215
+
216
+ switch (handle) {
217
+ case "nw":
218
+ x = origin.x + dx;
219
+ y = origin.y + dy;
220
+ width = Math.max(MIN_SIZE, origin.width - dx);
221
+ height = Math.max(MIN_SIZE, origin.height - dy);
222
+ // Keep anchor at bottom-right
223
+ if (origin.width - dx < MIN_SIZE) x = origin.x + origin.width - MIN_SIZE;
224
+ if (origin.height - dy < MIN_SIZE)
225
+ y = origin.y + origin.height - MIN_SIZE;
226
+ break;
227
+ case "ne":
228
+ y = origin.y + dy;
229
+ width = Math.max(MIN_SIZE, origin.width + dx);
230
+ height = Math.max(MIN_SIZE, origin.height - dy);
231
+ if (origin.height - dy < MIN_SIZE)
232
+ y = origin.y + origin.height - MIN_SIZE;
233
+ break;
234
+ case "se":
235
+ width = Math.max(MIN_SIZE, origin.width + dx);
236
+ height = Math.max(MIN_SIZE, origin.height + dy);
237
+ break;
238
+ case "sw":
239
+ x = origin.x + dx;
240
+ width = Math.max(MIN_SIZE, origin.width - dx);
241
+ height = Math.max(MIN_SIZE, origin.height + dy);
242
+ if (origin.width - dx < MIN_SIZE) x = origin.x + origin.width - MIN_SIZE;
243
+ break;
244
+ default:
245
+ break;
246
+ }
247
+
248
+ return { ...origin, x, y, width, height };
249
+ }
250
+
251
+ /**
252
+ * Map a resize handle string to the CSS cursor value shown while dragging.
253
+ */
254
+ export function getBoardObjectResizeCursor(handle: string): string {
255
+ switch (handle) {
256
+ case "nw":
257
+ case "se":
258
+ return "nwse-resize";
259
+ case "ne":
260
+ case "sw":
261
+ return "nesw-resize";
262
+ default:
263
+ return "se-resize";
264
+ }
265
+ }
266
+
267
+ function isValidEntry(entry: unknown): entry is BoardObjectEntry {
268
+ if (entry == null || typeof entry !== "object" || Array.isArray(entry)) {
269
+ return false;
270
+ }
271
+ const e = entry as Record<string, unknown>;
272
+ if (typeof e["id"] !== "string" || !e["id"]) return false;
273
+ if (typeof e["kind"] !== "string" || !VALID_KINDS.has(e["kind"])) {
274
+ return false;
275
+ }
276
+ if (typeof e["createdAt"] !== "string") return false;
277
+ const geo = e["geometry"];
278
+ if (geo == null || typeof geo !== "object" || Array.isArray(geo)) {
279
+ return false;
280
+ }
281
+ const g = geo as Record<string, unknown>;
282
+ return (
283
+ typeof g["x"] === "number" &&
284
+ typeof g["y"] === "number" &&
285
+ typeof g["width"] === "number" &&
286
+ typeof g["height"] === "number"
287
+ );
288
+ }
@@ -1659,6 +1659,17 @@ function layerNameFor(
1659
1659
  }
1660
1660
 
1661
1661
  function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
1662
+ // Canvas primitives (drawn shapes / board objects) carry their kind via
1663
+ // data-an-primitive so the layers panel shows a true shape/text/frame icon
1664
+ // instead of the generic code glyph. The marker wins over tag heuristics:
1665
+ // these primitives are <div>s, which would otherwise classify as "element".
1666
+ const primitiveKind = node.dataAttributes["data-an-primitive"];
1667
+ if (primitiveKind) {
1668
+ if (primitiveKind === "text") return "text";
1669
+ if (primitiveKind === "frame") return "frame";
1670
+ if (primitiveKind === "image") return "image";
1671
+ return "shape";
1672
+ }
1662
1673
  if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
1663
1674
  if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
1664
1675
  if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
@@ -3371,6 +3382,47 @@ export function applyVisualEdit(
3371
3382
  };
3372
3383
  }
3373
3384
 
3385
+ /**
3386
+ * Attributes injected by the editor at runtime that must NOT appear in
3387
+ * on-disk source files. These are stripped before any write-back so that
3388
+ * the saved file stays clean and matches what a developer would author.
3389
+ *
3390
+ * - `data-agent-native-node-id` — stable selection id stamped by the editor.
3391
+ * - `data-agent-native-layer-name` is intentionally kept: it is a
3392
+ * developer-authored attribute (the canonical layer-name hint) and is
3393
+ * useful in committed source. Only ephemeral runtime stamps are removed.
3394
+ */
3395
+ const EDITOR_ONLY_ATTRIBUTES: readonly string[] = ["data-agent-native-node-id"];
3396
+
3397
+ /**
3398
+ * Strip editor-only runtime attributes from an HTML string, returning clean
3399
+ * source suitable for writing back to disk.
3400
+ *
3401
+ * Currently removes `data-agent-native-node-id` (and any future attributes
3402
+ * listed in EDITOR_ONLY_ATTRIBUTES). The function operates on the raw HTML
3403
+ * string with a regex that handles both quoted forms and unquoted values, and
3404
+ * is safe to apply to already-clean source (idempotent).
3405
+ *
3406
+ * @param html The raw HTML string, potentially containing editor stamps.
3407
+ * @returns A new string with all editor-only attributes removed.
3408
+ */
3409
+ export function stripEditorOnlyAttributes(html: string): string {
3410
+ if (!html || typeof html !== "string") return html ?? "";
3411
+ let result = html;
3412
+ for (const attr of EDITOR_ONLY_ATTRIBUTES) {
3413
+ // Match the attribute with optional surrounding whitespace. The value may
3414
+ // be double-quoted, single-quoted, or unquoted (no spaces / > chars).
3415
+ // A leading \s+ is required so we only strip the attribute name+value pair
3416
+ // and leave surrounding markup intact.
3417
+ const re = new RegExp(
3418
+ `\\s+${attr.replace(/-/g, "\\-")}\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s"'=><\`]+)`,
3419
+ "gi",
3420
+ );
3421
+ result = result.replace(re, "");
3422
+ }
3423
+ return result;
3424
+ }
3425
+
3374
3426
  export interface MoveNodeBetweenDocumentsOptions {
3375
3427
  nodeId: string;
3376
3428
  anchorNodeId?: string;
@@ -40,6 +40,22 @@ export interface ComponentPropValue {
40
40
  value: string;
41
41
  }
42
42
 
43
+ /**
44
+ * Convert a JavaScript/React prop name to the `data-agent-native-prop-*`
45
+ * attribute name used in rendered HTML.
46
+ *
47
+ * HTML attribute names are lower-cased by parsers, so camelCase must be written
48
+ * as kebab-case to round-trip through {@link extractProps}.
49
+ */
50
+ export function propNameToDataAttribute(propName: string): string {
51
+ const suffix = propName
52
+ .trim()
53
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
54
+ .replace(/[\s_]+/g, "-")
55
+ .toLowerCase();
56
+ return `${COMPONENT_PROP_PREFIX}${suffix}`;
57
+ }
58
+
43
59
  // ─── Component instance ───────────────────────────────────────────────────────
44
60
 
45
61
  /**
@@ -118,6 +134,25 @@ export function componentNameFor(node: CodeLayerNode): string | null {
118
134
  return raw.trim();
119
135
  }
120
136
 
137
+ /**
138
+ * Match a selected component handle against both generated code-layer ids and
139
+ * durable runtime/source ids stamped into the rendered DOM.
140
+ */
141
+ export function componentNodeIdMatches(
142
+ node: CodeLayerNode,
143
+ nodeId: string,
144
+ ): boolean {
145
+ return (
146
+ node.id === nodeId ||
147
+ node.dataAttributes["data-agent-native-node-id"] === nodeId ||
148
+ node.dataAttributes["data-code-layer-id"] === nodeId ||
149
+ node.dataAttributes["data-layer-id"] === nodeId ||
150
+ node.dataAttributes["data-builder-id"] === nodeId ||
151
+ node.dataAttributes["data-loc"] === nodeId ||
152
+ node.attributes.id === nodeId
153
+ );
154
+ }
155
+
121
156
  /**
122
157
  * Extract simple prop values from `data-agent-native-prop-*` attributes on a
123
158
  * code-layer node.
@@ -47,6 +47,7 @@ export interface CompileResult {
47
47
  */
48
48
  export function compile(timeline: MotionTimeline): CompileResult {
49
49
  const { tracks, durationMs, defaultEase } = timeline;
50
+ assertSafeMotionCssToken(defaultEase, "defaultEase");
50
51
 
51
52
  if (!tracks || tracks.length === 0) {
52
53
  const css = reducedMotionBlock([]);
@@ -55,7 +56,15 @@ export function compile(timeline: MotionTimeline): CompileResult {
55
56
 
56
57
  const animNames: string[] = [];
57
58
  const kfBlocks: string[] = [];
58
- const ruleBlocks: string[] = [];
59
+ const rulesByTarget = new Map<
60
+ string,
61
+ {
62
+ names: string[];
63
+ durations: string[];
64
+ timings: string[];
65
+ fillModes: string[];
66
+ }
67
+ >();
59
68
 
60
69
  // Sort tracks for determinism: targetNodeId ASC, property ASC.
61
70
  const sorted = [...tracks].sort((a, b) => {
@@ -66,6 +75,7 @@ export function compile(timeline: MotionTimeline): CompileResult {
66
75
  for (const track of sorted) {
67
76
  const { targetNodeId, property, keyframes } = track;
68
77
  if (!keyframes || keyframes.length === 0) continue;
78
+ assertSafeMotionCssProperty(property, "track.property");
69
79
 
70
80
  const name = animationName(targetNodeId, property);
71
81
  animNames.push(name);
@@ -73,15 +83,31 @@ export function compile(timeline: MotionTimeline): CompileResult {
73
83
 
74
84
  const dur = formatDuration(durationMs);
75
85
  const ease = keyframes[0]?.ease ?? defaultEase;
76
- ruleBlocks.push(
77
- `[data-agent-native-node-id="${escAttr(targetNodeId)}"] {\n` +
78
- ` animation-name: ${name};\n` +
79
- ` animation-duration: ${dur};\n` +
80
- ` animation-timing-function: ${ease};\n` +
81
- ` animation-fill-mode: both;\n` +
86
+ assertSafeMotionCssToken(ease, "track ease");
87
+ const targetRule = rulesByTarget.get(targetNodeId) ?? {
88
+ names: [],
89
+ durations: [],
90
+ timings: [],
91
+ fillModes: [],
92
+ };
93
+ targetRule.names.push(name);
94
+ targetRule.durations.push(dur);
95
+ targetRule.timings.push(ease);
96
+ targetRule.fillModes.push("both");
97
+ rulesByTarget.set(targetNodeId, targetRule);
98
+ }
99
+
100
+ const ruleBlocks = [...rulesByTarget.entries()]
101
+ .sort(([a], [b]) => a.localeCompare(b))
102
+ .map(
103
+ ([targetNodeId, rule]) =>
104
+ `[data-agent-native-node-id="${escAttr(targetNodeId)}"] {\n` +
105
+ ` animation-name: ${rule.names.join(", ")};\n` +
106
+ ` animation-duration: ${rule.durations.join(", ")};\n` +
107
+ ` animation-timing-function: ${rule.timings.join(", ")};\n` +
108
+ ` animation-fill-mode: ${rule.fillModes.join(", ")};\n` +
82
109
  `}`,
83
110
  );
84
- }
85
111
 
86
112
  const css = [...kfBlocks, ...ruleBlocks, reducedMotionBlock(animNames)].join(
87
113
  "\n\n",
@@ -99,6 +125,7 @@ export function compile(timeline: MotionTimeline): CompileResult {
99
125
  */
100
126
  export function parse(css: string): MotionTrack[] {
101
127
  const tracks: MotionTrack[] = [];
128
+ const targetByAnimationName = parseAnimationTargets(css);
102
129
  const kfRe = /@keyframes\s+(an-motion-[^\s{]+)\s*\{/g;
103
130
  let m: RegExpExecArray | null;
104
131
 
@@ -112,15 +139,33 @@ export function parse(css: string): MotionTrack[] {
112
139
  if (body === null) continue;
113
140
 
114
141
  tracks.push({
115
- targetNodeId: decoded.targetNodeId,
142
+ targetNodeId: targetByAnimationName.get(fullName) ?? decoded.targetNodeId,
116
143
  property: decoded.property,
117
- keyframes: parseKeyframeBody(body),
144
+ keyframes: parseKeyframeBody(body, decoded.property),
118
145
  });
119
146
  }
120
147
 
121
148
  return tracks;
122
149
  }
123
150
 
151
+ /**
152
+ * Extract the CSS body from a managed `<style data-agent-native-motion>` block
153
+ * inside an HTML document. Returns `null` when the document has no managed block
154
+ * or the block is malformed.
155
+ */
156
+ export function extractManagedMotionCss(html: string): string | null {
157
+ const openRe = /<style\b(?=[^>]*\bdata-agent-native-motion\b)[^>]*>/i;
158
+ const openMatch = openRe.exec(html);
159
+ if (!openMatch) return null;
160
+
161
+ const bodyStart = openMatch.index + openMatch[0].length;
162
+ const afterOpen = html.slice(bodyStart);
163
+ const closeMatch = /<\s*\/\s*style\b[^>]*>/i.exec(afterOpen);
164
+ if (!closeMatch) return null;
165
+
166
+ return afterOpen.slice(0, closeMatch.index).trim();
167
+ }
168
+
124
169
  /**
125
170
  * Return the djb2 hash of a CSS string — useful for verifying stored
126
171
  * `compiled_hash` values without re-compiling a full timeline.
@@ -129,8 +174,51 @@ export function hashCss(css: string): string {
129
174
  return djb2(css);
130
175
  }
131
176
 
177
+ /**
178
+ * Reject caller-supplied CSS declaration values before interpolation into the
179
+ * managed motion stylesheet. Motion values still allow useful CSS functions
180
+ * such as `translateY(...)`, `calc(...)`, `cubic-bezier(...)`, and `var(...)`,
181
+ * but block declaration/rule/style breakouts and remote-resource hooks.
182
+ */
183
+ export function assertSafeMotionCssToken(value: string, field: string): string {
184
+ if (typeof value !== "string") {
185
+ throw new Error(`Invalid ${field}: expected a CSS string value.`);
186
+ }
187
+ if (value.trim().length === 0) {
188
+ throw new Error(`Invalid ${field}: motion CSS values cannot be empty.`);
189
+ }
190
+ if (CSS_TOKEN_CONTROL_RE.test(value) || CSS_TOKEN_BREAKOUT_RE.test(value)) {
191
+ throw new Error(
192
+ `Invalid ${field}: semicolons, braces, comments, angle brackets, control characters, and url(...) are not allowed in motion CSS values.`,
193
+ );
194
+ }
195
+ return value;
196
+ }
197
+
198
+ /**
199
+ * Validate that a CSS property name is a safe CSS identifier.
200
+ *
201
+ * Accepts standard and vendor-prefixed property names (e.g. "opacity",
202
+ * "transform", "-webkit-transform") and nothing else.
203
+ */
204
+ export function assertSafeMotionCssProperty(
205
+ property: string,
206
+ field: string,
207
+ ): string {
208
+ if (!/^-?[a-zA-Z][a-zA-Z0-9-]*$/.test(property)) {
209
+ throw new Error(
210
+ `Invalid ${field}: "${property}" is not a valid CSS property identifier. ` +
211
+ "Only ASCII letters, digits, hyphens, and an optional leading hyphen are allowed.",
212
+ );
213
+ }
214
+ return property;
215
+ }
216
+
132
217
  // ─── Internal helpers ─────────────────────────────────────────────────────────
133
218
 
219
+ const CSS_TOKEN_BREAKOUT_RE = /[;{}<>]|\/\*|\*\/|\burl\s*\(/i;
220
+ const CSS_TOKEN_CONTROL_RE = /[\u0000-\u001f\u007f]/;
221
+
134
222
  /**
135
223
  * Build a deterministic CSS animation name from a node id and CSS property.
136
224
  * Non-ident characters are replaced with `_`.
@@ -169,6 +257,8 @@ function keyframesBlock(
169
257
  const stops = sorted.map((kf) => {
170
258
  const pct = formatPercent(kf.t);
171
259
  const ease = kf.ease ?? defaultEase;
260
+ assertSafeMotionCssToken(kf.value, "keyframe value");
261
+ assertSafeMotionCssToken(ease, "keyframe ease");
172
262
  return (
173
263
  ` ${pct} {\n` +
174
264
  ` ${property}: ${kf.value};\n` +
@@ -221,6 +311,30 @@ function escAttr(value: string): string {
221
311
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
222
312
  }
223
313
 
314
+ function unescAttr(value: string): string {
315
+ return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
316
+ }
317
+
318
+ function parseAnimationTargets(css: string): Map<string, string> {
319
+ const targets = new Map<string, string>();
320
+ const ruleRe =
321
+ /\[data-agent-native-node-id="((?:\\.|[^"\\])*)"\]\s*\{([^}]*)\}/g;
322
+ let m: RegExpExecArray | null;
323
+
324
+ while ((m = ruleRe.exec(css)) !== null) {
325
+ const targetNodeId = unescAttr(m[1]);
326
+ const body = m[2];
327
+ const nameMatch = body.match(/animation-name\s*:\s*([^;]+)/);
328
+ if (!nameMatch) continue;
329
+ for (const name of nameMatch[1].split(",")) {
330
+ const trimmed = name.trim();
331
+ if (trimmed) targets.set(trimmed, targetNodeId);
332
+ }
333
+ }
334
+
335
+ return targets;
336
+ }
337
+
224
338
  /**
225
339
  * Find the content of the CSS block that starts just after position `start`
226
340
  * (i.e., just after the opening `{`). Returns `null` on unbalanced braces.
@@ -238,10 +352,14 @@ function extractBlock(css: string, start: number): string | null {
238
352
  }
239
353
 
240
354
  /** Parse the interior of a `@keyframes` block into `MotionKeyframe[]`. */
241
- function parseKeyframeBody(body: string): MotionKeyframe[] {
355
+ function parseKeyframeBody(body: string, property: string): MotionKeyframe[] {
242
356
  const frames: MotionKeyframe[] = [];
243
357
  const stopRe = /([\d.]+%|from|to)\s*\{([^}]*)\}/g;
244
358
  let m: RegExpExecArray | null;
359
+ const propRe = new RegExp(
360
+ `^\\s*${escapeRegExp(property)}\\s*:\\s*([^;]+)`,
361
+ "m",
362
+ );
245
363
 
246
364
  while ((m = stopRe.exec(body)) !== null) {
247
365
  const pctStr = m[1];
@@ -252,10 +370,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
252
370
  const easeMatch = content.match(/animation-timing-function\s*:\s*([^;]+)/);
253
371
  const ease = easeMatch ? (easeMatch[1].trim() as MotionEase) : undefined;
254
372
 
255
- // Extract the first non-timing property as the animated value.
256
- const propMatch = content.match(
257
- /^(?!.*animation-timing-function)[a-zA-Z-]+\s*:\s*([^;]+)/m,
258
- );
373
+ // Extract the animated property's value. The compiler emits the same
374
+ // property for every stop, so parsing by the decoded property avoids
375
+ // confusing `animation-timing-function` with the animated value.
376
+ const propMatch = content.match(propRe);
259
377
  const value = propMatch ? propMatch[1].trim() : "";
260
378
 
261
379
  frames.push({ t, value, ...(ease !== undefined ? { ease } : {}) });
@@ -264,6 +382,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
264
382
  return frames;
265
383
  }
266
384
 
385
+ function escapeRegExp(value: string): string {
386
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
387
+ }
388
+
267
389
  /**
268
390
  * djb2 string hash — deterministic, no crypto dependency.
269
391
  * Returns a 32-bit unsigned integer as a decimal string.
@@ -138,8 +138,8 @@ export const MOTION_PROPERTY_PRESETS: MotionPropertyPreset[] = [
138
138
  /**
139
139
  * Build a brand-new {@link MotionTrack} for a target node + property, seeded
140
140
  * with two keyframes (start/end) so it is immediately valid for both the live
141
- * preview bridge and the `apply-motion-edit` "Write to CSS" commit. Used by the
142
- * MotionDock "create first track" path.
141
+ * preview bridge and the `apply-motion-edit` managed CSS persist path. Used by
142
+ * the MotionDock "create first track" path.
143
143
  *
144
144
  * When `preset` is omitted, a neutral 0 → 1 opacity-style pair is used so the
145
145
  * track still compiles; callers normally pass a {@link MotionPropertyPreset}.