@hyperframes/core 0.6.87 → 0.6.89

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.
@@ -122,11 +122,6 @@ function updateReferences(projectDir, oldPath, newPath) {
122
122
  */
123
123
  function extractGsapScriptBlock(html) {
124
124
  const { document } = parseHTML(html);
125
- // linkedom's querySelectorAll doesn't descend into <template> content, but
126
- // sub-compositions wrap their markup (and the GSAP <script>) in a <template>.
127
- // Search top-level scripts first, then each template's own scripts. Operate
128
- // on the template element directly (NOT .content) so textContent writes are
129
- // reflected in document.toString().
130
125
  const scripts = [
131
126
  ...document.querySelectorAll("script:not([src])"),
132
127
  ...Array.from(document.querySelectorAll("template")).flatMap((tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))),
@@ -138,6 +133,7 @@ function extractGsapScriptBlock(html) {
138
133
  content.includes(".to(")) {
139
134
  return {
140
135
  scriptText: content,
136
+ document,
141
137
  replaceScript(newText) {
142
138
  script.textContent = newText;
143
139
  return document.toString();
@@ -147,10 +143,279 @@ function extractGsapScriptBlock(html) {
147
143
  }
148
144
  return null;
149
145
  }
146
+ function stripStudioEditsFromTarget(document, selector) {
147
+ if (!selector)
148
+ return 0;
149
+ let stripped = 0;
150
+ try {
151
+ for (const el of document.querySelectorAll(selector)) {
152
+ if (!el.getAttribute("data-hf-studio-path-offset"))
153
+ continue;
154
+ const htmlEl = el;
155
+ const originalTranslate = el.getAttribute("data-hf-studio-original-inline-translate");
156
+ htmlEl.style.removeProperty("--hf-studio-offset-x");
157
+ htmlEl.style.removeProperty("--hf-studio-offset-y");
158
+ if (originalTranslate) {
159
+ htmlEl.style.setProperty("translate", originalTranslate);
160
+ }
161
+ else {
162
+ htmlEl.style.removeProperty("translate");
163
+ }
164
+ el.removeAttribute("data-hf-studio-path-offset");
165
+ el.removeAttribute("data-hf-studio-original-translate");
166
+ el.removeAttribute("data-hf-studio-original-inline-translate");
167
+ stripped++;
168
+ }
169
+ }
170
+ catch {
171
+ // Invalid selector — skip silently.
172
+ }
173
+ return stripped;
174
+ }
175
+ function bakeVisibilityOnDelete(document, anim) {
176
+ let finalOpacity;
177
+ if (anim.method === "from") {
178
+ return;
179
+ }
180
+ if (anim.keyframes) {
181
+ const kfs = anim.keyframes.keyframes;
182
+ for (let i = kfs.length - 1; i >= 0; i--) {
183
+ if ("opacity" in kfs[i].properties) {
184
+ finalOpacity = kfs[i].properties.opacity;
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ else if ("opacity" in anim.properties) {
190
+ finalOpacity = anim.properties.opacity;
191
+ }
192
+ if (finalOpacity == null) {
193
+ return;
194
+ }
195
+ if (typeof finalOpacity === "string" && /^[+\-*]=/.test(finalOpacity)) {
196
+ return;
197
+ }
198
+ const numOpacity = Number(finalOpacity);
199
+ if (!Number.isFinite(numOpacity) || numOpacity === 0)
200
+ return;
201
+ try {
202
+ for (const el of document.querySelectorAll(anim.targetSelector)) {
203
+ el.style.setProperty("opacity", String(numOpacity));
204
+ }
205
+ }
206
+ catch {
207
+ // Invalid selector — skip silently.
208
+ }
209
+ }
150
210
  /** Lazy-load gsapParser to avoid pulling recast into every file-route import. */
151
211
  async function loadGsapParser() {
152
212
  return import("../../parsers/gsapParser.js");
153
213
  }
214
+ // ── GSAP mutation executor ──────────────────────────────────────────────────
215
+ async function executeGsapMutation(body, block, respond) {
216
+ const parser = await loadGsapParser();
217
+ const { parseGsapScript, updateAnimationInScript, addAnimationToScript, removeAnimationFromScript, addKeyframeToScript, removeKeyframeFromScript, updateKeyframeInScript, convertToKeyframesInScript, removeAllKeyframesFromScript, materializeKeyframesInScript, unrollDynamicAnimations, setArcPathInScript, updateArcSegmentInScript, removeArcPathFromScript, addAnimationWithKeyframesToScript, } = parser;
218
+ function requireAnimation(scriptText, animationId) {
219
+ const parsed = parseGsapScript(scriptText);
220
+ const anim = parsed.animations.find((a) => a.id === animationId);
221
+ if (!anim)
222
+ return { err: respond({ error: "animation not found" }, 404) };
223
+ return { anim };
224
+ }
225
+ function requireFromToAnimation(scriptText, animationId) {
226
+ const result = requireAnimation(scriptText, animationId);
227
+ if ("err" in result)
228
+ return result;
229
+ if (result.anim.method !== "fromTo")
230
+ return { err: respond({ error: "animation is not a fromTo" }, 400) };
231
+ return result;
232
+ }
233
+ switch (body.type) {
234
+ case "update-property": {
235
+ const r = requireAnimation(block.scriptText, body.animationId);
236
+ if ("err" in r)
237
+ return r.err;
238
+ return updateAnimationInScript(block.scriptText, body.animationId, {
239
+ properties: { ...r.anim.properties, [body.property]: body.value },
240
+ });
241
+ }
242
+ case "update-from-property": {
243
+ const r = requireFromToAnimation(block.scriptText, body.animationId);
244
+ if ("err" in r)
245
+ return r.err;
246
+ return updateAnimationInScript(block.scriptText, body.animationId, {
247
+ fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: body.value },
248
+ });
249
+ }
250
+ case "update-meta": {
251
+ return updateAnimationInScript(block.scriptText, body.animationId, body.updates);
252
+ }
253
+ case "add": {
254
+ if (body.fromProperties && body.method !== "fromTo") {
255
+ return respond({ error: "fromProperties is only valid for method=fromTo" }, 400);
256
+ }
257
+ const result = addAnimationToScript(block.scriptText, {
258
+ targetSelector: body.targetSelector,
259
+ method: body.method,
260
+ position: body.position,
261
+ duration: body.duration,
262
+ ease: body.ease,
263
+ properties: body.properties,
264
+ fromProperties: body.fromProperties,
265
+ });
266
+ return result.script;
267
+ }
268
+ case "delete": {
269
+ const delTarget = requireAnimation(block.scriptText, body.animationId);
270
+ if (!("err" in delTarget) && body.stripStudioEdits) {
271
+ stripStudioEditsFromTarget(block.document, delTarget.anim.targetSelector);
272
+ bakeVisibilityOnDelete(block.document, delTarget.anim);
273
+ }
274
+ return removeAnimationFromScript(block.scriptText, body.animationId);
275
+ }
276
+ case "add-property": {
277
+ const r = requireAnimation(block.scriptText, body.animationId);
278
+ if ("err" in r)
279
+ return r.err;
280
+ return updateAnimationInScript(block.scriptText, body.animationId, {
281
+ properties: { ...r.anim.properties, [body.property]: body.defaultValue },
282
+ });
283
+ }
284
+ case "add-from-property": {
285
+ const r = requireFromToAnimation(block.scriptText, body.animationId);
286
+ if ("err" in r)
287
+ return r.err;
288
+ return updateAnimationInScript(block.scriptText, body.animationId, {
289
+ fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: body.defaultValue },
290
+ });
291
+ }
292
+ case "remove-property": {
293
+ const r = requireAnimation(block.scriptText, body.animationId);
294
+ if ("err" in r)
295
+ return r.err;
296
+ const filtered = { ...r.anim.properties };
297
+ delete filtered[body.property];
298
+ return updateAnimationInScript(block.scriptText, body.animationId, {
299
+ properties: filtered,
300
+ });
301
+ }
302
+ case "remove-from-property": {
303
+ const r = requireFromToAnimation(block.scriptText, body.animationId);
304
+ if ("err" in r)
305
+ return r.err;
306
+ const filtered = { ...(r.anim.fromProperties ?? {}) };
307
+ delete filtered[body.property];
308
+ return updateAnimationInScript(block.scriptText, body.animationId, {
309
+ fromProperties: filtered,
310
+ });
311
+ }
312
+ case "add-keyframe": {
313
+ return addKeyframeToScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease, body.backfillDefaults);
314
+ }
315
+ case "remove-keyframe": {
316
+ return removeKeyframeFromScript(block.scriptText, body.animationId, body.percentage);
317
+ }
318
+ case "update-keyframe": {
319
+ return updateKeyframeInScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease);
320
+ }
321
+ case "convert-to-keyframes": {
322
+ return convertToKeyframesInScript(block.scriptText, body.animationId, body.resolvedFromValues);
323
+ }
324
+ case "remove-all-keyframes": {
325
+ const preCollapse = requireAnimation(block.scriptText, body.animationId);
326
+ if (!("err" in preCollapse)) {
327
+ bakeVisibilityOnDelete(block.document, preCollapse.anim);
328
+ }
329
+ return removeAllKeyframesFromScript(block.scriptText, body.animationId);
330
+ }
331
+ case "materialize-keyframes": {
332
+ if (body.allElements && body.allElements.length > 0) {
333
+ return unrollDynamicAnimations(block.scriptText, body.animationId, body.allElements);
334
+ }
335
+ return materializeKeyframesInScript(block.scriptText, body.animationId, body.keyframes, body.easeEach, body.resolvedSelector);
336
+ }
337
+ case "set-arc-path": {
338
+ return setArcPathInScript(block.scriptText, body.animationId, {
339
+ enabled: body.enabled,
340
+ autoRotate: body.autoRotate ?? false,
341
+ segments: body.segments ?? [],
342
+ });
343
+ }
344
+ case "update-arc-segment": {
345
+ return updateArcSegmentInScript(block.scriptText, body.animationId, body.segmentIndex, {
346
+ ...(body.curviness !== undefined ? { curviness: body.curviness } : {}),
347
+ ...(body.cp1 ? { cp1: body.cp1 } : {}),
348
+ ...(body.cp2 ? { cp2: body.cp2 } : {}),
349
+ });
350
+ }
351
+ case "remove-arc-path": {
352
+ return removeArcPathFromScript(block.scriptText, body.animationId);
353
+ }
354
+ case "add-with-keyframes": {
355
+ const result = addAnimationWithKeyframesToScript(block.scriptText, body.targetSelector, body.position, body.duration, body.keyframes, body.ease);
356
+ return result.script;
357
+ }
358
+ default:
359
+ return respond({ error: `unknown mutation type: ${body.type}` }, 400);
360
+ }
361
+ }
362
+ // ── Upload file processing ──────────────────────────────────────────────────
363
+ async function processUploadedFiles(formData, targetDir, projectDir) {
364
+ const MAX_UPLOAD_BYTES = 500 * 1024 * 1024; // 500 MB per file
365
+ const uploaded = [];
366
+ const skipped = [];
367
+ const invalid = [];
368
+ const entries = formData.entries();
369
+ // Derive the subdirectory prefix from targetDir relative to projectDir
370
+ const subDir = targetDir === projectDir ? "" : targetDir.slice(projectDir.length + 1);
371
+ for (const [, value] of entries) {
372
+ if (typeof value === "string")
373
+ continue;
374
+ // Strip path separators — browsers may include directory components
375
+ const name = value.name.split("/").pop()?.split("\\").pop() ?? "";
376
+ if (!name || name.includes("\0") || name.includes(".."))
377
+ continue;
378
+ // Reject individual files that exceed the size limit
379
+ if (value.size > MAX_UPLOAD_BYTES) {
380
+ skipped.push(name);
381
+ continue;
382
+ }
383
+ const destPath = resolve(targetDir, name);
384
+ if (!isSafePath(projectDir, destPath))
385
+ continue;
386
+ // Don't overwrite — append (2), (3), etc.
387
+ let finalPath = destPath;
388
+ let finalName = name;
389
+ if (existsSync(finalPath)) {
390
+ // Handle dotfiles correctly: .gitignore → ext="", base=".gitignore"
391
+ const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
392
+ const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
393
+ const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
394
+ let n = 2;
395
+ while (n < 10000 && existsSync(resolve(targetDir, `${base} (${n})${ext}`)))
396
+ n++;
397
+ if (n >= 10000) {
398
+ skipped.push(name);
399
+ continue;
400
+ }
401
+ finalName = `${base} (${n})${ext}`;
402
+ finalPath = resolve(targetDir, finalName);
403
+ }
404
+ const buffer = Buffer.from(await value.arrayBuffer());
405
+ const validation = validateUploadedMediaBuffer(finalName, buffer);
406
+ if (!validation.ok) {
407
+ invalid.push({ name: finalName, reason: validation.reason });
408
+ continue;
409
+ }
410
+ writeFileSync(finalPath, buffer);
411
+ const relativePath = subDir ? join(subDir, finalName) : finalName;
412
+ uploaded.push(relativePath);
413
+ if (isAudioFile(finalName)) {
414
+ generateWaveformCache(projectDir, relativePath).catch(() => { });
415
+ }
416
+ }
417
+ return { uploaded, skipped, invalid };
418
+ }
154
419
  // ── Route registration ──────────────────────────────────────────────────────
155
420
  export function registerFileRoutes(api, adapter) {
156
421
  // ── Read ──
@@ -343,57 +608,8 @@ export function registerFileRoutes(api, adapter) {
343
608
  if (subDir && !existsSync(targetDir))
344
609
  mkdirSync(targetDir, { recursive: true });
345
610
  const formData = await c.req.formData();
346
- const uploaded = [];
347
- const skipped = [];
348
- const invalid = [];
349
- const entries = formData.entries();
350
- for (const [, value] of entries) {
351
- if (typeof value === "string")
352
- continue;
353
- // Strip path separators — browsers may include directory components
354
- const name = value.name.split("/").pop()?.split("\\").pop() ?? "";
355
- if (!name || name.includes("\0") || name.includes(".."))
356
- continue;
357
- // Reject individual files that exceed the size limit
358
- if (value.size > MAX_UPLOAD_BYTES) {
359
- skipped.push(name);
360
- continue;
361
- }
362
- const destPath = resolve(targetDir, name);
363
- if (!isSafePath(project.dir, destPath))
364
- continue;
365
- // Don't overwrite — append (2), (3), etc.
366
- let finalPath = destPath;
367
- let finalName = name;
368
- if (existsSync(finalPath)) {
369
- // Handle dotfiles correctly: .gitignore → ext="", base=".gitignore"
370
- const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
371
- const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
372
- const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
373
- let n = 2;
374
- while (n < 10000 && existsSync(resolve(targetDir, `${base} (${n})${ext}`)))
375
- n++;
376
- if (n >= 10000) {
377
- skipped.push(name);
378
- continue;
379
- }
380
- finalName = `${base} (${n})${ext}`;
381
- finalPath = resolve(targetDir, finalName);
382
- }
383
- const buffer = Buffer.from(await value.arrayBuffer());
384
- const validation = validateUploadedMediaBuffer(finalName, buffer);
385
- if (!validation.ok) {
386
- invalid.push({ name: finalName, reason: validation.reason });
387
- continue;
388
- }
389
- writeFileSync(finalPath, buffer);
390
- const relativePath = subDir ? join(subDir, finalName) : finalName;
391
- uploaded.push(relativePath);
392
- if (isAudioFile(finalName)) {
393
- generateWaveformCache(project.dir, relativePath).catch(() => { });
394
- }
395
- }
396
- return c.json({ ok: true, files: uploaded, skipped, invalid }, 201);
611
+ const result = await processUploadedFiles(formData, targetDir, project.dir);
612
+ return c.json({ ok: true, files: result.uploaded, skipped: result.skipped, invalid: result.invalid }, 201);
397
613
  });
398
614
  // ── GSAP Animations (parse) ──
399
615
  api.get("/projects/:id/gsap-animations/*", async (c) => {
@@ -416,6 +632,7 @@ export function registerFileRoutes(api, adapter) {
416
632
  const parsed = parseGsapScript(block.scriptText);
417
633
  return c.json(parsed);
418
634
  });
635
+ // ── GSAP Mutations ──
419
636
  api.post("/projects/:id/gsap-mutations/*", async (c) => {
420
637
  const res = await resolveProjectPath(c, adapter, (id) => `/projects/${id}/gsap-mutations/`, {
421
638
  mustExist: true,
@@ -426,184 +643,44 @@ export function registerFileRoutes(api, adapter) {
426
643
  if (!body || !body.type) {
427
644
  return c.json({ error: "mutation type required" }, 400);
428
645
  }
429
- const html = readFileSync(res.absPath, "utf-8");
430
- const block = extractGsapScriptBlock(html);
646
+ let html = readFileSync(res.absPath, "utf-8");
647
+ let block = extractGsapScriptBlock(html);
648
+ if (!block && (body.type === "add" || body.type === "add-with-keyframes")) {
649
+ const compId = html.match(/data-composition-id="([^"]+)"/)?.[1] ?? "main";
650
+ const { GSAP_CDN } = await import("../../templates/constants.js");
651
+ const gsapCdn = `<script src="${GSAP_CDN}"></script>`;
652
+ const bootstrap = [
653
+ gsapCdn,
654
+ "<script>",
655
+ "window.__timelines = window.__timelines || {};",
656
+ `const tl = gsap.timeline({ paused: true });`,
657
+ `window.__timelines["${compId}"] = tl;`,
658
+ "</script>",
659
+ ].join("\n");
660
+ if (html.includes("</body>")) {
661
+ html = html.replace("</body>", `${bootstrap}\n</body>`);
662
+ }
663
+ else {
664
+ html += `\n${bootstrap}`;
665
+ }
666
+ block = extractGsapScriptBlock(html);
667
+ }
431
668
  if (!block) {
432
669
  return c.json({ error: "no GSAP script found in file" }, 400);
433
670
  }
434
- const { parseGsapScript, updateAnimationInScript, addAnimationToScript, removeAnimationFromScript, } = await loadGsapParser();
435
- function requireAnimation(scriptText, animationId) {
436
- const parsed = parseGsapScript(scriptText);
437
- const anim = parsed.animations.find((a) => a.id === animationId);
438
- if (!anim)
439
- return { err: c.json({ error: "animation not found" }, 404) };
440
- return { anim };
441
- }
442
- function requireFromToAnimation(scriptText, animationId) {
443
- const result = requireAnimation(scriptText, animationId);
444
- if ("err" in result)
445
- return result;
446
- if (result.anim.method !== "fromTo")
447
- return { err: c.json({ error: "animation is not a fromTo" }, 400) };
671
+ const respond = (data, status) =>
672
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge between generic status and Hono's literal union
673
+ status ? c.json(data, status) : c.json(data);
674
+ const result = await executeGsapMutation(body, block, respond);
675
+ if (result instanceof Response)
448
676
  return result;
449
- }
450
- let newScript;
451
- // fallow-ignore-next-line complexity
452
- switch (body.type) {
453
- case "update-property": {
454
- const r = requireAnimation(block.scriptText, body.animationId);
455
- if ("err" in r)
456
- return r.err;
457
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
458
- properties: { ...r.anim.properties, [body.property]: body.value },
459
- });
460
- break;
461
- }
462
- case "update-from-property": {
463
- const r = requireFromToAnimation(block.scriptText, body.animationId);
464
- if ("err" in r)
465
- return r.err;
466
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
467
- fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: body.value },
468
- });
469
- break;
470
- }
471
- case "update-meta": {
472
- newScript = updateAnimationInScript(block.scriptText, body.animationId, body.updates);
473
- break;
474
- }
475
- case "add": {
476
- if (body.fromProperties && body.method !== "fromTo") {
477
- return c.json({ error: "fromProperties is only valid for method=fromTo" }, 400);
478
- }
479
- const result = addAnimationToScript(block.scriptText, {
480
- targetSelector: body.targetSelector,
481
- method: body.method,
482
- position: body.position,
483
- duration: body.duration,
484
- ease: body.ease,
485
- properties: body.properties,
486
- fromProperties: body.fromProperties,
487
- });
488
- newScript = result.script;
489
- break;
490
- }
491
- case "delete": {
492
- newScript = removeAnimationFromScript(block.scriptText, body.animationId);
493
- break;
494
- }
495
- case "add-property": {
496
- const r = requireAnimation(block.scriptText, body.animationId);
497
- if ("err" in r)
498
- return r.err;
499
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
500
- properties: { ...r.anim.properties, [body.property]: body.defaultValue },
501
- });
502
- break;
503
- }
504
- case "add-from-property": {
505
- const r = requireFromToAnimation(block.scriptText, body.animationId);
506
- if ("err" in r)
507
- return r.err;
508
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
509
- fromProperties: { ...(r.anim.fromProperties ?? {}), [body.property]: body.defaultValue },
510
- });
511
- break;
512
- }
513
- case "remove-property": {
514
- const r = requireAnimation(block.scriptText, body.animationId);
515
- if ("err" in r)
516
- return r.err;
517
- const filtered = { ...r.anim.properties };
518
- delete filtered[body.property];
519
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
520
- properties: filtered,
521
- });
522
- break;
523
- }
524
- case "remove-from-property": {
525
- const r = requireFromToAnimation(block.scriptText, body.animationId);
526
- if ("err" in r)
527
- return r.err;
528
- const filtered = { ...(r.anim.fromProperties ?? {}) };
529
- delete filtered[body.property];
530
- newScript = updateAnimationInScript(block.scriptText, body.animationId, {
531
- fromProperties: filtered,
532
- });
533
- break;
534
- }
535
- case "add-keyframe": {
536
- const { addKeyframeToScript } = await loadGsapParser();
537
- newScript = addKeyframeToScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease, body.backfillDefaults);
538
- break;
539
- }
540
- case "remove-keyframe": {
541
- const { removeKeyframeFromScript } = await loadGsapParser();
542
- newScript = removeKeyframeFromScript(block.scriptText, body.animationId, body.percentage);
543
- break;
544
- }
545
- case "update-keyframe": {
546
- const { updateKeyframeInScript } = await loadGsapParser();
547
- newScript = updateKeyframeInScript(block.scriptText, body.animationId, body.percentage, body.properties, body.ease);
548
- break;
549
- }
550
- case "convert-to-keyframes": {
551
- const { convertToKeyframesInScript } = await loadGsapParser();
552
- newScript = convertToKeyframesInScript(block.scriptText, body.animationId, body.resolvedFromValues);
553
- break;
554
- }
555
- case "remove-all-keyframes": {
556
- const { removeAllKeyframesFromScript } = await loadGsapParser();
557
- newScript = removeAllKeyframesFromScript(block.scriptText, body.animationId);
558
- break;
559
- }
560
- case "materialize-keyframes": {
561
- const { materializeKeyframesInScript, unrollDynamicAnimations } = await loadGsapParser();
562
- if (body.allElements && body.allElements.length > 0) {
563
- newScript = unrollDynamicAnimations(block.scriptText, body.animationId, body.allElements);
564
- }
565
- else {
566
- newScript = materializeKeyframesInScript(block.scriptText, body.animationId, body.keyframes, body.easeEach, body.resolvedSelector);
567
- }
568
- break;
569
- }
570
- case "set-arc-path": {
571
- const { setArcPathInScript } = await loadGsapParser();
572
- newScript = setArcPathInScript(block.scriptText, body.animationId, {
573
- enabled: body.enabled,
574
- autoRotate: body.autoRotate ?? false,
575
- segments: body.segments ?? [],
576
- });
577
- break;
578
- }
579
- case "update-arc-segment": {
580
- const { updateArcSegmentInScript } = await loadGsapParser();
581
- newScript = updateArcSegmentInScript(block.scriptText, body.animationId, body.segmentIndex, {
582
- ...(body.curviness !== undefined ? { curviness: body.curviness } : {}),
583
- ...(body.cp1 ? { cp1: body.cp1 } : {}),
584
- ...(body.cp2 ? { cp2: body.cp2 } : {}),
585
- });
586
- break;
587
- }
588
- case "remove-arc-path": {
589
- const { removeArcPathFromScript } = await loadGsapParser();
590
- newScript = removeArcPathFromScript(block.scriptText, body.animationId);
591
- break;
592
- }
593
- case "add-with-keyframes": {
594
- const { addAnimationWithKeyframesToScript } = await loadGsapParser();
595
- const result = addAnimationWithKeyframesToScript(block.scriptText, body.targetSelector, body.position, body.duration, body.keyframes, body.ease);
596
- newScript = result.script;
597
- break;
598
- }
599
- default:
600
- return c.json({ error: `unknown mutation type: ${body.type}` }, 400);
601
- }
677
+ const newScript = result;
602
678
  const newHtml = block.replaceScript(newScript);
603
679
  if (newHtml !== html) {
604
680
  writeFileSync(res.absPath, newHtml, "utf-8");
605
681
  }
606
682
  // Re-parse the mutated script so the UI gets fresh state
683
+ const { parseGsapScript } = await loadGsapParser();
607
684
  const freshParsed = parseGsapScript(newScript);
608
685
  return c.json({
609
686
  ok: true,