@bycrux/editor 1.2.1 → 1.3.0

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 (56) hide show
  1. package/package.json +2 -2
  2. package/src/ControlsInfoModal.tsx +4 -4
  3. package/src/__tests__/no-opacity-modifier-on-editor-vars.test.ts +84 -0
  4. package/src/__tests__/node-shims.d.ts +19 -0
  5. package/src/carousel/AddElementMenu.tsx +1 -1
  6. package/src/carousel/CarouselEditor.tsx +11 -11
  7. package/src/carousel/CarouselRenderModal.tsx +11 -11
  8. package/src/carousel/OverlayPicker.tsx +3 -3
  9. package/src/carousel/SlidePropertyPanel.tsx +6 -6
  10. package/src/carousel/__tests__/CarouselEditor.test.tsx +12 -0
  11. package/src/components/FilmstripScrubber.tsx +1 -1
  12. package/src/crop/VideoSourceCropModal.tsx +4 -4
  13. package/src/index.ts +1 -0
  14. package/src/state/__tests__/use-project-sync.test.tsx +76 -0
  15. package/src/state/use-project-sync.ts +21 -1
  16. package/src/text/FontPicker.tsx +3 -3
  17. package/src/text/TextFormattingToolbar.tsx +3 -3
  18. package/src/types.ts +84 -0
  19. package/src/ui/Loader.tsx +1 -1
  20. package/src/ui/NumberField.tsx +3 -3
  21. package/src/ui/SwatchInput.tsx +1 -1
  22. package/src/ui/Tooltip.tsx +1 -1
  23. package/src/ui/input.tsx +1 -1
  24. package/src/ui/label.tsx +1 -1
  25. package/src/ui/textarea.tsx +1 -1
  26. package/src/video/AudioPolishModal.tsx +21 -21
  27. package/src/video/CaptionListPanel.test.tsx +8 -7
  28. package/src/video/CaptionListPanel.tsx +17 -17
  29. package/src/video/CaptionRegenModal.tsx +41 -15
  30. package/src/video/CaptionStyleGallery.tsx +1 -1
  31. package/src/video/CommandPalette.tsx +7 -7
  32. package/src/video/ImageToneMenu.tsx +7 -7
  33. package/src/video/OverlayInspector.tsx +7 -7
  34. package/src/video/RenderModal.tsx +52 -52
  35. package/src/video/VersionCompare.tsx +7 -7
  36. package/src/video/VersionPanel.tsx +2 -2
  37. package/src/video/VideoEditor.tsx +55 -25
  38. package/src/video/__tests__/CaptionRegenModal.test.tsx +161 -2
  39. package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +102 -0
  40. package/src/video/__tests__/VideoEditor.pendingSurface.test.tsx +233 -0
  41. package/src/video/__tests__/VideoEditor.saveError.test.tsx +105 -0
  42. package/src/video/__tests__/captionProfileDefaults.test.ts +121 -0
  43. package/src/video/captionProfileDefaults.ts +86 -0
  44. package/src/video/panels/ClipPropertiesPanel.tsx +4 -4
  45. package/src/video/panels/LeftPanelTabs.tsx +20 -2
  46. package/src/video/panels/OverlayContentPanel.tsx +5 -5
  47. package/src/video/preview/OverlayItemsLayer.tsx +7 -2
  48. package/src/video/preview/SocialPreviewMenu.tsx +5 -5
  49. package/src/video/preview/__tests__/resolveOverlayPropPaths.test.ts +21 -0
  50. package/src/video/timeline/EditableSegment.tsx +1 -1
  51. package/src/video/timeline/Scrubber.tsx +2 -2
  52. package/src/video/timeline/SpeedControl.tsx +3 -3
  53. package/src/video/timeline/Timeline.tsx +3 -3
  54. package/src/video/timeline/TrackGutter.tsx +6 -6
  55. package/src/video/timeline/TrackSettingsPopover.tsx +3 -3
  56. package/src/video/timeline/VolumeControl.tsx +3 -3
