@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
|
@@ -113,13 +113,18 @@ function OverlayVideo({ src, currentTime, itemStart, inPoint, speed = 1, isPlayi
|
|
|
113
113
|
// already recurses — without this, image paths nested in array/object props
|
|
114
114
|
// (e.g. players[].src, items[].src) reach <img> raw as /var/hub-scratch/... and
|
|
115
115
|
// render blank in preview even though they render correctly in the final MP4.
|
|
116
|
-
// Already-proxied /api/ URLs are left untouched (idempotent).
|
|
116
|
+
// Already-proxied /api/ URLs are left untouched (idempotent). On Windows an
|
|
117
|
+
// absolute path is a drive-letter one (C:\ or C:/); this runs in the browser,
|
|
118
|
+
// so that is a regex, not Node's path module. UNC (\\host) is not rewritten.
|
|
119
|
+
const DRIVE_ABS_PATH = /^[A-Za-z]:[\\/]/
|
|
120
|
+
|
|
117
121
|
export function resolveOverlayPropPaths(
|
|
118
122
|
value: unknown,
|
|
119
123
|
fileUrl: (path: string) => string,
|
|
120
124
|
): unknown {
|
|
121
125
|
if (typeof value === 'string') {
|
|
122
|
-
|
|
126
|
+
const abs = (value.startsWith('/') && !value.startsWith('/api/')) || DRIVE_ABS_PATH.test(value)
|
|
127
|
+
return abs ? fileUrl(value) : value
|
|
123
128
|
}
|
|
124
129
|
if (Array.isArray(value)) {
|
|
125
130
|
return value.map((v) => resolveOverlayPropPaths(v, fileUrl))
|
|
@@ -157,8 +157,8 @@ export default function SocialPreviewMenu({ anchorRef, value, onChange, onClose,
|
|
|
157
157
|
}}
|
|
158
158
|
>
|
|
159
159
|
<p className="px-2 pt-1 pb-1.5">
|
|
160
|
-
<span className="block text-[11px] font-semibold text-[var(--editor-text)]
|
|
161
|
-
<span className="block text-[10px] text-[var(--editor-text)]
|
|
160
|
+
<span className="block text-[11px] font-semibold text-[color-mix(in_srgb,var(--editor-text)_90%,transparent)]">Preview for social media</span>
|
|
161
|
+
<span className="block text-[10px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">What you see may vary depending on your device.</span>
|
|
162
162
|
</p>
|
|
163
163
|
|
|
164
164
|
{PLATFORM_OPTIONS.map(opt => {
|
|
@@ -175,7 +175,7 @@ export default function SocialPreviewMenu({ anchorRef, value, onChange, onClose,
|
|
|
175
175
|
className={`flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition-colors ${
|
|
176
176
|
active
|
|
177
177
|
? 'text-[var(--editor-text)] bg-sky-400/10'
|
|
178
|
-
: 'text-[var(--editor-text)]
|
|
178
|
+
: 'text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
|
|
179
179
|
}`}
|
|
180
180
|
>
|
|
181
181
|
<span className="flex items-center gap-2">
|
|
@@ -199,11 +199,11 @@ export default function SocialPreviewMenu({ anchorRef, value, onChange, onClose,
|
|
|
199
199
|
className={`flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-xs transition-colors ${
|
|
200
200
|
value === null
|
|
201
201
|
? 'text-[var(--editor-text)] bg-sky-400/10'
|
|
202
|
-
: 'text-[var(--editor-text)]
|
|
202
|
+
: 'text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
|
|
203
203
|
}`}
|
|
204
204
|
>
|
|
205
205
|
<span className="flex items-center gap-2">
|
|
206
|
-
<PlatformGlyph icon={Slash} badgeClassName="bg-[var(--editor-text)]
|
|
206
|
+
<PlatformGlyph icon={Slash} badgeClassName="bg-[color-mix(in_srgb,var(--editor-text)_15%,transparent)]" />
|
|
207
207
|
<span>None</span>
|
|
208
208
|
</span>
|
|
209
209
|
{value === null && <Check size={13} className={mode === 'light' ? 'text-sky-600' : 'text-sky-400'} />}
|
|
@@ -50,4 +50,25 @@ describe('resolveOverlayPropPaths', () => {
|
|
|
50
50
|
expect(out.duration).toBe(90)
|
|
51
51
|
expect(out.nested.deep[0].src).toBe(fileUrl('/var/x.png'))
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
// Windows: an absolute image prop is a drive-letter path. It must reach
|
|
55
|
+
// fileUrl like a posix absolute path does; a UNC path stays refused.
|
|
56
|
+
it('rewrites drive-letter absolute paths in either slash style', () => {
|
|
57
|
+
expect(resolveOverlayPropPaths('C:\\a\\x.png', fileUrl)).toBe(fileUrl('C:\\a\\x.png'))
|
|
58
|
+
expect(resolveOverlayPropPaths('C:/a/x.png', fileUrl)).toBe(fileUrl('C:/a/x.png'))
|
|
59
|
+
expect(resolveOverlayPropPaths('d:\\x.png', fileUrl)).toBe(fileUrl('d:\\x.png'))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves UNC, drive-relative and relative paths untouched', () => {
|
|
63
|
+
for (const v of ['\\\\server\\x', 'C:x.png', 'a\\x.png', 'a/x.png']) {
|
|
64
|
+
expect(resolveOverlayPropPaths(v, fileUrl)).toBe(v)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('keeps posix behaviour exactly: /abs and //host rewritten, /api/ untouched', () => {
|
|
69
|
+
expect(resolveOverlayPropPaths('/abs/x.png', fileUrl)).toBe(fileUrl('/abs/x.png'))
|
|
70
|
+
expect(resolveOverlayPropPaths('//host/x.png', fileUrl)).toBe(fileUrl('//host/x.png'))
|
|
71
|
+
expect(resolveOverlayPropPaths('/api/x', fileUrl)).toBe('/api/x')
|
|
72
|
+
expect(resolveOverlayPropPaths('/apix', fileUrl)).toBe(fileUrl('/apix'))
|
|
73
|
+
})
|
|
53
74
|
})
|
|
@@ -22,7 +22,7 @@ export function EditableSegment({ seg, onEdit }: { seg: CaptionSegment; onEdit:
|
|
|
22
22
|
suppressContentEditableWarning
|
|
23
23
|
onBlur={handleBlur}
|
|
24
24
|
onKeyDown={handleKeyDown}
|
|
25
|
-
className="cursor-text rounded px-0.5 hover:bg-[var(--editor-text)]
|
|
25
|
+
className="cursor-text rounded px-0.5 hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)] focus:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] focus:outline-none focus:ring-1 focus:ring-purple-500/40"
|
|
26
26
|
>
|
|
27
27
|
{seg.text}
|
|
28
28
|
</span>
|
|
@@ -34,14 +34,14 @@ export default function Scrubber({ onOpenGoToTime }: ScrubberProps) {
|
|
|
34
34
|
/* `cursor-pointer`: the row is a strip of timeline background, and
|
|
35
35
|
Timeline's container click seeks through it (see `handleContainerClick`),
|
|
36
36
|
so it should look live rather than dead. */
|
|
37
|
-
<div className="flex items-center justify-between text-[10px] font-mono text-[var(--editor-text)]
|
|
37
|
+
<div className="flex items-center justify-between text-[10px] font-mono text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)] cursor-pointer">
|
|
38
38
|
{onOpenGoToTime ? (
|
|
39
39
|
<Tooltip label="Go to time">
|
|
40
40
|
<button
|
|
41
41
|
type="button"
|
|
42
42
|
aria-label="Go to time"
|
|
43
43
|
onClick={(e) => { e.stopPropagation(); onOpenGoToTime() }}
|
|
44
|
-
className="hover:text-[var(--editor-text)]
|
|
44
|
+
className="hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] transition-colors"
|
|
45
45
|
>
|
|
46
46
|
{formatTime(currentTime)}
|
|
47
47
|
</button>
|
|
@@ -52,9 +52,9 @@ export default function SpeedControl({
|
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
54
|
<div className="flex flex-col gap-2">
|
|
55
|
-
<div className={`flex items-center text-[11px] text-[var(--editor-text)]
|
|
55
|
+
<div className={`flex items-center text-[11px] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] ${label ? 'justify-between' : 'justify-end'}`}>
|
|
56
56
|
{label && <span>{label}</span>}
|
|
57
|
-
<span className="font-mono text-[10px] text-[var(--editor-text)]
|
|
57
|
+
<span className="font-mono text-[10px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">{value.toFixed(2)}×</span>
|
|
58
58
|
</div>
|
|
59
59
|
<Slider
|
|
60
60
|
id={sliderId}
|
|
@@ -82,7 +82,7 @@ export default function SpeedControl({
|
|
|
82
82
|
className={`rounded-md border px-2 py-0.5 text-[11px] font-mono transition-colors focus:outline-none focus:ring-1 focus:ring-[var(--editor-accent)] ${
|
|
83
83
|
active
|
|
84
84
|
? 'border-[var(--editor-accent)] bg-[var(--editor-accent)] text-[var(--editor-accent-foreground)]'
|
|
85
|
-
: 'border-[var(--editor-border)] bg-[var(--editor-surface)] text-[var(--editor-text)]
|
|
85
|
+
: 'border-[var(--editor-border)] bg-[var(--editor-surface)] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)]'
|
|
86
86
|
}`}
|
|
87
87
|
>
|
|
88
88
|
{p}×
|
|
@@ -940,7 +940,7 @@ export default function Timeline({ project, clock, onProjectChange, onOverlayEdi
|
|
|
940
940
|
<div className="flex items-center justify-end gap-0.5 -mb-1 cursor-pointer">
|
|
941
941
|
<Tooltip label="Zoom out">
|
|
942
942
|
<button
|
|
943
|
-
className="text-[11px] leading-none text-[var(--editor-text)]
|
|
943
|
+
className="text-[11px] leading-none text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] w-5 h-5 flex items-center justify-center rounded hover:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] transition-colors"
|
|
944
944
|
aria-label="Zoom out"
|
|
945
945
|
onClick={(e) => { e.stopPropagation(); zoomControls.zoomOut() }}
|
|
946
946
|
>−</button>
|
|
@@ -948,7 +948,7 @@ export default function Timeline({ project, clock, onProjectChange, onOverlayEdi
|
|
|
948
948
|
{zoomControls.badge}
|
|
949
949
|
<Tooltip label="Zoom in">
|
|
950
950
|
<button
|
|
951
|
-
className="text-[11px] leading-none text-[var(--editor-text)]
|
|
951
|
+
className="text-[11px] leading-none text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] w-5 h-5 flex items-center justify-center rounded hover:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] transition-colors"
|
|
952
952
|
aria-label="Zoom in"
|
|
953
953
|
onClick={(e) => { e.stopPropagation(); zoomControls.zoomIn() }}
|
|
954
954
|
>+</button>
|
|
@@ -956,7 +956,7 @@ export default function Timeline({ project, clock, onProjectChange, onOverlayEdi
|
|
|
956
956
|
{zoomControls.showFit && (
|
|
957
957
|
<Tooltip label="Fit to view" className="ml-0.5">
|
|
958
958
|
<button
|
|
959
|
-
className="text-[10px] text-[var(--editor-text)]
|
|
959
|
+
className="text-[10px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] px-1.5 h-5 rounded hover:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] transition-colors"
|
|
960
960
|
aria-label="Fit to view"
|
|
961
961
|
onClick={(e) => { e.stopPropagation(); zoomControls.fit() }}
|
|
962
962
|
>fit</button>
|
|
@@ -118,7 +118,7 @@ function RailCell({ height, accent, icon, label, action, dimmed, settingsButton
|
|
|
118
118
|
icons could be clicked — a decorative-looking glyph that happened to
|
|
119
119
|
open a dialog. Settings moved to the gear beside it. */}
|
|
120
120
|
<Tooltip label={label}>
|
|
121
|
-
<span aria-label={label} className="flex h-4 w-4 items-center justify-center text-[var(--editor-text)]
|
|
121
|
+
<span aria-label={label} className="flex h-4 w-4 items-center justify-center text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
|
|
122
122
|
{icon}
|
|
123
123
|
</span>
|
|
124
124
|
</Tooltip>
|
|
@@ -144,8 +144,8 @@ function RailCell({ height, accent, icon, label, action, dimmed, settingsButton
|
|
|
144
144
|
onClick={settingsButton.onClick}
|
|
145
145
|
className={`flex h-3.5 w-3.5 items-center justify-center rounded transition-colors ${
|
|
146
146
|
settingsButton.open
|
|
147
|
-
? 'text-[var(--editor-text)]
|
|
148
|
-
: 'text-[var(--editor-text)]
|
|
147
|
+
? 'text-[color-mix(in_srgb,var(--editor-text)_90%,transparent)] bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)]'
|
|
148
|
+
: 'text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_90%,transparent)] hover:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)]'
|
|
149
149
|
}`}
|
|
150
150
|
>
|
|
151
151
|
<Settings2 size={13} />
|
|
@@ -179,7 +179,7 @@ function MuteToggle({ muted, onToggle, trackLabel, mode }: { muted: boolean; onT
|
|
|
179
179
|
className={`flex h-3.5 w-3.5 items-center justify-center rounded transition-colors ${
|
|
180
180
|
muted
|
|
181
181
|
? (mode === 'light' ? 'text-amber-600 hover:text-amber-700' : 'text-amber-400/90 hover:text-amber-300')
|
|
182
|
-
: 'text-[var(--editor-text)]
|
|
182
|
+
: 'text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]'
|
|
183
183
|
}`}
|
|
184
184
|
>
|
|
185
185
|
{muted ? <VolumeX size={13} /> : <Volume2 size={13} />}
|
|
@@ -206,7 +206,7 @@ function MagnetToggle({ magnetic, onToggle, trackLabel, mode }: { magnetic: bool
|
|
|
206
206
|
className={`flex h-3.5 w-3.5 items-center justify-center rounded transition-colors ${
|
|
207
207
|
magnetic
|
|
208
208
|
? (mode === 'light' ? 'text-emerald-600 hover:text-emerald-700' : 'text-emerald-400/90 hover:text-emerald-300')
|
|
209
|
-
: 'text-[var(--editor-text)]
|
|
209
|
+
: 'text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]'
|
|
210
210
|
}`}
|
|
211
211
|
>
|
|
212
212
|
<Magnet size={13} />
|
|
@@ -227,7 +227,7 @@ function SkipToggle({ enabled, onToggle, trackLabel, mode }: { enabled: boolean;
|
|
|
227
227
|
onClick={onToggle}
|
|
228
228
|
className={`flex h-3.5 w-3.5 items-center justify-center rounded transition-colors ${
|
|
229
229
|
enabled
|
|
230
|
-
? 'text-[var(--editor-text)]
|
|
230
|
+
? 'text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]'
|
|
231
231
|
: (mode === 'light' ? 'text-amber-600/90 hover:text-amber-700' : 'text-amber-400/80 hover:text-amber-300')
|
|
232
232
|
}`}
|
|
233
233
|
>
|
|
@@ -186,7 +186,7 @@ export default function TrackSettingsPopover({
|
|
|
186
186
|
transform: position?.flipped ? 'translateY(-100%)' : undefined,
|
|
187
187
|
}}
|
|
188
188
|
>
|
|
189
|
-
<p className="text-xs font-semibold text-[var(--editor-text)]
|
|
189
|
+
<p className="text-xs font-semibold text-[color-mix(in_srgb,var(--editor-text)_90%,transparent)]">{title}</p>
|
|
190
190
|
|
|
191
191
|
{showVolume && (
|
|
192
192
|
<VolumeControl
|
|
@@ -211,14 +211,14 @@ export default function TrackSettingsPopover({
|
|
|
211
211
|
|
|
212
212
|
{showMute && (
|
|
213
213
|
<div className="flex items-center justify-between">
|
|
214
|
-
<span className="text-[11px] text-[var(--editor-text)]
|
|
214
|
+
<span className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]">Mute</span>
|
|
215
215
|
<Switch checked={!!muted} onCheckedChange={onMutedChange!} aria-label={muteAriaLabel} mode={mode} />
|
|
216
216
|
</div>
|
|
217
217
|
)}
|
|
218
218
|
|
|
219
219
|
{skip && (
|
|
220
220
|
<div className="flex items-center justify-between">
|
|
221
|
-
<span className="text-[11px] text-[var(--editor-text)]
|
|
221
|
+
<span className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]">Skip</span>
|
|
222
222
|
<Switch checked={skip.skipped} onCheckedChange={() => skip.onToggle()} aria-label={skip.ariaLabel} mode={mode} />
|
|
223
223
|
</div>
|
|
224
224
|
)}
|
|
@@ -69,9 +69,9 @@ export default function VolumeControl({
|
|
|
69
69
|
|
|
70
70
|
return (
|
|
71
71
|
<div className="flex flex-col gap-2">
|
|
72
|
-
<div className={`flex items-center text-[11px] text-[var(--editor-text)]
|
|
72
|
+
<div className={`flex items-center text-[11px] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] ${label ? 'justify-between' : 'justify-end'}`}>
|
|
73
73
|
{label && <span>{label}</span>}
|
|
74
|
-
<span className="font-mono text-[10px] text-[var(--editor-text)]
|
|
74
|
+
<span className="font-mono text-[10px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
|
|
75
75
|
{value.toFixed(2)} ({volumeToDb(value)})
|
|
76
76
|
</span>
|
|
77
77
|
</div>
|
|
@@ -100,7 +100,7 @@ export default function VolumeControl({
|
|
|
100
100
|
className={`rounded-md border px-2 py-0.5 text-[11px] font-mono transition-colors focus:outline-none focus:ring-1 focus:ring-[var(--editor-accent)] ${
|
|
101
101
|
active
|
|
102
102
|
? 'border-[var(--editor-accent)] bg-[var(--editor-accent)] text-[var(--editor-accent-foreground)]'
|
|
103
|
-
: 'border-[var(--editor-border)] bg-[var(--editor-surface)] text-[var(--editor-text)]
|
|
103
|
+
: 'border-[var(--editor-border)] bg-[var(--editor-surface)] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)]'
|
|
104
104
|
}`}
|
|
105
105
|
>
|
|
106
106
|
{Math.round(p * 100)}%
|