@agent-native/core 0.85.5 → 0.85.7

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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +1 -1
  5. package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
  6. package/corpus/core/src/collab/presence.ts +63 -3
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
  8. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
  9. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
  10. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
  11. package/corpus/templates/design/AGENTS.md +40 -4
  12. package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
  14. package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
  15. package/corpus/templates/design/actions/connect-localhost.ts +25 -18
  16. package/corpus/templates/design/actions/edit-design.ts +5 -3
  17. package/corpus/templates/design/actions/export-pdf.ts +8 -1
  18. package/corpus/templates/design/actions/export-zip.ts +10 -1
  19. package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
  20. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
  21. package/corpus/templates/design/actions/insert-asset.ts +190 -7
  22. package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
  23. package/corpus/templates/design/actions/present-design-variants.ts +12 -8
  24. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
  25. package/corpus/templates/design/actions/run-design-audit.ts +7 -4
  26. package/corpus/templates/design/actions/write-local-file.ts +47 -22
  27. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
  28. package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
  29. package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
  30. package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1520 -410
  32. package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
  34. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
  36. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
  37. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
  38. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
  39. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
  40. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
  41. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
  42. package/corpus/templates/design/app/components/design/types.ts +16 -0
  43. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
  44. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
  45. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  46. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
  47. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  48. package/corpus/templates/design/app/i18n-data.ts +18 -0
  49. package/corpus/templates/design/app/lib/design-import.ts +95 -0
  50. package/corpus/templates/design/app/pages/DesignEditor.tsx +4015 -840
  51. package/corpus/templates/design/changelog/2026-07-02-creating-two-frames-in-a-row-no-longer-freezes-and-reloads-t.md +6 -0
  52. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
  53. package/corpus/templates/design/changelog/2026-07-03-design-retries-stalled-screen-edits-with-a-smaller-safer-fol.md +6 -0
  54. package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
  55. package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
  60. package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
  61. package/corpus/templates/design/server/lib/design-export.ts +43 -1
  62. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  63. package/corpus/templates/design/shared/board-file.ts +25 -5
  64. package/corpus/templates/design/shared/canvas-math.ts +754 -9
  65. package/corpus/templates/design/shared/code-layer.ts +304 -26
  66. package/corpus/templates/design/shared/color-utils.ts +84 -0
  67. package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
  68. package/corpus/templates/design/shared/motion-compiler.ts +75 -31
  69. package/corpus/templates/design/shared/motion-timeline.ts +335 -0
  70. package/corpus/templates/design/shared/pen-path.ts +200 -23
  71. package/dist/client/AssistantChat.js +1 -1
  72. package/dist/client/AssistantChat.js.map +1 -1
  73. package/dist/client/use-pinch-zoom.d.ts.map +1 -1
  74. package/dist/client/use-pinch-zoom.js +76 -11
  75. package/dist/client/use-pinch-zoom.js.map +1 -1
  76. package/dist/collab/awareness.d.ts +2 -2
  77. package/dist/collab/awareness.d.ts.map +1 -1
  78. package/dist/collab/presence.d.ts.map +1 -1
  79. package/dist/collab/presence.js +54 -3
  80. package/dist/collab/presence.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  84. package/dist/observability/routes.d.ts +2 -2
  85. package/dist/secrets/routes.d.ts +3 -3
  86. package/package.json +1 -1
