@bycrux/editor 0.8.9 → 0.10.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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/carousel/CarouselEditor.tsx +34 -14
  3. package/src/carousel/SlidePropertyPanel.tsx +1 -35
  4. package/src/index.ts +4 -0
  5. package/src/schema.ts +11 -3
  6. package/src/ui/SwatchInput.tsx +57 -0
  7. package/src/ui/index.ts +2 -0
  8. package/src/video/VideoEditor.tsx +107 -14
  9. package/src/video/__tests__/VideoEditor.test.tsx +34 -0
  10. package/src/video/__tests__/playback-clock.test.tsx +45 -0
  11. package/src/video/playback-clock.ts +31 -0
  12. package/src/video/preview/OverlayItemsLayer.tsx +36 -13
  13. package/src/video/preview/OverlayPropsModal.tsx +292 -0
  14. package/src/video/preview/PreviewPlayer.tsx +14 -6
  15. package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +106 -0
  16. package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +32 -0
  17. package/src/video/preview/__tests__/overlay-prop-fields.test.ts +44 -0
  18. package/src/video/preview/overlay-prop-fields.ts +39 -0
  19. package/src/video/preview/useDragOverlay.ts +21 -1
  20. package/src/video/preview/useVideoPlayback.ts +12 -3
  21. package/src/video/timeline/AudioTrackRow.tsx +6 -8
  22. package/src/video/timeline/PlayheadLine.tsx +18 -0
  23. package/src/video/timeline/Scrubber.tsx +7 -5
  24. package/src/video/timeline/Timeline.tsx +64 -29
  25. package/src/video/timeline/TimelineContext.ts +2 -2
  26. package/src/video/timeline/TranscriptPanel.tsx +57 -0
  27. package/src/video/timeline/VisualTrackRow.tsx +21 -14
  28. package/src/video/timeline/__tests__/PlayheadLine.test.tsx +60 -0
  29. package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +85 -0
