@agent-native/core 0.80.11 → 0.81.1

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 (230) 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/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/AgentPanel.tsx +33 -1
  6. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  7. package/corpus/core/src/client/chat/index.ts +1 -0
  8. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  9. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  10. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  11. package/corpus/core/src/client/index.ts +1 -0
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  13. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  14. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  15. package/corpus/core/src/db/client.ts +29 -2
  16. package/corpus/core/src/provider-api/index.ts +29 -0
  17. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  18. package/corpus/core/src/styles/agent-native.css +54 -0
  19. package/corpus/core/src/usage/store.ts +2 -2
  20. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  21. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  22. package/corpus/templates/analytics/app/global.css +8 -0
  23. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  24. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  25. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  27. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  28. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  29. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  34. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  35. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  36. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  37. package/corpus/templates/assets/app/global.css +15 -0
  38. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  39. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  40. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  41. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  42. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  43. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  44. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  46. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  47. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  48. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  49. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  50. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  51. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  52. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  56. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  57. package/corpus/templates/design/AGENTS.md +27 -0
  58. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  59. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  60. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  61. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  62. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  63. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  64. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  65. package/corpus/templates/design/actions/export-zip.ts +23 -8
  66. package/corpus/templates/design/actions/generate-design.ts +53 -1
  67. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  68. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  69. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  70. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  71. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  72. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  73. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  74. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  75. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  76. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  77. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  78. package/corpus/templates/design/actions/navigate.ts +18 -3
  79. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  80. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  81. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  82. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  83. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  84. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  85. package/corpus/templates/design/actions/view-screen.ts +1 -1
  86. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  87. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  88. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  89. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  90. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  91. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  92. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  93. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  94. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  95. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  96. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  97. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  98. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  99. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  100. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  101. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  102. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  103. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  104. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  105. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  106. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  107. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  108. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  109. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  110. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  111. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  112. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  113. package/corpus/templates/design/app/global.css +7 -0
  114. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  115. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  116. package/corpus/templates/design/app/i18n-data.ts +358 -0
  117. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  118. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  119. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  120. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  144. package/corpus/templates/design/e2e/helpers.ts +168 -65
  145. package/corpus/templates/design/package.json +3 -1
  146. package/corpus/templates/design/server/db/schema.ts +23 -0
  147. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  148. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  149. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  150. package/corpus/templates/design/server/plugins/db.ts +39 -0
  151. package/corpus/templates/design/server/register-secrets.ts +38 -0
  152. package/corpus/templates/design/shared/board-file.ts +228 -0
  153. package/corpus/templates/design/shared/board-objects.ts +288 -0
  154. package/corpus/templates/design/shared/code-layer.ts +52 -0
  155. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  156. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  157. package/corpus/templates/design/tsconfig.json +2 -1
  158. package/corpus/templates/slides/AGENTS.md +9 -0
  159. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  160. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  161. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  162. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  163. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  164. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  166. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  167. package/corpus/templates/slides/package.json +1 -0
  168. package/corpus/templates/slides/vite.config.ts +1 -0
  169. package/dist/cli/design-connect.d.ts +25 -0
  170. package/dist/cli/design-connect.d.ts.map +1 -1
  171. package/dist/cli/design-connect.js +357 -18
  172. package/dist/cli/design-connect.js.map +1 -1
  173. package/dist/client/AgentPanel.d.ts +1 -1
  174. package/dist/client/AgentPanel.d.ts.map +1 -1
  175. package/dist/client/AgentPanel.js +18 -3
  176. package/dist/client/AgentPanel.js.map +1 -1
  177. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  178. package/dist/client/ConnectBuilderCard.js +1 -0
  179. package/dist/client/ConnectBuilderCard.js.map +1 -1
  180. package/dist/client/chat/index.d.ts +1 -1
  181. package/dist/client/chat/index.d.ts.map +1 -1
  182. package/dist/client/chat/index.js +1 -1
  183. package/dist/client/chat/index.js.map +1 -1
  184. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  185. package/dist/client/chat/run-recovery.js +5 -5
  186. package/dist/client/chat/run-recovery.js.map +1 -1
  187. package/dist/client/chat-view-transition.d.ts +6 -0
  188. package/dist/client/chat-view-transition.d.ts.map +1 -1
  189. package/dist/client/chat-view-transition.js +23 -2
  190. package/dist/client/chat-view-transition.js.map +1 -1
  191. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  192. package/dist/client/composer/PromptComposer.js +3 -2
  193. package/dist/client/composer/PromptComposer.js.map +1 -1
  194. package/dist/client/index.d.ts +1 -1
  195. package/dist/client/index.d.ts.map +1 -1
  196. package/dist/client/index.js +1 -1
  197. package/dist/client/index.js.map +1 -1
  198. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  199. package/dist/client/org/OrgSwitcher.js +11 -4
  200. package/dist/client/org/OrgSwitcher.js.map +1 -1
  201. package/dist/client/sharing/ShareButton.js +3 -3
  202. package/dist/client/sharing/ShareButton.js.map +1 -1
  203. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  204. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  205. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  206. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  207. package/dist/collab/routes.d.ts +1 -1
  208. package/dist/db/client.d.ts +1 -0
  209. package/dist/db/client.d.ts.map +1 -1
  210. package/dist/db/client.js +28 -2
  211. package/dist/db/client.js.map +1 -1
  212. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  213. package/dist/notifications/routes.d.ts +2 -2
  214. package/dist/observability/routes.d.ts +8 -8
  215. package/dist/progress/routes.d.ts +1 -1
  216. package/dist/provider-api/index.d.ts +5 -4
  217. package/dist/provider-api/index.d.ts.map +1 -1
  218. package/dist/provider-api/index.js +28 -0
  219. package/dist/provider-api/index.js.map +1 -1
  220. package/dist/resources/handlers.d.ts +3 -3
  221. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +25 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +43 -18
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/dist/server/transcribe-voice.d.ts +1 -1
  227. package/dist/styles/agent-native.css +54 -0
  228. package/dist/usage/store.js +2 -2
  229. package/dist/usage/store.js.map +1 -1
  230. 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;