@@ -0,0 +1,605 @@
1
+ /**
2
+ * Motion-preview bridge — injected into every canvas iframe.
3
+ *
4
+ * Allows the MotionDock's scrubbing preview to animate elements in ALL editor
5
+ * modes without writing anything to the DB, Yjs state, or source files.
6
+ *
7
+ * Protocol (parent → iframe):
8
+ *
9
+ * { type: 'motion-load-tracks', tracks: Array<{
10
+ * targetNodeId: string,
11
+ * property: string,
12
+ * keyframes: Array<{ t: number, value: string, ease?: string }>
13
+ * }>, defaultEase?: string }
14
+ * Load (or replace) the track list. t ∈ [0, 1]. defaultEase is the
15
+ * timeline-level fallback easing applied to any keyframe that omits its
16
+ * own `ease` (mirrors the compiled stylesheet); defaults to "ease".
17
+ *
18
+ * { type: 'motion-preview', t, durationMs }
19
+ * Seek all loaded tracks to normalised position t ∈ [0, 1] and apply
20
+ * interpolated CSS property values as inline styles on the matching
21
+ * [data-agent-native-node-id="…"] elements. Never writes to storage.
22
+ *
23
+ * { type: 'motion-preview-clear' }
24
+ * Remove all motion-preview inline-style overrides and the in-memory
25
+ * track list. Called when the dock is closed or the timeline is discarded.
26
+ *
27
+ * Rules:
28
+ * • No import/require of any module (DOM globals only).
29
+ * • No references to outer/module scope (the code runs inside an iframe).
30
+ * • Wrap everything in a self-executing IIFE.
31
+ */
32
+ (function () {
33
+ // Track list loaded by 'motion-load-tracks'.
34
+ var loadedTracks: Array<{
35
+ targetNodeId: string;
36
+ property: string;
37
+ keyframes: Array<{ t: number; value: string; ease?: string }>;
38
+ }> = [];
39
+ // Timeline-level default easing sent alongside the tracks. Applied to any
40
+ // keyframe that omits its own `ease`, matching the compiled stylesheet's
41
+ // defaultEase fallback so scrub/playback preview equals the persisted CSS.
42
+ // Falls back to "ease" when the loader doesn't provide one.
43
+ var loadedDefaultEase = "ease";
44
+ // Map of nodeId -> [property, ...] we have touched, for cleanup.
45
+ var touchedProps: Record<string, string[]> = {};
46
+ // Map of nodeId -> property -> original inline style value.
47
+ var originalInlineValues: Record<string, Record<string, string>> = {};
48
+
49
+ function camelizeProp(prop: string): string {
50
+ return String(prop).replace(/-([a-z])/g, function (_m: string, c: string) {
51
+ return c.toUpperCase();
52
+ });
53
+ }
54
+
55
+ function formatNum(n: number): string {
56
+ if (!Number.isFinite(n)) return "0";
57
+ return (Math.round(n * 10000) / 10000).toString();
58
+ }
59
+
60
+ function clamp255(x: number): number {
61
+ return x < 0 ? 0 : x > 255 ? 255 : x;
62
+ }
63
+ function clamp01(x: number): number {
64
+ return x < 0 ? 0 : x > 1 ? 1 : x;
65
+ }
66
+
67
+ function parseAlpha(s: string): number {
68
+ if (typeof s === "string" && s.indexOf("%") >= 0)
69
+ return parseFloat(s) / 100;
70
+ return parseFloat(s);
71
+ }
72
+
73
+ function hueToRgb(p: number, q: number, h: number): number {
74
+ if (h < 0) h += 1;
75
+ if (h > 1) h -= 1;
76
+ if (h < 1 / 6) return p + (q - p) * 6 * h;
77
+ if (h < 1 / 2) return q;
78
+ if (h < 2 / 3) return p + (q - p) * (2 / 3 - h) * 6;
79
+ return p;
80
+ }
81
+
82
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
83
+ h = (((h % 360) + 360) % 360) / 360;
84
+ s = clamp01(s);
85
+ l = clamp01(l);
86
+ if (s === 0) return [l * 255, l * 255, l * 255];
87
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
88
+ var p = 2 * l - q;
89
+ return [
90
+ hueToRgb(p, q, h + 1 / 3) * 255,
91
+ hueToRgb(p, q, h) * 255,
92
+ hueToRgb(p, q, h - 1 / 3) * 255,
93
+ ];
94
+ }
95
+
96
+ // Parse a CSS color (hex, rgb/rgba, hsl/hsla) into [r, g, b, a] or null.
97
+ function parseColor(str: string): [number, number, number, number] | null {
98
+ if (typeof str !== "string") return null;
99
+ var s = str.trim();
100
+ var hex = /^#([0-9a-fA-F]{3,8})$/.exec(s);
101
+ if (hex) {
102
+ var h = hex[1];
103
+ if (h.length === 3 || h.length === 4) {
104
+ return [
105
+ parseInt(h.charAt(0) + h.charAt(0), 16),
106
+ parseInt(h.charAt(1) + h.charAt(1), 16),
107
+ parseInt(h.charAt(2) + h.charAt(2), 16),
108
+ h.length === 4 ? parseInt(h.charAt(3) + h.charAt(3), 16) / 255 : 1,
109
+ ];
110
+ }
111
+ if (h.length === 6 || h.length === 8) {
112
+ return [
113
+ parseInt(h.slice(0, 2), 16),
114
+ parseInt(h.slice(2, 4), 16),
115
+ parseInt(h.slice(4, 6), 16),
116
+ h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
117
+ ];
118
+ }
119
+ return null;
120
+ }
121
+ var rgb = /^rgba?\(([^)]+)\)$/i.exec(s);
122
+ if (rgb) {
123
+ var rp = rgb[1].split(/[\s,\/]+/).filter(function (x: string) {
124
+ return x.length;
125
+ });
126
+ if (rp.length >= 3) {
127
+ return [
128
+ parseFloat(rp[0]),
129
+ parseFloat(rp[1]),
130
+ parseFloat(rp[2]),
131
+ rp.length >= 4 ? parseAlpha(rp[3]) : 1,
132
+ ];
133
+ }
134
+ return null;
135
+ }
136
+ var hsl = /^hsla?\(([^)]+)\)$/i.exec(s);
137
+ if (hsl) {
138
+ var hp = hsl[1].split(/[\s,\/]+/).filter(function (x: string) {
139
+ return x.length;
140
+ });
141
+ if (hp.length >= 3) {
142
+ var c = hslToRgb(
143
+ parseFloat(hp[0]),
144
+ parseFloat(hp[1]) / 100,
145
+ parseFloat(hp[2]) / 100,
146
+ );
147
+ return [c[0], c[1], c[2], hp.length >= 4 ? parseAlpha(hp[3]) : 1];
148
+ }
149
+ return null;
150
+ }
151
+ return null;
152
+ }
153
+
154
+ function lerpColorArr(
155
+ a: [number, number, number, number],
156
+ b: [number, number, number, number],
157
+ ratio: number,
158
+ ): [number, number, number, number] {
159
+ return [
160
+ a[0] + (b[0] - a[0]) * ratio,
161
+ a[1] + (b[1] - a[1]) * ratio,
162
+ a[2] + (b[2] - a[2]) * ratio,
163
+ a[3] + (b[3] - a[3]) * ratio,
164
+ ];
165
+ }
166
+
167
+ function formatColor(c: [number, number, number, number]): string {
168
+ var r = Math.round(clamp255(c[0]));
169
+ var g = Math.round(clamp255(c[1]));
170
+ var b = Math.round(clamp255(c[2]));
171
+ var a = clamp01(c[3]);
172
+ if (a >= 1) return "rgb(" + r + ", " + g + ", " + b + ")";
173
+ return (
174
+ "rgba(" +
175
+ r +
176
+ ", " +
177
+ g +
178
+ ", " +
179
+ b +
180
+ ", " +
181
+ Math.round(a * 1000) / 1000 +
182
+ ")"
183
+ );
184
+ }
185
+
186
+ type Segment =
187
+ | { lit: string }
188
+ | { type: "color"; value: [number, number, number, number] }
189
+ | { type: "num"; value: number; unit: string };
190
+
191
+ // Split a value into literal + typed (number / color) segments so two values
192
+ // that share a skeleton (e.g. 'translateY(16px)' / 'translateY(0px)') can be
193
+ // interpolated component-wise instead of snapping at the midpoint.
194
+ function tokenizeSegments(str: string): Segment[] | null {
195
+ if (typeof str !== "string") return null;
196
+ var segs: Segment[] = [];
197
+ var i = 0;
198
+ var n = str.length;
199
+ var litStart = 0;
200
+ var colorRe = /^(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\))/i;
201
+ var numRe = /^[+-]?(?:\d+\.?\d*|\.\d+)/;
202
+ while (i < n) {
203
+ var rest = str.slice(i);
204
+ var cm = colorRe.exec(rest);
205
+ if (cm) {
206
+ var parsed = parseColor(cm[0]);
207
+ if (parsed) {
208
+ if (i > litStart) segs.push({ lit: str.slice(litStart, i) });
209
+ segs.push({ type: "color", value: parsed });
210
+ i += cm[0].length;
211
+ litStart = i;
212
+ continue;
213
+ }
214
+ }
215
+ // Skip a number when the previous char is a letter — it belongs to an
216
+ // identifier such as translate3d / matrix3d, not a numeric argument.
217
+ var prevCh = i > 0 ? str.charAt(i - 1) : "";
218
+ if (!/[a-zA-Z]/.test(prevCh)) {
219
+ var nm = numRe.exec(rest);
220
+ if (nm) {
221
+ var numText = nm[0];
222
+ var unit = "";
223
+ var um = /^[a-z%]+/i.exec(str.slice(i + numText.length));
224
+ if (um) unit = um[0];
225
+ if (i > litStart) segs.push({ lit: str.slice(litStart, i) });
226
+ segs.push({ type: "num", value: parseFloat(numText), unit: unit });
227
+ i += numText.length + unit.length;
228
+ litStart = i;
229
+ continue;
230
+ }
231
+ }
232
+ i++;
233
+ }
234
+ if (n > litStart) segs.push({ lit: str.slice(litStart, n) });
235
+ return segs;
236
+ }
237
+
238
+ function segShape(segs: Segment[]): string {
239
+ var parts: string[] = [];
240
+ for (var i = 0; i < segs.length; i++) {
241
+ var s = segs[i];
242
+ parts.push("lit" in s ? "L" + s.lit : "T" + s.type);
243
+ }
244
+ return parts.join("\x1f");
245
+ }
246
+
247
+ // ── Easing evaluation ─────────────────────────────────────────────────────
248
+ // Dependency-free copy of the shared evaluateMotionEase algorithm in
249
+ // shared/motion-timeline.ts (this bridge cannot import modules) — keep the
250
+ // two in sync. Evaluates CSS timing functions so scrubbing matches the real
251
+ // compiled animation instead of always lerping linearly.
252
+
253
+ // Evaluate y for a CSS cubic-bezier at progress x (Newton + bisection).
254
+ function cubicBezierY(
255
+ x1: number,
256
+ y1: number,
257
+ x2: number,
258
+ y2: number,
259
+ x: number,
260
+ ): number {
261
+ if (x <= 0) return 0;
262
+ if (x >= 1) return 1;
263
+ var cx = 3 * x1;
264
+ var bx = 3 * (x2 - x1) - cx;
265
+ var ax = 1 - cx - bx;
266
+ var cy = 3 * y1;
267
+ var by = 3 * (y2 - y1) - cy;
268
+ var ay = 1 - cy - by;
269
+ function sampleX(u: number): number {
270
+ return ((ax * u + bx) * u + cx) * u;
271
+ }
272
+ function sampleY(u: number): number {
273
+ return ((ay * u + by) * u + cy) * u;
274
+ }
275
+ function sampleDX(u: number): number {
276
+ return (3 * ax * u + 2 * bx) * u + cx;
277
+ }
278
+ var u = x;
279
+ for (var i = 0; i < 8; i++) {
280
+ var err = sampleX(u) - x;
281
+ if (Math.abs(err) < 1e-6) return sampleY(u);
282
+ var d = sampleDX(u);
283
+ if (Math.abs(d) < 1e-6) break;
284
+ u = Math.min(1, Math.max(0, u - err / d));
285
+ }
286
+ var lo = 0;
287
+ var hi = 1;
288
+ u = x;
289
+ while (hi - lo > 1e-6) {
290
+ u = (lo + hi) / 2;
291
+ if (sampleX(u) < x) lo = u;
292
+ else hi = u;
293
+ }
294
+ return sampleY(u);
295
+ }
296
+
297
+ // Evaluate a CSS timing function (keywords, cubic-bezier, steps, spring
298
+ // approximation) at linear progress x ∈ [0, 1]. Result may overshoot [0, 1]
299
+ // for overshoot beziers (the dock's "Spring" preset).
300
+ function evalEase(ease: string | undefined, x: number): number {
301
+ var clamped = x <= 0 ? 0 : x >= 1 ? 1 : x;
302
+ var raw = String(ease == null ? "ease" : ease)
303
+ .trim()
304
+ .toLowerCase();
305
+ if (raw === "linear") return clamped;
306
+ if (raw === "step-start") return clamped > 0 ? 1 : 0;
307
+ if (raw === "step-end") return clamped >= 1 ? 1 : 0;
308
+ if (raw === "ease") return cubicBezierY(0.25, 0.1, 0.25, 1, clamped);
309
+ if (raw === "ease-in") return cubicBezierY(0.42, 0, 1, 1, clamped);
310
+ if (raw === "ease-out") return cubicBezierY(0, 0, 0.58, 1, clamped);
311
+ if (raw === "ease-in-out") return cubicBezierY(0.42, 0, 0.58, 1, clamped);
312
+ var bezier = /^cubic-bezier\(([^)]+)\)$/.exec(raw);
313
+ if (bezier) {
314
+ var parts = bezier[1].split(",");
315
+ if (parts.length === 4) {
316
+ var p0 = parseFloat(parts[0]);
317
+ var p1 = parseFloat(parts[1]);
318
+ var p2 = parseFloat(parts[2]);
319
+ var p3 = parseFloat(parts[3]);
320
+ if (
321
+ Number.isFinite(p0) &&
322
+ Number.isFinite(p1) &&
323
+ Number.isFinite(p2) &&
324
+ Number.isFinite(p3)
325
+ ) {
326
+ // x control points must stay in [0, 1]; y may overshoot.
327
+ return cubicBezierY(
328
+ Math.min(1, Math.max(0, p0)),
329
+ p1,
330
+ Math.min(1, Math.max(0, p2)),
331
+ p3,
332
+ clamped,
333
+ );
334
+ }
335
+ }
336
+ }
337
+ var steps = /^steps\(([^)]+)\)$/.exec(raw);
338
+ if (steps) {
339
+ var stepArgs = steps[1].split(",");
340
+ var count = parseInt(stepArgs[0], 10);
341
+ if (Number.isFinite(count) && count > 0) {
342
+ if (clamped >= 1) return 1;
343
+ var pos = (stepArgs[1] || "").trim();
344
+ var jumpStart = pos === "start" || pos === "jump-start";
345
+ return Math.min(
346
+ 1,
347
+ (Math.floor(clamped * count) + (jumpStart ? 1 : 0)) / count,
348
+ );
349
+ }
350
+ }
351
+ // Spring approximation: gentle overshoot bezier.
352
+ if (raw.indexOf("spring") === 0) {
353
+ return cubicBezierY(0.34, 1.56, 0.64, 1, clamped);
354
+ }
355
+ return clamped;
356
+ }
357
+
358
+ // Build the identity counterpart of a transform/filter list so a
359
+ // `none` endpoint interpolates smoothly instead of midpoint-snapping
360
+ // (e.g. none -> translateY(16px) lerps from translateY(0px)).
361
+ function identityForFunctions(value: string): string | null {
362
+ var fnRe = /([a-zA-Z][a-zA-Z0-9]*)\(([^)]*)\)/g;
363
+ var out = "";
364
+ var lastIndex = 0;
365
+ var matched = false;
366
+ var m: RegExpExecArray | null;
367
+ while ((m = fnRe.exec(value)) !== null) {
368
+ // Only whitespace may sit between functions in a transform/filter list.
369
+ if (value.slice(lastIndex, m.index).trim() !== "") return null;
370
+ var fn = m[1].toLowerCase();
371
+ var args = m[2];
372
+ var identity: string | null = null;
373
+ if (/^translate/.test(fn) || /^skew/.test(fn)) {
374
+ identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
375
+ } else if (/^scale/.test(fn)) {
376
+ identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)/g, "1");
377
+ } else if (/^rotate/.test(fn) || fn === "hue-rotate") {
378
+ identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
379
+ } else if (fn === "matrix") {
380
+ identity = "1, 0, 0, 1, 0, 0";
381
+ } else if (
382
+ fn === "blur" ||
383
+ fn === "grayscale" ||
384
+ fn === "sepia" ||
385
+ fn === "invert"
386
+ ) {
387
+ identity = args.replace(/[+-]?(?:\d+\.?\d*|\.\d+)([a-z%]*)/gi, "0$1");
388
+ } else if (
389
+ fn === "brightness" ||
390
+ fn === "contrast" ||
391
+ fn === "saturate" ||
392
+ fn === "opacity"
393
+ ) {
394
+ identity = args.indexOf("%") >= 0 ? "100%" : "1";
395
+ } else {
396
+ // matrix3d, perspective, drop-shadow, unknown functions: bail out.
397
+ return null;
398
+ }
399
+ out += (out ? " " : "") + m[1] + "(" + identity + ")";
400
+ lastIndex = m.index + m[0].length;
401
+ matched = true;
402
+ }
403
+ if (!matched) return null;
404
+ if (value.slice(lastIndex).trim() !== "") return null;
405
+ return out;
406
+ }
407
+
408
+ // Interpolate two keyframe values. Handles plain numbers (with units), CSS
409
+ // function values (translate/scale/blur/…), numbers embedded in gradients,
410
+ // and colors (hex / rgb(a) / hsl(a)). Falls back to a midpoint snap only for
411
+ // genuinely non-interpolable keywords (e.g. none -> block).
412
+ function lerp(a: string, b: string, ratio: number): string {
413
+ a = a == null ? "" : String(a);
414
+ b = b == null ? "" : String(b);
415
+ if (a === b) return a;
416
+ // Map a `none` endpoint to the identity form of the other endpoint so
417
+ // transform/filter tracks seeded from a `none` base animate smoothly.
418
+ if (a.trim() === "none") {
419
+ var identA = identityForFunctions(b);
420
+ if (identA) a = identA;
421
+ } else if (b.trim() === "none") {
422
+ var identB = identityForFunctions(a);
423
+ if (identB) b = identB;
424
+ }
425
+ if (a === b) return a;
426
+ var ca = parseColor(a);
427
+ var cb = parseColor(b);
428
+ if (ca && cb) return formatColor(lerpColorArr(ca, cb, ratio));
429
+ var aSegs = tokenizeSegments(a);
430
+ var bSegs = tokenizeSegments(b);
431
+ if (
432
+ aSegs &&
433
+ bSegs &&
434
+ aSegs.length === bSegs.length &&
435
+ segShape(aSegs) === segShape(bSegs)
436
+ ) {
437
+ var out = "";
438
+ var ok = true;
439
+ var touched = false;
440
+ for (var k = 0; k < aSegs.length; k++) {
441
+ var seg = aSegs[k];
442
+ if ("lit" in seg) {
443
+ out += seg.lit;
444
+ continue;
445
+ }
446
+ var other = bSegs[k];
447
+ if (!other || !("type" in other) || other.type !== seg.type) {
448
+ ok = false;
449
+ break;
450
+ }
451
+ touched = true;
452
+ if (seg.type === "color" && "value" in seg && "value" in other) {
453
+ out += formatColor(
454
+ lerpColorArr(
455
+ seg.value as [number, number, number, number],
456
+ other.value as [number, number, number, number],
457
+ ratio,
458
+ ),
459
+ );
460
+ } else if ("value" in seg && "value" in other) {
461
+ var unit =
462
+ ("unit" in seg ? seg.unit : "") ||
463
+ ("unit" in other ? other.unit : "") ||
464
+ "";
465
+ out +=
466
+ formatNum(
467
+ (seg.value as number) +
468
+ ((other.value as number) - (seg.value as number)) * ratio,
469
+ ) + unit;
470
+ }
471
+ }
472
+ if (ok && touched) return out;
473
+ }
474
+ // Non-interpolable (keywords, mismatched shapes): snap at the midpoint.
475
+ return ratio < 0.5 ? a : b;
476
+ }
477
+
478
+ function interpolate(
479
+ keyframes: Array<{ t: number; value: string; ease?: string }>,
480
+ t: number,
481
+ ): string {
482
+ if (!keyframes || keyframes.length === 0) return "";
483
+ if (keyframes.length === 1) return keyframes[0].value;
484
+ // Find surrounding keyframes.
485
+ var prev = keyframes[0];
486
+ var next = keyframes[keyframes.length - 1];
487
+ for (var i = 0; i < keyframes.length - 1; i++) {
488
+ if (t >= keyframes[i].t && t <= keyframes[i + 1].t) {
489
+ prev = keyframes[i];
490
+ next = keyframes[i + 1];
491
+ break;
492
+ }
493
+ }
494
+ var span = next.t - prev.t;
495
+ if (span <= 0) return prev.value;
496
+ var ratio = Math.max(0, Math.min(1, (t - prev.t) / span));
497
+ // Per-keyframe ease shapes the interval to the NEXT keyframe (standard
498
+ // CSS keyframe semantics). A keyframe that omits its own ease falls back to
499
+ // the timeline's defaultEase (loadedDefaultEase) so scrub/playback preview
500
+ // matches the compiled stylesheet, which uses that same fallback when it
501
+ // emits animation-timing-function. Overshoot beziers may push the eased
502
+ // ratio outside [0, 1] on purpose — lerp extrapolates.
503
+ var kfEase = prev.ease == null ? loadedDefaultEase : prev.ease;
504
+ var eased = evalEase(kfEase, ratio);
505
+ return lerp(prev.value, next.value, eased);
506
+ }
507
+
508
+ function applyPreview(t: number): void {
509
+ for (var i = 0; i < loadedTracks.length; i++) {
510
+ var track = loadedTracks[i];
511
+ var el = document.querySelector(
512
+ '[data-agent-native-node-id="' + track.targetNodeId + '"]',
513
+ ) as HTMLElement | null;
514
+ if (!el) continue;
515
+ var value = interpolate(track.keyframes, t);
516
+ if (value === "") continue;
517
+ // Normalize kebab properties (e.g. background-color) to the camelCase
518
+ // CSSOM accessor; el.style['background-color'] = … is unreliable.
519
+ var prop = camelizeProp(track.property);
520
+ if (!originalInlineValues[track.targetNodeId])
521
+ originalInlineValues[track.targetNodeId] = {};
522
+ if (!(prop in originalInlineValues[track.targetNodeId])) {
523
+ originalInlineValues[track.targetNodeId][prop] =
524
+ (el.style as unknown as Record<string, string>)[prop] || "";
525
+ }
526
+ (el.style as unknown as Record<string, string>)[prop] = value;
527
+ if (!touchedProps[track.targetNodeId])
528
+ touchedProps[track.targetNodeId] = [];
529
+ if (touchedProps[track.targetNodeId].indexOf(prop) === -1) {
530
+ touchedProps[track.targetNodeId].push(prop);
531
+ }
532
+ }
533
+ }
534
+
535
+ function clearPreview(): void {
536
+ var nodeIds = Object.keys(touchedProps);
537
+ for (var i = 0; i < nodeIds.length; i++) {
538
+ var el = document.querySelector(
539
+ '[data-agent-native-node-id="' + nodeIds[i] + '"]',
540
+ ) as HTMLElement | null;
541
+ if (!el) continue;
542
+ var props = touchedProps[nodeIds[i]];
543
+ for (var j = 0; j < props.length; j++) {
544
+ var originals = originalInlineValues[nodeIds[i]] || {};
545
+ (el.style as unknown as Record<string, string>)[props[j]] =
546
+ Object.prototype.hasOwnProperty.call(originals, props[j])
547
+ ? originals[props[j]]
548
+ : "";
549
+ }
550
+ }
551
+ touchedProps = {};
552
+ originalInlineValues = {};
553
+ loadedTracks = [];
554
+ loadedDefaultEase = "ease";
555
+ }
556
+
557
+ // Last previewed playhead position, so reloading the track list (which
558
+ // resets touched styles to base) can immediately re-apply the current
559
+ // scrub position instead of leaving the canvas at t=0 while the dock's
560
+ // playhead still shows the old time.
561
+ var lastPreviewT: number | null = null;
562
+
563
+ window.addEventListener("message", function (e: MessageEvent) {
564
+ if (e.source !== window.parent) return;
565
+ if (!e.data || typeof e.data.type !== "string") return;
566
+ if (e.data.type === "motion-load-tracks") {
567
+ clearPreview();
568
+ loadedTracks = Array.isArray(e.data.tracks) ? e.data.tracks : [];
569
+ // Timeline-level default easing for keyframes that omit their own ease.
570
+ // Read AFTER clearPreview() (which resets it to "ease") so a non-default
571
+ // defaultEase from API/agent-created timelines drives the preview.
572
+ loadedDefaultEase =
573
+ typeof e.data.defaultEase === "string" && e.data.defaultEase
574
+ ? e.data.defaultEase
575
+ : "ease";
576
+ // Defensive: interpolate() scans keyframe pairs in array order, so an
577
+ // unsorted track (e.g. mid-drag ordering) would fail the window test.
578
+ for (var i = 0; i < loadedTracks.length; i++) {
579
+ var kfs = loadedTracks[i] && loadedTracks[i].keyframes;
580
+ if (Array.isArray(kfs)) {
581
+ kfs.sort(function (a, b) {
582
+ return (a && a.t) - (b && b.t);
583
+ });
584
+ }
585
+ }
586
+ if (lastPreviewT !== null && loadedTracks.length > 0) {
587
+ applyPreview(lastPreviewT);
588
+ }
589
+ return;
590
+ }
591
+ if (e.data.type === "motion-preview") {
592
+ var t = Number(e.data.t);
593
+ if (!Number.isFinite(t)) return;
594
+ t = Math.max(0, Math.min(1, t));
595
+ lastPreviewT = t;
596
+ applyPreview(t);
597
+ return;
598
+ }
599
+ if (e.data.type === "motion-preview-clear") {
600
+ clearPreview();
601
+ lastPreviewT = null;
602
+ return;
603
+ }
604
+ });
605
+ })();