@@ -0,0 +1,60 @@
1
+ /// <reference types="vitest/globals" />
2
+ import { render, act } from '@testing-library/react'
3
+ import { createPlaybackClock } from '../../playback-clock'
4
+ import { TimelineContext, type TimelineContextValue } from '../TimelineContext'
5
+ import PlayheadLine from '../PlayheadLine'
6
+
7
+ function makeCtx(overrides: Partial<TimelineContextValue> = {}): TimelineContextValue {
8
+ return {
9
+ totalDuration: 10,
10
+ contentDuration: 10,
11
+ snapBoundaries: [],
12
+ zoom: 1,
13
+ zoomRef: { current: 1 },
14
+ scrollRef: { current: null },
15
+ scrubberRef: { current: null },
16
+ overlayDraggedRef: { current: false },
17
+ clock: createPlaybackClock(),
18
+ markers: [null, null],
19
+ setMarkers: () => {},
20
+ selection: null,
21
+ ...overrides,
22
+ }
23
+ }
24
+
25
+ test('PlayheadLine moves on clock.set without re-rendering a sibling row', () => {
26
+ const ctx = makeCtx()
27
+ let siblingRenders = 0
28
+
29
+ function Sibling() {
30
+ siblingRenders++
31
+ return null
32
+ }
33
+ function Root() {
34
+ return (
35
+ <TimelineContext.Provider value={ctx}>
36
+ <PlayheadLine />
37
+ <Sibling />
38
+ </TimelineContext.Provider>
39
+ )
40
+ }
41
+
42
+ const { container } = render(<Root />)
43
+ const before = siblingRenders
44
+
45
+ act(() => { ctx.clock.set(4) })
46
+
47
+ const line = container.querySelector('div') as HTMLDivElement
48
+ expect(line.style.left).toBe('40%')
49
+ expect(siblingRenders).toBe(before)
50
+ })
51
+
52
+ test('renders nothing when totalDuration is 0', () => {
53
+ const ctx = makeCtx({ totalDuration: 0 })
54
+ const { container } = render(
55
+ <TimelineContext.Provider value={ctx}>
56
+ <PlayheadLine />
57
+ </TimelineContext.Provider>,
58
+ )
59
+ expect(container.firstChild).toBeNull()
60
+ })
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react'
3
+ import type { Project } from '../../../types'
4
+ import type { Captions } from '../../../schema'
5
+ import TranscriptPanel from '../TranscriptPanel'
6
+
7
+ afterEach(() => cleanup())
8
+
9
+ function makeProject(style: Captions['style'], extra: Partial<Captions> = {}): Project {
10
+ return { id: 'p1', captions: { style, segments: [], ...extra } } as unknown as Project
11
+ }
12
+
13
+ function renderPanel(style: Captions['style'], extra: Partial<Captions> = {}) {
14
+ const onCaptionEdit = vi.fn()
15
+ const onProjectChange = vi.fn()
16
+ const project = makeProject(style, extra)
17
+ render(
18
+ <TranscriptPanel
19
+ project={project}
20
+ captionTrack={project.captions}
21
+ currentTime={0}
22
+ onCaptionEdit={onCaptionEdit}
23
+ onProjectChange={onProjectChange}
24
+ onExpand={() => {}}
25
+ />,
26
+ )
27
+ return { onCaptionEdit, onProjectChange }
28
+ }
29
+
30
+ describe('TranscriptPanel caption color controls', () => {
31
+ it('previews live on change and commits on blur, writing captions.color', () => {
32
+ const { onCaptionEdit, onProjectChange } = renderPanel('karaoke')
33
+ const input = screen.getByLabelText('Caption text color') as HTMLInputElement
34
+
35
+ fireEvent.change(input, { target: { value: '#76b900' } })
36
+ expect(onProjectChange).toHaveBeenCalledTimes(1)
37
+ expect(onProjectChange.mock.calls[0][0].captions.color).toBe('#76b900')
38
+ expect(onCaptionEdit).not.toHaveBeenCalled() // live preview must not persist
39
+
40
+ fireEvent.blur(input, { target: { value: '#76b900' } })
41
+ expect(onCaptionEdit).toHaveBeenCalledTimes(1)
42
+ expect(onCaptionEdit.mock.calls[0][0].captions.color).toBe('#76b900')
43
+ })
44
+
45
+ it('writes the accent to the field the active style actually reads', () => {
46
+ // karaoke → highlightColor
47
+ let h = renderPanel('karaoke')
48
+ fireEvent.blur(screen.getByLabelText('Caption highlight color'), { target: { value: '#111111' } })
49
+ expect(h.onCaptionEdit.mock.calls[0][0].captions.highlightColor).toBe('#111111')
50
+ cleanup()
51
+
52
+ // pop → activeColor
53
+ h = renderPanel('pop')
54
+ fireEvent.blur(screen.getByLabelText('Caption active color'), { target: { value: '#222222' } })
55
+ expect(h.onCaptionEdit.mock.calls[0][0].captions.activeColor).toBe('#222222')
56
+ cleanup()
57
+
58
+ // subtitle → backgroundColor
59
+ h = renderPanel('subtitle')
60
+ fireEvent.blur(screen.getByLabelText('Caption box color'), { target: { value: '#333333' } })
61
+ expect(h.onCaptionEdit.mock.calls[0][0].captions.backgroundColor).toBe('#333333')
62
+ cleanup()
63
+
64
+ // highlight-box → accentColor
65
+ h = renderPanel('highlight-box')
66
+ fireEvent.blur(screen.getByLabelText('Caption accent color'), { target: { value: '#444444' } })
67
+ expect(h.onCaptionEdit.mock.calls[0][0].captions.accentColor).toBe('#444444')
68
+ })
69
+
70
+ it('hides the accent swatch for styles with no accent', () => {
71
+ renderPanel('clean')
72
+ expect(screen.getByLabelText('Caption text color')).toBeTruthy()
73
+ expect(screen.queryByLabelText(/(accent|highlight|active|box) color/i)).toBeNull()
74
+ cleanup()
75
+
76
+ renderPanel('word-by-word')
77
+ expect(screen.queryByLabelText(/(accent|highlight|active|box) color/i)).toBeNull()
78
+ })
79
+
80
+ it('reflects the stored hex value on the swatch input', () => {
81
+ renderPanel('karaoke', { color: '#abcdef', highlightColor: '#00ff00' })
82
+ expect((screen.getByLabelText('Caption text color') as HTMLInputElement).value).toBe('#abcdef')
83
+ expect((screen.getByLabelText('Caption highlight color') as HTMLInputElement).value).toBe('#00ff00')
84
+ })
85
+ })