@hyperframes/studio 0.8.59 → 0.8.60
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.
- package/dist/assets/{hyperframes-player-BBub3ZdI.js → hyperframes-player-Di7GyCOP.js} +1 -1
- package/dist/assets/{index-DZBHG9TW.css → index-CBKb_0TI.css} +1 -1
- package/dist/assets/{index-D1KNPijK.js → index-Dpq2lkOm.js} +111 -111
- package/dist/assets/{index-BxZ5Cw5w.js → index-Dx_pt9m4.js} +1 -1
- package/dist/assets/{index-CI7WON-E.js → index-jAjhQTrB.js} +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.html +2 -2
- package/dist/index.js +4439 -4118
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/StudioHeader.tsx +2 -13
- package/src/components/TimelineToolbar.test.tsx +42 -0
- package/src/components/TimelineToolbar.tsx +5 -10
- package/src/components/editor/propertyPanelFill.tsx +3 -2
- package/src/components/icons/AudioMetersIcon.tsx +16 -0
- package/src/components/icons/InspectorIcon.tsx +18 -0
- package/src/components/icons/ReverseGradientIcon.tsx +23 -0
- package/src/components/icons/RippleEditIcon.tsx +19 -0
- package/src/components/nle/AudioMeterStrip.test.tsx +278 -7
- package/src/components/nle/AudioMeterStrip.tsx +264 -47
- package/src/components/ui/Menu.test.tsx +3 -4
- package/src/components/ui/Menu.tsx +18 -5
- package/src/components/ui/Select.tsx +7 -1
- package/src/components/ui/Tooltip.tsx +6 -1
- package/src/player/components/AudioWaveform.tsx +16 -7
- package/src/player/components/ImageThumbnail.tsx +3 -5
- package/src/player/components/LayerDisclosureRow.tsx +9 -4
- package/src/player/components/PlayheadIndicator.tsx +3 -3
- package/src/player/components/TimelineAutomationLane.test.tsx +1 -1
- package/src/player/components/TimelineAutomationLane.tsx +5 -5
- package/src/player/components/TimelineCanvas.tsx +18 -15
- package/src/player/components/TimelineClip.test.tsx +41 -4
- package/src/player/components/TimelineClip.tsx +11 -4
- package/src/player/components/TimelineClipDiamonds.test.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.tsx +2 -2
- package/src/player/components/TimelineDiamondConnectors.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +1 -1
- package/src/player/components/TimelineFxButton.tsx +10 -8
- package/src/player/components/TimelineGroupHeader.tsx +18 -9
- package/src/player/components/TimelineGroupLaneLabels.tsx +12 -5
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +14 -0
- package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
- package/src/player/components/TimelineRazorInteraction.tsx +1 -1
- package/src/player/components/TimelineRuler.tsx +3 -2
- package/src/player/components/TimelineShortcutHint.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +4 -6
- package/src/player/components/TimelineTrackPlainHeader.tsx +10 -3
- package/src/player/components/VideoThumbnail.tsx +3 -5
- package/src/player/components/timelineMotionStyles.test.ts +96 -30
- package/src/player/components/timelineTheme.test.ts +14 -0
- package/src/player/components/timelineTheme.ts +12 -11
- package/src/styles/hex-baseline.json +4 -23
- package/src/styles/studio.css +43 -47
- package/src/styles/theme.css +105 -1
- package/src/styles/theme.test.ts +9 -0
- package/src/utils/audioMeterVisibility.ts +2 -2
- package/src/utils/studioUiPreferences.ts +1 -1
|
@@ -112,8 +112,7 @@ export const VideoThumbnail = memo(function VideoThumbnail({
|
|
|
112
112
|
<div
|
|
113
113
|
className="absolute inset-0 animate-pulse motion-reduce:animate-none"
|
|
114
114
|
style={{
|
|
115
|
-
background:
|
|
116
|
-
"linear-gradient(90deg, rgba(255,255,255,0.02) 0%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 100%)",
|
|
115
|
+
background: "var(--timeline-thumbnail-shimmer)",
|
|
117
116
|
}}
|
|
118
117
|
/>
|
|
119
118
|
)}
|
|
@@ -128,13 +127,12 @@ export const VideoThumbnail = memo(function VideoThumbnail({
|
|
|
128
127
|
<div
|
|
129
128
|
className="absolute inset-x-0 bottom-0 z-10 px-1.5 pb-0.5 pt-3"
|
|
130
129
|
style={{
|
|
131
|
-
background:
|
|
132
|
-
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)",
|
|
130
|
+
background: "var(--timeline-thumbnail-label-gradient)",
|
|
133
131
|
}}
|
|
134
132
|
>
|
|
135
133
|
<span
|
|
136
134
|
className="block truncate text-[9px] font-semibold leading-tight"
|
|
137
|
-
style={{ color: labelColor, textShadow: "
|
|
135
|
+
style={{ color: labelColor, textShadow: "var(--timeline-thumbnail-label-shadow)" }}
|
|
138
136
|
>
|
|
139
137
|
{label}
|
|
140
138
|
</span>
|
|
@@ -2,17 +2,17 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
const studioCss = readFileSync(new URL("../../styles/studio.css", import.meta.url), "utf8");
|
|
5
|
+
const themeCss = readFileSync(new URL("../../styles/theme.css", import.meta.url), "utf8");
|
|
6
|
+
const timelineOverlaySources = [
|
|
7
|
+
"TimelineShortcutHint.tsx",
|
|
8
|
+
"LayerDisclosureRow.tsx",
|
|
9
|
+
"ImageThumbnail.tsx",
|
|
10
|
+
"AudioWaveform.tsx",
|
|
11
|
+
"VideoThumbnail.tsx",
|
|
12
|
+
].map((fileName) => readFileSync(new URL(`./${fileName}`, import.meta.url), "utf8"));
|
|
5
13
|
const timelineClipSource = readFileSync(new URL("./TimelineClip.tsx", import.meta.url), "utf8");
|
|
6
14
|
const playheadSource = readFileSync(new URL("./PlayheadIndicator.tsx", import.meta.url), "utf8");
|
|
7
15
|
|
|
8
|
-
const allowedTimelineTransitionProperties = [
|
|
9
|
-
"background-color",
|
|
10
|
-
"border-color",
|
|
11
|
-
"box-shadow",
|
|
12
|
-
"color",
|
|
13
|
-
"opacity",
|
|
14
|
-
];
|
|
15
|
-
|
|
16
16
|
function expectRule(css: string, selector: string): string {
|
|
17
17
|
const selectorStart = css.indexOf(`${selector} {`);
|
|
18
18
|
expect(selectorStart).toBeGreaterThanOrEqual(0);
|
|
@@ -31,28 +31,26 @@ function expectDeclaration(ruleBody: string, property: string): string {
|
|
|
31
31
|
return declarationMatch?.[1].trim() ?? "";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const char of transitionDeclaration) {
|
|
40
|
-
if (char === "(") depth += 1;
|
|
41
|
-
if (char === ")") depth -= 1;
|
|
42
|
-
if (char === "," && depth === 0) {
|
|
43
|
-
items.push(item.trim());
|
|
44
|
-
item = "";
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
item += char;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (item.trim().length > 0) items.push(item.trim());
|
|
51
|
-
|
|
52
|
-
return items.map((transition) => transition.split(/\s+/)[0]);
|
|
34
|
+
function themeTokenValue(token: string): string {
|
|
35
|
+
const match = new RegExp(`${token}:\\s*([^;]+);`).exec(themeCss);
|
|
36
|
+
expect(match?.[1]).toBeDefined();
|
|
37
|
+
return match?.[1].trim() ?? "";
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
describe("timeline motion styles", () => {
|
|
41
|
+
it.each([
|
|
42
|
+
["--timeline-track-label", "rgba(255, 255, 255, 0.5)"],
|
|
43
|
+
["--timeline-tick-text", "rgba(255, 255, 255, 0.34)"],
|
|
44
|
+
["--timeline-border-strong", "rgba(255, 255, 255, 0.2)"],
|
|
45
|
+
["--timeline-group-member-tint", "rgba(255, 255, 255, 0.035)"],
|
|
46
|
+
["--timeline-overlay-text", "rgba(255, 255, 255, 0.8)"],
|
|
47
|
+
["--timeline-clip-shadow-dragging", "rgba(0, 0, 0, 0.4)"],
|
|
48
|
+
["--timeline-playhead-glow", "rgba(60, 230, 172, 0.14)"],
|
|
49
|
+
["--timeline-playhead-shadow", "rgba(0, 0, 0, 0.55)"],
|
|
50
|
+
])("keeps the migrated default for %s", (token, expected) => {
|
|
51
|
+
expect(themeTokenValue(token)).toBe(expected);
|
|
52
|
+
});
|
|
53
|
+
|
|
56
54
|
it("keeps clip motion reduced-motion gated and layout safe", () => {
|
|
57
55
|
const mediaStart = studioCss.indexOf("@media (prefers-reduced-motion: no-preference)");
|
|
58
56
|
expect(mediaStart).toBeGreaterThanOrEqual(0);
|
|
@@ -63,19 +61,73 @@ describe("timeline motion styles", () => {
|
|
|
63
61
|
|
|
64
62
|
const motionMediaCss = studioCss.slice(mediaStart);
|
|
65
63
|
const timelineClipMotionRule = expectRule(motionMediaCss, ".timeline-clip");
|
|
66
|
-
const clipTransition = expectDeclaration(timelineClipMotionRule, "transition");
|
|
67
64
|
|
|
68
|
-
expect(
|
|
69
|
-
|
|
65
|
+
expect(expectDeclaration(timelineClipMotionRule, "transition")).toBe(
|
|
66
|
+
"background-color 120ms ease-out",
|
|
67
|
+
);
|
|
70
68
|
});
|
|
71
69
|
|
|
72
70
|
it("layers the active mint bloom through opacity instead of a gradient background swap", () => {
|
|
73
71
|
const baseTimelineClipRule = expectRule(studioCss, ".timeline-clip");
|
|
72
|
+
const timelineClipLabelRule = expectRule(studioCss, ".timeline-clip__label");
|
|
73
|
+
const timelineClipTimecodeRule = expectRule(studioCss, ".timeline-clip__timecode");
|
|
74
|
+
const audioClipRule = expectRule(studioCss, ".timeline-clip.is-audio");
|
|
75
|
+
const audioClipHoverRule = expectRule(studioCss, ".timeline-clip.is-audio.is-hovered");
|
|
76
|
+
const audioClipDraggingRule = expectRule(studioCss, ".timeline-clip.is-audio.is-dragging");
|
|
74
77
|
const activeTimelineClipRule = expectRule(studioCss, ".timeline-clip[data-active]");
|
|
78
|
+
const selectedTimelineClipRule = expectRule(studioCss, ".timeline-clip.is-selected");
|
|
79
|
+
const activeSelectedTimelineClipRule = expectRule(
|
|
80
|
+
studioCss,
|
|
81
|
+
".timeline-clip[data-active].is-selected",
|
|
82
|
+
);
|
|
75
83
|
const bloomOverlayRule = expectRule(studioCss, ".timeline-clip::before");
|
|
76
84
|
const activeBloomOverlayRule = expectRule(studioCss, ".timeline-clip[data-active]::before");
|
|
77
85
|
|
|
78
86
|
expect(baseTimelineClipRule).toContain("background-color: var(--clip-bg)");
|
|
87
|
+
expect(baseTimelineClipRule).toContain("border: 1px solid var(--clip-border)");
|
|
88
|
+
expect(baseTimelineClipRule).toContain("box-shadow:");
|
|
89
|
+
expect(baseTimelineClipRule).toContain("inset 0 1px 0 var(--timeline-clip-highlight)");
|
|
90
|
+
expect(baseTimelineClipRule).toContain("var(--timeline-clip-shadow)");
|
|
91
|
+
expect(timelineClipLabelRule).toContain("background: var(--timeline-clip-chip-bg)");
|
|
92
|
+
expect(timelineClipLabelRule).toContain("color: var(--timeline-clip-chip-text)");
|
|
93
|
+
expect(timelineClipLabelRule).toContain("text-overflow: ellipsis");
|
|
94
|
+
expect(timelineClipLabelRule).toContain("max-width: calc(100% - 22px)");
|
|
95
|
+
expect(timelineClipTimecodeRule).toContain("color: var(--timeline-tick-text)");
|
|
96
|
+
expect(themeCss).toContain("--timeline-clip-bg: rgba(255, 255, 255, 0.12)");
|
|
97
|
+
expect(themeCss).toContain("--timeline-clip-border: rgba(255, 255, 255, 0.22)");
|
|
98
|
+
expect(themeCss).toContain("--timeline-track-label: rgba(255, 255, 255, 0.5)");
|
|
99
|
+
expect(themeCss).toContain("--timeline-tick-text: rgba(255, 255, 255, 0.34)");
|
|
100
|
+
expect(themeCss).toContain("--timeline-clip-label-active: rgba(232, 255, 247, 0.95)");
|
|
101
|
+
expect(themeCss).toContain("--timeline-clip-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.85)");
|
|
102
|
+
expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)");
|
|
103
|
+
expect(themeCss).toContain("--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55)");
|
|
104
|
+
expect(themeCss).toContain("--timeline-clip-radius: 8px");
|
|
105
|
+
expect(themeCss).toContain("--timeline-clip-audio-radius: 21px");
|
|
106
|
+
expect(themeCss).toContain("--timeline-clip-highlight: rgb(255 255 255 / 0.08)");
|
|
107
|
+
expect(themeCss).toContain("--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.3)");
|
|
108
|
+
expect(themeCss).toContain("--timeline-clip-chip-bg: rgba(15, 22, 18, 0.73)");
|
|
109
|
+
expect(themeCss).toContain("--timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7)");
|
|
110
|
+
expect(themeCss).toContain("--timeline-clip-chip-text: #ffffff");
|
|
111
|
+
expect(themeCss).toContain("--timeline-row-bg: #edf0ed");
|
|
112
|
+
expect(themeCss).toContain("--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.12)");
|
|
113
|
+
expect(themeCss).toContain("--timeline-clip-selection: #0f766e");
|
|
114
|
+
expect(themeCss).toContain("--timeline-clip-audio-bg: rgba(167, 139, 250, 0.16)");
|
|
115
|
+
expect(themeCss).toContain("--timeline-clip-audio-border: rgba(167, 139, 250, 0.4)");
|
|
116
|
+
expect(themeCss).toContain("--timeline-clip-audio-bg-hover: rgba(167, 139, 250, 0.24)");
|
|
117
|
+
expect(themeCss).toContain("--timeline-clip-audio-bg-dragging: rgba(60, 52, 84, 0.96)");
|
|
118
|
+
expect(audioClipRule).toContain("background-color: var(--timeline-clip-audio-bg)");
|
|
119
|
+
expect(audioClipRule).toContain("border-color: var(--timeline-clip-audio-border)");
|
|
120
|
+
expect(audioClipHoverRule).toContain("background-color: var(--timeline-clip-audio-bg-hover)");
|
|
121
|
+
expect(audioClipDraggingRule).toContain("var(--timeline-clip-audio-bg-dragging)");
|
|
122
|
+
expect(themeCss).toContain("--timeline-clip-selection: rgba(255, 255, 255, 0.85)");
|
|
123
|
+
expect(selectedTimelineClipRule).toContain(
|
|
124
|
+
"outline: 1.5px solid var(--timeline-clip-selection)",
|
|
125
|
+
);
|
|
126
|
+
expect(selectedTimelineClipRule).toContain("outline-offset: -1.5px");
|
|
127
|
+
expect(activeSelectedTimelineClipRule).toContain(
|
|
128
|
+
"outline: 1.5px solid var(--timeline-clip-selection)",
|
|
129
|
+
);
|
|
130
|
+
expect(activeSelectedTimelineClipRule).toContain("outline-offset: -1.5px");
|
|
79
131
|
expect(activeTimelineClipRule).not.toContain("background: linear-gradient");
|
|
80
132
|
expect(activeTimelineClipRule).toContain("border-color: var(--clip-border-active)");
|
|
81
133
|
expect(activeTimelineClipRule).not.toContain("box-shadow");
|
|
@@ -85,6 +137,20 @@ describe("timeline motion styles", () => {
|
|
|
85
137
|
expect(activeBloomOverlayRule).toContain("opacity: 1");
|
|
86
138
|
});
|
|
87
139
|
|
|
140
|
+
it("routes timeline overlay colours through theme tokens", () => {
|
|
141
|
+
const overlaySource = timelineOverlaySources.join("\n");
|
|
142
|
+
|
|
143
|
+
expect(overlaySource).toContain("var(--timeline-shortcut-bg)");
|
|
144
|
+
expect(overlaySource).toContain("var(--timeline-thumbnail-shimmer)");
|
|
145
|
+
expect(overlaySource).toContain("var(--timeline-thumbnail-label-gradient)");
|
|
146
|
+
expect(overlaySource).toContain("var(--timeline-thumbnail-label-shadow)");
|
|
147
|
+
expect(overlaySource).toContain('"--timeline-waveform-bar-rgb"');
|
|
148
|
+
expect(overlaySource).toContain("var(--timeline-waveform-error)");
|
|
149
|
+
expect(overlaySource).toContain("var(--timeline-waveform-label-shadow)");
|
|
150
|
+
expect(overlaySource).toContain("var(--timeline-text-solid)");
|
|
151
|
+
expect(overlaySource).toContain("var(--timeline-accent)");
|
|
152
|
+
});
|
|
153
|
+
|
|
88
154
|
it("targets trim handle bars without changing drag geometry", () => {
|
|
89
155
|
const handleClassMatches = timelineClipSource.match(/className="timeline-clip__handle-bar"/g);
|
|
90
156
|
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
clipWidthLadder,
|
|
4
|
+
defaultTimelineTheme,
|
|
3
5
|
getClipHandleOpacity,
|
|
4
6
|
getRenderedTimelineElement,
|
|
5
7
|
getTimelineTrackStyle,
|
|
6
8
|
} from "./timelineTheme";
|
|
7
9
|
import { getTrackStyle } from "./timelineIcons";
|
|
8
10
|
|
|
11
|
+
describe("clipWidthLadder", () => {
|
|
12
|
+
it("labels from 60px, keeps the picture below that, and uses one frame below 24px", () => {
|
|
13
|
+
expect(clipWidthLadder(200)).toBe("labeled");
|
|
14
|
+
expect(clipWidthLadder(60)).toBe("labeled");
|
|
15
|
+
expect(clipWidthLadder(59)).toBe("picture");
|
|
16
|
+
expect(clipWidthLadder(24)).toBe("picture");
|
|
17
|
+
expect(clipWidthLadder(23)).toBe("frame");
|
|
18
|
+
expect(defaultTimelineTheme.clipRadius).toBe("var(--timeline-clip-radius)");
|
|
19
|
+
expect(defaultTimelineTheme.audioClipRadius).toBe("var(--timeline-clip-audio-radius)");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
9
23
|
describe("getTimelineTrackStyle", () => {
|
|
10
24
|
it("uses one neutral clip style for every timeline tag", () => {
|
|
11
25
|
const expectedStyle = {
|
|
@@ -27,14 +27,11 @@ export interface TimelineTheme {
|
|
|
27
27
|
clipBorder: string;
|
|
28
28
|
clipBorderHover: string;
|
|
29
29
|
clipBorderActive: string;
|
|
30
|
-
clipShadow: string;
|
|
31
|
-
clipShadowHover: string;
|
|
32
|
-
clipShadowActive: string;
|
|
33
|
-
clipShadowDragging: string;
|
|
34
30
|
handleColor: string;
|
|
35
31
|
panelResizeSeam: string;
|
|
36
32
|
panelResizeActive: string;
|
|
37
33
|
clipRadius: string;
|
|
34
|
+
audioClipRadius: string;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
const TRACK_STYLE: TimelineTrackStyle = {
|
|
@@ -67,18 +64,22 @@ export const defaultTimelineTheme: TimelineTheme = {
|
|
|
67
64
|
clipBorder: "var(--timeline-clip-border)",
|
|
68
65
|
clipBorderHover: "var(--timeline-clip-border-hover)",
|
|
69
66
|
clipBorderActive: "var(--timeline-clip-border-active)",
|
|
70
|
-
// Shadows stay literal: a light host still wants a dark contact shadow, so
|
|
71
|
-
// these are not part of the surface/hairline/text/clip-colour theming ask.
|
|
72
|
-
clipShadow: "none",
|
|
73
|
-
clipShadowHover: "0 2px 8px rgba(0,0,0,0.2)",
|
|
74
|
-
clipShadowActive: "0 2px 8px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.04)",
|
|
75
|
-
clipShadowDragging: "0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
76
67
|
handleColor: "var(--timeline-handle)",
|
|
77
68
|
panelResizeSeam: "var(--timeline-resize-seam)",
|
|
78
69
|
panelResizeActive: "var(--timeline-resize-active)",
|
|
79
|
-
clipRadius: "var(--radius
|
|
70
|
+
clipRadius: "var(--timeline-clip-radius)",
|
|
71
|
+
audioClipRadius: "var(--timeline-clip-audio-radius)",
|
|
80
72
|
};
|
|
81
73
|
|
|
74
|
+
export type ClipWidthLadder = "labeled" | "picture" | "frame";
|
|
75
|
+
|
|
76
|
+
/** Label chip from 60px. Under 24px the clip is one cropped frame. */
|
|
77
|
+
export function clipWidthLadder(widthPx: number): ClipWidthLadder {
|
|
78
|
+
if (widthPx < 24) return "frame";
|
|
79
|
+
if (widthPx < 60) return "picture";
|
|
80
|
+
return "labeled";
|
|
81
|
+
}
|
|
82
|
+
|
|
82
83
|
export function getTimelineTrackStyle(_tag: string): TimelineTrackStyle {
|
|
83
84
|
return TRACK_STYLE;
|
|
84
85
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"total":
|
|
2
|
+
"total": 302,
|
|
3
3
|
"files": {
|
|
4
4
|
"packages/studio/src/captions/components/CaptionOverlay.tsx": 5,
|
|
5
5
|
"packages/studio/src/captions/components/CaptionTimeline.tsx": 11,
|
|
@@ -49,35 +49,16 @@
|
|
|
49
49
|
"packages/studio/src/components/sidebar/AudioRow.tsx": 2,
|
|
50
50
|
"packages/studio/src/components/ui/HyperframesMark.tsx": 4,
|
|
51
51
|
"packages/studio/src/hooks/useInlineTextEdit.ts": 1,
|
|
52
|
-
"packages/studio/src/player/components/AudioWaveform.tsx":
|
|
52
|
+
"packages/studio/src/player/components/AudioWaveform.tsx": 1,
|
|
53
53
|
"packages/studio/src/player/components/BeatStrip.tsx": 6,
|
|
54
54
|
"packages/studio/src/player/components/CompositionThumbnail.tsx": 2,
|
|
55
|
-
"packages/studio/src/player/components/ImageThumbnail.tsx": 6,
|
|
56
|
-
"packages/studio/src/player/components/LayerDisclosureRow.tsx": 3,
|
|
57
55
|
"packages/studio/src/player/components/Player.tsx": 2,
|
|
58
56
|
"packages/studio/src/player/components/PlayerControls.tsx": 1,
|
|
59
|
-
"packages/studio/src/player/components/PlayheadIndicator.tsx": 3,
|
|
60
57
|
"packages/studio/src/player/components/ShortcutsPanel.tsx": 7,
|
|
61
58
|
"packages/studio/src/player/components/SpeedMenu.tsx": 2,
|
|
62
|
-
"packages/studio/src/player/components/TimelineAutomationLane.tsx": 4,
|
|
63
|
-
"packages/studio/src/player/components/TimelineCanvas.tsx": 13,
|
|
64
|
-
"packages/studio/src/player/components/TimelineClipDiamonds.tsx": 2,
|
|
65
|
-
"packages/studio/src/player/components/TimelineDiamondConnectors.tsx": 2,
|
|
66
|
-
"packages/studio/src/player/components/TimelineEmptyState.tsx": 1,
|
|
67
|
-
"packages/studio/src/player/components/TimelineFxButton.tsx": 8,
|
|
68
|
-
"packages/studio/src/player/components/TimelineGroupHeader.tsx": 6,
|
|
69
|
-
"packages/studio/src/player/components/TimelineGroupLaneLabels.tsx": 1,
|
|
70
|
-
"packages/studio/src/player/components/TimelineGroupRow.tsx": 1,
|
|
71
|
-
"packages/studio/src/player/components/TimelineRazorInteraction.tsx": 1,
|
|
72
|
-
"packages/studio/src/player/components/TimelineRuler.tsx": 1,
|
|
73
|
-
"packages/studio/src/player/components/TimelineShortcutHint.tsx": 2,
|
|
74
|
-
"packages/studio/src/player/components/TimelineTrackHeader.tsx": 2,
|
|
75
|
-
"packages/studio/src/player/components/TimelineTrackPlainHeader.tsx": 2,
|
|
76
|
-
"packages/studio/src/player/components/VideoThumbnail.tsx": 6,
|
|
77
|
-
"packages/studio/src/player/components/timelineTheme.ts": 5,
|
|
78
59
|
"packages/studio/src/player/components/trackHeaderLabelRows.tsx": 6,
|
|
79
|
-
"packages/studio/src/styles/studio.css": 63,
|
|
80
60
|
"packages/studio/src/telemetry/client.ts": 1,
|
|
81
|
-
"packages/studio/src/webmcp/webmcpTestUtils.ts": 1
|
|
61
|
+
"packages/studio/src/webmcp/webmcpTestUtils.ts": 1,
|
|
62
|
+
"packages/studio/src/styles/studio.css": 48
|
|
82
63
|
}
|
|
83
64
|
}
|
package/src/styles/studio.css
CHANGED
|
@@ -202,7 +202,10 @@ body {
|
|
|
202
202
|
.timeline-clip {
|
|
203
203
|
background-color: var(--clip-bg);
|
|
204
204
|
border: 1px solid var(--clip-border);
|
|
205
|
-
border-radius:
|
|
205
|
+
border-radius: var(--timeline-clip-radius);
|
|
206
|
+
box-shadow:
|
|
207
|
+
inset 0 1px 0 var(--timeline-clip-highlight),
|
|
208
|
+
var(--timeline-clip-shadow);
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
.timeline-clip::before {
|
|
@@ -227,16 +230,19 @@ body {
|
|
|
227
230
|
accented visual clips — so the audio zone at the bottom is visually separate
|
|
228
231
|
at a glance (the timeline's kinds: visual on top, audio below). */
|
|
229
232
|
.timeline-clip.is-audio {
|
|
230
|
-
background-color:
|
|
231
|
-
border-color:
|
|
233
|
+
background-color: var(--timeline-clip-audio-bg);
|
|
234
|
+
border-color: var(--timeline-clip-audio-border);
|
|
235
|
+
border-radius: var(--timeline-clip-audio-radius);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
.timeline-clip.is-audio.is-hovered {
|
|
235
|
-
background-color:
|
|
239
|
+
background-color: var(--timeline-clip-audio-bg-hover);
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
.timeline-clip.is-audio.is-dragging {
|
|
239
|
-
|
|
243
|
+
box-shadow:
|
|
244
|
+
inset 0 0 0 999px var(--timeline-clip-audio-bg-dragging),
|
|
245
|
+
0 8px 24px var(--timeline-clip-shadow-dragging);
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
.timeline-clip.is-hovered {
|
|
@@ -248,53 +254,56 @@ body {
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
.timeline-clip.is-selected {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.timeline-clip[data-active].is-selected {
|
|
255
|
-
box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.85);
|
|
257
|
+
outline: 1.5px solid var(--timeline-clip-selection);
|
|
258
|
+
outline-offset: -1.5px;
|
|
256
259
|
}
|
|
257
260
|
|
|
258
|
-
.timeline-clip.is-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
.timeline-clip.is-selected::after {
|
|
262
|
+
content: "";
|
|
263
|
+
position: absolute;
|
|
264
|
+
inset: 0;
|
|
265
|
+
z-index: 4;
|
|
266
|
+
border-radius: inherit;
|
|
267
|
+
box-shadow: inset 0 0 0 1px var(--timeline-clip-selection-highlight);
|
|
268
|
+
pointer-events: none;
|
|
263
269
|
}
|
|
264
270
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
box-shadow:
|
|
269
|
-
0 0 0 1.5px rgba(255, 255, 255, 0.85),
|
|
270
|
-
0 8px 24px rgba(0, 0, 0, 0.4);
|
|
271
|
+
.timeline-clip[data-active].is-selected {
|
|
272
|
+
outline: 1.5px solid var(--timeline-clip-selection);
|
|
273
|
+
outline-offset: -1.5px;
|
|
271
274
|
}
|
|
272
275
|
|
|
273
|
-
.timeline-clip
|
|
276
|
+
.timeline-clip.is-dragging {
|
|
277
|
+
/* Veil is a shadow, not background-color, so drop does not fade the hover fill. */
|
|
274
278
|
box-shadow:
|
|
275
|
-
0 0 0
|
|
276
|
-
0 8px 24px
|
|
279
|
+
inset 0 0 0 999px var(--clip-bg-dragging),
|
|
280
|
+
0 8px 24px var(--timeline-clip-shadow-dragging);
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
.timeline-clip__label {
|
|
280
284
|
position: absolute;
|
|
281
|
-
top:
|
|
282
|
-
left:
|
|
283
|
-
right: 10px;
|
|
285
|
+
top: 5px;
|
|
286
|
+
left: 6px;
|
|
284
287
|
z-index: 6;
|
|
288
|
+
width: fit-content;
|
|
289
|
+
max-width: calc(100% - 22px);
|
|
285
290
|
overflow: hidden;
|
|
286
|
-
|
|
291
|
+
padding: 1px 5px;
|
|
292
|
+
border-radius: 4px;
|
|
293
|
+
background: var(--timeline-clip-chip-bg);
|
|
294
|
+
color: var(--timeline-clip-chip-text);
|
|
287
295
|
font-size: 10px;
|
|
288
296
|
font-weight: 500;
|
|
289
|
-
line-height:
|
|
297
|
+
line-height: 16px;
|
|
290
298
|
pointer-events: none;
|
|
291
|
-
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
|
|
292
299
|
text-overflow: ellipsis;
|
|
293
300
|
white-space: nowrap;
|
|
294
301
|
}
|
|
295
302
|
|
|
296
|
-
.timeline-clip
|
|
297
|
-
|
|
303
|
+
.timeline-clip.is-audio .timeline-clip__label {
|
|
304
|
+
top: 11px;
|
|
305
|
+
left: 9px;
|
|
306
|
+
background: var(--timeline-clip-chip-audio-bg);
|
|
298
307
|
}
|
|
299
308
|
|
|
300
309
|
.timeline-clip__timecode {
|
|
@@ -302,7 +311,7 @@ body {
|
|
|
302
311
|
bottom: 6px;
|
|
303
312
|
left: 12px;
|
|
304
313
|
overflow: hidden;
|
|
305
|
-
color:
|
|
314
|
+
color: var(--timeline-tick-text);
|
|
306
315
|
font-family: var(--font-mono);
|
|
307
316
|
font-size: 9px;
|
|
308
317
|
font-variant-numeric: tabular-nums;
|
|
@@ -313,20 +322,7 @@ body {
|
|
|
313
322
|
|
|
314
323
|
@media (prefers-reduced-motion: no-preference) {
|
|
315
324
|
.timeline-clip {
|
|
316
|
-
transition:
|
|
317
|
-
background-color 200ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
318
|
-
border-color 200ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
319
|
-
box-shadow 200ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
320
|
-
color 200ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
321
|
-
opacity 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.timeline-clip::before {
|
|
325
|
-
transition: opacity 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
.timeline-clip__label {
|
|
329
|
-
transition: color 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
325
|
+
transition: background-color 120ms ease-out;
|
|
330
326
|
}
|
|
331
327
|
|
|
332
328
|
.timeline-clip__handle-bar {
|
package/src/styles/theme.css
CHANGED
|
@@ -62,6 +62,42 @@
|
|
|
62
62
|
--timeline-tick-major: rgba(255, 255, 255, 0.1);
|
|
63
63
|
--timeline-tick-minor: rgba(255, 255, 255, 0.06);
|
|
64
64
|
--timeline-clip-bg: rgba(255, 255, 255, 0.12);
|
|
65
|
+
--timeline-clip-label-active: rgba(232, 255, 247, 0.95);
|
|
66
|
+
--timeline-clip-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
|
|
67
|
+
--timeline-clip-audio-bg: rgba(167, 139, 250, 0.16);
|
|
68
|
+
--timeline-clip-audio-border: rgba(167, 139, 250, 0.4);
|
|
69
|
+
--timeline-clip-audio-bg-hover: rgba(167, 139, 250, 0.24);
|
|
70
|
+
--timeline-clip-audio-bg-dragging: rgba(60, 52, 84, 0.96);
|
|
71
|
+
--timeline-clip-selection: rgba(255, 255, 255, 0.85);
|
|
72
|
+
--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55);
|
|
73
|
+
--timeline-clip-radius: 8px;
|
|
74
|
+
--timeline-clip-audio-radius: 21px;
|
|
75
|
+
--timeline-clip-highlight: rgb(255 255 255 / 0.08);
|
|
76
|
+
--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.3);
|
|
77
|
+
--timeline-clip-chip-bg: rgba(15, 22, 18, 0.73);
|
|
78
|
+
--timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7);
|
|
79
|
+
--timeline-clip-chip-text: #ffffff;
|
|
80
|
+
--timeline-shortcut-bg: rgba(17, 23, 35, 0.84);
|
|
81
|
+
--timeline-thumbnail-shimmer: linear-gradient(
|
|
82
|
+
90deg,
|
|
83
|
+
rgba(255, 255, 255, 0.02) 0%,
|
|
84
|
+
rgba(255, 255, 255, 0.05) 50%,
|
|
85
|
+
rgba(255, 255, 255, 0.02) 100%
|
|
86
|
+
);
|
|
87
|
+
--timeline-thumbnail-label-gradient: linear-gradient(
|
|
88
|
+
to top,
|
|
89
|
+
rgba(0, 0, 0, 0.85) 0%,
|
|
90
|
+
rgba(0, 0, 0, 0.4) 60%,
|
|
91
|
+
transparent 100%
|
|
92
|
+
);
|
|
93
|
+
--timeline-thumbnail-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
|
|
94
|
+
--timeline-waveform-bar-rgb: 75, 163, 210;
|
|
95
|
+
--timeline-waveform-error: repeating-linear-gradient(
|
|
96
|
+
90deg,
|
|
97
|
+
rgba(75, 163, 210, 0.35) 0 2px,
|
|
98
|
+
transparent 2px 5px
|
|
99
|
+
);
|
|
100
|
+
--timeline-waveform-label-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
|
|
65
101
|
--timeline-clip-bg-active: rgba(60, 230, 172, 0.2);
|
|
66
102
|
--timeline-clip-bg-hover: rgba(255, 255, 255, 0.16);
|
|
67
103
|
--timeline-clip-bg-dragging: rgba(38, 42, 52, 0.96);
|
|
@@ -78,6 +114,40 @@
|
|
|
78
114
|
--timeline-track-clip-fill: rgba(255, 255, 255, 0.055);
|
|
79
115
|
--timeline-track-clip-active: rgba(60, 230, 172, 0.16);
|
|
80
116
|
--timeline-track-label: rgba(255, 255, 255, 0.5);
|
|
117
|
+
--timeline-border-strong: rgba(255, 255, 255, 0.2);
|
|
118
|
+
--timeline-group-member-tint: rgba(255, 255, 255, 0.035);
|
|
119
|
+
--timeline-overlay-text: rgba(255, 255, 255, 0.8);
|
|
120
|
+
--timeline-accent: #3ce6ac;
|
|
121
|
+
--timeline-accent-soft: rgba(60, 230, 172, 0.18);
|
|
122
|
+
--timeline-accent-faint: rgba(60, 230, 172, 0.055);
|
|
123
|
+
--timeline-accent-fill: rgba(60, 230, 172, 0.1);
|
|
124
|
+
--timeline-accent-glow: rgba(60, 230, 172, 0.5);
|
|
125
|
+
--timeline-accent-border: rgba(60, 230, 172, 0.7);
|
|
126
|
+
--timeline-accent-rail: rgba(60, 230, 172, 0.35);
|
|
127
|
+
--timeline-accent-hover: rgba(60, 230, 172, 0.25);
|
|
128
|
+
--timeline-accent-button-bg: rgba(60, 230, 172, 0.15);
|
|
129
|
+
--timeline-danger: rgba(239, 68, 68, 0.7);
|
|
130
|
+
--timeline-info-bg: rgba(59, 130, 246, 0.12);
|
|
131
|
+
--timeline-info-border: rgba(59, 130, 246, 0.4);
|
|
132
|
+
--timeline-dialog-bg: #1b1b1f;
|
|
133
|
+
--timeline-diamond-bg: #171717;
|
|
134
|
+
--timeline-diamond-border: rgba(255, 255, 255, 0.14);
|
|
135
|
+
--timeline-text-solid: #ffffff;
|
|
136
|
+
--timeline-text-faint: rgba(255, 255, 255, 0.35);
|
|
137
|
+
--timeline-text-dim: rgba(255, 255, 255, 0.4);
|
|
138
|
+
--timeline-text-soft: rgba(255, 255, 255, 0.75);
|
|
139
|
+
--timeline-text-bright: rgba(255, 255, 255, 0.7);
|
|
140
|
+
--timeline-snap-guide: rgba(255, 255, 255, 0.6);
|
|
141
|
+
--timeline-overlay-bg: rgba(0, 0, 0, 0.8);
|
|
142
|
+
--timeline-overlay-bg-strong: rgba(0, 0, 0, 0.85);
|
|
143
|
+
--timeline-input-bg: rgba(0, 0, 0, 0.3);
|
|
144
|
+
--timeline-automation-rail: rgba(255, 255, 255, 0.08);
|
|
145
|
+
--timeline-automation-point-muted: rgba(0, 0, 0, 0.5);
|
|
146
|
+
--timeline-diamond-muted: #a3a3a3;
|
|
147
|
+
--timeline-beat: rgb(34, 197, 94);
|
|
148
|
+
--timeline-clip-shadow-dragging: rgba(0, 0, 0, 0.4);
|
|
149
|
+
--timeline-playhead-glow: rgba(60, 230, 172, 0.14);
|
|
150
|
+
--timeline-playhead-shadow: rgba(0, 0, 0, 0.55);
|
|
81
151
|
|
|
82
152
|
/* -------------------------------------------------------------- geometry */
|
|
83
153
|
|
|
@@ -146,7 +216,14 @@
|
|
|
146
216
|
--duration-press: 120ms;
|
|
147
217
|
--duration-hover: 150ms;
|
|
148
218
|
--duration-expand: 160ms;
|
|
149
|
-
|
|
219
|
+
/* Menus, popovers, and dropdowns. Open is ease-out; close is shorter and ease-in. */
|
|
220
|
+
--duration-open: 120ms;
|
|
221
|
+
--duration-close: 90ms;
|
|
222
|
+
/* Tooltips use the same enter shape on a shorter open. */
|
|
223
|
+
--duration-tooltip: 100ms;
|
|
224
|
+
/* Visible on the first frame. Never enter from scale 0 or opacity 0. */
|
|
225
|
+
--popup-enter-opacity: 0.8;
|
|
226
|
+
--popup-enter-scale: 0.97;
|
|
150
227
|
|
|
151
228
|
/* ------------------------------------------------------------------ icon */
|
|
152
229
|
|
|
@@ -264,6 +341,33 @@
|
|
|
264
341
|
--color-white: #fff;
|
|
265
342
|
}
|
|
266
343
|
|
|
344
|
+
/* One paper block. A host sets data-theme="paper" on the document element. */
|
|
345
|
+
:root[data-theme="paper"] {
|
|
346
|
+
--timeline-shell-bg: #f4f6f3;
|
|
347
|
+
--timeline-row-bg: #edf0ed;
|
|
348
|
+
--timeline-clip-bg: #c9d1cb;
|
|
349
|
+
--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.12);
|
|
350
|
+
--timeline-clip-highlight: rgb(255 255 255 / 0.08);
|
|
351
|
+
--timeline-clip-selection: #0f766e;
|
|
352
|
+
--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55);
|
|
353
|
+
--timeline-clip-chip-bg: rgba(15, 22, 18, 0.73);
|
|
354
|
+
--timeline-clip-chip-audio-bg: rgba(25, 16, 38, 0.7);
|
|
355
|
+
--timeline-clip-chip-text: #ffffff;
|
|
356
|
+
--timeline-clip-audio-bg: rgb(150, 128, 180);
|
|
357
|
+
--timeline-clip-audio-bg-hover: rgb(135, 109, 170);
|
|
358
|
+
--timeline-clip-audio-bg-dragging: rgb(232, 224, 242);
|
|
359
|
+
--timeline-clip-audio-border: rgba(113, 80, 155, 0.45);
|
|
360
|
+
--timeline-clip-audio-muted-bg: rgb(156, 161, 157);
|
|
361
|
+
--timeline-audio-link: #626d66;
|
|
362
|
+
--timeline-waveform-bar-rgb: 20, 12, 32;
|
|
363
|
+
--timeline-waveform-muted-rgb: 24, 28, 26;
|
|
364
|
+
--timeline-waveform-error: repeating-linear-gradient(
|
|
365
|
+
90deg,
|
|
366
|
+
rgba(20, 12, 32, 0.35) 0 2px,
|
|
367
|
+
transparent 2px 5px
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
267
371
|
/* Motion durations as utilities, since Tailwind has no `--duration-*`
|
|
268
372
|
* namespace. Each one zeroes itself under `prefers-reduced-motion`, so a
|
|
269
373
|
* caller cannot forget the reduced-motion case. */
|
package/src/styles/theme.test.ts
CHANGED
|
@@ -109,6 +109,15 @@ describe("studio theme", () => {
|
|
|
109
109
|
expect(vars.get("--text-lg")).toBe("1.125rem");
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
it("opens menus from a visible shape and closes them faster", () => {
|
|
113
|
+
expect(declaredValue("--duration-open")).toBe("120ms");
|
|
114
|
+
expect(declaredValue("--duration-close")).toBe("90ms");
|
|
115
|
+
expect(declaredValue("--duration-tooltip")).toBe("100ms");
|
|
116
|
+
expect(declaredValue("--popup-enter-opacity")).toBe("0.8");
|
|
117
|
+
expect(declaredValue("--popup-enter-scale")).toBe("0.97");
|
|
118
|
+
expect(declaredValue("--duration-hover")).toBe("150ms");
|
|
119
|
+
});
|
|
120
|
+
|
|
112
121
|
it("compiles a motion-duration utility with a reduced-motion variant", async () => {
|
|
113
122
|
const css = await build("studio.css", ["duration-press", "duration-open"]);
|
|
114
123
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "./studioUiPreferences";
|
|
3
3
|
|
|
4
|
-
/** Whether the audio meter strip is shown; persisted,
|
|
4
|
+
/** Whether the audio meter strip is shown; persisted, off unless the user showed it. */
|
|
5
5
|
export const useAudioMetersVisible = create<{
|
|
6
6
|
visible: boolean;
|
|
7
7
|
setVisible: (visible: boolean) => void;
|
|
8
8
|
}>((set) => ({
|
|
9
|
-
visible: readStudioUiPreferences().audioMetersVisible ??
|
|
9
|
+
visible: readStudioUiPreferences().audioMetersVisible ?? false,
|
|
10
10
|
setVisible: (visible) => {
|
|
11
11
|
writeStudioUiPreferences({ audioMetersVisible: visible });
|
|
12
12
|
set({ visible });
|
|
@@ -25,7 +25,7 @@ export interface StudioUiPreferences {
|
|
|
25
25
|
snapToGrid?: boolean;
|
|
26
26
|
/** Timeline magnet: snap clip drags/trims/drops to playhead, clip edges, and beats. */
|
|
27
27
|
timelineSnapEnabled?: boolean;
|
|
28
|
-
/** Audio level meters at the timeline's right edge;
|
|
28
|
+
/** Audio level meters at the timeline's right edge; hidden unless enabled here. */
|
|
29
29
|
audioMetersVisible?: boolean;
|
|
30
30
|
/** Keeps the main track gapless: deleting a clip closes the gap. Distinct
|
|
31
31
|
* from `timelineSnapEnabled` ("Magnet", drag/trim snapping). */
|