@@ -1,21 +1,35 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
- import type { EditorAdapter, Project } from '../types'
3
+ import type { CaptionProfileDefaults, EditorAdapter, Project } from '../types'
4
4
  import type { Captions } from '../schema'
5
+ import { resolveCaptionProfileDefaults } from './captionProfileDefaults'
5
6
 
6
7
  interface CaptionRegenModalProps<P extends Project = Project> {
7
8
  projectId: string
8
9
  /** Adapter driving the caption-regeneration stream. Must implement
9
10
  * `generateCaptions` — callers gate rendering on its presence. */
10
11
  adapter: EditorAdapter<P>
12
+ /** The project's attached profile name (`Project.profile`), if it has one.
13
+ * Passed to `adapter.getCaptionProfileDefaults` to seed the new track's
14
+ * style/font/color. Absent — or a host with no such seam — and the run is
15
+ * byte-identical to what it was before profile seeding existed. */
16
+ profile?: string
11
17
  /** Caption rows the project has right now (`maxCaptionLane(segments) + 1`,
12
18
  * so 1 for a lane-less or empty track). Regeneration replaces the whole
13
19
  * track with a single fresh row, so this is the count the warning banner
14
20
  * below reports as about to be discarded. */
15
21
  existingRowCount: number
16
- /** Fired on terminal success with the freshly transcribed caption track. The
17
- * caller patches `project.captions` from this; the modal then closes. */
18
- onDone: (captions: Captions) => void
22
+ /** Fired on terminal success with the freshly transcribed caption track,
23
+ * plus whatever the host answered for `profile` (null when it answered
24
+ * nothing). The caller patches `project.captions` from the first and folds
25
+ * the second onto it — see `mergeCaptionProfileDefaults`. The modal then
26
+ * closes.
27
+ *
28
+ * The defaults are handed OVER rather than re-fetched by the caller so the
29
+ * profile is resolved exactly once per run: the modal already had to ask
30
+ * before the stream started (it needs the style for the request), and a
31
+ * second lookup at apply time could answer differently. */
32
+ onDone: (captions: Captions, profileDefaults: CaptionProfileDefaults | null) => void
19
33
  /** Fired when the modal closes (cancel, error dismiss, or post-done). */
20
34
  onClose: () => void
21
35
  /** Editor theme mode — light/dark. The panel and log box follow
@@ -25,7 +39,7 @@ interface CaptionRegenModalProps<P extends Project = Project> {
25
39
  }
26
40
 
27
41
  function LogLine({ text, mode = 'dark' }: { text: string; mode?: 'light' | 'dark' }) {
28
- let color = 'text-[var(--editor-text)]/60'
42
+ let color = 'text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]'
29
43
  if (/ready|complete|done|transcribed/i.test(text)) color = mode === 'light' ? 'text-green-700' : 'text-green-400'
30
44
  else if (/transcrib|detecting|loading|model/i.test(text)) color = mode === 'light' ? 'text-sky-700' : 'text-sky-400'
31
45
  else if (/extract|building|composing|mixing/i.test(text)) color = mode === 'light' ? 'text-amber-700' : 'text-amber-400'
@@ -38,7 +52,7 @@ function LogLine({ text, mode = 'dark' }: { text: string; mode?: 'light' | 'dark
38
52
  )
39
53
  }
40
54
 
41
- export default function CaptionRegenModal<P extends Project = Project>({ projectId, adapter, existingRowCount, onDone, onClose, mode = 'dark' }: CaptionRegenModalProps<P>) {
55
+ export default function CaptionRegenModal<P extends Project = Project>({ projectId, adapter, profile, existingRowCount, onDone, onClose, mode = 'dark' }: CaptionRegenModalProps<P>) {
42
56
  const [logs, setLogs] = useState<string[]>([])
43
57
  const [status, setStatus] = useState<'running' | 'done' | 'error'>('running')
44
58
  const [errorMsg, setError] = useState<string | null>(null)
@@ -66,13 +80,25 @@ export default function CaptionRegenModal<P extends Project = Project>({ project
66
80
 
67
81
  void (async () => {
68
82
  try {
69
- for await (const ev of adapter.generateCaptions!(projectId)) {
83
+ // Seeding, before anything is spawned: the style has to be on the
84
+ // request, so this one await sits in front of the stream. It cannot
85
+ // reject (see `resolveCaptionProfileDefaults`) and it cannot stop the
86
+ // run — a host with no profile seam, or a lookup that failed, simply
87
+ // produces `null` and everything below behaves exactly as it did
88
+ // before this existed, down to the arity of the call.
89
+ const profileDefaults = await resolveCaptionProfileDefaults(adapter, profile)
90
+ if (unmountedRef.current || cancelledRef.current) return
91
+ const stream = profileDefaults?.style
92
+ ? adapter.generateCaptions!(projectId, { style: profileDefaults.style })
93
+ : adapter.generateCaptions!(projectId)
94
+
95
+ for await (const ev of stream) {
70
96
  if (unmountedRef.current || cancelledRef.current) break
71
97
  if (ev.type === 'log') {
72
98
  setLogs(l => [...l, ev.message])
73
99
  } else if (ev.type === 'done') {
74
100
  setStatus('done')
75
- onDone(ev.captions)
101
+ onDone(ev.captions, profileDefaults)
76
102
  } else {
77
103
  setError(ev.message)
78
104
  setStatus('error')
@@ -94,7 +120,7 @@ export default function CaptionRegenModal<P extends Project = Project>({ project
94
120
  unmountedRef.current = true
95
121
  }, 0)
96
122
  }
97
- }, [projectId, adapter, onDone])
123
+ }, [projectId, adapter, profile, onDone])
98
124
 
99
125
  // Auto-scroll logs
100
126
  useEffect(() => {
@@ -139,13 +165,13 @@ export default function CaptionRegenModal<P extends Project = Project>({ project
139
165
  question about why a clip's audio showed up in the
140
166
  captions. Read at generation time only: unmuting later just
141
167
  means the next run sees it. */}
