@bycrux/editor 0.12.0 → 1.0.1
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 +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +75 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +134 -2
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +586 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +6 -2
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2202 -251
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -2,17 +2,32 @@ import { describe, it, expect } from 'vitest'
|
|
|
2
2
|
import type {
|
|
3
3
|
EditorAdapter,
|
|
4
4
|
RenderEvent,
|
|
5
|
+
RenderExport,
|
|
5
6
|
RenderOptions,
|
|
7
|
+
SampleFrameOptions,
|
|
6
8
|
VersionEntry,
|
|
7
9
|
WaveformChunk,
|
|
10
|
+
PeaksData,
|
|
11
|
+
FilmstripIndex,
|
|
12
|
+
AudioPolishAnalysis,
|
|
8
13
|
} from '../types'
|
|
9
14
|
import type { EditorProject, ImageElement } from '../schema'
|
|
10
15
|
|
|
11
16
|
// ── Video adapter contract ────────────────────────────────────────────────────
|
|
12
|
-
// The video editor adds
|
|
13
|
-
//
|
|
14
|
-
//
|
|
17
|
+
// The video editor adds ELEVEN optional adapter methods. This file pins EIGHT
|
|
18
|
+
// of them:
|
|
19
|
+
// listVersionHistory, restoreVersion, getWaveformChunks, clearOverlayCache,
|
|
20
|
+
// getWaveformPeaks, getFilmstrip, getSampleFrame, analyzeAudioPolish
|
|
21
|
+
// It fails to compile if those are mistyped, and verifies that (a) an adapter
|
|
15
22
|
// implementing them type-checks and (b) one omitting them still type-checks.
|
|
23
|
+
// It also pins the `RenderOptions` shape both `render` and `renderAsync` accept.
|
|
24
|
+
//
|
|
25
|
+
// The remaining three — resolveCaptionTemplate, generateCaptions, reportContext
|
|
26
|
+
// — are NOT pinned here; they are exercised by their own feature tests
|
|
27
|
+
// (CaptionRegenModal, VideoEditor.context, use-report-context). Stated
|
|
28
|
+
// explicitly because this list previously read as an exhaustive enumeration of
|
|
29
|
+
// the optional surface while silently omitting them, which misleads anyone
|
|
30
|
+
// consulting it to learn what a host may implement.
|
|
16
31
|
|
|
17
32
|
const project: EditorProject = {
|
|
18
33
|
version: '1',
|
|
@@ -23,7 +38,7 @@ const project: EditorProject = {
|
|
|
23
38
|
editingPrompt: '',
|
|
24
39
|
settings: { resolution: [1080, 1920] },
|
|
25
40
|
assets: [],
|
|
26
|
-
tracks: [[]],
|
|
41
|
+
tracks: [{ id: 'trk-0', items: [] }],
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
const baseRequired = {
|
|
@@ -41,7 +56,7 @@ const baseRequired = {
|
|
|
41
56
|
fileUrl: (path: string): string => path,
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
// (a) Adapter implementing all
|
|
59
|
+
// (a) Adapter implementing all seven new optional methods.
|
|
45
60
|
function makeVideoAdapter(): EditorAdapter<EditorProject> {
|
|
46
61
|
return {
|
|
47
62
|
...baseRequired,
|
|
@@ -56,6 +71,34 @@ function makeVideoAdapter(): EditorAdapter<EditorProject> {
|
|
|
56
71
|
_chunkDurationS?: number,
|
|
57
72
|
): Promise<WaveformChunk[]> => [{ path: '/wf/0.png', start: 0, end: 15 }],
|
|
58
73
|
clearOverlayCache: (_src?: string): void => {},
|
|
74
|
+
getWaveformPeaks: async ({ samplesPerSecond }): Promise<PeaksData> => ({
|
|
75
|
+
samplesPerSecond,
|
|
76
|
+
start: 0,
|
|
77
|
+
duration: 15,
|
|
78
|
+
peaks: [0, 0],
|
|
79
|
+
}),
|
|
80
|
+
getFilmstrip: async (_args): Promise<FilmstripIndex> => ({
|
|
81
|
+
sheets: [{ path: '/fs/sheet_01.jpg', cols: 10, rows: 1, tiles: [{ t: 0, row: 0, col: 0 }] }],
|
|
82
|
+
interval: 1,
|
|
83
|
+
tileWidth: 160,
|
|
84
|
+
}),
|
|
85
|
+
getSampleFrame: async (
|
|
86
|
+
_projectId: string,
|
|
87
|
+
at: number,
|
|
88
|
+
opts?: SampleFrameOptions,
|
|
89
|
+
): Promise<{ url: string }> => ({ url: `/files?at=${at}&curve=${opts?.sdrCurve ?? ''}` }),
|
|
90
|
+
analyzeAudioPolish: async (args): Promise<AudioPolishAnalysis> => {
|
|
91
|
+
if (args.piece === 'silence' || args.piece === 'fillers') {
|
|
92
|
+
return { piece: args.piece, removals: [{ start: 1, end: 2, text: 'um' }] }
|
|
93
|
+
}
|
|
94
|
+
if (args.piece === 'silence-check') {
|
|
95
|
+
return { piece: 'silence-check', keeps: [[0, 1], [2, 3]] }
|
|
96
|
+
}
|
|
97
|
+
if (args.piece === 'loudness') {
|
|
98
|
+
return { piece: 'loudness', measuredI: -18, measuredTP: -1, measuredLRA: 5, targetI: -14, gainDb: 4 }
|
|
99
|
+
}
|
|
100
|
+
return { piece: 'voice', vocalsPath: '/audio/vocals.wav', url: '/files?path=vocals.wav' }
|
|
101
|
+
},
|
|
59
102
|
}
|
|
60
103
|
}
|
|
61
104
|
|
|
@@ -71,12 +114,62 @@ describe('EditorAdapter video methods', () => {
|
|
|
71
114
|
expect(typeof a.restoreVersion).toBe('function')
|
|
72
115
|
expect(typeof a.getWaveformChunks).toBe('function')
|
|
73
116
|
expect(typeof a.clearOverlayCache).toBe('function')
|
|
117
|
+
expect(typeof a.getWaveformPeaks).toBe('function')
|
|
118
|
+
expect(typeof a.getFilmstrip).toBe('function')
|
|
119
|
+
expect(typeof a.analyzeAudioPolish).toBe('function')
|
|
74
120
|
|
|
75
121
|
const versions = await a.listVersionHistory!('p1')
|
|
76
122
|
expect(versions[0]).toMatchObject({ hash: 'abc', message: 'init' })
|
|
77
123
|
|
|
78
124
|
const chunks = await a.getWaveformChunks!('p1', 't1', 'a.mp3', 15)
|
|
79
125
|
expect(chunks[0]).toMatchObject({ path: '/wf/0.png', start: 0, end: 15 })
|
|
126
|
+
|
|
127
|
+
const peaks = await a.getWaveformPeaks!({
|
|
128
|
+
projectId: 'p1',
|
|
129
|
+
src: 'a.mp4',
|
|
130
|
+
samplesPerSecond: 200,
|
|
131
|
+
start: 0,
|
|
132
|
+
duration: 15,
|
|
133
|
+
})
|
|
134
|
+
expect(peaks).toMatchObject({ samplesPerSecond: 200, start: 0, duration: 15 })
|
|
135
|
+
|
|
136
|
+
const filmstrip = await a.getFilmstrip!({ projectId: 'p1', src: 'a.mp4' })
|
|
137
|
+
expect(filmstrip.sheets[0]).toMatchObject({ path: '/fs/sheet_01.jpg', cols: 10, rows: 1 })
|
|
138
|
+
expect(filmstrip.sheets[0].tiles[0]).toMatchObject({ t: 0, row: 0, col: 0 })
|
|
139
|
+
|
|
140
|
+
// A directly displayable URL: the modal puts it straight into an <img>.
|
|
141
|
+
const sample = await a.getSampleFrame!('p1', 4.5, { sdrCurve: 'vivid1-neutral' })
|
|
142
|
+
expect(sample.url).toBe('/files?at=4.5&curve=vivid1-neutral')
|
|
143
|
+
// Options are optional; the host falls back to its default curve.
|
|
144
|
+
expect((await a.getSampleFrame!('p1', 4.5)).url).toBe('/files?at=4.5&curve=')
|
|
145
|
+
|
|
146
|
+
// Discriminated on `piece`; times are source time, never timeline time.
|
|
147
|
+
const removals = await a.analyzeAudioPolish!({ projectId: 'p1', piece: 'silence', src: 'a.mp4' })
|
|
148
|
+
if (removals.piece === 'silence' || removals.piece === 'fillers') {
|
|
149
|
+
expect(removals.removals[0]).toMatchObject({ start: 1, end: 2 })
|
|
150
|
+
}
|
|
151
|
+
const keeps = await a.analyzeAudioPolish!({ projectId: 'p1', piece: 'silence-check', src: 'a.mp4' })
|
|
152
|
+
if (keeps.piece === 'silence-check') {
|
|
153
|
+
expect(keeps.keeps[0]).toEqual([0, 1])
|
|
154
|
+
}
|
|
155
|
+
const loudness = await a.analyzeAudioPolish!({
|
|
156
|
+
projectId: 'p1',
|
|
157
|
+
piece: 'loudness',
|
|
158
|
+
src: 'a.mp4',
|
|
159
|
+
options: { targetLufs: -14 },
|
|
160
|
+
})
|
|
161
|
+
if (loudness.piece === 'loudness') {
|
|
162
|
+
expect(loudness).toMatchObject({ targetI: -14, gainDb: 4 })
|
|
163
|
+
}
|
|
164
|
+
const voice = await a.analyzeAudioPolish!({
|
|
165
|
+
projectId: 'p1',
|
|
166
|
+
piece: 'voice',
|
|
167
|
+
src: 'a.mp4',
|
|
168
|
+
window: { in: 0, out: 10 },
|
|
169
|
+
})
|
|
170
|
+
if (voice.piece === 'voice') {
|
|
171
|
+
expect(voice.url).toBe('/files?path=vocals.wav')
|
|
172
|
+
}
|
|
80
173
|
})
|
|
81
174
|
|
|
82
175
|
it('an adapter omitting the new optional methods still type-checks', () => {
|
|
@@ -85,5 +178,35 @@ describe('EditorAdapter video methods', () => {
|
|
|
85
178
|
expect(a.restoreVersion).toBeUndefined()
|
|
86
179
|
expect(a.getWaveformChunks).toBeUndefined()
|
|
87
180
|
expect(a.clearOverlayCache).toBeUndefined()
|
|
181
|
+
expect(a.getWaveformPeaks).toBeUndefined()
|
|
182
|
+
expect(a.getFilmstrip).toBeUndefined()
|
|
183
|
+
expect(a.getSampleFrame).toBeUndefined()
|
|
184
|
+
expect(a.analyzeAudioPolish).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ── Render options shape ──────────────────────────────────────────────────────
|
|
189
|
+
// `export` selects the deliverables for an HDR project; `sdrCurve` names the
|
|
190
|
+
// HDR→SDR tone curve (ids live in montaj_assets/luts/looks.json). Both optional,
|
|
191
|
+
// so an empty object and an omitted argument stay valid calls.
|
|
192
|
+
|
|
193
|
+
describe('RenderOptions', () => {
|
|
194
|
+
it('accepts every export mode plus a curve id, and stays fully optional', async () => {
|
|
195
|
+
const modes: RenderExport[] = ['auto', 'sdr', 'both']
|
|
196
|
+
const opts: RenderOptions[] = [
|
|
197
|
+
{},
|
|
198
|
+
{ scale: 2 },
|
|
199
|
+
...modes.map(m => ({ export: m, sdrCurve: 'vivid1' })),
|
|
200
|
+
]
|
|
201
|
+
expect(opts.map(o => o.export)).toEqual([undefined, undefined, 'auto', 'sdr', 'both'])
|
|
202
|
+
|
|
203
|
+
// Both render entry points take the same options object.
|
|
204
|
+
const a = makeVideoAdapter()
|
|
205
|
+
for await (const ev of a.render('p1', { export: 'both', sdrCurve: 'vivid1-neutral' })) {
|
|
206
|
+
expect(ev.type).toBe('done')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sampleOpts: SampleFrameOptions = { sdrCurve: 'vivid1' }
|
|
210
|
+
expect(sampleOpts.sdrCurve).toBe('vivid1')
|
|
88
211
|
})
|
|
89
212
|
})
|
|
@@ -8,9 +8,14 @@ interface Props {
|
|
|
8
8
|
selectedSlideId: string | null
|
|
9
9
|
adapter: EditorAdapter<Project>
|
|
10
10
|
onAddElement: (slideId: string, element: CarouselElement) => void
|
|
11
|
+
/** Editor theme mode — light/dark. Threaded to `OverlayPicker`, and used
|
|
12
|
+
* locally for the upload/text/generate error hues (red-400 is sub-AA on
|
|
13
|
+
* the light side panel). Absent -> dark, matching every existing
|
|
14
|
+
* caller. */
|
|
15
|
+
mode?: 'light' | 'dark'
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
export default function AddElementMenu({ project, selectedSlideId, adapter, onAddElement }: Props) {
|
|
18
|
+
export default function AddElementMenu({ project, selectedSlideId, adapter, onAddElement, mode = 'dark' }: Props) {
|
|
14
19
|
const [showPrompt, setShowPrompt] = useState(false)
|
|
15
20
|
const [prompt, setPrompt] = useState('')
|
|
16
21
|
const [generating, setGenerating] = useState(false)
|
|
@@ -171,8 +176,8 @@ export default function AddElementMenu({ project, selectedSlideId, adapter, onAd
|
|
|
171
176
|
+ Overlay
|
|
172
177
|
</Button>
|
|
173
178
|
</div>
|
|
174
|
-
{uploadError && <div className=
|
|
175
|
-
{textError && <div className=
|
|
179
|
+
{uploadError && <div className={`text-xs ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{uploadError}</div>}
|
|
180
|
+
{textError && <div className={`text-xs ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{textError}</div>}
|
|
176
181
|
|
|
177
182
|
{showPrompt && !disabled && (
|
|
178
183
|
<div className="flex flex-col gap-2 p-3 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded-lg">
|
|
@@ -185,7 +190,7 @@ export default function AddElementMenu({ project, selectedSlideId, adapter, onAd
|
|
|
185
190
|
disabled={generating}
|
|
186
191
|
onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleGenerate() }}
|
|
187
192
|
/>
|
|
188
|
-
{genError && <div className=
|
|
193
|
+
{genError && <div className={`text-xs ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{genError}</div>}
|
|
189
194
|
<div className="flex gap-2">
|
|
190
195
|
<Button size="sm" onClick={handleGenerate} disabled={generating || !prompt.trim()} className="text-xs">
|
|
191
196
|
{generating ? 'Generating…' : 'Generate'}
|
|
@@ -205,6 +210,7 @@ export default function AddElementMenu({ project, selectedSlideId, adapter, onAd
|
|
|
205
210
|
onPick={element => {
|
|
206
211
|
if (selectedSlideId) onAddElement(selectedSlideId, element)
|
|
207
212
|
}}
|
|
213
|
+
mode={mode}
|
|
208
214
|
/>
|
|
209
215
|
</div>
|
|
210
216
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { RefreshCw, AlertCircle, Download, Info, Undo2, Redo2 } from 'lucide-react'
|
|
3
3
|
import type { Project, Slide, CarouselElement, ImageElement, CarouselEditorProps, OverlayFactory } from '../types'
|
|
4
|
-
import { applyTheme, defaultMontajTheme } from '../theme'
|
|
4
|
+
import { applyTheme, defaultMontajTheme, isLightTheme } from '../theme'
|
|
5
5
|
import { useProjectState } from '../state/use-project-state'
|
|
6
6
|
import SlideCanvas from './SlideCanvas'
|
|
7
7
|
import SlidePropertyPanel from './SlidePropertyPanel'
|
|
@@ -180,6 +180,14 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
180
180
|
if (containerRef.current) applyTheme(containerRef.current, theme ?? defaultMontajTheme)
|
|
181
181
|
}, [theme])
|
|
182
182
|
|
|
183
|
+
// Classified once here, off the same theme object VideoEditor's
|
|
184
|
+
// `timelineMode` uses — see that file's comment for why (three independent
|
|
185
|
+
// consumers resolving light/dark separately is three chances to disagree).
|
|
186
|
+
const mode = useMemo<'light' | 'dark'>(
|
|
187
|
+
() => (isLightTheme(theme ?? defaultMontajTheme) ? 'light' : 'dark'),
|
|
188
|
+
[theme],
|
|
189
|
+
)
|
|
190
|
+
|
|
183
191
|
// ── Keyboard shortcuts: undo / redo. Guarded against text inputs. ──
|
|
184
192
|
useEffect(() => {
|
|
185
193
|
const onKey = (e: KeyboardEvent) => {
|
|
@@ -415,7 +423,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
415
423
|
disabled={refreshing}
|
|
416
424
|
className={`flex items-center gap-2 px-3 py-2 rounded-md border transition-colors ${
|
|
417
425
|
refreshState === 'err'
|
|
418
|
-
? 'text-red-300 border-red-500/40 bg-red-950/60 hover:bg-red-900/70'
|
|
426
|
+
? (mode === 'light' ? 'text-red-700 border-red-300 bg-red-50 hover:bg-red-100' : 'text-red-300 border-red-500/40 bg-red-950/60 hover:bg-red-900/70')
|
|
419
427
|
: 'text-[var(--editor-text)] border-[var(--editor-border)] bg-[var(--editor-surface)]/80 hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)] hover:bg-[var(--editor-surface)]'
|
|
420
428
|
}`}
|
|
421
429
|
title={refreshState === 'err' ? 'Refresh failed — check connection' : 'Refresh project'}
|
|
@@ -476,9 +484,29 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
476
484
|
)}
|
|
477
485
|
{!slots?.pendingStatus && skillPath && (
|
|
478
486
|
<div className="w-full rounded-xl border-2 border-[var(--editor-accent)] bg-[var(--editor-surface)] p-5 flex flex-col gap-3 text-left shadow-lg shadow-[var(--editor-accent)]/10">
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
487
|
+
{/* indigo-600 in light mode: the accent (indigo-500) is ~4.06:1 on
|
|
488
|
+
`--editor-surface`, and 12px bold is still NORMAL text under WCAG
|
|
489
|
+
(large starts at 18.66px bold), so it needs the 4.5:1 floor. The
|
|
490
|
+
border/shadow around it stay accent - this is small-TEXT only. */}
|
|
491
|
+
<p className={`text-xs font-bold uppercase tracking-widest ${mode === 'light' ? 'text-indigo-600' : 'text-[var(--editor-accent)]'}`}>Send this to your agent</p>
|
|
492
|
+
{/* Deliberately hardcoded dark chrome, not `--editor-*` tokens: this
|
|
493
|
+
is the literal text the user copies and pastes to their coding
|
|
494
|
+
agent, styled as terminal/code chrome — same precedent as the
|
|
495
|
+
video preview's black canvas, which also stays dark regardless
|
|
496
|
+
of editor theme.
|
|
497
|
+
|
|
498
|
+
This was `bg-black/60`, which composited against whatever sat
|
|
499
|
+
behind it: near-black over the dark surface, but only ~#666
|
|
500
|
+
over a light one — ~3:1 for 12px copyable text, below AA.
|
|
501
|
+
It is now an OPAQUE colour so the box no longer depends on
|
|
502
|
+
its backdrop. The specific value is not arbitrary: #070a10 is
|
|
503
|
+
exactly what `rgba(0,0,0,0.6)` over the dark theme's
|
|
504
|
+
`--editor-surface` (#111827) composited to, so dark mode is
|
|
505
|
+
pixel-identical to before while light mode is fixed. Text is
|
|
506
|
+
pinned to #f3f4f6 (= the dark theme's `--editor-text`), so it
|
|
507
|
+
is likewise unchanged in dark mode and ~18:1 in both. */}
|
|
508
|
+
<div className="flex items-start justify-between bg-[#070a10] border border-transparent rounded-lg px-3 py-3 font-mono gap-3">
|
|
509
|
+
<span className="text-gray-100 text-[12px] leading-relaxed break-all">
|
|
482
510
|
There is a new project pending: "{project.name ?? project.id}". Please see @{skillPath} and start. Talk to me if you run into questions.
|
|
483
511
|
</span>
|
|
484
512
|
<button
|
|
@@ -490,7 +518,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
490
518
|
setTimeout(() => setCopied(false), 2000)
|
|
491
519
|
}}
|
|
492
520
|
className={`shrink-0 flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md transition-colors ${
|
|
493
|
-
copied ? 'bg-green-700 text-green-200' : 'bg-white/10 text-
|
|
521
|
+
copied ? 'bg-green-700 text-green-200' : 'bg-white/10 text-gray-100 hover:bg-white/20 hover:text-gray-100'
|
|
494
522
|
}`}
|
|
495
523
|
title="Copy prompt"
|
|
496
524
|
>
|
|
@@ -503,7 +531,14 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
503
531
|
</div>
|
|
504
532
|
) : selectedSlide ? (
|
|
505
533
|
<>
|
|
506
|
-
|
|
534
|
+
{/* Edge definition for the slide canvas, which sits on `--editor-bg`.
|
|
535
|
+
Was a fixed `rgba(255,255,255,0.08)`, which read as a hairline
|
|
536
|
+
highlight on the dark ground but composited to under 1/255 on the
|
|
537
|
+
light one — the edge silently vanished. Keyed to `--editor-text`
|
|
538
|
+
instead, the same "tint with the foreground colour" idiom the rest
|
|
539
|
+
of the chrome uses, so it is a light ring on dark and a dark ring
|
|
540
|
+
on light at the same subtle strength. */}
|
|
541
|
+
<div className="flex-shrink-0 ring-1 ring-[var(--editor-text)]/10">
|
|
507
542
|
<SlideCanvas
|
|
508
543
|
slide={selectedSlide}
|
|
509
544
|
slideId={selectedSlide.id}
|
|
@@ -566,6 +601,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
566
601
|
selectedSlideId={selectedSlideId}
|
|
567
602
|
adapter={adapter}
|
|
568
603
|
onAddElement={handleAddElement}
|
|
604
|
+
mode={mode}
|
|
569
605
|
/>
|
|
570
606
|
</div>
|
|
571
607
|
)}
|
|
@@ -587,6 +623,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
587
623
|
onToggleElementVisibility={onToggleElementVisibility}
|
|
588
624
|
// Fills the right column (drop the default w-80 width + left border).
|
|
589
625
|
className="w-full border-l-0"
|
|
626
|
+
mode={mode}
|
|
590
627
|
/>
|
|
591
628
|
</div>
|
|
592
629
|
</div>
|
|
@@ -616,6 +653,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
616
653
|
exportActions={slots?.exportActions}
|
|
617
654
|
onClose={() => setRenderOpen(false)}
|
|
618
655
|
onCancel={() => setRenderOpen(false)}
|
|
656
|
+
mode={mode}
|
|
619
657
|
/>
|
|
620
658
|
)}
|
|
621
659
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import type { EditorAdapter, Project, RenderStatus } from '../types'
|
|
4
|
+
import { Loader } from '../ui/Loader'
|
|
4
5
|
|
|
5
6
|
interface CarouselRenderModalProps {
|
|
6
7
|
projectId: string
|
|
@@ -19,6 +20,10 @@ interface CarouselRenderModalProps {
|
|
|
19
20
|
* in the done-state info panel. The package no longer hardcodes host URLs.
|
|
20
21
|
*/
|
|
21
22
|
exportActions?: ReactNode
|
|
23
|
+
/** Editor theme mode — light/dark. The panel and log box follow
|
|
24
|
+
* `--editor-surface`/`--editor-bg`, so log/status hues need to darken in
|
|
25
|
+
* light mode. Absent -> dark, matching every existing caller. */
|
|
26
|
+
mode?: 'light' | 'dark'
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/** Promoted render output (R2-presigned), as carried by `RenderStatus.media`. */
|
|
@@ -57,12 +62,12 @@ function slidesFromMedia(media: RenderMedia[]): SlideView[] {
|
|
|
57
62
|
.map(({ m }) => ({ url: m.url, filename: m.filename }))
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
function LogLine({ text }: { text: string }) {
|
|
65
|
+
function LogLine({ text, mode = 'dark' }: { text: string; mode?: 'light' | 'dark' }) {
|
|
61
66
|
const t = text.replace(/^\[render\]\s*/, '')
|
|
62
67
|
let color = 'text-[var(--editor-text)]/60'
|
|
63
|
-
if (/done|complete|→/i.test(t)) color = 'text-green-400'
|
|
64
|
-
else if (/rendering|launching|bundling/i.test(t)) color = 'text-sky-400'
|
|
65
|
-
else if (/error|fail/i.test(t)) color = 'text-red-400'
|
|
68
|
+
if (/done|complete|→/i.test(t)) color = mode === 'light' ? 'text-green-700' : 'text-green-400'
|
|
69
|
+
else if (/rendering|launching|bundling/i.test(t)) color = mode === 'light' ? 'text-sky-700' : 'text-sky-400'
|
|
70
|
+
else if (/error|fail/i.test(t)) color = mode === 'light' ? 'text-red-600' : 'text-red-400'
|
|
66
71
|
|
|
67
72
|
const prefix = text.startsWith('[render]')
|
|
68
73
|
? <span className="text-[var(--editor-text)]/40">[render] </span>
|
|
@@ -75,7 +80,7 @@ function LogLine({ text }: { text: string }) {
|
|
|
75
80
|
)
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
export default function CarouselRenderModal({ projectId, adapter, slidesCount, resolution, onClose, onCancel, exportActions }: CarouselRenderModalProps) {
|
|
83
|
+
export default function CarouselRenderModal({ projectId, adapter, slidesCount, resolution, onClose, onCancel, exportActions, mode = 'dark' }: CarouselRenderModalProps) {
|
|
79
84
|
const [logs, setLogs] = useState<string[]>([])
|
|
80
85
|
const [status, setStatus] = useState<'running' | 'done' | 'error'>('running')
|
|
81
86
|
const [media, setMedia] = useState<RenderMedia[] | null>(null)
|
|
@@ -328,12 +333,12 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
328
333
|
<div className="px-5 py-8 flex flex-col items-center justify-center gap-4 min-h-[12rem]">
|
|
329
334
|
{status === 'running' ? (
|
|
330
335
|
<>
|
|
331
|
-
<
|
|
336
|
+
<Loader size="lg" />
|
|
332
337
|
<p className="text-sm text-[var(--editor-text)]/70">Rendering slides…</p>
|
|
333
338
|
<p className="text-xs text-[var(--editor-text)]/50">This can take a moment.</p>
|
|
334
339
|
</>
|
|
335
340
|
) : (
|
|
336
|
-
<p className=
|
|
341
|
+
<p className={`text-sm whitespace-pre-wrap break-words text-center ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>
|
|
337
342
|
{errorMsg ?? 'Render failed.'}
|
|
338
343
|
</p>
|
|
339
344
|
)}
|
|
@@ -355,10 +360,10 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
355
360
|
<span className="text-[var(--editor-text)]/40 italic">Starting render engine…</span>
|
|
356
361
|
)}
|
|
357
362
|
{logs.map((line, i) => (
|
|
358
|
-
<LogLine key={i} text={line} />
|
|
363
|
+
<LogLine key={i} text={line} mode={mode} />
|
|
359
364
|
))}
|
|
360
365
|
{status === 'error' && errorMsg && (
|
|
361
|
-
<span className=
|
|
366
|
+
<span className={`mt-1 ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{errorMsg}</span>
|
|
362
367
|
)}
|
|
363
368
|
</div>
|
|
364
369
|
</div>
|
|
@@ -368,7 +373,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
368
373
|
{status === 'running' ? (
|
|
369
374
|
<button
|
|
370
375
|
onClick={handleCancel}
|
|
371
|
-
className=
|
|
376
|
+
className={`text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)] 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'}`}
|
|
372
377
|
>
|
|
373
378
|
Cancel
|
|
374
379
|
</button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import type { Project, OverlayElement, GlobalOverlay, EditorAdapter } from '../types'
|
|
3
|
+
import { Loader } from '../ui/Loader'
|
|
3
4
|
|
|
4
5
|
// TODO (v2): live thumbnail rendering for each overlay card
|
|
5
6
|
// TODO (v2): respect overlay staticFrame when rendering thumbnails — requires API extension
|
|
@@ -10,9 +11,13 @@ interface Props {
|
|
|
10
11
|
project: Project
|
|
11
12
|
adapter: EditorAdapter<Project>
|
|
12
13
|
onPick: (element: OverlayElement) => void
|
|
14
|
+
/** Editor theme mode — light/dark. Only affects the load-error text
|
|
15
|
+
* (red-400 is sub-AA on a light `--editor-surface`). Absent -> dark,
|
|
16
|
+
* matching every existing caller. */
|
|
17
|
+
mode?: 'light' | 'dark'
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
export default function OverlayPicker({ open, onClose, project, adapter, onPick }: Props) {
|
|
20
|
+
export default function OverlayPicker({ open, onClose, project, adapter, onPick, mode = 'dark' }: Props) {
|
|
16
21
|
const [overlays, setOverlays] = useState<GlobalOverlay[]>([])
|
|
17
22
|
const [loaded, setLoaded] = useState(false)
|
|
18
23
|
const [loading, setLoading] = useState(false)
|
|
@@ -111,10 +116,12 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick
|
|
|
111
116
|
|
|
112
117
|
<div className="flex-1 overflow-y-auto p-4">
|
|
113
118
|
{loading && (
|
|
114
|
-
<div className="
|
|
119
|
+
<div className="flex justify-center py-8">
|
|
120
|
+
<Loader size="md" label="Loading overlays" />
|
|
121
|
+
</div>
|
|
115
122
|
)}
|
|
116
123
|
{error && (
|
|
117
|
-
<div className=
|
|
124
|
+
<div className={`text-center text-sm py-8 ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{error}</div>
|
|
118
125
|
)}
|
|
119
126
|
{!loading && !error && overlays.length === 0 && (
|
|
120
127
|
<div className="text-center text-[var(--editor-text)]/60 text-sm py-8">No overlays available</div>
|
|
@@ -9,14 +9,10 @@ import type {
|
|
|
9
9
|
GlobalOverlayProp,
|
|
10
10
|
EditorAdapter,
|
|
11
11
|
} from '../types'
|
|
12
|
-
import { Button, cn, inspectorInputClass, SwatchInput } from '../ui'
|
|
12
|
+
import { Button, cn, inspectorInputClass, NumberField, stepValue, SwatchInput } from '../ui'
|
|
13
|
+
import { Loader } from '../ui/Loader'
|
|
13
14
|
import { TextFormattingToolbar } from '../text/TextFormattingToolbar'
|
|
14
15
|
|
|
15
|
-
function parseNumber(v: string): number | null {
|
|
16
|
-
const n = Number(v)
|
|
17
|
-
return Number.isFinite(n) ? n : null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
// Shared small muted label that sits just above an inspector control.
|
|
21
17
|
const fieldLabelClass = 'text-[11px] uppercase tracking-wide text-[var(--editor-text)]/55'
|
|
22
18
|
|
|
@@ -50,6 +46,10 @@ interface Props {
|
|
|
50
46
|
// full-width (e.g. below the canvas) pass this to drop the default `w-80`
|
|
51
47
|
// sidebar constraint.
|
|
52
48
|
className?: string
|
|
49
|
+
/** Editor theme mode — light/dark. Threaded to `TextFormattingToolbar`'s
|
|
50
|
+
* delete-icon hover hue. Absent -> dark, matching every existing
|
|
51
|
+
* caller. */
|
|
52
|
+
mode?: 'light' | 'dark'
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Small eye toggle to hide/show the selected element in the editor preview only
|
|
@@ -78,6 +78,26 @@ function HideToggle({
|
|
|
78
78
|
)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// `label` doubles as NumberField's accessible `name` — there is no separate
|
|
82
|
+
// visible `<label htmlFor>` wired to the box, just the wrapping `<label>`
|
|
83
|
+
// element below (which already associates the two natively), so reusing the
|
|
84
|
+
// same string keeps the accessible name identical to what a screen reader
|
|
85
|
+
// announced before this converted from a bare `<input>`.
|
|
86
|
+
//
|
|
87
|
+
// This box has no distinct preview/commit split today — every valid keystroke
|
|
88
|
+
// already writes straight through the caller's `onChange`, which is a
|
|
89
|
+
// COMMITTED mutator (see CarouselEditor's `handlePanelElementChange`: it goes
|
|
90
|
+
// through `state.resizeElement(...).then(() => state.commit())`, an async
|
|
91
|
+
// round trip). `onPreview` reproduces that keystroke-writes-through behaviour
|
|
92
|
+
// exactly. `onCommit` is deliberately a no-op rather than the same `onChange`:
|
|
93
|
+
// the old bare `<input>` had no `onBlur` handler at all, so blur committed
|
|
94
|
+
// nothing extra, and because the mutator above is async, `value` cannot be
|
|
95
|
+
// trusted to have caught up with the last keystroke by blur time — wiring
|
|
96
|
+
// `onCommit` to `onChange` would risk firing a second, fully redundant
|
|
97
|
+
// resize/commit (and undo entry) for the same edit. What DOES change here is
|
|
98
|
+
// that typing no longer gets stomped mid-edit by a re-render carrying a
|
|
99
|
+
// stale value back down while the async write is in flight (NumberField's own
|
|
100
|
+
// `draft`, see its doc comment, is the fix).
|
|
81
101
|
function numInput(
|
|
82
102
|
label: string,
|
|
83
103
|
value: number,
|
|
@@ -87,17 +107,15 @@ function numInput(
|
|
|
87
107
|
return (
|
|
88
108
|
<label className="flex flex-col gap-1">
|
|
89
109
|
<span className={fieldLabelClass}>{label}</span>
|
|
90
|
-
<
|
|
91
|
-
|
|
110
|
+
<NumberField
|
|
111
|
+
name={label}
|
|
92
112
|
value={value}
|
|
113
|
+
onPreview={onChange}
|
|
114
|
+
onCommit={() => {}}
|
|
93
115
|
min={opts?.min}
|
|
94
116
|
max={opts?.max}
|
|
95
117
|
step={opts?.step ?? 1}
|
|
96
|
-
|
|
97
|
-
const parsed = parseNumber(e.target.value)
|
|
98
|
-
if (parsed !== null) onChange(parsed)
|
|
99
|
-
}}
|
|
100
|
-
className={cn(inspectorInputClass, 'text-right')}
|
|
118
|
+
onStep={d => onChange(stepValue(value, d, { step: opts?.step ?? 1, min: opts?.min, max: opts?.max }))}
|
|
101
119
|
/>
|
|
102
120
|
</label>
|
|
103
121
|
)
|
|
@@ -145,15 +163,17 @@ function PropEditor({
|
|
|
145
163
|
return (
|
|
146
164
|
<label className="flex flex-col gap-1" title={description}>
|
|
147
165
|
<span className={fieldLabelClass}>{name}</span>
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
{/* No distinct preview/commit split here either — see `numInput`'s
|
|
167
|
+
comment above for why `onCommit` is a no-op rather than `onChange`
|
|
168
|
+
again: `onChange` already writes through per keystroke via an
|
|
169
|
+
async committed mutator, and blur previously fired nothing. */}
|
|
170
|
+
<NumberField
|
|
171
|
+
name={name}
|
|
150
172
|
value={Number(value ?? 0)}
|
|
151
173
|
step={type === 'float' ? 0.1 : 1}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}}
|
|
156
|
-
className={cn(inspectorInputClass, 'text-right')}
|
|
174
|
+
onPreview={onChange}
|
|
175
|
+
onCommit={() => {}}
|
|
176
|
+
onStep={d => onChange(stepValue(Number(value ?? 0), d, { step: type === 'float' ? 0.1 : 1 }))}
|
|
157
177
|
/>
|
|
158
178
|
</label>
|
|
159
179
|
)
|
|
@@ -190,6 +210,7 @@ export default function SlidePropertyPanel({
|
|
|
190
210
|
hiddenElementIds,
|
|
191
211
|
onToggleElementVisibility,
|
|
192
212
|
className,
|
|
213
|
+
mode = 'dark',
|
|
193
214
|
}: Props) {
|
|
194
215
|
// Map of jsxPath → GlobalOverlay for overlay prop schemas
|
|
195
216
|
const [overlaySchemas, setOverlaySchemas] = useState<Map<string, GlobalOverlay>>(new Map())
|
|
@@ -337,6 +358,7 @@ export default function SlidePropertyPanel({
|
|
|
337
358
|
slideId={slide.id}
|
|
338
359
|
element={overlayEl}
|
|
339
360
|
updateOverlayProp={updateOverlayProp}
|
|
361
|
+
mode={mode}
|
|
340
362
|
/>
|
|
341
363
|
)}
|
|
342
364
|
|
|
@@ -345,7 +367,7 @@ export default function SlidePropertyPanel({
|
|
|
345
367
|
</div>
|
|
346
368
|
|
|
347
369
|
{schemasLoading && (
|
|
348
|
-
<
|
|
370
|
+
<Loader size="sm" label="Loading overlay props" />
|
|
349
371
|
)}
|
|
350
372
|
|
|
351
373
|
{!schemasLoading && overlaySchema && overlaySchema.props.length > 0 && (
|