@@ -125,6 +125,7 @@ export function compile(timeline: MotionTimeline): CompileResult {
125
125
  */
126
126
  export function parse(css: string): MotionTrack[] {
127
127
  const tracks: MotionTrack[] = [];
128
+ const targetByAnimationName = parseAnimationTargets(css);
128
129
  const kfRe = /@keyframes\s+(an-motion-[^\s{]+)\s*\{/g;
129
130
  let m: RegExpExecArray | null;
130
131
 
@@ -138,15 +139,33 @@ export function parse(css: string): MotionTrack[] {
138
139
  if (body === null) continue;
139
140
 
140
141
  tracks.push({
141
- targetNodeId: decoded.targetNodeId,
142
+ targetNodeId: targetByAnimationName.get(fullName) ?? decoded.targetNodeId,
142
143
  property: decoded.property,
143
- keyframes: parseKeyframeBody(body),
144
+ keyframes: parseKeyframeBody(body, decoded.property),
144
145
  });
145
146
  }
146
147
 
147
148
  return tracks;
148
149
  }
149
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
+
150
169
  /**
151
170
  * Return the djb2 hash of a CSS string — useful for verifying stored
152
171
  * `compiled_hash` values without re-compiling a full timeline.
@@ -292,6 +311,30 @@ function escAttr(value: string): string {
292
311
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
293
312
  }
294
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
+
295
338
  /**
296
339
  * Find the content of the CSS block that starts just after position `start`
297
340
  * (i.e., just after the opening `{`). Returns `null` on unbalanced braces.
@@ -309,10 +352,14 @@ function extractBlock(css: string, start: number): string | null {
309
352
  }
310
353
 
311
354
  /** Parse the interior of a `@keyframes` block into `MotionKeyframe[]`. */
312
- function parseKeyframeBody(body: string): MotionKeyframe[] {
355
+ function parseKeyframeBody(body: string, property: string): MotionKeyframe[] {
313
356
  const frames: MotionKeyframe[] = [];
314
357
  const stopRe = /([\d.]+%|from|to)\s*\{([^}]*)\}/g;
315
358
  let m: RegExpExecArray | null;
359
+ const propRe = new RegExp(
360
+ `^\\s*${escapeRegExp(property)}\\s*:\\s*([^;]+)`,
361
+ "m",
362
+ );
316
363
 
317
364
  while ((m = stopRe.exec(body)) !== null) {
318
365
  const pctStr = m[1];
@@ -323,10 +370,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
323
370
  const easeMatch = content.match(/animation-timing-function\s*:\s*([^;]+)/);
324
371
  const ease = easeMatch ? (easeMatch[1].trim() as MotionEase) : undefined;
325
372
 
326
- // Extract the first non-timing property as the animated value.
327
- const propMatch = content.match(
328
- /^(?!.*animation-timing-function)[a-zA-Z-]+\s*:\s*([^;]+)/m,
329
- );
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);
330
377
  const value = propMatch ? propMatch[1].trim() : "";
331
378
 
332
379
  frames.push({ t, value, ...(ease !== undefined ? { ease } : {}) });
@@ -335,6 +382,10 @@ function parseKeyframeBody(body: string): MotionKeyframe[] {
335
382
  return frames;
336
383
  }
337
384
 
385
+ function escapeRegExp(value: string): string {
386
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
387
+ }
388
+
338
389
  /**
339
390
  * djb2 string hash — deterministic, no crypto dependency.
340
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}.
@@ -17,5 +17,6 @@
17
17
  "react-router.config.ts",
18
18
  ".react-router/types/**/*",
19
19
  ".generated/**/*"
20
- ]
20
+ ],
21
+ "exclude": ["node_modules", "dist", "app/**/*.bridge.ts"]
21
22
  }
@@ -54,6 +54,15 @@ extending the editor's save path, enqueue a granular op (`patch-slide`,
54
54
  - `navigate` moves the UI to decks, slides, imports, and exports.
55
55
  - Use app actions for full deck/slide data instead of relying on ambient context.
56
56
 
57
+ ## Export Behavior
58
+
59
+ - Browser PowerPoint export uses the rendered slide DOM to generate native,
60
+ editable PPTX text/shapes/images. Do not replace it with full-slide images
61
+ unless the user explicitly asks for non-editable visual snapshots.
62
+ - Google Slides export is a PPTX import workflow: generate the same editable
63
+ PPTX and have the user import it into Google Slides. Creating a native Google
64
+ Slides file directly requires a separate Google Slides API batchUpdate path.
65
+
57
66
  ## Skills
58
67
 
59
68
  Read the relevant skill before deeper work:
@@ -129,6 +129,8 @@ interface EditorToolbarProps {
129
129
  onDuplicateDeck?: () => void;
130
130
  /** Export the deck as PDF */
131
131
  onExportPdf?: () => void;
132
+ /** Export the deck as PPTX */
133
+ onExportPptx?: () => Promise<void> | void;
132
134
  /** Active deck aspect ratio (defaults to 16:9 when omitted) */
133
135
  aspectRatio?: AspectRatio;
134
136
  /** Change the deck's aspect ratio */
@@ -251,6 +253,7 @@ export default function EditorToolbar({
251
253
  onTogglePinMode,
252
254
  onDuplicateDeck,
253
255
  onExportPdf,
256
+ onExportPptx,
254
257
  aspectRatio,
255
258
  onSetAspectRatio,
256
259
  designSystemTitle,
@@ -929,6 +932,7 @@ graph TD
929
932
  deckTitle={deckTitle}
930
933
  onDuplicate={onDuplicateDeck ?? (() => {})}
931
934
  onExportPdf={onExportPdf ?? (() => {})}
935
+ onExportPptx={onExportPptx ?? (() => {})}
932
936
  />
933
937
  </div>
934
938
 
@@ -23,6 +23,7 @@ interface ExportMenuProps {
23
23
  deckTitle: string;
24
24
  onDuplicate: () => void;
25
25
  onExportPdf: () => void;
26
+ onExportPptx: () => Promise<void> | void;
26
27
  onShareLink?: () => void;
27
28
  onShareTeam?: () => void;
28
29
  }
@@ -32,6 +33,7 @@ export function ExportMenu({
32
33
  deckTitle,
33
34
  onDuplicate,
34
35
  onExportPdf,
36
+ onExportPptx,
35
37
  onShareLink,
36
38
  onShareTeam,
37
39
  }: ExportMenuProps) {
@@ -66,30 +68,9 @@ export function ExportMenu({
66
68
  }
67
69
  };
68
70
 
69
- const fetchPptxExport = async () => {
70
- const res = await fetch(`${appBasePath()}/api/exports/pptx`, {
71
- method: "POST",
72
- headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({ deckId }),
74
- });
75
- if (!res.ok) {
76
- throw new Error(
77
- await readErrorMessage(res, t("editorExport.pptxFailed")),
78
- );
79
- }
80
- return {
81
- blob: await res.blob(),
82
- filename: filenameFromDisposition(
83
- res.headers.get("content-disposition"),
84
- ".pptx",
85
- ),
86
- };
87
- };
88
-
89
71
  const handleExportPptx = async () => {
90
72
  try {
91
- const { blob, filename } = await fetchPptxExport();
92
- triggerBlobDownload(blob, filename);
73
+ await onExportPptx();
93
74
  } catch (err) {
94
75
  console.error("Export failed:", err);
95
76
  toast({
@@ -105,8 +86,7 @@ export function ExportMenu({
105
86
 
106
87
  const handleExportGoogleSlides = async () => {
107
88
  try {
108
- const { blob, filename } = await fetchPptxExport();
109
- triggerBlobDownload(blob, filename);
89
+ await onExportPptx();
110
90
  toast({
111
91
  title: t("editorExport.googleSlidesDownloaded"),
112
92
  description: t("editorExport.googleSlidesImportHint"),
@@ -20,7 +20,7 @@ import { type AspectRatio, getAspectRatioDims } from "./aspect-ratios";
20
20
  * setting the attribute and re-assigning the same src. This is the root
21
21
  * cause of the "blank images in exported PDF" bug Rochkind reported.
22
22
  */
23
- async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
23
+ export async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
24
24
  const imgs = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
25
25
  await Promise.all(
26
26
  imgs.map(async (img) => {
@@ -60,6 +60,33 @@ async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
60
60
  );
61
61
  }
62
62
 
63
+ export function findSlideExportSource(
64
+ slideId: string,
65
+ slideIndex: number,
66
+ slideCount: number,
67
+ ): HTMLElement {
68
+ const candidates = Array.from(
69
+ document.querySelectorAll<HTMLElement>(
70
+ `[data-slide-canvas="${CSS.escape(slideId)}"]`,
71
+ ),
72
+ );
73
+ // Don't silently drop missing slides — a collapsed sidebar (mobile
74
+ // default) would otherwise produce a partial export with no warning.
75
+ if (candidates.length === 0) {
76
+ throw new Error(
77
+ `Slide ${slideIndex + 1} of ${slideCount} is not currently rendered. Open the slide sidebar and try again.`,
78
+ );
79
+ }
80
+
81
+ // A given slide can appear multiple times (sidebar thumbnail + active
82
+ // editor canvas); pick the one with the largest natural width so we
83
+ // capture full-resolution pixels even when the visible copy is scaled
84
+ // down via CSS transform.
85
+ return candidates.reduce((best, el) =>
86
+ el.offsetWidth > best.offsetWidth ? el : best,
87
+ );
88
+ }
89
+
63
90
  export async function exportDeckAsPdf(
64
91
  deckTitle: string,
65
92
  slideIds: string[],
@@ -99,25 +126,7 @@ export async function exportDeckAsPdf(
99
126
 
100
127
  for (let i = 0; i < slideIds.length; i++) {
101
128
  const slideId = slideIds[i];
102
- // A given slide can appear multiple times (sidebar thumbnail + active
103
- // editor canvas); pick the one with the largest natural width so we
104
- // capture full-resolution pixels even when the visible copy is scaled
105
- // down via CSS transform.
106
- const candidates = Array.from(
107
- document.querySelectorAll<HTMLElement>(
108
- `[data-slide-canvas="${CSS.escape(slideId)}"]`,
109
- ),
110
- );
111
- // Don't silently drop missing slides — a collapsed sidebar (mobile
112
- // default) would otherwise produce a partial PDF with no warning.
113
- if (candidates.length === 0) {
114
- throw new Error(
115
- `Slide ${i + 1} of ${slideIds.length} is not currently rendered. Open the slide sidebar and try again.`,
116
- );
117
- }
118
- const source = candidates.reduce((best, el) =>
119
- el.offsetWidth > best.offsetWidth ? el : best,
120
- );
129
+ const source = findSlideExportSource(slideId, i, slideIds.length);
121
130
 
122
131
  // Force CORS-enabled re-fetch on every cross-origin <img> before
123
132
  // capture — otherwise the canvas tainting check inside modern-screenshot