142
- <p className="text-[11px] text-[var(--editor-text)]/50">
168
+ <p className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
143
169
  Transcribes everything audible on the timeline. Muted clips and tracks are skipped.
144
170
  </p>
145
171
  </div>
146
172
  </div>
147
173
  {status !== 'running' && (
148
- <button onClick={onClose} className="text-[var(--editor-text)]/55 hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
174
+ <button onClick={onClose} className="text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
149
175
  )}
150
176
  </div>
151
177
 
@@ -163,17 +189,17 @@ export default function CaptionRegenModal<P extends Project = Project>({ project
163
189
  <div className="relative">
164
190
  <button
165
191
  onClick={() => navigator.clipboard.writeText(logs.join('\n') + (errorMsg ? '\n' + errorMsg : ''))}
166
- className="absolute top-2 right-2 z-10 text-[10px] px-2 py-0.5 rounded bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] hover:border-[var(--editor-border)] transition-colors"
192
+ className="absolute top-2 right-2 z-10 text-[10px] px-2 py-0.5 rounded bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] hover:border-[var(--editor-border)] transition-colors"
167
193
  title="Copy logs"
168
194
  >
169
195
  Copy
170
196
  </button>
171
197
  <div
172
198
  ref={logRef}
173
- className="h-96 overflow-y-auto px-4 py-3 font-mono text-[11px] text-[var(--editor-text)]/80 bg-[var(--editor-surface)] flex flex-col gap-0.5"
199
+ className="h-96 overflow-y-auto px-4 py-3 font-mono text-[11px] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] bg-[var(--editor-surface)] flex flex-col gap-0.5"
174
200
  >
175
201
  {logs.length === 0 && status === 'running' && (
176
- <span className="text-[var(--editor-text)]/40 italic">Starting transcription…</span>
202
+ <span className="text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] italic">Starting transcription…</span>
177
203
  )}
178
204
  {logs.map((line, i) => (
179
205
  <LogLine key={i} text={line} mode={mode} />
@@ -189,7 +215,7 @@ export default function CaptionRegenModal<P extends Project = Project>({ project
189
215
  {status === 'running' ? (
190
216
  <button
191
217
  onClick={handleCancel}
192
- className={`text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)]/80 transition-colors ${mode === 'light' ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-700' : 'hover:bg-red-900/40 hover:border-red-700 hover:text-red-300'}`}
218
+ className={`text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] transition-colors ${mode === 'light' ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-700' : 'hover:bg-red-900/40 hover:border-red-700 hover:text-red-300'}`}
193
219
  >
194
220
  Cancel
195
221
  </button>
@@ -449,7 +449,7 @@ export function CaptionStyleCard({
449
449
  </div>
450
450
  )}
451
451
 
452
- {/* NOT `text-[var(--editor-text)]/60` — Tailwind cannot generate an
452
+ {/* NOT `text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]` — Tailwind cannot generate an
453
453
  opacity modifier on an arbitrary var() colour, so that class is a
454
454
  silent no-op. See the long note in CaptionListPanel.tsx. */}
455
455
  <span
@@ -103,7 +103,7 @@ export default function CommandPalette({ commands, initialMode = 'list', onGoToT
103
103
  >
104
104
  {mode === 'goto' ? (
105
105
  <div className="flex flex-col gap-2 px-4 py-3">
106
- <span className="text-xs font-medium text-[var(--editor-text)]/60">Go to time</span>
106
+ <span className="text-xs font-medium text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">Go to time</span>
107
107
  <input
108
108
  ref={inputRef}
109
109
  type="text"
@@ -111,14 +111,14 @@ export default function CommandPalette({ commands, initialMode = 'list', onGoToT
111
111
  onChange={(e) => { setGotoValue(e.target.value); setGotoError(false) }}
112
112
  onKeyDown={handleGotoKeyDown}
113
113
  placeholder="mm:ss, hh:mm:ss, or seconds"
114
- className="w-full bg-transparent text-sm text-[var(--editor-text)] placeholder-[var(--editor-text)]/25 outline-none border border-[var(--editor-border)] rounded px-2 py-1.5"
114
+ className="w-full bg-transparent text-sm text-[var(--editor-text)] placeholder-[color-mix(in_srgb,var(--editor-text)_25%,transparent)] outline-none border border-[var(--editor-border)] rounded px-2 py-1.5"
115
115
  />
116
116
  {gotoError && <span className={`text-xs ${themeMode === 'light' ? 'text-red-600' : 'text-red-400'}`}>Couldn&rsquo;t parse that timecode</span>}
117
117
  </div>
118
118
  ) : (
119
119
  <>
120
120
  <div className="flex items-center gap-2 px-4 py-3 border-b border-[var(--editor-border)]">
121
- <Search size={14} className="text-[var(--editor-text)]/60 shrink-0" />
121
+ <Search size={14} className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] shrink-0" />
122
122
  <input
123
123
  ref={inputRef}
124
124
  type="text"
@@ -126,12 +126,12 @@ export default function CommandPalette({ commands, initialMode = 'list', onGoToT
126
126
  onChange={(e) => setQuery(e.target.value)}
127
127
  onKeyDown={handleListKeyDown}
128
128
  placeholder="Type a command…"
129
- className="w-full bg-transparent text-sm text-[var(--editor-text)] placeholder-[var(--editor-text)]/25 outline-none"
129
+ className="w-full bg-transparent text-sm text-[var(--editor-text)] placeholder-[color-mix(in_srgb,var(--editor-text)_25%,transparent)] outline-none"
130
130
  />
131
131
  </div>
132
132
  <div className="max-h-80 overflow-y-auto py-1">
133
133
  {filtered.length === 0 && (
134
- <div className="px-4 py-3 text-xs text-[var(--editor-text)]/35">No matching commands</div>
134
+ <div className="px-4 py-3 text-xs text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)]">No matching commands</div>
135
135
  )}
136
136
  {filtered.map((cmd, i) => (
137
137
  <button
@@ -140,14 +140,14 @@ export default function CommandPalette({ commands, initialMode = 'list', onGoToT
140
140
  onMouseEnter={() => setHighlight(i)}
141
141
  onClick={() => { cmd.run(); onClose() }}
142
142
  className={`w-full flex items-center justify-between gap-3 px-4 py-2 text-left text-sm transition-colors ${
143
- i === highlight ? 'bg-[var(--editor-text)]/10 text-[var(--editor-text)]' : 'text-[var(--editor-text)]/70'
143
+ i === highlight ? 'bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] text-[var(--editor-text)]' : 'text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]'
144
144
  }`}
145
145
  >
146
146
  <span>{cmd.label}</span>
147
147
  {cmd.keyHint && cmd.keyHint.length > 0 && (
148
148
  <span className="flex items-center gap-1 shrink-0">
149
149
  {cmd.keyHint.map((k, j) => (
150
- <kbd key={j} className="text-[10px] font-mono px-1.5 py-0.5 rounded border border-[var(--editor-border)] text-[var(--editor-text)]/60">
150
+ <kbd key={j} className="text-[10px] font-mono px-1.5 py-0.5 rounded border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">
151
151
  {k}
152
152
  </kbd>
153
153
  ))}
@@ -63,7 +63,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
63
63
  title="How photos and logos are converted for the HDR render"
64
64
  aria-haspopup="menu"
65
65
  aria-expanded={open}
66
- className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] text-[var(--editor-text)]/80 hover:text-[var(--editor-text)] transition-colors"
66
+ className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:text-[var(--editor-text)] transition-colors"
67
67
  >
68
68
  <Palette size={12} className="opacity-70" />
69
69
  <span className="opacity-60">Image color:</span>
@@ -79,7 +79,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
79
79
  className={`flex items-center justify-center w-5 h-5 rounded transition-colors ${
80
80
  open
81
81
  ? (mode === 'light' ? 'text-violet-600 bg-violet-400/15 hover:bg-violet-400/25' : 'text-violet-400 bg-violet-400/15 hover:bg-violet-400/25')
82
- : 'text-[var(--editor-text)]/60 bg-transparent hover:text-[var(--editor-text)]'
82
+ : 'text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] bg-transparent hover:text-[var(--editor-text)]'
83
83
  }`}
84
84
  >
85
85
  <Palette size={12} />
@@ -91,9 +91,9 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
91
91
  role="menu"
92
92
  className={`${menuPosition} z-40 w-[300px] rounded-xl border border-[var(--editor-border)] bg-[var(--editor-surface)] shadow-2xl p-2 flex flex-col gap-1`}
93
93
  >
94
- <p className="px-2 pt-1 pb-1.5 text-[11px] font-semibold text-[var(--editor-text)]/80">
94
+ <p className="px-2 pt-1 pb-1.5 text-[11px] font-semibold text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]">
95
95
  Image color mapping
96
- <span className="block font-normal text-[10px] text-[var(--editor-text)]/50">
96
+ <span className="block font-normal text-[10px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
97
97
  How photos and logos are converted for the HDR render.
98
98
  </span>
99
99
  </p>
@@ -108,7 +108,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
108
108
  className={`flex items-start gap-2.5 rounded-lg p-2 text-left transition-colors border ${
109
109
  active
110
110
  ? 'border-violet-400/60 bg-violet-400/10'
111
- : 'border-transparent hover:bg-[var(--editor-text)]/5'
111
+ : 'border-transparent hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
112
112
  }`}
113
113
  >
114
114
  <img
@@ -120,10 +120,10 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
120
120
  <span className="text-xs font-semibold text-[var(--editor-text)] flex items-center gap-1.5">
121
121
  {tone.label}
122
122
  {tone.id === DEFAULT_IMAGE_TONE && (
123
- <span className="text-[9px] font-normal px-1 py-px rounded bg-[var(--editor-text)]/10 text-[var(--editor-text)]/55">default</span>
123
+ <span className="text-[9px] font-normal px-1 py-px rounded bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">default</span>
124
124
  )}
125
125
  </span>
126
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/60">
126
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">
127
127
  {tone.summary}
128
128
  </span>
129
129
  </span>
@@ -216,9 +216,9 @@ function normalizeDegrees(deg: number): number {
216
216
 
217
217
  // ── Shared chrome ────────────────────────────────────────────────────────
218
218
 
219
- const ROW_LABEL_CLASS = 'w-14 shrink-0 text-[11px] text-[var(--editor-text)]/55'
219
+ const ROW_LABEL_CLASS = 'w-14 shrink-0 text-[11px] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]'
220
220
  const ICON_BUTTON_CLASS =
221
- 'flex h-5 w-5 shrink-0 items-center justify-center rounded text-[var(--editor-text)]/45 transition-colors hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)] disabled:pointer-events-none disabled:opacity-25'
221
+ 'flex h-5 w-5 shrink-0 items-center justify-center rounded text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)] transition-colors hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)] disabled:pointer-events-none disabled:opacity-25'
222
222
 
223
223
  function IconButton({
224
224
  label,
@@ -282,10 +282,10 @@ function KeyframeNav({
282
282
  // invites a support question; one that is visibly unavailable and
283
283
  // says why reads as a limitation, which is what it is.
284
284
  diamondDisabled
285
- ? 'cursor-not-allowed text-[var(--editor-text)]/20'
285
+ ? 'cursor-not-allowed text-[color-mix(in_srgb,var(--editor-text)_20%,transparent)]'
286
286
  : pressed
287
287
  ? 'text-[var(--editor-accent)]'
288
- : 'text-[var(--editor-text)]/40 hover:text-[var(--editor-text)]/70',
288
+ : 'text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]',
289
289
  )}
290
290
  >
291
291
  <Diamond size={11} fill={pressed ? 'currentColor' : 'none'} />
@@ -518,7 +518,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
518
518
  if (!canKeyframe(item)) {
519
519
  return (
520
520
  <div className={SECTION_CLASS}>
521
- <div className="px-3 py-6 text-center text-[11px] text-[var(--editor-text)]/45">
521
+ <div className="px-3 py-6 text-center text-[11px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)]">
522
522
  Select an overlay to edit its properties.
523
523
  </div>
524
524
  </div>
@@ -769,7 +769,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
769
769
  only hosts the ALL-properties actions (reset-all + keyframe-all); the
770
770
  muted "All" scopes them apart from each row's own per-property unit. */}
771
771
  <div className="shrink-0 flex items-center gap-1 border-b border-[var(--editor-border)] px-2 py-1.5">
772
- <span className="px-1 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[var(--editor-text)]/40">
772
+ <span className="px-1 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">
773
773
  All
774
774
  </span>
775
775
  <div className="ml-auto flex items-center gap-1">
@@ -857,7 +857,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
857
857
  (Space/Enter both fire click), so it needs no key handler of its
858
858
  own — only a visible focus ring. */}
859
859
  <div className="flex items-center gap-2">
860
- <span className="shrink-0 text-[11px] text-[var(--editor-text)]/55">Uniform scale</span>
860
+ <span className="shrink-0 text-[11px] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">Uniform scale</span>
861
861
  <button
862
862
  type="button"
863
863
  role="checkbox"