@bycrux/editor 1.2.2 → 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 (50) hide show
  1. package/package.json +1 -1
  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/state/__tests__/use-project-sync.test.tsx +76 -0
  14. package/src/state/use-project-sync.ts +21 -1
  15. package/src/text/FontPicker.tsx +3 -3
  16. package/src/text/TextFormattingToolbar.tsx +3 -3
  17. package/src/types.ts +16 -0
  18. package/src/ui/Loader.tsx +1 -1
  19. package/src/ui/NumberField.tsx +3 -3
  20. package/src/ui/SwatchInput.tsx +1 -1
  21. package/src/ui/Tooltip.tsx +1 -1
  22. package/src/ui/input.tsx +1 -1
  23. package/src/ui/label.tsx +1 -1
  24. package/src/ui/textarea.tsx +1 -1
  25. package/src/video/AudioPolishModal.tsx +21 -21
  26. package/src/video/CaptionListPanel.test.tsx +8 -7
  27. package/src/video/CaptionListPanel.tsx +17 -17
  28. package/src/video/CaptionRegenModal.tsx +7 -7
  29. package/src/video/CaptionStyleGallery.tsx +1 -1
  30. package/src/video/CommandPalette.tsx +7 -7
  31. package/src/video/ImageToneMenu.tsx +7 -7
  32. package/src/video/OverlayInspector.tsx +7 -7
  33. package/src/video/RenderModal.tsx +52 -52
  34. package/src/video/VersionCompare.tsx +7 -7
  35. package/src/video/VersionPanel.tsx +2 -2
  36. package/src/video/VideoEditor.tsx +43 -23
  37. package/src/video/__tests__/VideoEditor.pendingSurface.test.tsx +233 -0
  38. package/src/video/__tests__/VideoEditor.saveError.test.tsx +105 -0
  39. package/src/video/panels/ClipPropertiesPanel.tsx +4 -4
  40. package/src/video/panels/OverlayContentPanel.tsx +5 -5
  41. package/src/video/preview/OverlayItemsLayer.tsx +7 -2
  42. package/src/video/preview/SocialPreviewMenu.tsx +5 -5
  43. package/src/video/preview/__tests__/resolveOverlayPropPaths.test.ts +21 -0
  44. package/src/video/timeline/EditableSegment.tsx +1 -1
  45. package/src/video/timeline/Scrubber.tsx +2 -2
  46. package/src/video/timeline/SpeedControl.tsx +3 -3
  47. package/src/video/timeline/Timeline.tsx +3 -3
  48. package/src/video/timeline/TrackGutter.tsx +6 -6
  49. package/src/video/timeline/TrackSettingsPopover.tsx +3 -3
  50. package/src/video/timeline/VolumeControl.tsx +3 -3
@@ -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)]/90">Preview for social media</span>
161
- <span className="block text-[10px] text-[var(--editor-text)]/50">What you see may vary depending on your device.</span>
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)]/80 hover:bg-[var(--editor-text)]/5'
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)]/80 hover:bg-[var(--editor-text)]/5'
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)]/15" />
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)]/5 focus:bg-[var(--editor-text)]/10 focus:outline-none focus:ring-1 focus:ring-purple-500/40"
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)]/35 cursor-pointer">
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)]/80 transition-colors"
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)]/70 ${label ? 'justify-between' : 'justify-end'}`}>
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)]/50">{value.toFixed(2)}×</span>
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)]/70 hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)]'
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)]/45 hover:text-[var(--editor-text)]/80 w-5 h-5 flex items-center justify-center rounded hover:bg-[var(--editor-text)]/10 transition-colors"
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)]/45 hover:text-[var(--editor-text)]/80 w-5 h-5 flex items-center justify-center rounded hover:bg-[var(--editor-text)]/10 transition-colors"
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)]/45 hover:text-[var(--editor-text)]/80 px-1.5 h-5 rounded hover:bg-[var(--editor-text)]/10 transition-colors"
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)]/50">
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)]/90 bg-[var(--editor-text)]/10'
148
- : 'text-[var(--editor-text)]/40 hover:text-[var(--editor-text)]/90 hover:bg-[var(--editor-text)]/10'
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)]/35 hover:text-[var(--editor-text)]/80'
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)]/35 hover:text-[var(--editor-text)]/80'
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)]/35 hover:text-[var(--editor-text)]/80'
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)]/90">{title}</p>
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)]/70">Mute</span>
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)]/70">Skip</span>
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)]/70 ${label ? 'justify-between' : 'justify-end'}`}>
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)]/50">
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)]/70 hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)]'
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)}%