@hyperframes/studio 0.8.134 → 0.8.136

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 (54) hide show
  1. package/dist/assets/{hyperframes-player-Bro1gj0Z.js → hyperframes-player-ukvUFqdg.js} +1 -1
  2. package/dist/assets/{index-DnVGIbEC.js → index-BY_myArg.js} +1 -1
  3. package/dist/assets/{index-wnXyQjW-.js → index-C-pD9_xY.js} +185 -184
  4. package/dist/assets/{index-BwNDTmAD.js → index-CRRgIxsS.js} +1 -1
  5. package/dist/assets/{index-D11atkBt.css → index-syMK0_9C.css} +1 -1
  6. package/dist/{chunk-5VSXRGIX.js → chunk-NM7BXNZ7.js} +1 -1
  7. package/dist/chunk-NM7BXNZ7.js.map +1 -0
  8. package/dist/{chunk-FOEID72M.js → chunk-QRLKWENQ.js} +2 -2
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +227 -88
  12. package/dist/index.js.map +1 -1
  13. package/dist/player.js +2 -2
  14. package/dist/styles.css +1 -1
  15. package/dist/ui.d.ts +3 -2
  16. package/dist/ui.js +1 -1
  17. package/package.json +8 -8
  18. package/src/components/editor/AnimationCard.test.tsx +60 -0
  19. package/src/components/editor/AnimationCard.tsx +16 -6
  20. package/src/components/editor/KeyframeEaseList.tsx +4 -1
  21. package/src/components/editor/LayersPanel.tsx +39 -47
  22. package/src/components/editor/domEditInlineText.ts +1 -1
  23. package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
  24. package/src/components/editor/gsapAnimationHelpers.ts +34 -0
  25. package/src/components/editor/layerKind.test.ts +38 -0
  26. package/src/components/editor/layerKind.ts +19 -0
  27. package/src/components/editor/useDomEditNudge.test.tsx +48 -27
  28. package/src/components/ui/cn.ts +2 -1
  29. package/src/contexts/DomEditContext.tsx +3 -1
  30. package/src/hooks/gsapPlaybackTestHarness.ts +28 -0
  31. package/src/hooks/gsapRuntimePatch.ts +4 -0
  32. package/src/hooks/gsapShared.test.ts +11 -0
  33. package/src/hooks/gsapShared.ts +1 -1
  34. package/src/hooks/keyframeEase.realGsap.test.ts +37 -0
  35. package/src/hooks/useAnimatedPropertyCommit.test.tsx +33 -0
  36. package/src/hooks/useAnimatedPropertyCommit.ts +3 -1
  37. package/src/hooks/useDomGeometryCommits.test.tsx +21 -0
  38. package/src/hooks/useDomGeometryCommits.ts +6 -1
  39. package/src/hooks/useGestureCommit.test.tsx +89 -0
  40. package/src/hooks/useGestureCommit.ts +26 -9
  41. package/src/icons/SystemIcons.tsx +6 -0
  42. package/src/layersPanelExport.test.tsx +45 -4
  43. package/src/utils/gsapKeyframeEases.ts +2 -0
  44. package/src/utils/gsapLiveRetime.cache.test.ts +39 -0
  45. package/src/utils/gsapLiveRetime.realGsap.test.ts +39 -15
  46. package/src/utils/gsapLiveRetime.ts +39 -31
  47. package/src/utils/gsapSoftReload.test.ts +98 -14
  48. package/src/utils/gsapSoftReload.ts +14 -2
  49. package/src/utils/gsapUndoRestore.test.ts +21 -2
  50. package/src/utils/softReloadTargets.ts +56 -0
  51. package/src/utils/studioPendingEdits.test.ts +123 -0
  52. package/src/utils/studioPendingEdits.ts +27 -1
  53. package/dist/chunk-5VSXRGIX.js.map +0 -1
  54. /package/dist/{chunk-FOEID72M.js.map → chunk-QRLKWENQ.js.map} +0 -0
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  usePreviewVariablesStore,
40
40
  whenPreviewBooted,
41
41
  writeStudioUiPreferences
42
- } from "./chunk-FOEID72M.js";
42
+ } from "./chunk-QRLKWENQ.js";
43
43
  import {
44
44
  Button,
45
45
  ContextMenu,
@@ -71,7 +71,7 @@ import {
71
71
  fieldBase,
72
72
  fieldText,
73
73
  popupSurface
74
- } from "./chunk-5VSXRGIX.js";
74
+ } from "./chunk-NM7BXNZ7.js";
75
75
  import {
76
76
  DOM_EDIT_LAYER_OBSERVER_INIT,
77
77
  STUDIO_FLAT_INSPECTOR_ENABLED,
@@ -4504,6 +4504,7 @@ function timingEase(runEase) {
4504
4504
  }
4505
4505
  return timingEases.get(runEase) ?? null;
4506
4506
  }
