feat(studio): render queue, layout restructure, home page, hover preview (#95)

## Summary
- Add render queue panel with progress tracking, download, and delete actions
- Restructure App layout: home page with project picker, session-based routing
- Add ExpandOnHover component for preview-on-hover interactions (uses motion/react)
- CompositionsTab now supports hover preview with expanded iframe view
- Vite config: guard setInterval cleanup to dev-only (fixes CI build timeout)
- Add favicon and update studio package deps

## Test plan
- [x] Render queue shows progress, completes, and allows download
- [x] Home page lists projects and navigates to session view
- [x] ExpandOnHover shows expanded preview on mouse hover with spring animation
- [x] `vite build` exits cleanly (no hanging process from setInterval)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Miguel Ángel
2026-03-28 21:28:43 +01:00
committed by GitHub
parent 9ae239d177
commit 40bd159103
24 changed files with 2495 additions and 796 deletions
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { generateTicks, formatTick } from "./Timeline";
import { generateTicks } from "./Timeline";
import { formatTime } from "../lib/time";
describe("generateTicks", () => {
it("returns empty arrays for duration <= 0", () => {
@@ -70,40 +71,40 @@ describe("generateTicks", () => {
});
});
describe("formatTick", () => {
describe("formatTime", () => {
it("formats 0 seconds as 0:00", () => {
expect(formatTick(0)).toBe("0:00");
expect(formatTime(0)).toBe("0:00");
});
it("formats seconds below a minute", () => {
expect(formatTick(5)).toBe("0:05");
expect(formatTick(30)).toBe("0:30");
expect(formatTick(59)).toBe("0:59");
expect(formatTime(5)).toBe("0:05");
expect(formatTime(30)).toBe("0:30");
expect(formatTime(59)).toBe("0:59");
});
it("formats exactly one minute", () => {
expect(formatTick(60)).toBe("1:00");
expect(formatTime(60)).toBe("1:00");
});
it("formats minutes and seconds", () => {
expect(formatTick(90)).toBe("1:30");
expect(formatTick(125)).toBe("2:05");
expect(formatTime(90)).toBe("1:30");
expect(formatTime(125)).toBe("2:05");
});
it("floors fractional seconds", () => {
expect(formatTick(5.7)).toBe("0:05");
expect(formatTick(59.9)).toBe("0:59");
expect(formatTick(90.5)).toBe("1:30");
expect(formatTime(5.7)).toBe("0:05");
expect(formatTime(59.9)).toBe("0:59");
expect(formatTime(90.5)).toBe("1:30");
});
it("handles large values", () => {
expect(formatTick(600)).toBe("10:00");
expect(formatTick(3661)).toBe("61:01");
expect(formatTime(600)).toBe("10:00");
expect(formatTime(3661)).toBe("61:01");
});
it("zero-pads seconds to two digits", () => {
expect(formatTick(1)).toBe("0:01");
expect(formatTick(9)).toBe("0:09");
expect(formatTick(61)).toBe("1:01");
expect(formatTime(1)).toBe("0:01");
expect(formatTime(9)).toBe("0:09");
expect(formatTime(61)).toBe("1:01");
});
});