fix(studio): address family B timeline review findings

- revert diamond selection when a rejected retime leaves the source in place
- clear project-local ease focus and expansion on player store reset
- share one static-position-hold predicate across the tween cache
- invalidate the GSAP cache even when a group timing rewrite throws
- stamp each lane keyframe's ease from its own source tween
- memoize property lanes and row offsets so memo'd diamond lanes hold
- use the editable tween duration for drag position commits
- restore the pre-t=0 pad in the all-collapsed content origin
- clamp the drag ghost and drop placeholder to the collapsed clip height
- aria-expanded on the layer disclosure, aria-pressed plus state-specific
  labels on the keyframe toggle, 24px chevron targets, focus-visible parity
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 00:40:42 +02:00
parent 4a12eb9d9c
commit 282cc883e5
16 changed files with 284 additions and 103 deletions
@@ -312,25 +312,32 @@ export function useTimelineGroupEditing({
// reload (see the trackOnly doc above). Mixed batches (any start
// change) keep the full fallback below.
if (trackOnly) return;
await finishGroupTimingGsapFallback({
projectId,
iframe: previewIframeRef.current,
reloadPreview,
label: "Move timeline clips",
errorLabel: "Failed to shift GSAP positions",
coalesceKey,
recordEdit,
activeCompPath,
changes,
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
mutateChange: (change, changePath) => {
const delta = change.start - change.element.start;
const domId = change.element.domId;
if (delta === 0 || !domId) return null;
return shiftGsapPositions(projectId, changePath, domId, delta);
},
});
invalidateGsapCache?.();
// The timing persist above already committed to disk, so the cached
// GSAP read is stale whether or not the position rewrite succeeded —
// invalidate on the error path too (matches the single-element path's
// `.finally`), or a failed rewrite leaves the editor reading old tweens.
try {
await finishGroupTimingGsapFallback({
projectId,
iframe: previewIframeRef.current,
reloadPreview,
label: "Move timeline clips",
errorLabel: "Failed to shift GSAP positions",
coalesceKey,
recordEdit,
activeCompPath,
changes,
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
mutateChange: (change, changePath) => {
const delta = change.start - change.element.start;
const domId = change.element.domId;
if (delta === 0 || !domId) return null;
return shiftGsapPositions(projectId, changePath, domId, delta);
},
});
} finally {
invalidateGsapCache?.();
}
}).catch((error) => {
// Failed persist: revert the optimistic duration readout + live root
// alongside the gesture owner's store rollback.
@@ -410,34 +417,40 @@ export function useTimelineGroupEditing({
coalesceMs,
);
}
await finishGroupTimingGsapFallback({
projectId,
iframe: previewIframeRef.current,
reloadPreview,
label: "Resize timeline clips",
errorLabel: "Failed to scale GSAP positions",
coalesceKey,
recordEdit,
activeCompPath,
changes,
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
mutateChange: (change, changePath) => {
const domId = change.element.domId;
const timingChanged =
change.start !== change.element.start || change.duration !== change.element.duration;
if (!timingChanged || !domId) return null;
return scaleGsapPositions(
projectId,
changePath,
domId,
change.element.start,
change.element.duration,
change.start,
change.duration,
);
},
});
invalidateGsapCache?.();
// See the move path: the timing persist is already on disk, so the GSAP
// cache must be invalidated even when the position rewrite throws.
try {
await finishGroupTimingGsapFallback({
projectId,
iframe: previewIframeRef.current,
reloadPreview,
label: "Resize timeline clips",
errorLabel: "Failed to scale GSAP positions",
coalesceKey,
recordEdit,
activeCompPath,
changes,
resolveChangePath: (element) => targetPathFor(element, activeCompPath),
mutateChange: (change, changePath) => {
const domId = change.element.domId;
const timingChanged =
change.start !== change.element.start ||
change.duration !== change.element.duration;
if (!timingChanged || !domId) return null;
return scaleGsapPositions(
projectId,
changePath,
domId,
change.element.start,
change.element.duration,
change.start,
change.duration,
);
},
});
} finally {
invalidateGsapCache?.();
}
}).catch((error) => {
// Failed persist: revert the optimistic duration readout + live root
// alongside the gesture owner's store rollback.