4507
+ var warpsTime = (runEase) => timingEase(runEase) !== null;
4507
4508
  function progressAtTime(runEase, timePercentage) {
4508
4509
  const run = timingEase(runEase);
4509
4510
  return run ? run(timePercentage / 100) * 100 : timePercentage;
@@ -4592,7 +4593,7 @@ var PROPERTY_DEFAULTS = {
4592
4593
  };
4593
4594
  function keyframeEases(anim) {
4594
4595
  if (!anim.keyframes) return { easeEach: anim.ease };
4595
- return { ease: anim.ease, easeEach: anim.keyframes.easeEach };
4596
+ return { ease: runEaseOf(anim), easeEach: anim.keyframes.easeEach };
4596
4597
  }
4597
4598
  function isInstantHold(animation) {
4598
4599
  return animation.method === "set" || (animation.method === "to" || animation.method === "fromTo") && resolveTweenDuration(animation) === 0;
@@ -9510,14 +9511,35 @@ function trackStudioPendingEdit(result) {
9510
9511
  );
9511
9512
  return promise;
9512
9513
  }
9513
- function trackedStudioEdit(edit) {
9514
+ var flushedSavesStillWriting = null;
9515
+ function commitOlderDebouncedEdits() {
9516
+ if (typeof window === "undefined" || adopting) return;
9517
+ const detail = { promises: [] };
9518
+ window.dispatchEvent(
9519
+ new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
9520
+ );
9521
+ if (!detail.promises.length) return;
9522
+ const saves = Promise.allSettled([flushedSavesStillWriting, ...detail.promises]);
9523
+ flushedSavesStillWriting = saves;
9524
+ void saves.then(() => {
9525
+ if (flushedSavesStillWriting === saves) flushedSavesStillWriting = null;
9526
+ });
9527
+ }
9528
+ function afterOlderSaves(run) {
9529
+ return flushedSavesStillWriting && flushedSavesStillWriting.then(run);
9530
+ }
9531
+ function trackedStudioEdit(edit, { afterOlderSaves: waits = false } = {}) {
9514
9532
  return (...args) => {
9533
+ commitOlderDebouncedEdits();
9534
+ const deferred = waits && !adopting ? afterOlderSaves(() => edit(...args)) : null;
9535
+ if (deferred) return trackStudioPendingEdit(deferred);
9515
9536
  const result = edit(...args);
9516
9537
  if (result instanceof Promise) trackStudioPendingEdit(result);
9517
9538
  return result;
9518
9539
  };
9519
9540
  }
9520
9541
  function beginStudioPendingEdit(revert) {
9542
+ commitOlderDebouncedEdits();
9521
9543
  let settle;
9522
9544
  const promise = trackStudioPendingEdit(new Promise((resolve) => settle = resolve));
9523
9545
  const entry2 = pendingEdits.get(promise);
@@ -9557,7 +9579,8 @@ function beginStudioPendingEdit(revert) {
9557
9579
  // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
9558
9580
  adopt(start) {
9559
9581
  try {
9560
- const committed = inFlight.within(start);
9582
+ const deferred = afterOlderSaves(() => inFlight.within(start));
9583
+ const committed = deferred ?? inFlight.within(start);
9561
9584
  landed = Promise.resolve(committed).then(
9562
9585
  () => true,
9563
9586
  () => saved2
@@ -12142,7 +12165,9 @@ function trackEditCommits(actions) {
12142
12165
  const tracked = { ...actions };
12143
12166
  for (const key2 of Object.keys(SAVES_AN_EDIT)) {
12144
12167
  if (!SAVES_AN_EDIT[key2]) continue;
12145
- tracked[key2] = trackedStudioEdit(actions[key2]);
12168
+ tracked[key2] = trackedStudioEdit(actions[key2], {
12169
+ afterOlderSaves: true
12170
+ });
12146
12171
  }
12147
12172
  return tracked;
12148
12173
  }
@@ -17115,6 +17140,9 @@ function applyAuthoredInlineOpacity(style, authored) {
17115
17140
  else style.setProperty("opacity", authored);
17116
17141
  }
17117
17142
 
17143
+ // src/utils/softReloadTargets.ts
17144
+ import { parseGsapScript } from "@hyperframes/parsers/gsap-parser";
17145
+
17118
17146
  // src/utils/elementGsap.ts
17119
17147
  function gsapOf(element) {
17120
17148
  return element.ownerDocument.defaultView?.gsap;
@@ -17169,7 +17197,7 @@ function addTweenTargets(tween, targets) {
17169
17197
  targets.set(el, seen);
17170
17198
  }
17171
17199
  }
17172
- function collectResetTargets(win, doc, targetKeys) {
17200
+ function collectResetTargets(win, doc, targetKeys, outgoingScripts = []) {
17173
17201
  const targets = /* @__PURE__ */ new Map();
17174
17202
  const timelines = win.__timelines ?? {};
17175
17203
  const others = new Set(
@@ -17182,6 +17210,8 @@ function collectResetTargets(win, doc, targetKeys) {
17182
17210
  }
17183
17211
  }
17184
17212
  sweepHeldElements(doc, targetKeys, others, targets);
17213
+ addLiveSetTargets(doc, targetKeys, targets);
17214
+ for (const script of outgoingScripts) addStandaloneSetTargets(doc, script, targets);
17185
17215
  return targets;
17186
17216
  }
17187
17217
  function tweenTargetsIn(timelines) {
@@ -17215,6 +17245,43 @@ function sweepHeldElements(doc, targetKeys, others, targets) {
17215
17245
  }
17216
17246
  }
17217
17247
  }
17248
+ var liveSets = /* @__PURE__ */ new WeakMap();
17249
+ function recordLiveSet(el, vars) {
17250
+ const byElement = liveSets.get(el.ownerDocument) ?? /* @__PURE__ */ new Map();
17251
+ liveSets.set(el.ownerDocument, byElement);
17252
+ const props = byElement.get(el) ?? /* @__PURE__ */ new Set();
17253
+ for (const prop of tweenedProps(vars)) props.add(prop);
17254
+ byElement.set(el, props);
17255
+ }
17256
+ function addLiveSetTargets(doc, targetKeys, targets) {
17257
+ const roots = new Set(targetKeys.map((k) => compositionRoot(doc, k)));
17258
+ for (const [el, props] of liveSets.get(doc) ?? []) {
17259
+ if (!roots.has(el) && !roots.has(el.parentElement?.closest("[data-composition-id]"))) continue;
17260
+ const seen = targets.get(el) ?? /* @__PURE__ */ new Set();
17261
+ for (const prop of props) seen.add(prop);
17262
+ targets.set(el, seen);
17263
+ }
17264
+ }
17265
+ function forgetLiveSets(reset) {
17266
+ for (const el of reset.keys()) liveSets.get(el.ownerDocument)?.delete(el);
17267
+ }
17268
+ function addStandaloneSetTargets(doc, outgoingScript, targets) {
17269
+ if (!outgoingScript.includes("gsap.set(")) return;
17270
+ for (const set of parseGsapScript(outgoingScript).animations) {
17271
+ if (set.method !== "set" || !set.global) continue;
17272
+ let els;
17273
+ try {
17274
+ els = [...doc.querySelectorAll(set.targetSelector)];
17275
+ } catch {
17276
+ continue;
17277
+ }
17278
+ for (const el of els) {
17279
+ const seen = targets.get(el) ?? /* @__PURE__ */ new Set();
17280
+ for (const prop of tweenedProps(set.properties)) seen.add(prop);
17281
+ targets.set(el, seen);
17282
+ }
17283
+ }
17284
+ }
17218
17285
  function fileDocs(ownFile, written, nestedFiles) {
17219
17286
  const docs = /* @__PURE__ */ new Map();
17220
17287
  return (file) => {
@@ -17378,7 +17445,12 @@ function applySoftReload(iframe, scriptText, options = {}) {
17378
17445
  const readAuthoredOpacity = (el) => findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
17379
17446
  const doReload = () => {
17380
17447
  const timelines = win.__timelines;
17381
- const targets = collectResetTargets(win, doc, targetKeys);
17448
+ const targets = collectResetTargets(
17449
+ win,
17450
+ doc,
17451
+ targetKeys,
17452
+ staleScripts.map((script) => script.textContent ?? "")
17453
+ );
17382
17454
  if (timelines) {
17383
17455
  for (const key2 of targetKeys) {
17384
17456
  const tl = timelines[key2];
@@ -17409,6 +17481,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
17409
17481
  }
17410
17482
  }
17411
17483
  }
17484
+ forgetLiveSets(targets);
17412
17485
  for (const script of staleScripts) script.remove();
17413
17486
  const executeScript = () => {
17414
17487
  if (win.MotionPathPlugin && win.gsap?.registerPlugin) {
@@ -17636,25 +17709,33 @@ import { findTimelineScript } from "@hyperframes/core/gsap-parser-acorn";
17636
17709
  import { walkCompositionDescendants } from "@hyperframes/parsers/hf-ids";
17637
17710
 
17638
17711
  // src/utils/gsapLiveRetime.ts
17639
- import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
17712
+ import { parseGsapScriptAcorn, scriptShape } from "@hyperframes/parsers/gsap-parser-acorn";
17640
17713
  import { hasExplicitTime } from "@hyperframes/parsers/gsap-writer-acorn";
17641
17714
  import { RUNTIME_FILLER } from "@hyperframes/core/runtime/protocol";
17642
17715
  var RERUN = { kind: "rerun" };
17643
- var withoutLayout = (code) => code.replace(/\s+/g, " ").replace(/ ?([^\w$ ]) ?/g, "$1").trim();
17644
17716
  function untimedShape(animation) {
17645
17717
  const { id: _id, position: _p, resolvedStart: _s, duration: _d, ...rest } = animation;
17646
17718
  const provenance = rest.provenance && { ...rest.provenance, sourceRange: void 0 };
17647
17719
  return JSON.stringify(
17648
17720
  { ...rest, provenance },
17649
- (_key, value) => typeof value === "string" && value.startsWith("__raw:") ? withoutLayout(value) : value
17721
+ (_key, value) => typeof value === "string" && value.startsWith("__raw:") ? scriptShape(`(${value.slice(6)}
17722
+ )`) ?? value : value
17650
17723
  );
17651
17724
  }
17652
17725
  var onTimeline = (animation) => !animation.global;
17653
17726
  var placedAlone = (animation) => hasExplicitTime(animation) && typeof animation.resolvedStart === "number";
17654
17727
  var lengthOf = (animation) => animation.method === "set" ? 0 : animation.durationUnresolved ? void 0 : animation.duration;
17655
- var withoutNumbers = (script) => withoutLayout(script.replace(/(?<![\w$#.])\d*\.?\d+/g, "0"));
17728
+ var shapes = /* @__PURE__ */ new Map();
17729
+ function shapeOf(code) {
17730
+ const shape = shapes.has(code) ? shapes.get(code) : scriptShape(code, true);
17731
+ shapes.delete(code);
17732
+ shapes.set(code, shape);
17733
+ if (shapes.size > 2) shapes.delete(shapes.keys().next().value);
17734
+ return shape;
17735
+ }
17656
17736
  function planLiveRetime(before, after) {
17657
- if (withoutNumbers(before) !== withoutNumbers(after)) return RERUN;
17737
+ const shape = shapeOf(before);
17738
+ if (shape === null || shape !== shapeOf(after)) return RERUN;
17658
17739
  const keys = timelineKeys(after);
17659
17740
  if (keys.length !== 1 || timelineKeys(before).join() !== keys.join()) return RERUN;
17660
17741
  const was = parseGsapScriptAcorn(before).animations.filter(onTimeline);
@@ -17720,15 +17801,10 @@ function sharesOneStretch(tweens) {
17720
17801
  );
17721
17802
  return stretch > 0 && fits;
17722
17803
  }
17723
- function inOrHostsSubComposition(target, ownId) {
17724
- const element = target;
17725
- const owner = element.closest?.("[data-composition-id]");
17726
- return owner != null && owner.getAttribute("data-composition-id") !== ownId || element.querySelector?.("[data-composition-id]") != null;
17727
- }
17728
17804
  var keepsOrder = (tweens) => tweens.every(
17729
17805
  (t2, k) => k === 0 || (tweens[k - 1].start - t2.start || tweens[k - 1].source - t2.source) < 0
17730
17806
  );
17731
- function movesKeepEachElementsHistory(timeline, plan, children) {
17807
+ function movesKeepEachElementsHistory(timeline, plan, children, registry3) {
17732
17808
  const byElement = /* @__PURE__ */ new Map();
17733
17809
  children.forEach((child, i) => {
17734
17810
  for (const target of child.targets?.() ?? []) {
@@ -17740,16 +17816,25 @@ function movesKeepEachElementsHistory(timeline, plan, children) {
17740
17816
  const moved = /* @__PURE__ */ new Set();
17741
17817
  for (const [target, indexes] of byElement) {
17742
17818
  if (!indexes.some((i) => plan.tweens[i].moved)) continue;
17743
- if (inOrHostsSubComposition(target, plan.key)) return false;
17744
17819
  const tweens = indexes.map((i) => plan.tweens[i]);
17745
17820
  const shifted = tweens.every((t2) => t2.duration === t2.wasDuration);
17746
17821
  const undelayed = indexes.every((i) => !children[i].delay() && !children[i].repeatDelay?.());
17747
17822
  if (!keepsOrder(tweens) || !sharesOneStretch(tweens) || !(shifted || undelayed)) return false;
17748
17823
  moved.add(target);
17749
17824
  }
17750
- return !timeline.getChildren(false, false, true).some(
17751
- (sub) => sub.getChildren(true, true, false).some((tween) => tween.targets?.().some((t2) => moved.has(t2)))
17825
+ return !animatedElsewhere(timeline, registry3, moved);
17826
+ }
17827
+ function animatedElsewhere(own, registry3, targets) {
17828
+ const seen = /* @__PURE__ */ new Set([own]);
17829
+ const visit = (timeline) => {
17830
+ if (seen.has(timeline)) return false;
17831
+ seen.add(timeline);
17832
+ return timeline.getChildren(false, true, false).some((tween) => tween.targets?.().some((target) => targets.has(target))) || timeline.getChildren(false, false, true).some(visit);
17833
+ };
17834
+ const timelines = registry3.filter(
17835
+ (entry2) => typeof entry2?.getChildren === "function"
17752
17836
  );
17837
+ return own.getChildren(false, false, true).some(visit) || timelines.some(visit);
17753
17838
  }
17754
17839
  function applyLiveRetime(iframe, plan) {
17755
17840
  const win = iframe?.contentWindow;
@@ -17758,7 +17843,8 @@ function applyLiveRetime(iframe, plan) {
17758
17843
  const children = pairedLiveTweens(timeline, plan);
17759
17844
  if (!children) return false;
17760
17845
  if (plan.tweens.some((t2) => t2.moved)) {
17761
- if (!movesKeepEachElementsHistory(timeline, plan, children)) return false;
17846
+ const registry3 = Object.values(win?.__timelines ?? {});
17847
+ if (!movesKeepEachElementsHistory(timeline, plan, children, registry3)) return false;
17762
17848
  const bySource = plan.tweens.map((t2, i) => ({ ...t2, tween: children[i] })).sort((a, b) => a.source - b.source);
17763
17849
  for (const { tween } of bySource) timeline.remove(tween);
17764
17850
  for (const { tween, start, duration } of bySource) {
@@ -21261,7 +21347,10 @@ import {
21261
21347
  Scissors as PhScissors,
21262
21348
  Link as PhLink,
21263
21349
  Eyedropper as PhEyedropper,
21264
- Trash as PhTrash
21350
+ Trash as PhTrash,
21351
+ Image as PhImage,
21352
+ BezierCurve,
21353
+ FolderSimple
21265
21354
  } from "@phosphor-icons/react";
21266
21355
  import { jsx as jsx40 } from "react/jsx-runtime";
21267
21356
  var makeIcon = (Icon) => {
@@ -21306,6 +21395,9 @@ var Scissors = makeIcon(PhScissors);
21306
21395
  var Link = makeIcon(PhLink);
21307
21396
  var Eyedropper = makeIcon(PhEyedropper);
21308
21397
  var Trash = makeIcon(PhTrash);
21398
+ var ImageIcon = makeIcon(PhImage);
21399
+ var Vector = makeIcon(BezierCurve);
21400
+ var Folder = makeIcon(FolderSimple);
21309
21401
 
21310
21402
  // src/player/components/TrackClipCount.tsx
21311
21403
  import { jsx as jsx41 } from "react/jsx-runtime";
@@ -50812,7 +50904,7 @@ function revealedScroll(shape, scrollLeft) {
50812
50904
  if (shape.left < scrollLeft) return shape.left;
50813
50905
  return Math.max(scrollLeft, shape.left + shape.size + 1 - shape.width);
50814
50906
  }
50815
- function measureFill(list, shapes) {
50907
+ function measureFill(list, shapes2) {
50816
50908
  const fill = fillOf(list);
50817
50909
  const tab = list.querySelector(":scope > .dv-active-tab");
50818
50910
  const shape = {
@@ -50821,8 +50913,8 @@ function measureFill(list, shapes) {
50821
50913
  left: tab?.offsetLeft ?? 0,
50822
50914
  size: tab?.offsetWidth ?? 0
50823
50915
  };
50824
- const reveal2 = tab !== null && reshaped(shapes.get(list), shape);
50825
- if (shape.width > 0) shapes.set(list, shape);
50916
+ const reveal2 = tab !== null && reshaped(shapes2.get(list), shape);
50917
+ if (shape.width > 0) shapes2.set(list, shape);
50826
50918
  const scrollLeft = list.scrollLeft;
50827
50919
  const target = reveal2 ? revealedScroll(shape, scrollLeft) : scrollLeft;
50828
50920
  return () => {
@@ -50857,10 +50949,10 @@ function installTabFill(api, root) {
50857
50949
  observed.add(element);
50858
50950
  }
50859
50951
  }
50860
- const shapes = /* @__PURE__ */ new WeakMap();
50952
+ const shapes2 = /* @__PURE__ */ new WeakMap();
50861
50953
  function placeAll() {
50862
50954
  const strips2 = [...lists()];
50863
- const writes = strips2.map((list) => measureFill(list, shapes));
50955
+ const writes = strips2.map((list) => measureFill(list, shapes2));
50864
50956
  for (const write of writes) write();
50865
50957
  for (const list of strips2) markClippedEdges(list);
50866
50958
  observe(strips2.flatMap((list) => [list, ...list.querySelectorAll(":scope > .dv-tab")]));
@@ -58903,6 +58995,25 @@ function formatPropValue(prop, v) {
58903
58995
  if (PERCENT_PROPS.has(prop)) return `${Math.round(Number(v) * 100)}${unit}`;
58904
58996
  return `${v}${unit}`;
58905
58997
  }
58998
+ var easeLabel = (ease) => ease.startsWith("custom(") ? "custom" : EASE_LABELS[ease] ?? ease;
58999
+ function uniformSegmentEase(animation) {
59000
+ const eases = keyframedTweenEases(animation);
59001
+ const steps = animation.keyframes?.keyframes ?? [];
59002
+ const segmentEases = new Set(steps.slice(1).map((kf) => eases.segment(kf)));
59003
+ if (segmentEases.size === 0) return eases.segment({});
59004
+ return segmentEases.size === 1 ? [...segmentEases][0] : null;
59005
+ }
59006
+ function keyframedTweenSummary(animation, pos, dur) {
59007
+ const steps = animation.keyframes?.keyframes ?? [];
59008
+ const props = [...new Set(steps.flatMap((kf) => Object.keys(kf.properties)))];
59009
+ const propText = props.map((p) => (PROP_LABELS[p] ?? p).toLowerCase()).join(", ");
59010
+ const segmentEase = uniformSegmentEase(animation);
59011
+ const segmentText = steps.length < 2 ? "" : segmentEase ? `, each segment eased ${easeLabel(segmentEase)}` : ", with per-keyframe easing";
59012
+ const run = keyframedTweenEases(animation).run;
59013
+ const runText = run && run !== "none" ? `, across a ${easeLabel(run)} run` : "";
59014
+ const count = `${steps.length} keyframe${steps.length === 1 ? "" : "s"}`;
59015
+ return `Starting at ${pos}s, over ${dur}s, animate ${animation.targetSelector}'s ${propText || "no properties yet"} through ${count}${segmentText}${runText}.`;
59016
+ }
58906
59017
  function buildTweenSummary(animation) {
58907
59018
  const easeName = animation.ease ?? "none";
58908
59019
  const ease = EASE_LABELS[easeName] ?? easeName;
@@ -58911,6 +59022,7 @@ function buildTweenSummary(animation) {
58911
59022
  const dur = animation.duration ?? 0;
58912
59023
  const rawPos = animation.position;
58913
59024
  const pos = typeof rawPos === "number" ? parseFloat(rawPos.toFixed(3)) : rawPos;
59025
+ if (animation.keyframes) return keyframedTweenSummary(animation, pos, dur);
58914
59026
  const propDescs = props.map(([p, v]) => {
58915
59027
  const label = (PROP_LABELS[p] ?? p).toLowerCase();
58916
59028
  return `${label} to ${formatPropValue(p, v)}`;
@@ -59125,6 +59237,7 @@ var APPLY_ALL_EASES = [
59125
59237
  "bounce.out",
59126
59238
  "bounce.inOut"
59127
59239
  ];
59240
+ var percentLabel = (pct) => pct > 0 && pct < 1e-3 ? "<0.001" : String(roundTo3(pct));
59128
59241
  function KeyframeEaseList({
59129
59242
  keyframes,
59130
59243
  globalEase,
@@ -59159,8 +59272,8 @@ function KeyframeEaseList({
59159
59272
  if (i === 0) return null;
59160
59273
  const segEase = kf.ease ?? globalEase;
59161
59274
  const isExpanded = expandedPct === kf.percentage;
59162
- const label = `${keyframes[i - 1].percentage}% \u2192 ${kf.percentage}%`;
59163
- const easeLabel = segEase.startsWith("custom(") ? "Custom" : EASE_LABELS[segEase] ?? segEase;
59275
+ const label = `${percentLabel(keyframes[i - 1].percentage)}% \u2192 ${percentLabel(kf.percentage)}%`;
59276
+ const easeLabel2 = segEase.startsWith("custom(") ? "Custom" : EASE_LABELS[segEase] ?? segEase;
59164
59277
  return /* @__PURE__ */ jsxs105(
59165
59278
  "div",
59166
59279
  {
@@ -59176,7 +59289,7 @@ function KeyframeEaseList({
59176
59289
  className: "flex w-full items-center gap-2 px-2 py-1.5 text-left active:scale-[0.99]",
59177
59290
  children: [
59178
59291
  /* @__PURE__ */ jsx140("span", { className: "text-[10px] font-medium text-neutral-400", children: label }),
59179
- /* @__PURE__ */ jsx140("span", { className: "ml-auto text-[9px] text-neutral-500", children: easeLabel }),
59292
+ /* @__PURE__ */ jsx140("span", { className: "ml-auto text-[9px] text-neutral-500", children: easeLabel2 }),
59180
59293
  /* @__PURE__ */ jsx140(
59181
59294
  "svg",
59182
59295
  {
@@ -59389,6 +59502,7 @@ function parseNumericOrString(raw) {
59389
59502
 
59390
59503
  // src/components/editor/AnimationCard.tsx
59391
59504
  import { Fragment as Fragment40, jsx as jsx142, jsxs as jsxs107 } from "react/jsx-runtime";
59505
+ var easeDisplayName = (ease) => ease.startsWith("custom(") ? "Custom curve" : EASE_LABELS[ease] ?? ease;
59392
59506
  var AnimationCard = memo37(function AnimationCard2({
59393
59507
  animation,
59394
59508
  defaultExpanded,
@@ -59494,7 +59608,9 @@ var AnimationCard = memo37(function AnimationCard2({
59494
59608
  const [copied, setCopied] = useState75(false);
59495
59609
  const methodLabel = METHOD_LABELS[animation.method] ?? animation.method;
59496
59610
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
59497
- const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
59611
+ const headerEase = animation.keyframes ? uniformSegmentEase(animation) : easeName;
59612
+ const easeLabel2 = headerEase === null ? "Mixed" : easeDisplayName(headerEase);
59613
+ const runEase = animation.keyframes ? keyframedTweenEases(animation).run : void 0;
59498
59614
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
59499
59615
  const summary = useMemo57(() => buildTweenSummary(animation), [animation]);
59500
59616
  const setKeys = Object.keys(animation.properties);
@@ -59552,8 +59668,9 @@ var AnimationCard = memo37(function AnimationCard2({
59552
59668
  "span",
59553
59669
  {
59554
59670
  className: `ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`,
59555
- title: easeName,
59556
- children: easeLabel
59671
+ title: headerEase ?? "Mixed",
59672
+ "data-card-ease": true,
59673
+ children: easeLabel2
59557
59674
  }
59558
59675
  ),
59559
59676
  /* @__PURE__ */ jsx142(
@@ -59593,6 +59710,10 @@ var AnimationCard = memo37(function AnimationCard2({
59593
59710
  }
59594
59711
  ),
59595
59712
  "Keyframed \u2014 click a segment below to edit its curve"
59713
+ ] }),
59714
+ runEase && runEase !== "none" && /* @__PURE__ */ jsxs107("p", { className: "mt-1 text-[9px] text-neutral-500", "data-card-run-ease": true, children: [
59715
+ "Run ease: ",
59716
+ easeDisplayName(runEase)
59596
59717
  ] })
59597
59718
  ] }),
59598
59719
  /* @__PURE__ */ jsx142(
@@ -59636,7 +59757,7 @@ var AnimationCard = memo37(function AnimationCard2({
59636
59757
  KeyframeEaseList,
59637
59758
  {
59638
59759
  keyframes: animation.keyframes.keyframes,
59639
- globalEase: animation.keyframes.easeEach ?? animation.ease ?? "none",
59760
+ globalEase: keyframedTweenEases(animation).segment({}),
59640
59761
  expandedPct: expandedKfPct,
59641
59762
  collidingAnimationTargets: focusedCollidingAnimationTargets,
59642
59763
  onToggle: (pct) => {
@@ -70797,7 +70918,7 @@ import {
70797
70918
  Trash as Trash2,
70798
70919
  FilePlus,
70799
70920
  FolderSimplePlus,
70800
- FolderSimple
70921
+ FolderSimple as FolderSimple2
70801
70922
  } from "@phosphor-icons/react";
70802
70923
 
70803
70924
  // src/components/editor/FileTreeIcons.tsx
@@ -70818,7 +70939,7 @@ import {
70818
70939
  File as File2,
70819
70940
  Waveform,
70820
70941
  TextAa,
70821
- Image as PhImage
70942
+ Image as PhImage2
70822
70943
  } from "@phosphor-icons/react";
70823
70944
  import { jsx as jsx189 } from "react/jsx-runtime";
70824
70945
  var SZ = 14;
@@ -70841,7 +70962,7 @@ var ICONS = /* @__PURE__ */ new Map([
70841
70962
  ["txt", [FileTxt, GRAY]],
70842
70963
  ["png", [FilePng, GREEN]],
70843
70964
  ["jpg", [FileJpg, GREEN]],
70844
- ["webp", [PhImage, GREEN]],
70965
+ ["webp", [PhImage2, GREEN]],
70845
70966
  ["mp4", [FileVideo, "text-[light-dark(oklch(0.59_0.2_304),oklch(0.71_0.2_304))]"]],
70846
70967
  ["mp3", [Waveform, "text-[light-dark(oklch(0.53_0.159_165),#3CE6AC)]"]],
70847
70968
  ["woff", [TextAa, DIM]]
@@ -71139,7 +71260,7 @@ function InlineInput({
71139
71260
  className: "flex items-center gap-2 py-0.5 min-h-7",
71140
71261
  style: { paddingLeft: `${8 + depth * 12 + (isFolder ? 0 : 14)}px` },
71141
71262
  children: [
71142
- isFolder ? /* @__PURE__ */ jsx190(FolderSimple, { size: SZ_ICON, weight: "duotone", className: `shrink-0 ${DIM}` }) : /* @__PURE__ */ jsx190(FileIcon, { path: value }),
71263
+ isFolder ? /* @__PURE__ */ jsx190(FolderSimple2, { size: SZ_ICON, weight: "duotone", className: `shrink-0 ${DIM}` }) : /* @__PURE__ */ jsx190(FileIcon, { path: value }),
71143
71264
  /* @__PURE__ */ jsxs149("div", { className: "flex-1 min-w-0", children: [
71144
71265
  /* @__PURE__ */ jsx190(
71145
71266
  "input",
@@ -80537,6 +80658,7 @@ function savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selec
80537
80658
  }
80538
80659
 
80539
80660
  // src/hooks/useDomGeometryCommits.ts
80661
+ var boxSizeCommitCounter = 0;
80540
80662
  function useDomGeometryCommits({
80541
80663
  showToast,
80542
80664
  commitPositionPatchToHtml,
@@ -80566,7 +80688,10 @@ function useDomGeometryCommits({
80566
80688
  if (offset) patches.push(...writePlainMove(element, offset));
80567
80689
  return commitPositionPatchToHtml(selection, patches, {
80568
80690
  label: "Resize layer box",
80569
- ...undoKey ? { coalesceKey: undoKey, coalesceMs: Number.POSITIVE_INFINITY, deferRender: true } : { coalesceKey: `box-size:${getDomEditTargetKey(selection)}` }
80691
+ ...undoKey ? { coalesceKey: undoKey, coalesceMs: Number.POSITIVE_INFINITY, deferRender: true } : {
80692
+ coalesceKey: `box-size:${++boxSizeCommitCounter}`,
80693
+ coalesceMs: Number.POSITIVE_INFINITY
80694
+ }
80570
80695
  }).catch((error) => {
80571
80696
  restoreStudioBoxSize(element, beforeSize);
80572
80697
  if (offset) restoreStudioPathOffset(element, beforeOffset);
@@ -81004,6 +81129,7 @@ function applyGlobalSet(iframe, selector, props) {
81004
81129
  const numeric = finiteNumericProps(props);
81005
81130
  if (!gsapLib?.set || !el || !numeric) return false;
81006
81131
  gsapLib.set(el, numeric);
81132
+ recordLiveSet(el, numeric);
81007
81133
  return true;
81008
81134
  } catch {
81009
81135
  return false;
@@ -84045,7 +84171,7 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
84045
84171
  for (const k of Object.keys(properties)) {
84046
84172
  if (!(k in p) && backfillDefaults[k] != null) p[k] = backfillDefaults[k];
84047
84173
  }
84048
- return { percentage: newPct, properties: p };
84174
+ return { percentage: newPct, properties: p, ...kf.ease ? { ease: kf.ease } : {} };
84049
84175
  });
84050
84176
  remapped.push({ percentage: toNewPct(ct), properties });
84051
84177
  remapped.sort((a, b) => a.percentage - b.percentage);
@@ -84057,7 +84183,8 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
84057
84183
  targetSelector: anim.targetSelector,
84058
84184
  position: roundTo3(newStart),
84059
84185
  duration: roundTo3(newDuration),
84060
- keyframes: remapped
84186
+ keyframes: remapped,
84187
+ ...keyframeEases(anim)
84061
84188
  },
84062
84189
  { label: `Edit ${primaryProp} (extended keyframe)`, keyframeAction: "add", softReload: true }
84063
84190
  );
@@ -89818,19 +89945,28 @@ function useGestureCommit({
89818
89945
  const overlaps3 = recStart < tweenEnd + 0.05 && recEnd > tweenStart - 0.05;
89819
89946
  if (overlaps3) {
89820
89947
  const existingKfs = existingPositionTween.keyframes?.keyframes ?? [];
89821
- const rangeStartPct = tweenDur > 0 ? Math.max(0, (recStart - tweenStart) / tweenDur * 100) : 0;
89822
- const rangeEndPct = tweenDur > 0 ? Math.min(100, (recEnd - tweenStart) / tweenDur * 100) : 100;
89823
- const preserved = existingKfs.filter(
89824
- (kf) => kf.percentage < rangeStartPct - 0.5 || kf.percentage > rangeEndPct + 0.5
89825
- ).map((kf) => ({
89948
+ const startTimePct = tweenDur > 0 ? Math.max(0, (recStart - tweenStart) / tweenDur * 100) : 0;
89949
+ const endTimePct = tweenDur > 0 ? Math.min(100, (recEnd - tweenStart) / tweenDur * 100) : 100;
89950
+ const runEase = runEaseOf(existingPositionTween);
89951
+ const roundPct3 = warpsTime(runEase) ? (pct) => pct : roundTo3;
89952
+ const preserved = existingKfs.filter((kf) => {
89953
+ const playsAt = timeAtProgress(runEase, kf.percentage);
89954
+ return playsAt < startTimePct - 0.5 || playsAt > endTimePct + 0.5;
89955
+ }).map((kf) => ({
89826
89956
  percentage: kf.percentage,
89827
89957
  properties: kf.properties,
89828
89958
  ...kf.ease ? { ease: kf.ease } : {}
89829
89959
  }));
89830
- const mapped = keyframes.map((kf) => ({
89831
- percentage: rangeStartPct + kf.percentage / 100 * (rangeEndPct - rangeStartPct),
89960
+ const constantSpeed = { ease: "none" };
89961
+ const mapped = keyframes.map((kf, index) => ({
89962
+ percentage: roundPct3(
89963
+ progressAtTime(
89964
+ runEase,
89965
+ startTimePct + kf.percentage / 100 * (endTimePct - startTimePct)
89966
+ )
89967
+ ),
89832
89968
  properties: kf.properties,
89833
- ...kf.ease ? { ease: kf.ease } : {}
89969
+ ...kf.ease ? { ease: kf.ease } : index > 0 ? constantSpeed : {}
89834
89970
  }));
89835
89971
  const merged = [...preserved, ...mapped].sort((a, b) => a.percentage - b.percentage);
89836
89972
  await commit2(
@@ -89840,7 +89976,8 @@ function useGestureCommit({
89840
89976
  targetSelector: selector,
89841
89977
  position: typeof existingPositionTween.position === "number" ? existingPositionTween.position : tweenStart,
89842
89978
  duration: tweenDur,
89843
- keyframes: merged
89979
+ keyframes: merged,
89980
+ ...keyframeEases(existingPositionTween)
89844
89981
  },
89845
89982
  { label: "Gesture recording (merge)", softReload: true, keyframeAction: "add" }
89846
89983
  );
@@ -92735,6 +92872,21 @@ import { memo as memo59, useCallback as useCallback189 } from "react";
92735
92872
  // src/components/editor/LayersPanel.tsx
92736
92873
  import { memo as memo50, useState as useState143, useCallback as useCallback174, useEffect as useEffect145, useRef as useRef177 } from "react";
92737
92874
 
92875
+ // src/components/editor/layerKind.ts
92876
+ function isCompositionHost(el) {
92877
+ return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
92878
+ }
92879
+ function layerKindOf(el, hasChildren) {
92880
+ const tag = el.tagName.toLowerCase();
92881
+ if (tag === "img" || tag === "picture") return "image";
92882
+ if (tag === "video") return "video";
92883
+ if (tag === "audio") return "audio";
92884
+ if (tag === "svg") return "vector";
92885
+ if (el.hasAttribute("data-hf-group") || isCompositionHost(el)) return "group";
92886
+ if (hasOnlyFormattingChildren(el) && el.textContent?.trim()) return "text";
92887
+ return hasChildren ? "group" : "shape";
92888
+ }
92889
+
92738
92890
  // src/components/editor/useLayerDrag.ts
92739
92891
  import { useRef as useRef176, useState as useState142, useCallback as useCallback173 } from "react";
92740
92892
  var DRAG_THRESHOLD_PX4 = 4;
@@ -92946,36 +93098,17 @@ function getVisibleLayers(layers, collapsed) {
92946
93098
  // src/components/editor/LayersPanel.tsx
92947
93099
  import { jsx as jsx223, jsxs as jsxs178 } from "react/jsx-runtime";
92948
93100
  var LAYERS_PANEL_MAX_ROWS = 80;
92949
- var TAG_ICONS = {
92950
- video: "Vi",
92951
- audio: "Au",
92952
- img: "Im",
92953
- svg: "Sv",
92954
- canvas: "Cn",
92955
- div: "Di",
92956
- section: "Se",
92957
- span: "Sp",
92958
- p: "P",
92959
- h1: "H1",
92960
- h2: "H2",
92961
- h3: "H3",
92962
- h4: "H4",
92963
- h5: "H5",
92964
- h6: "H6",
92965
- a: "A",
92966
- button: "Bt",
92967
- ul: "Ul",
92968
- ol: "Ol",
92969
- li: "Li",
92970
- style: "St",
92971
- template: "Te"
93101
+ var [CARET, ICON, GAP] = [16, 20, 6];
93102
+ var CHILD_INDENT = CARET + GAP + ICON + GAP;
93103
+ var KIND_ICON = {
93104
+ image: ImageIcon,
93105
+ video: Film,
93106
+ audio: Music,
93107
+ vector: Vector,
93108
+ group: Folder,
93109
+ text: Type,
93110
+ shape: Square
92972
93111
  };
92973
- function getTagBadge(tagName) {
92974
- return TAG_ICONS[tagName] ?? tagName.slice(0, 2).toUpperCase();
92975
- }
92976
- function isCompositionHost(el) {
92977
- return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
92978
- }
92979
93112
  function createRafThrottle(run, throttleMs = 100) {
92980
93113
  let rafId = null;
92981
93114
  let lastFired = 0;
@@ -93270,7 +93403,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
93270
93403
  {
93271
93404
  type: "button",
93272
93405
  onClick: () => setActiveGroupElement(null),
93273
- className: "flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] text-panel-text-3 hover:bg-panel-hover/40 hover:text-panel-text-1",
93406
+ className: "mx-1 flex w-[calc(100%-0.5rem)] items-center gap-1.5 rounded-md px-2 py-1 text-left text-[11px] text-panel-text-3 hover:bg-panel-hover/40 hover:text-panel-text-1",
93274
93407
  children: [
93275
93408
  /* @__PURE__ */ jsx223("span", { "aria-hidden": "true", children: "\u2190" }),
93276
93409
  /* @__PURE__ */ jsx223("span", { className: "truncate", children: activeGroupElement.getAttribute("data-hf-group") || "Group" })
@@ -93284,6 +93417,10 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
93284
93417
  const isCollapsed = collapsed[layer.key] ?? false;
93285
93418
  const hasChildren = layer.childCount > 0;
93286
93419
  const isCompHost = isCompositionHost(layer.element);
93420
+ const kind = layerKindOf(layer.element, hasChildren);
93421
+ const KindIcon = KIND_ICON[kind];
93422
+ const kindName = kind[0].toUpperCase() + kind.slice(1);
93423
+ const hint = draggable ? layer.element.hasAttribute("data-hf-group") ? "Double-click to enter group" : void 0 : "This layer can't be reordered";
93287
93424
  return /* @__PURE__ */ jsxs178(
93288
93425
  "div",
93289
93426
  {
@@ -93300,9 +93437,9 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
93300
93437
  handleSelectLayer(layer);
93301
93438
  }
93302
93439
  },
93303
- className: `group flex w-full items-center gap-1.5 px-2 py-1 text-left transition-colors ${isDragged ? "opacity-40" : selected ? "bg-panel-accent/14 text-accent-ink" : "text-panel-text-2 hover:bg-panel-hover/40 hover:text-panel-text-1"} ${dragKey ? "cursor-grabbing" : "cursor-pointer"}`,
93304
- style: { paddingLeft: 8 + layer.depth * 16 },
93305
- title: draggable ? layer.element.hasAttribute("data-hf-group") ? "Double-click to enter group" : void 0 : "This layer can't be reordered",
93440
+ className: `group mx-1 flex items-center gap-1.5 rounded-md px-2 py-1 text-left transition-colors ${isDragged ? "opacity-40" : selected ? "bg-panel-accent/14 text-accent-ink" : "text-panel-text-2 hover:bg-panel-hover/40 hover:text-panel-text-1"} ${dragKey ? "cursor-grabbing" : "cursor-pointer"}`,
93441
+ style: { paddingLeft: 8 + layer.depth * CHILD_INDENT },
93442
+ title: hint,
93306
93443
  children: [
93307
93444
  hasChildren ? /* @__PURE__ */ jsx223(
93308
93445
  "button",
@@ -93324,12 +93461,14 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
93324
93461
  }
93325
93462
  )
93326
93463
  }
93327
- ) : /* @__PURE__ */ jsx223("span", { className: "w-4 shrink-0" }),
93464
+ ) : null,
93328
93465
  /* @__PURE__ */ jsx223(
93329
93466
  "span",
93330
93467
  {
93331
- className: `flex h-5 w-5 shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${selected ? "bg-panel-accent/18 text-accent-ink" : isCompHost ? "bg-on text-accent-ink" : "bg-panel-hover text-panel-text-4"}`,
93332
- children: getTagBadge(layer.tagName)
93468
+ "data-layer-kind": kind,
93469
+ title: hint ? `${kindName} \xB7 ${hint}` : kindName,
93470
+ className: `flex h-5 w-5 shrink-0 items-center justify-center rounded ${selected ? "bg-panel-accent/18 text-accent-ink" : isCompHost ? "bg-on text-accent-ink" : "bg-panel-hover text-panel-text-4"}`,
93471
+ children: /* @__PURE__ */ jsx223(KindIcon, { size: 12 })
93333
93472
  }
93334
93473
  ),
93335
93474
  /* @__PURE__ */ jsx223("span", { className: "min-w-0 flex-1 truncate text-[11px]", children: layer.label }),