@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.
- package/package.json +2 -2
- package/src/ControlsInfoModal.tsx +4 -4
- package/src/__tests__/no-opacity-modifier-on-editor-vars.test.ts +84 -0
- package/src/__tests__/node-shims.d.ts +19 -0
- package/src/carousel/AddElementMenu.tsx +1 -1
- package/src/carousel/CarouselEditor.tsx +11 -11
- package/src/carousel/CarouselRenderModal.tsx +11 -11
- package/src/carousel/OverlayPicker.tsx +3 -3
- package/src/carousel/SlidePropertyPanel.tsx +6 -6
- package/src/carousel/__tests__/CarouselEditor.test.tsx +12 -0
- package/src/components/FilmstripScrubber.tsx +1 -1
- package/src/crop/VideoSourceCropModal.tsx +4 -4
- package/src/index.ts +1 -0
- package/src/state/__tests__/use-project-sync.test.tsx +76 -0
- package/src/state/use-project-sync.ts +21 -1
- package/src/text/FontPicker.tsx +3 -3
- package/src/text/TextFormattingToolbar.tsx +3 -3
- package/src/types.ts +84 -0
- package/src/ui/Loader.tsx +1 -1
- package/src/ui/NumberField.tsx +3 -3
- package/src/ui/SwatchInput.tsx +1 -1
- package/src/ui/Tooltip.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/label.tsx +1 -1
- package/src/ui/textarea.tsx +1 -1
- package/src/video/AudioPolishModal.tsx +21 -21
- package/src/video/CaptionListPanel.test.tsx +8 -7
- package/src/video/CaptionListPanel.tsx +17 -17
- package/src/video/CaptionRegenModal.tsx +41 -15
- package/src/video/CaptionStyleGallery.tsx +1 -1
- package/src/video/CommandPalette.tsx +7 -7
- package/src/video/ImageToneMenu.tsx +7 -7
- package/src/video/OverlayInspector.tsx +7 -7
- package/src/video/RenderModal.tsx +52 -52
- package/src/video/VersionCompare.tsx +7 -7
- package/src/video/VersionPanel.tsx +2 -2
- package/src/video/VideoEditor.tsx +55 -25
- package/src/video/__tests__/CaptionRegenModal.test.tsx +161 -2
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +102 -0
- package/src/video/__tests__/VideoEditor.pendingSurface.test.tsx +233 -0
- package/src/video/__tests__/VideoEditor.saveError.test.tsx +105 -0
- package/src/video/__tests__/captionProfileDefaults.test.ts +121 -0
- package/src/video/captionProfileDefaults.ts +86 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +4 -4
- package/src/video/panels/LeftPanelTabs.tsx +20 -2
- package/src/video/panels/OverlayContentPanel.tsx +5 -5
- package/src/video/preview/OverlayItemsLayer.tsx +7 -2
- package/src/video/preview/SocialPreviewMenu.tsx +5 -5
- package/src/video/preview/__tests__/resolveOverlayPropPaths.test.ts +21 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +2 -2
- package/src/video/timeline/SpeedControl.tsx +3 -3
- package/src/video/timeline/Timeline.tsx +3 -3
- package/src/video/timeline/TrackGutter.tsx +6 -6
- package/src/video/timeline/TrackSettingsPopover.tsx +3 -3
- 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
|
|
17
|
-
*
|
|
18
|
-
|
|
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)]
|
|
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
|
-
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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’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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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)]
|
|
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"
|