@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.
Files changed (60) hide show
  1. package/dist/assets/{hyperframes-player-BBub3ZdI.js → hyperframes-player-Di7GyCOP.js} +1 -1
  2. package/dist/assets/{index-DZBHG9TW.css → index-CBKb_0TI.css} +1 -1
  3. package/dist/assets/{index-D1KNPijK.js → index-Dpq2lkOm.js} +111 -111
  4. package/dist/assets/{index-BxZ5Cw5w.js → index-Dx_pt9m4.js} +1 -1
  5. package/dist/assets/{index-CI7WON-E.js → index-jAjhQTrB.js} +1 -1
  6. package/dist/index.d.ts +1 -5
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +4439 -4118
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles.css +1 -1
  11. package/package.json +7 -7
  12. package/src/components/StudioHeader.tsx +2 -13
  13. package/src/components/TimelineToolbar.test.tsx +42 -0
  14. package/src/components/TimelineToolbar.tsx +5 -10
  15. package/src/components/editor/propertyPanelFill.tsx +3 -2
  16. package/src/components/icons/AudioMetersIcon.tsx +16 -0
  17. package/src/components/icons/InspectorIcon.tsx +18 -0
  18. package/src/components/icons/ReverseGradientIcon.tsx +23 -0
  19. package/src/components/icons/RippleEditIcon.tsx +19 -0
  20. package/src/components/nle/AudioMeterStrip.test.tsx +278 -7
  21. package/src/components/nle/AudioMeterStrip.tsx +264 -47
  22. package/src/components/ui/Menu.test.tsx +3 -4
  23. package/src/components/ui/Menu.tsx +18 -5
  24. package/src/components/ui/Select.tsx +7 -1
  25. package/src/components/ui/Tooltip.tsx +6 -1
  26. package/src/player/components/AudioWaveform.tsx +16 -7
  27. package/src/player/components/ImageThumbnail.tsx +3 -5
  28. package/src/player/components/LayerDisclosureRow.tsx +9 -4
  29. package/src/player/components/PlayheadIndicator.tsx +3 -3
  30. package/src/player/components/TimelineAutomationLane.test.tsx +1 -1
  31. package/src/player/components/TimelineAutomationLane.tsx +5 -5
  32. package/src/player/components/TimelineCanvas.tsx +18 -15
  33. package/src/player/components/TimelineClip.test.tsx +41 -4
  34. package/src/player/components/TimelineClip.tsx +11 -4
  35. package/src/player/components/TimelineClipDiamonds.test.tsx +3 -1
  36. package/src/player/components/TimelineClipDiamonds.tsx +2 -2
  37. package/src/player/components/TimelineDiamondConnectors.tsx +2 -2
  38. package/src/player/components/TimelineEmptyState.tsx +1 -1
  39. package/src/player/components/TimelineFxButton.tsx +10 -8
  40. package/src/player/components/TimelineGroupHeader.tsx +18 -9
  41. package/src/player/components/TimelineGroupLaneLabels.tsx +12 -5
  42. package/src/player/components/TimelineGroupRow.tsx +1 -1
  43. package/src/player/components/TimelineLanes.test.tsx +14 -0
  44. package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
  45. package/src/player/components/TimelineRazorInteraction.tsx +1 -1
  46. package/src/player/components/TimelineRuler.tsx +3 -2
  47. package/src/player/components/TimelineShortcutHint.tsx +2 -2
  48. package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
  49. package/src/player/components/TimelineTrackHeader.tsx +4 -6
  50. package/src/player/components/TimelineTrackPlainHeader.tsx +10 -3
  51. package/src/player/components/VideoThumbnail.tsx +3 -5
  52. package/src/player/components/timelineMotionStyles.test.ts +96 -30
  53. package/src/player/components/timelineTheme.test.ts +14 -0
  54. package/src/player/components/timelineTheme.ts +12 -11
  55. package/src/styles/hex-baseline.json +4 -23
  56. package/src/styles/studio.css +43 -47
  57. package/src/styles/theme.css +105 -1
  58. package/src/styles/theme.test.ts +9 -0
  59. package/src/utils/audioMeterVisibility.ts +2 -2
  60. 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: "0 1px 2px rgba(0,0,0,0.9)" }}
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 transitionProperties(transitionDeclaration: string): string[] {
35
- const items: string[] = [];
36
- let depth = 0;
37
- let item = "";
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(transitionProperties(clipTransition)).toEqual(allowedTimelineTransitionProperties);
69
- expect(clipTransition).not.toMatch(/\b(?:all|left|width|top|bottom|transform)\b/);
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-lg)",
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": 385,
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": 6,
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
  }
@@ -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: 8px;
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: rgba(167, 139, 250, 0.16);
231
- border-color: rgba(167, 139, 250, 0.4);
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: rgba(167, 139, 250, 0.24);
239
+ background-color: var(--timeline-clip-audio-bg-hover);
236
240
  }
237
241
 
238
242
  .timeline-clip.is-audio.is-dragging {
239
- background-color: rgba(60, 52, 84, 0.96);
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
- box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.85);
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-dragging {
259
- /* Solid background so the picked-up clip reads clearly while dragging — audio
260
- clips (waveform drawn on a separate layer) would otherwise look transparent. */
261
- background-color: var(--clip-bg-dragging);
262
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
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
- /* Keep the white selection outline while dragging (a plain box-shadow alone gets
266
- overridden by .is-dragging's drop shadow, so re-state both together). */
267
- .timeline-clip.is-selected.is-dragging {
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[data-active].is-selected.is-dragging {
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 1.5px rgba(255, 255, 255, 0.85),
276
- 0 8px 24px rgba(0, 0, 0, 0.4);
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: 6px;
282
- left: 12px;
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
- color: rgba(255, 255, 255, 0.5);
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: 1;
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[data-active] .timeline-clip__label {
297
- color: rgba(232, 255, 247, 0.95);
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: rgba(255, 255, 255, 0.34);
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 {
@@ -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
- --duration-open: 180ms;
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. */
@@ -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, on unless the user hid it. */
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 ?? true,
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; shown unless hidden here. */
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). */