Skip to content

Commit 5c41357

Browse files
committed
feat(studio): app shell swap — mount EditorShell, retire the legacy engine
What: the final swap (43 files): App mounts EditorShell; contexts, hotkeys, sidebar (AssetsTab→AssetCard, toolbar, audio row), useTimelineEditing and the store drop the legacy single-select API and trimmed callbacks; deletes the 7 files this orphans (StudioPreviewArea, NLELayout, useTimelineGroupEditing, timelineLayerDrag + suites, legacy hook test); .fallowrc.jsonc lands at final content, dropping every TEMP(studio-dnd) suppression — everything shipped unwired is now reachable. Why: last step of the NLE integration; after this the tree is byte-identical to the previously-green integration checkpoint. How: modified files to final content + gate-forced deletions + suppression cleanup. Tree parity with checkpoint 86dd2a32b verified. Test plan: tsc --noEmit in studio, studio-server, core; bun run --filter @hyperframes/studio build; bunx vitest run (full suite); fallow audit clean with zero remaining TEMP entries; tree-diff vs 86dd2a32b empty.
1 parent 08ca624 commit 5c41357

44 files changed

Lines changed: 1424 additions & 5800 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.fallowrc.jsonc

Lines changed: 1 addition & 278 deletions
Large diffs are not rendered by default.

packages/core/src/runtime/timeline.ts

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -51,60 +51,6 @@ function maxDefinedNumber(...values: Array<number | null>): number | null {
5151
return Math.max(...finite);
5252
}
5353

54-
/**
55-
* When multiple content kinds share the same track number, split them
56-
* onto separate tracks so the timeline UI shows distinct rows.
57-
*
58-
* Preferred kind order (top → bottom): composition, video, image, element, audio.
59-
* Tracks that contain only one kind are left untouched.
60-
*/
61-
const KIND_ORDER: Record<string, number> = {
62-
composition: 0,
63-
video: 1,
64-
image: 2,
65-
element: 3,
66-
audio: 4,
67-
};
68-
69-
function normalizeTrackAssignments(clips: RuntimeTimelineClip[]): void {
70-
if (clips.length === 0) return;
71-
72-
// Group clips by their raw track number and detect which tracks have mixed kinds
73-
const trackKinds = new Map<number, Set<string>>();
74-
for (const clip of clips) {
75-
const kinds = trackKinds.get(clip.track) ?? new Set();
76-
kinds.add(clip.kind);
77-
trackKinds.set(clip.track, kinds);
78-
}
79-
80-
const hasMixedTracks = Array.from(trackKinds.values()).some((kinds) => kinds.size > 1);
81-
if (!hasMixedTracks) return;
82-
83-
// Build new contiguous track numbers, splitting mixed tracks by kind
84-
let nextTrack = 0;
85-
const newTrackMap = new Map<string, number>(); // "origTrack:kind" → newTrack
86-
87-
const sortedTracks = [...trackKinds.keys()].sort((a, b) => a - b);
88-
for (const track of sortedTracks) {
89-
const kinds = trackKinds.get(track)!;
90-
if (kinds.size === 1) {
91-
newTrackMap.set(`${track}:${[...kinds][0]}`, nextTrack++);
92-
} else {
93-
// Split by kind in preferred order
94-
const sorted = [...kinds].sort((a, b) => (KIND_ORDER[a] ?? 99) - (KIND_ORDER[b] ?? 99));
95-
for (const kind of sorted) {
96-
newTrackMap.set(`${track}:${kind}`, nextTrack++);
97-
}
98-
}
99-
}
100-
101-
for (const clip of clips) {
102-
const key = `${clip.track}:${clip.kind}`;
103-
const newTrack = newTrackMap.get(key);
104-
if (newTrack != null) clip.track = newTrack;
105-
}
106-
}
107-
10854
function toAbsoluteAssetUrl(rawValue: string | null | undefined): string | null {
10955
const raw = String(rawValue ?? "").trim();
11056
if (!raw) return null;
@@ -638,11 +584,12 @@ export function collectRuntimeTimelinePayload(params: {
638584
}
639585
}
640586

641-
// ── Track normalization ────────────────────────────────────────────────
642-
// When multiple content kinds (composition, audio, video, …) share the same
643-
// data-track-index value, split them onto separate tracks so the timeline UI
644-
// shows distinct rows for each kind.
645-
normalizeTrackAssignments(clips);
587+
// Track assignment honors the authored data-track-index verbatim: a clip stays
588+
// on the track it was placed on, regardless of kind. (Previously mixed-kind
589+
// tracks were split onto separate rows, but that renumbered tracks — breaking
590+
// "drop a clip onto an existing track" and causing the written track to drift
591+
// from the displayed one on every move. Track index is display-only; render
592+
// never reads it, so honoring it verbatim is the correct NLE behavior.)
646593

647594
for (const compositionNode of compositionNodes) {
648595
if (compositionNode === root) continue;

packages/studio/src/App.tsx

Lines changed: 89 additions & 116 deletions
Large diffs are not rendered by default.

packages/studio/src/components/StudioLeftSidebar.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface StudioLeftSidebarProps {
1818
linting: boolean;
1919
lintFindingCount?: number;
2020
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
21+
onAddAssetToTimeline?: (path: string) => void;
2122
}
2223

2324
// fallow-ignore-next-line complexity
@@ -30,6 +31,7 @@ export function StudioLeftSidebar({
3031
linting,
3132
lintFindingCount,
3233
lintFindingsByFile,
34+
onAddAssetToTimeline,
3335
}: StudioLeftSidebarProps) {
3436
const {
3537
leftCollapsed,
@@ -69,7 +71,7 @@ export function StudioLeftSidebar({
6971

7072
if (leftCollapsed) {
7173
return (
72-
<div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
74+
<div className="mr-0.5 flex w-10 flex-shrink-0 flex-col items-center rounded-lg border border-neutral-800/50 bg-neutral-950 pt-1">
7375
<button
7476
type="button"
7577
onClick={toggleLeftSidebar}
@@ -147,13 +149,18 @@ export function StudioLeftSidebar({
147149
onToggleCollapse={toggleLeftSidebar}
148150
onAddBlock={onAddBlock}
149151
onPreviewBlock={onPreviewBlock}
152+
onAddAssetToTimeline={onAddAssetToTimeline}
150153
/>
154+
{/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
155+
the absolutely-positioned inner hit area. The outer element is w-[3px] so
156+
it contributes only 3px of gap in the flex row; the inner -left-[2.5px]
157+
element widens the hit area to 8px without affecting layout. */}
151158
<div
152159
role="separator"
153160
aria-label="Resize sidebar"
154161
aria-orientation="vertical"
155162
tabIndex={0}
156-
className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center outline-none focus-visible:bg-studio-accent/20"
163+
className="group relative w-[3px] flex-shrink-0 cursor-col-resize outline-none focus-visible:bg-studio-accent/20"
157164
style={{ touchAction: "none" }}
158165
onPointerDown={(e) => handlePanelResizeStart("left", e)}
159166
onPointerMove={handlePanelResizeMove}
@@ -167,7 +174,10 @@ export function StudioLeftSidebar({
167174
setLeftWidth(Math.max(160, Math.min(maxLeft, leftWidth + delta)));
168175
}}
169176
>
170-
<div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
177+
{/* Expanded hit zone: 8px wide, centered on the 3px seam */}
178+
<div className="absolute inset-y-0 -left-[2.5px] w-2" />
179+
{/* Visible hairline */}
180+
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
171181
</div>
172182
</>
173183
);

0 commit comments

Comments
 (0)