@hyperframes/studio 0.4.12 → 0.4.13-alpha.1

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 (33) hide show
  1. package/dist/assets/hyperframes-player-BOs_kypk.js +198 -0
  2. package/dist/assets/index-BKkR67xb.css +1 -0
  3. package/dist/assets/index-rN5doSq1.js +93 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +289 -11
  7. package/src/components/nle/NLELayout.tsx +24 -7
  8. package/src/components/nle/NLEPreview.test.ts +32 -0
  9. package/src/components/nle/NLEPreview.tsx +12 -1
  10. package/src/player/components/CompositionThumbnail.tsx +94 -17
  11. package/src/player/components/EditModal.tsx +48 -29
  12. package/src/player/components/Player.tsx +5 -2
  13. package/src/player/components/PlayerControls.test.ts +20 -0
  14. package/src/player/components/PlayerControls.tsx +12 -1
  15. package/src/player/components/Timeline.test.ts +44 -1
  16. package/src/player/components/Timeline.tsx +686 -169
  17. package/src/player/components/TimelineClip.tsx +112 -16
  18. package/src/player/components/timelineEditing.test.ts +310 -0
  19. package/src/player/components/timelineEditing.ts +213 -0
  20. package/src/player/components/timelineTheme.test.ts +56 -0
  21. package/src/player/components/timelineTheme.ts +141 -0
  22. package/src/player/components/timelineZoom.test.ts +62 -0
  23. package/src/player/components/timelineZoom.ts +38 -0
  24. package/src/player/hooks/useTimelinePlayer.test.ts +96 -0
  25. package/src/player/hooks/useTimelinePlayer.ts +313 -59
  26. package/src/player/store/playerStore.test.ts +30 -12
  27. package/src/player/store/playerStore.ts +23 -9
  28. package/src/types/hyperframes-player.d.ts +1 -0
  29. package/src/utils/sourcePatcher.test.ts +84 -0
  30. package/src/utils/sourcePatcher.ts +143 -0
  31. package/dist/assets/hyperframes-player-5iD9BZnx.js +0 -198
  32. package/dist/assets/index-CVDXfFQ6.js +0 -93
  33. package/dist/assets/index-jmDaI2F7.css +0 -1
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ getClipHandleOpacity,
4
+ getRenderedTimelineElement,
5
+ getTimelineTrackStyle,
6
+ } from "./timelineTheme";
7
+
8
+ describe("getTimelineTrackStyle", () => {
9
+ it("reuses heading styles for heading tags", () => {
10
+ expect(getTimelineTrackStyle("h2").accent).toBe(getTimelineTrackStyle("h1").accent);
11
+ });
12
+
13
+ it("falls back for unknown tags", () => {
14
+ expect(getTimelineTrackStyle("custom-tag").accent).toBe("#3CE6AC");
15
+ });
16
+ });
17
+
18
+ describe("getClipHandleOpacity", () => {
19
+ it("hides handles at rest", () => {
20
+ expect(getClipHandleOpacity({ isHovered: false, isSelected: false, isDragging: false })).toBe(
21
+ 0,
22
+ );
23
+ });
24
+
25
+ it("prioritizes dragging over hover and selection", () => {
26
+ expect(getClipHandleOpacity({ isHovered: true, isSelected: true, isDragging: true })).toBe(
27
+ 0.95,
28
+ );
29
+ });
30
+ });
31
+
32
+ describe("getRenderedTimelineElement", () => {
33
+ it("keeps non-dragged clips unchanged", () => {
34
+ const element = { id: "a", tag: "div", start: 1, duration: 2, track: 0 };
35
+ expect(
36
+ getRenderedTimelineElement({
37
+ element,
38
+ draggedElementId: "b",
39
+ previewStart: 2,
40
+ previewTrack: 1,
41
+ }),
42
+ ).toEqual(element);
43
+ });
44
+
45
+ it("moves the actual dragged clip to the preview position", () => {
46
+ const element = { id: "a", tag: "div", start: 1, duration: 2, track: 0 };
47
+ expect(
48
+ getRenderedTimelineElement({
49
+ element,
50
+ draggedElementId: "a",
51
+ previewStart: 2.4,
52
+ previewTrack: 3,
53
+ }),
54
+ ).toEqual({ ...element, start: 2.4, track: 3 });
55
+ });
56
+ });
@@ -0,0 +1,141 @@
1
+ import type { TimelineElement } from "../store/playerStore";
2
+
3
+ export interface TimelineTrackStyle {
4
+ clip: string;
5
+ accent: string;
6
+ label: string;
7
+ iconBackground: string;
8
+ }
9
+
10
+ export interface TimelineTheme {
11
+ shellBackground: string;
12
+ shellBorder: string;
13
+ rulerBorder: string;
14
+ rowBackground: string;
15
+ rowBorder: string;
16
+ gutterBackground: string;
17
+ gutterBorder: string;
18
+ textPrimary: string;
19
+ textSecondary: string;
20
+ tickText: string;
21
+ tickMajor: string;
22
+ tickMinor: string;
23
+ clipBackground: string;
24
+ clipBackgroundActive: string;
25
+ clipBorder: string;
26
+ clipBorderHover: string;
27
+ clipBorderActive: string;
28
+ clipShadow: string;
29
+ clipShadowHover: string;
30
+ clipShadowActive: string;
31
+ clipShadowDragging: string;
32
+ handleColor: string;
33
+ panelResizeSeam: string;
34
+ panelResizeActive: string;
35
+ clipRadius: string;
36
+ }
37
+
38
+ const TIMELINE_TEAL = "#3CE6AC";
39
+ const TIMELINE_TEAL_LABEL = "#E9FFF6";
40
+ const TIMELINE_TEAL_ICON_BACKGROUND = "rgba(60,230,172,0.12)";
41
+
42
+ function createTrackStyle(): TimelineTrackStyle {
43
+ return {
44
+ clip: TIMELINE_TEAL,
45
+ accent: TIMELINE_TEAL,
46
+ label: TIMELINE_TEAL_LABEL,
47
+ iconBackground: TIMELINE_TEAL_ICON_BACKGROUND,
48
+ };
49
+ }
50
+
51
+ const TRACK_STYLES: Record<string, TimelineTrackStyle> = {
52
+ video: createTrackStyle(),
53
+ audio: createTrackStyle(),
54
+ img: createTrackStyle(),
55
+ div: createTrackStyle(),
56
+ span: createTrackStyle(),
57
+ p: createTrackStyle(),
58
+ h1: createTrackStyle(),
59
+ section: createTrackStyle(),
60
+ sfx: createTrackStyle(),
61
+ };
62
+
63
+ const DEFAULT_TRACK_STYLE: TimelineTrackStyle = createTrackStyle();
64
+
65
+ export const defaultTimelineTheme: TimelineTheme = {
66
+ shellBackground: "#0A0E15",
67
+ shellBorder: "rgba(255,255,255,0.05)",
68
+ rulerBorder: "rgba(255,255,255,0.045)",
69
+ rowBackground: "#0A0E15",
70
+ rowBorder: "rgba(255,255,255,0.05)",
71
+ gutterBackground: "#0D121B",
72
+ gutterBorder: "rgba(255,255,255,0.05)",
73
+ textPrimary: "#E8EDF5",
74
+ textSecondary: "#8391A8",
75
+ tickText: "rgba(131,145,168,0.92)",
76
+ tickMajor: "rgba(255,255,255,0.13)",
77
+ tickMinor: "rgba(255,255,255,0.08)",
78
+ clipBackground: "linear-gradient(180deg, rgba(20,25,34,0.98), rgba(14,18,27,0.98))",
79
+ clipBackgroundActive: "linear-gradient(180deg, rgba(24,30,40,0.99), rgba(15,20,29,0.99))",
80
+ clipBorder: "rgba(255,255,255,0.07)",
81
+ clipBorderHover: "rgba(255,255,255,0.11)",
82
+ clipBorderActive: "rgba(255,255,255,0.14)",
83
+ clipShadow: "inset 0 1px 0 rgba(255,255,255,0.03), 0 6px 18px rgba(0,0,0,0.18)",
84
+ clipShadowHover: "inset 0 1px 0 rgba(255,255,255,0.035), 0 8px 20px rgba(0,0,0,0.2)",
85
+ clipShadowActive:
86
+ "inset 0 1px 0 rgba(255,255,255,0.04), 0 10px 24px rgba(0,0,0,0.22), 0 0 0 1px rgba(255,255,255,0.035)",
87
+ clipShadowDragging:
88
+ "inset 0 1px 0 rgba(255,255,255,0.04), 0 18px 36px rgba(0,0,0,0.34), 0 8px 16px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.04)",
89
+ handleColor: "rgba(255,255,255,0.11)",
90
+ panelResizeSeam: "rgba(255,255,255,0.12)",
91
+ panelResizeActive: "rgba(255,255,255,0.24)",
92
+ clipRadius: "11px 15px 13px 9px / 10px 14px 12px 10px",
93
+ };
94
+
95
+ export function getTimelineTrackStyle(tag: string): TimelineTrackStyle {
96
+ const normalized = tag.toLowerCase();
97
+ if (
98
+ normalized.startsWith("h") &&
99
+ normalized.length === 2 &&
100
+ "123456".includes(normalized[1] ?? "")
101
+ ) {
102
+ return TRACK_STYLES.h1;
103
+ }
104
+ return TRACK_STYLES[normalized] ?? DEFAULT_TRACK_STYLE;
105
+ }
106
+
107
+ export function getClipHandleOpacity({
108
+ isHovered,
109
+ isSelected,
110
+ isDragging,
111
+ }: {
112
+ isHovered: boolean;
113
+ isSelected: boolean;
114
+ isDragging: boolean;
115
+ }): number {
116
+ if (isDragging) return 0.95;
117
+ if (isSelected) return 0.82;
118
+ if (isHovered) return 0.76;
119
+ return 0;
120
+ }
121
+
122
+ export function getRenderedTimelineElement({
123
+ element,
124
+ draggedElementId,
125
+ previewStart,
126
+ previewTrack,
127
+ }: {
128
+ element: TimelineElement;
129
+ draggedElementId: string | null;
130
+ previewStart: number | null;
131
+ previewTrack: number | null;
132
+ }): TimelineElement {
133
+ if (element.id !== draggedElementId || previewStart === null || previewTrack === null) {
134
+ return element;
135
+ }
136
+ return {
137
+ ...element,
138
+ start: previewStart,
139
+ track: previewTrack,
140
+ };
141
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ clampTimelineZoomPercent,
4
+ getNextTimelineZoomPercent,
5
+ getTimelinePixelsPerSecond,
6
+ getTimelineZoomPercent,
7
+ MAX_TIMELINE_ZOOM_PERCENT,
8
+ MIN_TIMELINE_ZOOM_PERCENT,
9
+ } from "./timelineZoom";
10
+
11
+ describe("clampTimelineZoomPercent", () => {
12
+ it("defaults invalid values to 100", () => {
13
+ expect(clampTimelineZoomPercent(Number.NaN)).toBe(100);
14
+ });
15
+
16
+ it("clamps to the supported percent bounds", () => {
17
+ expect(clampTimelineZoomPercent(1)).toBe(MIN_TIMELINE_ZOOM_PERCENT);
18
+ expect(clampTimelineZoomPercent(5000)).toBe(MAX_TIMELINE_ZOOM_PERCENT);
19
+ });
20
+ });
21
+
22
+ describe("getTimelineZoomPercent", () => {
23
+ it("treats fit mode as 100 percent", () => {
24
+ expect(getTimelineZoomPercent("fit", 375)).toBe(100);
25
+ });
26
+
27
+ it("returns the clamped manual zoom percent", () => {
28
+ expect(getTimelineZoomPercent("manual", 125.2)).toBe(125);
29
+ });
30
+ });
31
+
32
+ describe("getTimelinePixelsPerSecond", () => {
33
+ it("uses fit pixels per second in fit mode", () => {
34
+ expect(getTimelinePixelsPerSecond(144, "fit", 250)).toBe(144);
35
+ });
36
+
37
+ it("scales from fit pixels per second in manual mode", () => {
38
+ expect(getTimelinePixelsPerSecond(144, "manual", 125)).toBe(180);
39
+ });
40
+ });
41
+
42
+ describe("getNextTimelineZoomPercent", () => {
43
+ it("zooms out from fit relative to 100 percent", () => {
44
+ expect(getNextTimelineZoomPercent("out", "fit", 375)).toBe(80);
45
+ });
46
+
47
+ it("zooms in from fit relative to 100 percent", () => {
48
+ expect(getNextTimelineZoomPercent("in", "fit", 375)).toBe(125);
49
+ });
50
+
51
+ it("clamps the lower bound", () => {
52
+ expect(getNextTimelineZoomPercent("out", "manual", MIN_TIMELINE_ZOOM_PERCENT)).toBe(
53
+ MIN_TIMELINE_ZOOM_PERCENT,
54
+ );
55
+ });
56
+
57
+ it("clamps the upper bound", () => {
58
+ expect(getNextTimelineZoomPercent("in", "manual", MAX_TIMELINE_ZOOM_PERCENT)).toBe(
59
+ MAX_TIMELINE_ZOOM_PERCENT,
60
+ );
61
+ });
62
+ });
@@ -0,0 +1,38 @@
1
+ import type { ZoomMode } from "../store/playerStore";
2
+
3
+ export const MIN_TIMELINE_ZOOM_PERCENT = 10;
4
+ export const MAX_TIMELINE_ZOOM_PERCENT = 2000;
5
+ const ZOOM_OUT_FACTOR = 0.8;
6
+ const ZOOM_IN_FACTOR = 1.25;
7
+
8
+ export function clampTimelineZoomPercent(percent: number): number {
9
+ if (!Number.isFinite(percent)) return 100;
10
+ return Math.max(
11
+ MIN_TIMELINE_ZOOM_PERCENT,
12
+ Math.min(MAX_TIMELINE_ZOOM_PERCENT, Math.round(percent)),
13
+ );
14
+ }
15
+
16
+ export function getTimelineZoomPercent(zoomMode: ZoomMode, manualZoomPercent: number): number {
17
+ return zoomMode === "fit" ? 100 : clampTimelineZoomPercent(manualZoomPercent);
18
+ }
19
+
20
+ export function getTimelinePixelsPerSecond(
21
+ fitPixelsPerSecond: number,
22
+ zoomMode: ZoomMode,
23
+ manualZoomPercent: number,
24
+ ): number {
25
+ if (!Number.isFinite(fitPixelsPerSecond) || fitPixelsPerSecond <= 0) return 100;
26
+ const zoomPercent = getTimelineZoomPercent(zoomMode, manualZoomPercent);
27
+ return zoomMode === "fit" ? fitPixelsPerSecond : fitPixelsPerSecond * (zoomPercent / 100);
28
+ }
29
+
30
+ export function getNextTimelineZoomPercent(
31
+ direction: "in" | "out",
32
+ zoomMode: ZoomMode,
33
+ manualZoomPercent: number,
34
+ ): number {
35
+ const current = getTimelineZoomPercent(zoomMode, manualZoomPercent);
36
+ const next = direction === "in" ? current * ZOOM_IN_FACTOR : current * ZOOM_OUT_FACTOR;
37
+ return clampTimelineZoomPercent(next);
38
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildStandaloneRootTimelineElement,
4
+ mergeTimelineElementsPreservingDowngrades,
5
+ resolveStandaloneRootCompositionSrc,
6
+ } from "./useTimelinePlayer";
7
+
8
+ describe("buildStandaloneRootTimelineElement", () => {
9
+ it("includes selector and source metadata for standalone composition fallback clips", () => {
10
+ expect(
11
+ buildStandaloneRootTimelineElement({
12
+ compositionId: "hero",
13
+ tagName: "DIV",
14
+ rootDuration: 8,
15
+ iframeSrc: "http://127.0.0.1:4173/api/projects/demo/preview/comp/scenes/hero.html?_t=123",
16
+ selector: '[data-composition-id="hero"]',
17
+ }),
18
+ ).toEqual({
19
+ id: "hero",
20
+ key: 'scenes/hero.html:[data-composition-id="hero"]:0',
21
+ tag: "div",
22
+ start: 0,
23
+ duration: 8,
24
+ track: 0,
25
+ compositionSrc: "scenes/hero.html",
26
+ selector: '[data-composition-id="hero"]',
27
+ selectorIndex: undefined,
28
+ sourceFile: "scenes/hero.html",
29
+ });
30
+ });
31
+
32
+ it("returns null for invalid fallback durations", () => {
33
+ expect(
34
+ buildStandaloneRootTimelineElement({
35
+ compositionId: "hero",
36
+ tagName: "div",
37
+ rootDuration: 0,
38
+ iframeSrc: "http://localhost/preview/comp/hero.html",
39
+ }),
40
+ ).toBe(null);
41
+ expect(
42
+ buildStandaloneRootTimelineElement({
43
+ compositionId: "hero",
44
+ tagName: "div",
45
+ rootDuration: Number.NaN,
46
+ iframeSrc: "http://localhost/preview/comp/hero.html",
47
+ }),
48
+ ).toBe(null);
49
+ });
50
+ });
51
+
52
+ describe("resolveStandaloneRootCompositionSrc", () => {
53
+ it("extracts the composition path from a preview iframe url", () => {
54
+ expect(
55
+ resolveStandaloneRootCompositionSrc(
56
+ "http://127.0.0.1:4173/api/projects/demo/preview/comp/scenes/hero.html?_t=123",
57
+ ),
58
+ ).toBe("scenes/hero.html");
59
+ });
60
+
61
+ it("returns undefined for non-composition preview urls", () => {
62
+ expect(
63
+ resolveStandaloneRootCompositionSrc("http://127.0.0.1:4173/api/projects/demo/preview"),
64
+ ).toBe(undefined);
65
+ });
66
+ });
67
+
68
+ describe("mergeTimelineElementsPreservingDowngrades", () => {
69
+ it("preserves missing current elements when a shorter manifest arrives", () => {
70
+ expect(
71
+ mergeTimelineElementsPreservingDowngrades(
72
+ [
73
+ { id: "hero", tag: "div", start: 0, duration: 4, track: 0 },
74
+ { id: "cta", tag: "div", start: 4, duration: 2, track: 1 },
75
+ ],
76
+ [{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
77
+ 8,
78
+ 8,
79
+ ),
80
+ ).toEqual([
81
+ { id: "hero", tag: "div", start: 0, duration: 4, track: 0 },
82
+ { id: "cta", tag: "div", start: 4, duration: 2, track: 1 },
83
+ ]);
84
+ });
85
+
86
+ it("accepts longer-duration or same-size updates as authoritative", () => {
87
+ expect(
88
+ mergeTimelineElementsPreservingDowngrades(
89
+ [{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
90
+ [{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
91
+ 4,
92
+ 6,
93
+ ),
94
+ ).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
95
+ });
96
+ });