From 6673c328686c39b153d2487289573396794cc620 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 9 Jul 2026 16:36:02 -0400 Subject: [PATCH] fix(studio): keep timeline selection authoritative in the preview sync The store-to-preview sync no longer applies a partial selection: if a resolvable member's DOM node is not ready yet it bails and retries on the next effect run, so the write-back can never shrink the store's selection by dropping an unresolved member. Marquee row hit-testing reuses shouldShowTimelineLayerGroupHeader instead of re-deriving the group-header placement rule, keeping one owner for that predicate. --- .../studio/src/hooks/useTimelineSelectionPreviewSync.ts | 7 +++++++ packages/studio/src/player/components/timelineEditing.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/hooks/useTimelineSelectionPreviewSync.ts b/packages/studio/src/hooks/useTimelineSelectionPreviewSync.ts index d26005c0e..33281c4df 100644 --- a/packages/studio/src/hooks/useTimelineSelectionPreviewSync.ts +++ b/packages/studio/src/hooks/useTimelineSelectionPreviewSync.ts @@ -88,13 +88,20 @@ export function useTimelineSelectionPreviewSync({ let cancelled = false; const syncSelection = async () => { const selections: DomEditSelection[] = []; + let resolvableCount = 0; for (const id of selectedIds) { const element = timelineElements.find((item) => (item.key ?? item.id) === id); if (!element) continue; + resolvableCount += 1; const selection = await buildDomSelectionForTimelineElement(element); if (selection) selections.push(selection); } if (cancelled) return; + // The store is the source of truth: applying a partial set would write that + // shrunk set back and silently drop the members whose DOM node was not ready. + // Bail instead; a later effect run (on timelineElements/DOM change) applies the + // full set once every resolvable member has a live node. + if (selections.length < resolvableCount) return; if (selections.length === 0) { applyDomSelection(null, { revealPanel: false }); } else if (selections.length === 1) { diff --git a/packages/studio/src/player/components/timelineEditing.ts b/packages/studio/src/player/components/timelineEditing.ts index 1ec873812..a8d1bb0a6 100644 --- a/packages/studio/src/player/components/timelineEditing.ts +++ b/packages/studio/src/player/components/timelineEditing.ts @@ -2,6 +2,7 @@ import { formatTime } from "../lib/time"; import { roundToCenti } from "../../utils/rounding"; import type { StackingTimelineLayer, TimelineLayerId } from "./timelineTrackOrder"; import { resolveTimelineLayerStackingMove } from "./timelineLayerDrag"; +import { shouldShowTimelineLayerGroupHeader } from "./TimelineLayerGroupHeader"; import type { TimelineStackingElement, TimelineStackingReorderIntent } from "./timelineStacking"; import { @@ -348,7 +349,7 @@ export function selectTimelineElementsInMarquee({ for (const layerId of layerOrder) { const layer = layerById.get(layerId); if (!layer) continue; - if (layer.contextKey !== "" && layer.contextKey !== previousContextKey) { + if (shouldShowTimelineLayerGroupHeader(layer.contextKey, previousContextKey)) { rowTop += groupHeaderHeight; } const rowBottom = rowTop + trackHeight;