@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +53 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +419 -19
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
- package/corpus/core/src/client/chat-view-transition.ts +33 -2
- package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
- package/corpus/core/src/db/client.ts +29 -2
- package/corpus/core/src/provider-api/index.ts +29 -0
- package/corpus/core/src/server/core-routes-plugin.ts +58 -20
- package/corpus/core/src/styles/agent-native.css +54 -0
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
- package/corpus/templates/analytics/app/global.css +8 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/global.css +15 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +233 -52
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
- package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
- package/corpus/templates/design/AGENTS.md +27 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
- package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
- package/corpus/templates/design/actions/capture-design-state.ts +27 -4
- package/corpus/templates/design/actions/connect-localhost.ts +59 -22
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +21 -26
- package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
- package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
- package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
- package/corpus/templates/design/actions/navigate.ts +18 -3
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
- package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
- package/corpus/templates/design/actions/provider-api-request.ts +3 -3
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
- package/corpus/templates/design/actions/run-design-audit.ts +74 -10
- package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/actions/write-local-file.ts +213 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
- package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
- package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
- package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
- package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/global.css +7 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +369 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
- package/corpus/templates/design/app/pages/Index.tsx +5 -1
- package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/e2e/helpers.ts +168 -65
- package/corpus/templates/design/package.json +3 -1
- package/corpus/templates/design/server/db/schema.ts +23 -0
- package/corpus/templates/design/server/handlers/uploads.ts +22 -1
- package/corpus/templates/design/server/lib/provider-api.ts +7 -4
- package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
- package/corpus/templates/design/server/plugins/db.ts +39 -0
- package/corpus/templates/design/server/register-secrets.ts +38 -0
- package/corpus/templates/design/shared/board-file.ts +228 -0
- package/corpus/templates/design/shared/board-objects.ts +288 -0
- package/corpus/templates/design/shared/code-layer.ts +52 -0
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +137 -15
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/corpus/templates/design/tsconfig.json +2 -1
- package/corpus/templates/slides/AGENTS.md +9 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
- package/corpus/templates/slides/app/vite-env.d.ts +18 -0
- package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/vite.config.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +25 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +357 -18
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -0
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +5 -5
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/chat-view-transition.d.ts +6 -0
- package/dist/client/chat-view-transition.d.ts.map +1 -1
- package/dist/client/chat-view-transition.js +23 -2
- package/dist/client/chat-view-transition.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +3 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +11 -4
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -3
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +20 -6
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +28 -2
- package/dist/db/client.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/index.d.ts +5 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -0
- package/dist/provider-api/index.js.map +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +43 -18
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +54 -0
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- 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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
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`
|
|
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}.
|