@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
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent as ReactMouseEvent } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import { cn, inspectorInputClass, SwatchInput } from '../../ui'
|
|
4
|
-
import { inferOverlayPropFields, type PropField } from './overlay-prop-fields'
|
|
5
|
-
|
|
6
|
-
// Remember the dialog's dragged position for the browser session, so it re-opens
|
|
7
|
-
// where the operator left it instead of always at the default corner. Scoped to
|
|
8
|
-
// sessionStorage (per-tab, survives reloads, clears when the tab closes) and
|
|
9
|
-
// guarded so a non-browser/blocked-storage host degrades to the default.
|
|
10
|
-
const POS_KEY = 'montaj.overlayPropsModal.pos'
|
|
11
|
-
|
|
12
|
-
function loadSavedPos(): { x: number; y: number } | null {
|
|
13
|
-
try {
|
|
14
|
-
const raw = sessionStorage.getItem(POS_KEY)
|
|
15
|
-
if (!raw) return null
|
|
16
|
-
const p = JSON.parse(raw) as { x?: unknown; y?: unknown }
|
|
17
|
-
if (typeof p.x !== 'number' || typeof p.y !== 'number') return null
|
|
18
|
-
// Clamp into the current viewport so a saved spot never lands off-screen
|
|
19
|
-
// (e.g. after the window shrank), keeping the header grabbable.
|
|
20
|
-
return {
|
|
21
|
-
x: Math.min(Math.max(0, p.x), window.innerWidth - 120),
|
|
22
|
-
y: Math.min(Math.max(0, p.y), window.innerHeight - 60),
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function saveSavedPos(p: { x: number; y: number }) {
|
|
30
|
-
try {
|
|
31
|
-
sessionStorage.setItem(POS_KEY, JSON.stringify(p))
|
|
32
|
-
} catch {
|
|
33
|
-
/* storage blocked/unavailable — position just won't persist */
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Props {
|
|
38
|
-
itemProps: Record<string, unknown>
|
|
39
|
-
onSave: (next: Record<string, unknown>) => void
|
|
40
|
-
onClose: () => void
|
|
41
|
-
/** Live-preview every in-progress edit (no history/save) so the overlay
|
|
42
|
-
* updates as the operator tweaks. Commit happens on Save; Cancel reverts. */
|
|
43
|
-
onPreview?: (next: Record<string, unknown>) => void
|
|
44
|
-
/** Resolve a workspace path to a servable URL (for image thumbnails). */
|
|
45
|
-
fileUrl?: (path: string) => string
|
|
46
|
-
/** Upload a picked file, returning its new workspace path (for image "change"). */
|
|
47
|
-
uploadFile?: (file: File) => Promise<string>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Image prop control: a thumbnail preview plus a file picker that uploads the
|
|
51
|
-
// chosen file (via the host adapter) and swaps the prop to the returned path.
|
|
52
|
-
// When no uploadFile is available (non-Montaj host), it degrades to an editable
|
|
53
|
-
// path text field so the value is still reachable.
|
|
54
|
-
function ImageField({
|
|
55
|
-
name,
|
|
56
|
-
value,
|
|
57
|
-
fileUrl,
|
|
58
|
-
uploadFile,
|
|
59
|
-
onChange,
|
|
60
|
-
}: {
|
|
61
|
-
name: string
|
|
62
|
-
value: string
|
|
63
|
-
fileUrl?: (path: string) => string
|
|
64
|
-
uploadFile?: (file: File) => Promise<string>
|
|
65
|
-
onChange: (v: string) => void
|
|
66
|
-
}) {
|
|
67
|
-
const [busy, setBusy] = useState(false)
|
|
68
|
-
const [err, setErr] = useState<string | null>(null)
|
|
69
|
-
const preview = value ? (fileUrl ? fileUrl(value) : value) : ''
|
|
70
|
-
|
|
71
|
-
async function onPick(e: ChangeEvent<HTMLInputElement>) {
|
|
72
|
-
const file = e.target.files?.[0]
|
|
73
|
-
e.target.value = '' // let the same file be re-picked later
|
|
74
|
-
if (!file || !uploadFile) return
|
|
75
|
-
setBusy(true)
|
|
76
|
-
setErr(null)
|
|
77
|
-
try {
|
|
78
|
-
onChange(await uploadFile(file))
|
|
79
|
-
} catch (x) {
|
|
80
|
-
setErr(String(x))
|
|
81
|
-
} finally {
|
|
82
|
-
setBusy(false)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div className="flex items-center gap-3">
|
|
88
|
-
{preview ? (
|
|
89
|
-
<img
|
|
90
|
-
src={preview}
|
|
91
|
-
alt={name}
|
|
92
|
-
className="h-14 w-14 shrink-0 rounded-md border border-[var(--editor-border,#1f2937)] object-cover bg-black/20"
|
|
93
|
-
/>
|
|
94
|
-
) : (
|
|
95
|
-
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-md border border-dashed border-[var(--editor-border,#1f2937)] text-[10px] text-[var(--editor-text)]/40">
|
|
96
|
-
none
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
100
|
-
<span className="truncate font-mono text-xs text-[var(--editor-text)]/70" title={value}>
|
|
101
|
-
{value ? value.split('/').pop() : '—'}
|
|
102
|
-
</span>
|
|
103
|
-
{uploadFile ? (
|
|
104
|
-
<label className="inline-flex w-fit cursor-pointer items-center gap-1.5 rounded-md border border-[var(--editor-border,#1f2937)] px-2.5 py-1 text-xs text-[var(--editor-text)]/80 hover:bg-white/5">
|
|
105
|
-
{busy ? 'Uploading…' : 'Change…'}
|
|
106
|
-
<input
|
|
107
|
-
type="file"
|
|
108
|
-
accept="image/*"
|
|
109
|
-
aria-label={name}
|
|
110
|
-
className="hidden"
|
|
111
|
-
onChange={onPick}
|
|
112
|
-
disabled={busy}
|
|
113
|
-
/>
|
|
114
|
-
</label>
|
|
115
|
-
) : (
|
|
116
|
-
<input
|
|
117
|
-
type="text"
|
|
118
|
-
aria-label={name}
|
|
119
|
-
value={value}
|
|
120
|
-
onChange={e => onChange(e.target.value)}
|
|
121
|
-
className={cn(inspectorInputClass, 'font-mono')}
|
|
122
|
-
/>
|
|
123
|
-
)}
|
|
124
|
-
{err && <span className="text-[11px] text-red-400">{err}</span>}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export default function OverlayPropsModal({ itemProps, onSave, onClose, onPreview, fileUrl, uploadFile }: Props) {
|
|
131
|
-
const [draft, setDraft] = useState<PropField[]>(() => inferOverlayPropFields(itemProps))
|
|
132
|
-
|
|
133
|
-
// Floating, draggable panel (no dimming backdrop) so the overlay stays visible
|
|
134
|
-
// and updates live while editing. `pos` is null until first drag, then a
|
|
135
|
-
// fixed viewport position; the default anchors it top-right, off the centered
|
|
136
|
-
// preview.
|
|
137
|
-
const panelRef = useRef<HTMLDivElement>(null)
|
|
138
|
-
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadSavedPos())
|
|
139
|
-
const [dragging, setDragging] = useState(false)
|
|
140
|
-
const grabRef = useRef({ dx: 0, dy: 0 })
|
|
141
|
-
const posRef = useRef<{ x: number; y: number } | null>(pos)
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
const onKey = (e: KeyboardEvent) => {
|
|
145
|
-
if (e.key === 'Escape') onClose()
|
|
146
|
-
}
|
|
147
|
-
document.addEventListener('keydown', onKey)
|
|
148
|
-
return () => document.removeEventListener('keydown', onKey)
|
|
149
|
-
}, [onClose])
|
|
150
|
-
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if (!dragging) return
|
|
153
|
-
function onMove(e: MouseEvent) {
|
|
154
|
-
const np = { x: e.clientX - grabRef.current.dx, y: e.clientY - grabRef.current.dy }
|
|
155
|
-
posRef.current = np
|
|
156
|
-
setPos(np)
|
|
157
|
-
}
|
|
158
|
-
function onUp() {
|
|
159
|
-
setDragging(false)
|
|
160
|
-
if (posRef.current) saveSavedPos(posRef.current) // persist once, on drop
|
|
161
|
-
}
|
|
162
|
-
window.addEventListener('mousemove', onMove)
|
|
163
|
-
window.addEventListener('mouseup', onUp)
|
|
164
|
-
return () => {
|
|
165
|
-
window.removeEventListener('mousemove', onMove)
|
|
166
|
-
window.removeEventListener('mouseup', onUp)
|
|
167
|
-
}
|
|
168
|
-
}, [dragging])
|
|
169
|
-
|
|
170
|
-
function startDrag(e: ReactMouseEvent) {
|
|
171
|
-
const rect = panelRef.current?.getBoundingClientRect()
|
|
172
|
-
if (!rect) return
|
|
173
|
-
grabRef.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top }
|
|
174
|
-
setPos({ x: rect.left, y: rect.top })
|
|
175
|
-
setDragging(true)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function mergeProps(fields: PropField[]): Record<string, unknown> {
|
|
179
|
-
const next: Record<string, unknown> = { ...itemProps }
|
|
180
|
-
for (const f of fields) next[f.name] = f.value
|
|
181
|
-
return next
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Update one field and immediately preview the merged props (live) so the
|
|
185
|
-
// overlay reflects the edit without waiting for Save.
|
|
186
|
-
function setField(name: string, value: string | number | boolean) {
|
|
187
|
-
const nd = draft.map(f => (f.name === name ? { ...f, value } : f))
|
|
188
|
-
setDraft(nd)
|
|
189
|
-
onPreview?.(mergeProps(nd))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const panelStyle: CSSProperties = {
|
|
193
|
-
position: 'fixed',
|
|
194
|
-
...(pos ? { left: pos.x, top: pos.y } : { top: 16, right: 16 }),
|
|
195
|
-
// The panel portals to <body>, outside the VideoEditor element that carries
|
|
196
|
-
// the --editor-* theme vars, so those are undefined here — give opaque
|
|
197
|
-
// fallbacks so the panel is solid, not see-through.
|
|
198
|
-
background: 'var(--editor-surface, #111827)',
|
|
199
|
-
borderColor: 'var(--editor-border, #1f2937)',
|
|
200
|
-
color: 'var(--editor-text, #f3f4f6)',
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return createPortal(
|
|
204
|
-
<div
|
|
205
|
-
ref={panelRef}
|
|
206
|
-
className="z-50 w-96 max-h-[85vh] border rounded-xl shadow-2xl flex flex-col overflow-hidden"
|
|
207
|
-
style={panelStyle}
|
|
208
|
-
>
|
|
209
|
-
{/* Header — drag handle */}
|
|
210
|
-
<div
|
|
211
|
-
onMouseDown={startDrag}
|
|
212
|
-
className="flex cursor-move select-none items-center justify-between px-5 py-4 border-b border-[var(--editor-border,#1f2937)]"
|
|
213
|
-
>
|
|
214
|
-
<h2 className="text-sm font-semibold">Edit overlay</h2>
|
|
215
|
-
<button
|
|
216
|
-
onMouseDown={e => e.stopPropagation()}
|
|
217
|
-
onClick={onClose}
|
|
218
|
-
aria-label="Close"
|
|
219
|
-
className="opacity-55 hover:opacity-100 transition-opacity text-lg leading-none"
|
|
220
|
-
>
|
|
221
|
-
×
|
|
222
|
-
</button>
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
{/* Fields */}
|
|
226
|
-
<div className="flex flex-col gap-3 px-5 py-4 overflow-y-auto">
|
|
227
|
-
{draft.map(f => (
|
|
228
|
-
<div key={f.name} className="flex flex-col gap-1">
|
|
229
|
-
<span className="text-[11px] uppercase tracking-wide text-[var(--editor-text)]/55">
|
|
230
|
-
{f.name}
|
|
231
|
-
</span>
|
|
232
|
-
{f.kind === 'boolean' ? (
|
|
233
|
-
<input
|
|
234
|
-
type="checkbox"
|
|
235
|
-
aria-label={f.name}
|
|
236
|
-
checked={f.value as boolean}
|
|
237
|
-
onChange={e => setField(f.name, e.target.checked)}
|
|
238
|
-
className="h-4 w-4 accent-[var(--editor-accent)]"
|
|
239
|
-
/>
|
|
240
|
-
) : f.kind === 'number' ? (
|
|
241
|
-
<input
|
|
242
|
-
type="number"
|
|
243
|
-
aria-label={f.name}
|
|
244
|
-
value={f.value as number}
|
|
245
|
-
onChange={e => setField(f.name, e.target.value === '' ? 0 : Number(e.target.value))}
|
|
246
|
-
className={cn(inspectorInputClass, 'text-right')}
|
|
247
|
-
/>
|
|
248
|
-
) : f.kind === 'color' ? (
|
|
249
|
-
<SwatchInput value={String(f.value)} ariaLabel={f.name} onChange={v => setField(f.name, v)} />
|
|
250
|
-
) : f.kind === 'image' ? (
|
|
251
|
-
<ImageField
|
|
252
|
-
name={f.name}
|
|
253
|
-
value={String(f.value)}
|
|
254
|
-
fileUrl={fileUrl}
|
|
255
|
-
uploadFile={uploadFile}
|
|
256
|
-
onChange={v => setField(f.name, v)}
|
|
257
|
-
/>
|
|
258
|
-
) : (
|
|
259
|
-
<textarea
|
|
260
|
-
aria-label={f.name}
|
|
261
|
-
value={f.value as string}
|
|
262
|
-
rows={1}
|
|
263
|
-
onChange={e => setField(f.name, e.target.value)}
|
|
264
|
-
className={cn(inspectorInputClass, 'h-auto resize-y')}
|
|
265
|
-
/>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
))}
|
|
269
|
-
{draft.length === 0 && (
|
|
270
|
-
<p className="text-sm text-[var(--editor-text)]/55">This overlay has no editable props.</p>
|
|
271
|
-
)}
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
{/* Footer */}
|
|
275
|
-
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-[var(--editor-border,#1f2937)]">
|
|
276
|
-
<button
|
|
277
|
-
onClick={onClose}
|
|
278
|
-
className="text-sm px-4 py-1.5 rounded-md border border-[var(--editor-border,#1f2937)] opacity-80 hover:opacity-100 hover:bg-white/5 transition-colors"
|
|
279
|
-
>
|
|
280
|
-
Cancel
|
|
281
|
-
</button>
|
|
282
|
-
<button
|
|
283
|
-
onClick={() => onSave(mergeProps(draft))}
|
|
284
|
-
className="text-sm px-4 py-1.5 rounded-md bg-[var(--editor-accent,#6366f1)] text-[var(--editor-accent-foreground,#ffffff)] hover:opacity-90 transition-colors"
|
|
285
|
-
>
|
|
286
|
-
Save
|
|
287
|
-
</button>
|
|
288
|
-
</div>
|
|
289
|
-
</div>,
|
|
290
|
-
document.body,
|
|
291
|
-
)
|
|
292
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/// <reference types="vitest/globals" />
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
-
import OverlayPropsModal from '../OverlayPropsModal'
|
|
4
|
-
|
|
5
|
-
const props = { homeName: 'Colombia', homeScore: 2, accent: '#FCD116', players: [{ n: 1 }] }
|
|
6
|
-
|
|
7
|
-
test('renders a field per primitive prop and commits merged props', () => {
|
|
8
|
-
const onSave = vi.fn()
|
|
9
|
-
render(<OverlayPropsModal itemProps={props} onSave={onSave} onClose={() => {}} />)
|
|
10
|
-
|
|
11
|
-
const name = screen.getByLabelText('homeName')
|
|
12
|
-
fireEvent.change(name, { target: { value: 'Argentina' } })
|
|
13
|
-
const score = screen.getByLabelText('homeScore')
|
|
14
|
-
fireEvent.change(score, { target: { value: '3' } })
|
|
15
|
-
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
|
16
|
-
|
|
17
|
-
expect(onSave).toHaveBeenCalledWith({
|
|
18
|
-
homeName: 'Argentina',
|
|
19
|
-
homeScore: 3, // number kind round-trips as number
|
|
20
|
-
accent: '#FCD116',
|
|
21
|
-
players: [{ n: 1 }], // non-primitive prop preserved untouched
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('escape closes without saving', () => {
|
|
26
|
-
const onSave = vi.fn()
|
|
27
|
-
const onClose = vi.fn()
|
|
28
|
-
render(<OverlayPropsModal itemProps={props} onSave={onSave} onClose={onClose} />)
|
|
29
|
-
fireEvent.keyDown(document, { key: 'Escape' })
|
|
30
|
-
expect(onClose).toHaveBeenCalled()
|
|
31
|
-
expect(onSave).not.toHaveBeenCalled()
|
|
32
|
-
})
|