@agent-native/core 0.80.9 → 0.80.10
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 +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
- package/corpus/templates/design/app/i18n-data.ts +269 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, side-effect-free motion compiler for the Design Studio (§6.3).
|
|
3
|
+
*
|
|
4
|
+
* Converts a `MotionTimeline` (JSON tracks) into a single managed
|
|
5
|
+
* `<style data-agent-native-motion>` block and back again.
|
|
6
|
+
*
|
|
7
|
+
* Guarantees:
|
|
8
|
+
* - **Deterministic**: given the same input, output is byte-identical.
|
|
9
|
+
* - **Targets by node id**: rules use `[data-agent-native-node-id="<id>"]`
|
|
10
|
+
* selectors — no class/id coupling.
|
|
11
|
+
* - **Reduced-motion safe**: always emits an
|
|
12
|
+
* `@media (prefers-reduced-motion: reduce)` block that disables every
|
|
13
|
+
* generated animation.
|
|
14
|
+
* - **No dependencies**: uses djb2 (not crypto) for hashing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
MotionEase,
|
|
19
|
+
MotionKeyframe,
|
|
20
|
+
MotionTimeline,
|
|
21
|
+
MotionTrack,
|
|
22
|
+
} from "./motion-timeline";
|
|
23
|
+
|
|
24
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Result of {@link compile}. */
|
|
27
|
+
export interface CompileResult {
|
|
28
|
+
/**
|
|
29
|
+
* Full CSS string — the body of the managed `<style data-agent-native-motion>`
|
|
30
|
+
* block (no enclosing tag).
|
|
31
|
+
*/
|
|
32
|
+
css: string;
|
|
33
|
+
/**
|
|
34
|
+
* djb2 decimal hash of `css`. Stored in `motion_timeline.compiled_hash` so
|
|
35
|
+
* `apply-motion-edit` can detect drift between the JSON tracks and the CSS.
|
|
36
|
+
*/
|
|
37
|
+
hash: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compile a `MotionTimeline` into the CSS body of the managed style block.
|
|
42
|
+
*
|
|
43
|
+
* Output order (deterministic):
|
|
44
|
+
* 1. `@keyframes` blocks, sorted by animation name.
|
|
45
|
+
* 2. Element animation rules, sorted by node id then property.
|
|
46
|
+
* 3. `@media (prefers-reduced-motion: reduce)` block.
|
|
47
|
+
*/
|
|
48
|
+
export function compile(timeline: MotionTimeline): CompileResult {
|
|
49
|
+
const { tracks, durationMs, defaultEase } = timeline;
|
|
50
|
+
|
|
51
|
+
if (!tracks || tracks.length === 0) {
|
|
52
|
+
const css = reducedMotionBlock([]);
|
|
53
|
+
return { css, hash: djb2(css) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const animNames: string[] = [];
|
|
57
|
+
const kfBlocks: string[] = [];
|
|
58
|
+
const ruleBlocks: string[] = [];
|
|
59
|
+
|
|
60
|
+
// Sort tracks for determinism: targetNodeId ASC, property ASC.
|
|
61
|
+
const sorted = [...tracks].sort((a, b) => {
|
|
62
|
+
const cmp = a.targetNodeId.localeCompare(b.targetNodeId);
|
|
63
|
+
return cmp !== 0 ? cmp : a.property.localeCompare(b.property);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const track of sorted) {
|
|
67
|
+
const { targetNodeId, property, keyframes } = track;
|
|
68
|
+
if (!keyframes || keyframes.length === 0) continue;
|
|
69
|
+
|
|
70
|
+
const name = animationName(targetNodeId, property);
|
|
71
|
+
animNames.push(name);
|
|
72
|
+
kfBlocks.push(keyframesBlock(name, property, keyframes, defaultEase));
|
|
73
|
+
|
|
74
|
+
const dur = formatDuration(durationMs);
|
|
75
|
+
const ease = keyframes[0]?.ease ?? defaultEase;
|
|
76
|
+
ruleBlocks.push(
|
|
77
|
+
`[data-agent-native-node-id="${escAttr(targetNodeId)}"] {\n` +
|
|
78
|
+
` animation-name: ${name};\n` +
|
|
79
|
+
` animation-duration: ${dur};\n` +
|
|
80
|
+
` animation-timing-function: ${ease};\n` +
|
|
81
|
+
` animation-fill-mode: both;\n` +
|
|
82
|
+
`}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const css = [...kfBlocks, ...ruleBlocks, reducedMotionBlock(animNames)].join(
|
|
87
|
+
"\n\n",
|
|
88
|
+
);
|
|
89
|
+
return { css, hash: djb2(css) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse the CSS body of a managed `<style data-agent-native-motion>` block
|
|
94
|
+
* back into `MotionTrack[]`.
|
|
95
|
+
*
|
|
96
|
+
* Best-effort round-trip. Does not recover `durationMs` or `defaultEase`
|
|
97
|
+
* (those live on the DB row). Sufficient for drift detection and basic
|
|
98
|
+
* editing recovery.
|
|
99
|
+
*/
|
|
100
|
+
export function parse(css: string): MotionTrack[] {
|
|
101
|
+
const tracks: MotionTrack[] = [];
|
|
102
|
+
const kfRe = /@keyframes\s+(an-motion-[^\s{]+)\s*\{/g;
|
|
103
|
+
let m: RegExpExecArray | null;
|
|
104
|
+
|
|
105
|
+
while ((m = kfRe.exec(css)) !== null) {
|
|
106
|
+
const fullName = m[1];
|
|
107
|
+
const decoded = decodeAnimationName(fullName);
|
|
108
|
+
if (!decoded) continue;
|
|
109
|
+
|
|
110
|
+
const bodyStart = m.index + m[0].length;
|
|
111
|
+
const body = extractBlock(css, bodyStart);
|
|
112
|
+
if (body === null) continue;
|
|
113
|
+
|
|
114
|
+
tracks.push({
|
|
115
|
+
targetNodeId: decoded.targetNodeId,
|
|
116
|
+
property: decoded.property,
|
|
117
|
+
keyframes: parseKeyframeBody(body),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return tracks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Return the djb2 hash of a CSS string — useful for verifying stored
|
|
126
|
+
* `compiled_hash` values without re-compiling a full timeline.
|
|
127
|
+
*/
|
|
128
|
+
export function hashCss(css: string): string {
|
|
129
|
+
return djb2(css);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build a deterministic CSS animation name from a node id and CSS property.
|
|
136
|
+
* Non-ident characters are replaced with `_`.
|
|
137
|
+
*
|
|
138
|
+
* Format: `an-motion-<nodeId>--<property>`
|
|
139
|
+
*/
|
|
140
|
+
function animationName(nodeId: string, property: string): string {
|
|
141
|
+
const safe = (s: string) => s.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
142
|
+
return `an-motion-${safe(nodeId)}--${safe(property)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Reverse `animationName` — returns `null` when the name doesn't match. */
|
|
146
|
+
function decodeAnimationName(
|
|
147
|
+
name: string,
|
|
148
|
+
): { targetNodeId: string; property: string } | null {
|
|
149
|
+
const prefix = "an-motion-";
|
|
150
|
+
if (!name.startsWith(prefix)) return null;
|
|
151
|
+
const rest = name.slice(prefix.length);
|
|
152
|
+
const sep = rest.indexOf("--");
|
|
153
|
+
if (sep === -1) return null;
|
|
154
|
+
return { targetNodeId: rest.slice(0, sep), property: rest.slice(sep + 2) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build a `@keyframes` block for one (property, keyframes) pair.
|
|
159
|
+
* Each stop sets `animation-timing-function` to control easing to the NEXT
|
|
160
|
+
* stop (standard CSS keyframe easing semantics).
|
|
161
|
+
*/
|
|
162
|
+
function keyframesBlock(
|
|
163
|
+
name: string,
|
|
164
|
+
property: string,
|
|
165
|
+
keyframes: MotionKeyframe[],
|
|
166
|
+
defaultEase: MotionEase,
|
|
167
|
+
): string {
|
|
168
|
+
const sorted = [...keyframes].sort((a, b) => a.t - b.t);
|
|
169
|
+
const stops = sorted.map((kf) => {
|
|
170
|
+
const pct = formatPercent(kf.t);
|
|
171
|
+
const ease = kf.ease ?? defaultEase;
|
|
172
|
+
return (
|
|
173
|
+
` ${pct} {\n` +
|
|
174
|
+
` ${property}: ${kf.value};\n` +
|
|
175
|
+
` animation-timing-function: ${ease};\n` +
|
|
176
|
+
` }`
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
return `@keyframes ${name} {\n${stops.join("\n")}\n}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build the `@media (prefers-reduced-motion: reduce)` block.
|
|
184
|
+
* Always emitted so managed blocks are easily identified by parsers.
|
|
185
|
+
*/
|
|
186
|
+
function reducedMotionBlock(names: string[]): string {
|
|
187
|
+
if (names.length === 0) {
|
|
188
|
+
return `@media (prefers-reduced-motion: reduce) {\n /* no animations */\n}`;
|
|
189
|
+
}
|
|
190
|
+
// Disable every named animation on any element that carries it.
|
|
191
|
+
const selector = names.map((n) => `[style*="${n}"]`).join(",\n ");
|
|
192
|
+
return (
|
|
193
|
+
`@media (prefers-reduced-motion: reduce) {\n` +
|
|
194
|
+
` ${selector},\n` +
|
|
195
|
+
` [data-agent-native-node-id] {\n` +
|
|
196
|
+
` animation: none !important;\n` +
|
|
197
|
+
` }\n` +
|
|
198
|
+
`}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Format a millisecond duration as a CSS `<time>` value with trailing zeros stripped. */
|
|
203
|
+
function formatDuration(ms: number): string {
|
|
204
|
+
const s = (ms / 1000).toFixed(3).replace(/\.?0+$/, "");
|
|
205
|
+
return `${s}s`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Format a normalised time `t ∈ [0, 1]` as a CSS percentage string. */
|
|
209
|
+
function formatPercent(t: number): string {
|
|
210
|
+
if (t <= 0) return "0%";
|
|
211
|
+
if (t >= 1) return "100%";
|
|
212
|
+
const pct = Math.round(t * 10000) / 100;
|
|
213
|
+
return `${pct}%`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Escape a string for safe use as a CSS attribute selector value.
|
|
218
|
+
* Escapes `\` and `"`.
|
|
219
|
+
*/
|
|
220
|
+
function escAttr(value: string): string {
|
|
221
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find the content of the CSS block that starts just after position `start`
|
|
226
|
+
* (i.e., just after the opening `{`). Returns `null` on unbalanced braces.
|
|
227
|
+
*/
|
|
228
|
+
function extractBlock(css: string, start: number): string | null {
|
|
229
|
+
let depth = 1;
|
|
230
|
+
let i = start;
|
|
231
|
+
while (i < css.length && depth > 0) {
|
|
232
|
+
if (css[i] === "{") depth++;
|
|
233
|
+
else if (css[i] === "}") depth--;
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
if (depth !== 0) return null;
|
|
237
|
+
return css.slice(start, i - 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Parse the interior of a `@keyframes` block into `MotionKeyframe[]`. */
|
|
241
|
+
function parseKeyframeBody(body: string): MotionKeyframe[] {
|
|
242
|
+
const frames: MotionKeyframe[] = [];
|
|
243
|
+
const stopRe = /([\d.]+%|from|to)\s*\{([^}]*)\}/g;
|
|
244
|
+
let m: RegExpExecArray | null;
|
|
245
|
+
|
|
246
|
+
while ((m = stopRe.exec(body)) !== null) {
|
|
247
|
+
const pctStr = m[1];
|
|
248
|
+
const content = m[2];
|
|
249
|
+
const t =
|
|
250
|
+
pctStr === "from" ? 0 : pctStr === "to" ? 1 : parseFloat(pctStr) / 100;
|
|
251
|
+
|
|
252
|
+
const easeMatch = content.match(/animation-timing-function\s*:\s*([^;]+)/);
|
|
253
|
+
const ease = easeMatch ? (easeMatch[1].trim() as MotionEase) : undefined;
|
|
254
|
+
|
|
255
|
+
// Extract the first non-timing property as the animated value.
|
|
256
|
+
const propMatch = content.match(
|
|
257
|
+
/^(?!.*animation-timing-function)[a-zA-Z-]+\s*:\s*([^;]+)/m,
|
|
258
|
+
);
|
|
259
|
+
const value = propMatch ? propMatch[1].trim() : "";
|
|
260
|
+
|
|
261
|
+
frames.push({ t, value, ...(ease !== undefined ? { ease } : {}) });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return frames;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* djb2 string hash — deterministic, no crypto dependency.
|
|
269
|
+
* Returns a 32-bit unsigned integer as a decimal string.
|
|
270
|
+
*/
|
|
271
|
+
function djb2(str: string): string {
|
|
272
|
+
let hash = 5381;
|
|
273
|
+
for (let i = 0; i < str.length; i++) {
|
|
274
|
+
// eslint-disable-next-line no-bitwise
|
|
275
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
|
276
|
+
}
|
|
277
|
+
return hash.toString(10);
|
|
278
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion timeline types for the Design Studio motion dock (§6.3 + §4.3).
|
|
3
|
+
*
|
|
4
|
+
* A MotionTimeline compiles into a managed `<style data-agent-native-motion>`
|
|
5
|
+
* block. The CSS is the runtime truth; the JSON `tracks` aid editing only.
|
|
6
|
+
* `compiledHash` keeps the two in lockstep — `apply-motion-edit` must update
|
|
7
|
+
* both atomically.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type MotionEase =
|
|
11
|
+
| "linear"
|
|
12
|
+
| "ease"
|
|
13
|
+
| "ease-in"
|
|
14
|
+
| "ease-out"
|
|
15
|
+
| "ease-in-out"
|
|
16
|
+
| "step-start"
|
|
17
|
+
| "step-end"
|
|
18
|
+
| string; // cubic-bezier(...) or steps(...)
|
|
19
|
+
|
|
20
|
+
export interface MotionKeyframe {
|
|
21
|
+
/** Normalised time in [0, 1] where 0 = 0% and 1 = 100% of `durationMs`. */
|
|
22
|
+
t: number;
|
|
23
|
+
/** CSS property value at this keyframe (e.g. "0px", "1", "#ff0000"). */
|
|
24
|
+
value: string;
|
|
25
|
+
/** Per-keyframe easing applied between this keyframe and the next. */
|
|
26
|
+
ease?: MotionEase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One property track for a single target node.
|
|
31
|
+
* A node may have multiple tracks (e.g. opacity + transform).
|
|
32
|
+
*/
|
|
33
|
+
export interface MotionTrack {
|
|
34
|
+
/** Matches `data-agent-native-node-id` stamped on the target DOM element. */
|
|
35
|
+
targetNodeId: string;
|
|
36
|
+
/** CSS property name being animated (e.g. "opacity", "transform", "color"). */
|
|
37
|
+
property: string;
|
|
38
|
+
keyframes: MotionKeyframe[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A complete animation timeline scoped to one design + source + screen/file.
|
|
43
|
+
* A design may have many timelines (one per screen or logical animation group).
|
|
44
|
+
*/
|
|
45
|
+
export interface MotionTimeline {
|
|
46
|
+
id: string;
|
|
47
|
+
designId: string;
|
|
48
|
+
/**
|
|
49
|
+
* Opaque source reference identifying the screen or file this timeline
|
|
50
|
+
* belongs to (fileId for inline designs, routeId for localhost/fusion).
|
|
51
|
+
* `null` when scoped to the entire design.
|
|
52
|
+
*/
|
|
53
|
+
sourceRef: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* File path for real-app CSS module output.
|
|
56
|
+
* `null` for inline designs (CSS lives in the managed `<style>` block).
|
|
57
|
+
*/
|
|
58
|
+
filePath: string | null;
|
|
59
|
+
tracks: MotionTrack[];
|
|
60
|
+
/** Total animation duration in milliseconds. */
|
|
61
|
+
durationMs: number;
|
|
62
|
+
/** Default easing applied to keyframe intervals that omit a per-keyframe ease. */
|
|
63
|
+
defaultEase: MotionEase;
|
|
64
|
+
/**
|
|
65
|
+
* Hash of the compiled CSS output. Used by `apply-motion-edit` to detect
|
|
66
|
+
* drift between the stored JSON tracks and the managed `<style>` block.
|
|
67
|
+
* Cleared to `null` when tracks are edited but CSS has not yet been recompiled.
|
|
68
|
+
*/
|
|
69
|
+
compiledHash: string | null;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Animatable-property catalog + track factory ──────────────────────────────
|
|
75
|
+
//
|
|
76
|
+
// Shared by the MotionDock UI (the "add a track" picker) and unit tests. These
|
|
77
|
+
// are pure helpers so the first-track-creation flow can be tested without
|
|
78
|
+
// mounting React. They power the "create the FIRST track" path: a freshly
|
|
79
|
+
// selected element has no tracks, so the dock seeds a default track from one of
|
|
80
|
+
// these presets and the keyframes below.
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One animatable-property preset offered when creating a brand-new track.
|
|
84
|
+
* `from`/`to` seed the two default keyframes so the track is immediately
|
|
85
|
+
* compilable and previewable (a track with < 1 keyframe is rejected by
|
|
86
|
+
* `apply-motion-edit`).
|
|
87
|
+
*/
|
|
88
|
+
export interface MotionPropertyPreset {
|
|
89
|
+
/** CSS property animated by the track (e.g. "opacity", "transform"). */
|
|
90
|
+
property: string;
|
|
91
|
+
/** Human-readable label for the picker (e.g. "Opacity", "Slide up"). */
|
|
92
|
+
label: string;
|
|
93
|
+
/** Value at t = 0. */
|
|
94
|
+
from: string;
|
|
95
|
+
/** Value at t = 1. */
|
|
96
|
+
to: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Built-in property presets for the "add a track" picker. Ordered most-common
|
|
101
|
+
* first. Every preset is a valid CSS identifier accepted by
|
|
102
|
+
* `assertSafeCssProperty` and yields two safe keyframe values.
|
|
103
|
+
*/
|
|
104
|
+
export const MOTION_PROPERTY_PRESETS: MotionPropertyPreset[] = [
|
|
105
|
+
{ property: "opacity", label: "Fade (opacity)", from: "0", to: "1" },
|
|
106
|
+
{
|
|
107
|
+
property: "transform",
|
|
108
|
+
label: "Slide up (translateY)",
|
|
109
|
+
from: "translateY(16px)",
|
|
110
|
+
to: "translateY(0px)",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
property: "transform",
|
|
114
|
+
label: "Scale (zoom in)",
|
|
115
|
+
from: "scale(0.8)",
|
|
116
|
+
to: "scale(1)",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
property: "filter",
|
|
120
|
+
label: "Blur in",
|
|
121
|
+
from: "blur(8px)",
|
|
122
|
+
to: "blur(0px)",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
property: "color",
|
|
126
|
+
label: "Color",
|
|
127
|
+
from: "#000000",
|
|
128
|
+
to: "#000000",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
property: "background-color",
|
|
132
|
+
label: "Background color",
|
|
133
|
+
from: "#ffffff",
|
|
134
|
+
to: "#ffffff",
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build a brand-new {@link MotionTrack} for a target node + property, seeded
|
|
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.
|
|
143
|
+
*
|
|
144
|
+
* When `preset` is omitted, a neutral 0 → 1 opacity-style pair is used so the
|
|
145
|
+
* track still compiles; callers normally pass a {@link MotionPropertyPreset}.
|
|
146
|
+
*/
|
|
147
|
+
export function createMotionTrack(
|
|
148
|
+
targetNodeId: string,
|
|
149
|
+
property: string,
|
|
150
|
+
options: { from?: string; to?: string; ease?: MotionEase } = {},
|
|
151
|
+
): MotionTrack {
|
|
152
|
+
const from = options.from ?? "0";
|
|
153
|
+
const to = options.to ?? "1";
|
|
154
|
+
return {
|
|
155
|
+
targetNodeId,
|
|
156
|
+
property,
|
|
157
|
+
keyframes: [
|
|
158
|
+
{ t: 0, value: from, ...(options.ease ? { ease: options.ease } : {}) },
|
|
159
|
+
{ t: 1, value: to, ...(options.ease ? { ease: options.ease } : {}) },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build a track from a {@link MotionPropertyPreset}. Thin wrapper over
|
|
166
|
+
* {@link createMotionTrack} that forwards the preset's seed values.
|
|
167
|
+
*/
|
|
168
|
+
export function createMotionTrackFromPreset(
|
|
169
|
+
targetNodeId: string,
|
|
170
|
+
preset: MotionPropertyPreset,
|
|
171
|
+
ease?: MotionEase,
|
|
172
|
+
): MotionTrack {
|
|
173
|
+
return createMotionTrack(targetNodeId, preset.property, {
|
|
174
|
+
from: preset.from,
|
|
175
|
+
to: preset.to,
|
|
176
|
+
ease,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Return `true` when a track for the given (targetNodeId, property) pair already
|
|
182
|
+
* exists in `tracks`. The dock uses this to decide between "create a new track"
|
|
183
|
+
* and "add a keyframe to the existing track" so a property is never duplicated.
|
|
184
|
+
*/
|
|
185
|
+
export function hasTrackFor(
|
|
186
|
+
tracks: MotionTrack[],
|
|
187
|
+
targetNodeId: string,
|
|
188
|
+
property: string,
|
|
189
|
+
): boolean {
|
|
190
|
+
return tracks.some(
|
|
191
|
+
(t) => t.targetNodeId === targetNodeId && t.property === property,
|
|
192
|
+
);
|
|
193
|
+
}
|