@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bycrux/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
"lint": "eslint src"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@bycrux/timeline-core": "^0.1.0",
|
|
20
21
|
"class-variance-authority": "^0.7",
|
|
21
22
|
"clsx": "^2",
|
|
22
23
|
"lucide-react": "^0.400",
|
|
24
|
+
"mp4box": "^0.5.4",
|
|
23
25
|
"tailwind-merge": "^2"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
@@ -1,10 +1,46 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, type ComponentType } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ChevronsLeftRight,
|
|
4
|
+
Clapperboard,
|
|
5
|
+
Command,
|
|
6
|
+
Crop,
|
|
7
|
+
Keyboard,
|
|
8
|
+
LayoutPanelTop,
|
|
9
|
+
Magnet,
|
|
10
|
+
Maximize2,
|
|
11
|
+
MousePointer2,
|
|
12
|
+
MousePointerClick,
|
|
13
|
+
Move,
|
|
14
|
+
MoveHorizontal,
|
|
15
|
+
RotateCw,
|
|
16
|
+
SeparatorVertical,
|
|
17
|
+
SlidersHorizontal,
|
|
18
|
+
Timer,
|
|
19
|
+
X,
|
|
20
|
+
type LucideProps,
|
|
21
|
+
} from 'lucide-react'
|
|
3
22
|
|
|
4
23
|
/** A single control/shortcut row. `keys` renders as <kbd> chips; omit for a pure gesture. */
|
|
5
24
|
export interface ControlEntry {
|
|
6
25
|
keys?: string[]
|
|
7
26
|
label: string
|
|
27
|
+
/** The row's own glyph, drawn in place of its bullet: a toolbar button's
|
|
28
|
+
* actual icon, or a picture of the gesture (four-way arrows for a move,
|
|
29
|
+
* facing chevrons for an edge-trim). It carries the meaning ahead of the
|
|
30
|
+
* sentence — and for a toolbar row it also ties the words to a button
|
|
31
|
+
* that's easy to miss, the crop one especially, since it's a 12px glyph
|
|
32
|
+
* that greys out whenever no clip is selected. */
|
|
33
|
+
icon?: ComponentType<LucideProps>
|
|
34
|
+
/** Where the gesture applies — "Preview", "Timeline". Rendered as a pill on
|
|
35
|
+
* the right, in the same slot the keyboard rows put their key chips.
|
|
36
|
+
*
|
|
37
|
+
* This exists because Preview and Timeline used to be two separate cards,
|
|
38
|
+
* and the heading was the only thing saying which surface a gesture was
|
|
39
|
+
* for. Merging them into one Mouse card would have thrown that away; the
|
|
40
|
+
* pill puts it back per row, where it's easier to read anyway — you no
|
|
41
|
+
* longer have to look up to a heading to find out where "corner-drag to
|
|
42
|
+
* scale" applies. */
|
|
43
|
+
where?: string
|
|
8
44
|
}
|
|
9
45
|
|
|
10
46
|
export interface ControlSection {
|
|
@@ -18,11 +54,47 @@ export interface ControlsInfoModalProps {
|
|
|
18
54
|
onClose: () => void
|
|
19
55
|
}
|
|
20
56
|
|
|
57
|
+
/** Heading → icon. Lives here rather than on `ControlSection` so the content
|
|
58
|
+
* arrays below stay pure data (a heading and some labels) and a host passing
|
|
59
|
+
* its own sections doesn't have to know about lucide. Unknown headings fall
|
|
60
|
+
* back to the neutral slider glyph rather than rendering a hole. */
|
|
61
|
+
const SECTION_ICONS: Record<string, ComponentType<LucideProps>> = {
|
|
62
|
+
Mouse: MousePointer2,
|
|
63
|
+
Canvas: MousePointer2,
|
|
64
|
+
Toolbar: SlidersHorizontal,
|
|
65
|
+
Keyboard: Keyboard,
|
|
66
|
+
// Not used by the two content arrays below any more (Preview and Timeline
|
|
67
|
+
// merged into Mouse), but kept for a host passing sections of its own.
|
|
68
|
+
Preview: Clapperboard,
|
|
69
|
+
Timeline: LayoutPanelTop,
|
|
70
|
+
}
|
|
71
|
+
|
|
21
72
|
/**
|
|
22
73
|
* Read-only "how to drive this editor" modal. Content is passed in per editor
|
|
23
74
|
* (video vs carousel) so this component stays a dumb, themed renderer. Matches
|
|
24
|
-
* the backdrop/Esc/close conventions of the other editor modals (
|
|
75
|
+
* the backdrop/Esc/close conventions of the other editor modals (RenderModal
|
|
25
76
|
* et al.) and uses the shared `--editor-*` theme tokens.
|
|
77
|
+
*
|
|
78
|
+
* The layout is deliberately not a flat list: each section is its own card with
|
|
79
|
+
* an icon and a heading, and each row is a hoverable line with the gesture or
|
|
80
|
+
* shortcut right-aligned. A dense unbroken list of ~20 rows is technically the
|
|
81
|
+
* same information, but nobody scans it — which is what made the old ⓘ feel
|
|
82
|
+
* like fine print rather than help.
|
|
83
|
+
*
|
|
84
|
+
* Sizing: the panel is wide (max-w-4xl) and the cards flow into TWO columns from
|
|
85
|
+
* `sm` up. At one narrow column the video reference ran past the viewport and
|
|
86
|
+
* the Keyboard section — the half people actually come here for — sat below the
|
|
87
|
+
* fold. Two columns put the whole thing on screen at once, which is also why the
|
|
88
|
+
* body text is a full 14px rather than the 12px used elsewhere in the chrome:
|
|
89
|
+
* this is a document you read, not a toolbar you glance at.
|
|
90
|
+
*
|
|
91
|
+
* Sections are cut by INPUT, not by surface: Mouse, Toolbar, Keyboard. "Where
|
|
92
|
+
* does this apply" then lives on the row that needs it (`ControlEntry.where`)
|
|
93
|
+
* instead of forcing a whole card per surface.
|
|
94
|
+
*
|
|
95
|
+
* Accent use is intentionally cheap to theme: everything tinted reads
|
|
96
|
+
* `var(--editor-accent)` directly (section and toolbar icons, row dots, the
|
|
97
|
+
* header wash), so a host theme re-skins the modal without touching this file.
|
|
26
98
|
*/
|
|
27
99
|
export default function ControlsInfoModal({ title, sections, onClose }: ControlsInfoModalProps) {
|
|
28
100
|
useEffect(() => {
|
|
@@ -42,7 +114,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
42
114
|
onClick={onClose}
|
|
43
115
|
>
|
|
44
116
|
<div
|
|
45
|
-
className="relative mx-4 flex max-h-[
|
|
117
|
+
className="relative mx-4 flex max-h-[86vh] w-full max-w-4xl flex-col overflow-hidden rounded-xl border shadow-2xl"
|
|
46
118
|
style={{
|
|
47
119
|
background: 'var(--editor-surface)',
|
|
48
120
|
borderColor: 'var(--editor-border)',
|
|
@@ -51,86 +123,204 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
51
123
|
onClick={(e) => e.stopPropagation()}
|
|
52
124
|
>
|
|
53
125
|
<div
|
|
54
|
-
className="
|
|
126
|
+
className="relative shrink-0 overflow-hidden border-b px-6 py-5"
|
|
55
127
|
style={{ borderColor: 'var(--editor-border)' }}
|
|
56
128
|
>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
aria-
|
|
62
|
-
className="
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
129
|
+
{/* Accent wash. A gradient that fades the accent var straight to
|
|
130
|
+
`transparent` needs no color-mix support, so it renders the same
|
|
131
|
+
everywhere; the opacity keeps it a tint rather than a colour. */}
|
|
132
|
+
<div
|
|
133
|
+
aria-hidden
|
|
134
|
+
className="pointer-events-none absolute inset-0 opacity-[0.13]"
|
|
135
|
+
style={{
|
|
136
|
+
background:
|
|
137
|
+
'linear-gradient(105deg, var(--editor-accent), transparent 62%)',
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
<div className="relative flex items-start justify-between gap-4">
|
|
141
|
+
<div className="flex items-center gap-3">
|
|
142
|
+
<span
|
|
143
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border bg-[var(--editor-text)]/[0.06]"
|
|
144
|
+
style={{ borderColor: 'var(--editor-border)' }}
|
|
145
|
+
>
|
|
146
|
+
<Command size={17} style={{ color: 'var(--editor-accent)' }} />
|
|
147
|
+
</span>
|
|
148
|
+
<div>
|
|
149
|
+
<h2 className="text-lg font-semibold leading-tight tracking-tight">{title}</h2>
|
|
150
|
+
<p className="mt-0.5 text-[13px] opacity-55">
|
|
151
|
+
Everything this editor responds to.
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={onClose}
|
|
158
|
+
aria-label="Close"
|
|
159
|
+
className="-mr-1 -mt-1 shrink-0 cursor-pointer rounded-md p-1.5 opacity-55 transition-all hover:bg-[var(--editor-text)]/10 hover:opacity-100"
|
|
160
|
+
>
|
|
161
|
+
<X size={18} />
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
66
164
|
</div>
|
|
67
165
|
|
|
68
|
-
<div className="
|
|
69
|
-
{sections
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
166
|
+
<div className="overflow-y-auto px-6 py-5">
|
|
167
|
+
{/* CSS columns rather than a grid: the sections are wildly uneven (a
|
|
168
|
+
few gestures vs eight shortcuts) and columns balance the total
|
|
169
|
+
height instead of pairing them row-by-row and leaving a tall
|
|
170
|
+
gap. */}
|
|
171
|
+
<div className="gap-5 sm:columns-2">
|
|
172
|
+
{sections.map((section) => {
|
|
173
|
+
const Icon = SECTION_ICONS[section.heading] ?? SlidersHorizontal
|
|
174
|
+
return (
|
|
175
|
+
<section
|
|
176
|
+
key={section.heading}
|
|
177
|
+
className="mb-5 break-inside-avoid rounded-lg border last:mb-0"
|
|
178
|
+
style={{ borderColor: 'var(--editor-border)', background: 'var(--editor-bg)' }}
|
|
179
|
+
>
|
|
180
|
+
<div
|
|
181
|
+
className="flex items-center gap-2.5 border-b px-3.5 py-3"
|
|
182
|
+
style={{ borderColor: 'var(--editor-border)' }}
|
|
183
|
+
>
|
|
184
|
+
<span
|
|
185
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[var(--editor-text)]/[0.06]"
|
|
186
|
+
>
|
|
187
|
+
<Icon size={14} style={{ color: 'var(--editor-accent)' }} />
|
|
188
|
+
</span>
|
|
189
|
+
<h3 className="text-xs font-semibold uppercase tracking-[0.14em] opacity-70">
|
|
190
|
+
{section.heading}
|
|
191
|
+
</h3>
|
|
192
|
+
</div>
|
|
193
|
+
<ul className="p-2">
|
|
194
|
+
{section.entries.map((entry, i) => (
|
|
195
|
+
<li
|
|
196
|
+
key={i}
|
|
197
|
+
className="flex items-center justify-between gap-3 rounded-md px-2 py-2 text-sm transition-colors hover:bg-[var(--editor-text)]/[0.06]"
|
|
198
|
+
>
|
|
199
|
+
<span className="flex items-start gap-2.5 opacity-90">
|
|
200
|
+
{/* A toolbar row shows that button's real glyph;
|
|
201
|
+
everything else gets a dot. Both occupy the same
|
|
202
|
+
width, so the labels stay on one left edge to scan
|
|
203
|
+
down, and the chip-less gesture rows don't read as
|
|
204
|
+
orphaned text beside the shortcut rows. */}
|
|
205
|
+
{entry.icon ? (
|
|
206
|
+
<entry.icon
|
|
207
|
+
size={14}
|
|
208
|
+
aria-hidden
|
|
209
|
+
className="mt-[1px] shrink-0"
|
|
210
|
+
style={{ color: 'var(--editor-accent)' }}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
<span
|
|
214
|
+
aria-hidden
|
|
215
|
+
className="mt-[7px] h-1 w-1 shrink-0 rounded-full opacity-50"
|
|
216
|
+
style={{ background: 'var(--editor-accent)' }}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
{entry.label}
|
|
220
|
+
</span>
|
|
221
|
+
{entry.where && (
|
|
222
|
+
/* Deliberately NOT a <kbd>: a surface name is not
|
|
223
|
+
something you press. Pill, not keycap. */
|
|
224
|
+
<span
|
|
225
|
+
className="shrink-0 rounded-full border px-1.5 py-[3px] text-[10px] uppercase opacity-55"
|
|
226
|
+
style={{
|
|
227
|
+
borderColor: 'var(--editor-border)',
|
|
228
|
+
background: 'var(--editor-surface)',
|
|
229
|
+
}}
|
|
85
230
|
>
|
|
86
|
-
{
|
|
87
|
-
</
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
231
|
+
{entry.where}
|
|
232
|
+
</span>
|
|
233
|
+
)}
|
|
234
|
+
{entry.keys && entry.keys.length > 0 && (
|
|
235
|
+
<span className="flex shrink-0 items-center gap-1">
|
|
236
|
+
{entry.keys.map((k, j) => (
|
|
237
|
+
<kbd
|
|
238
|
+
key={j}
|
|
239
|
+
className="rounded-md border border-b-2 px-2 py-1 font-mono text-[11px] leading-none shadow-sm"
|
|
240
|
+
style={{
|
|
241
|
+
borderColor: 'var(--editor-border)',
|
|
242
|
+
background: 'var(--editor-surface)',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{k}
|
|
246
|
+
</kbd>
|
|
247
|
+
))}
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
</li>
|
|
251
|
+
))}
|
|
252
|
+
</ul>
|
|
253
|
+
</section>
|
|
254
|
+
)
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div
|
|
260
|
+
className="flex shrink-0 items-center justify-end gap-2 border-t px-6 py-3 text-[13px]"
|
|
261
|
+
style={{ borderColor: 'var(--editor-border)' }}
|
|
262
|
+
>
|
|
263
|
+
<kbd
|
|
264
|
+
className="rounded-md border border-b-2 px-2 py-1 font-mono text-[11px] leading-none shadow-sm"
|
|
265
|
+
style={{ borderColor: 'var(--editor-border)', background: 'var(--editor-bg)' }}
|
|
266
|
+
>
|
|
267
|
+
Esc
|
|
268
|
+
</kbd>
|
|
269
|
+
<span className="opacity-55">to close</span>
|
|
96
270
|
</div>
|
|
97
271
|
</div>
|
|
98
272
|
</div>
|
|
99
273
|
)
|
|
100
274
|
}
|
|
101
275
|
|
|
102
|
-
/** Controls reference for the video/timeline editor. Sourced from
|
|
103
|
-
*
|
|
104
|
-
*
|
|
276
|
+
/** Controls reference for the video/timeline editor. Sourced from the shared
|
|
277
|
+
* keymap (`video/keymap.ts`, mounted by VideoEditor's ReviewSurface and
|
|
278
|
+
* Timeline), the track-controls toolbar, and PreviewPlayer's on-canvas
|
|
279
|
+
* transform (drag / corner-drag / scroll / rotate). */
|
|
105
280
|
export const VIDEO_CONTROLS: ControlSection[] = [
|
|
106
281
|
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
heading: 'Timeline',
|
|
282
|
+
// One card for every mouse gesture, Preview and Timeline alike. They were
|
|
283
|
+
// two cards, which read as two subjects when they are one — "what the
|
|
284
|
+
// mouse does" — and split seven short rows across a column break. The
|
|
285
|
+
// per-row `where` pill carries the surface the heading used to.
|
|
286
|
+
heading: 'Mouse',
|
|
116
287
|
entries: [
|
|
117
|
-
{ label: 'Drag
|
|
118
|
-
{
|
|
119
|
-
{
|
|
288
|
+
{ icon: Move, where: 'Preview', label: 'Drag to move the selected clip or overlay' },
|
|
289
|
+
{ icon: Maximize2, where: 'Preview', label: 'Corner-drag or scroll to scale it' },
|
|
290
|
+
{ icon: RotateCw, where: 'Preview', label: 'Drag the rotate handle to turn an overlay' },
|
|
291
|
+
{ icon: MoveHorizontal, where: 'Timeline', label: 'Drag a clip to reposition it' },
|
|
292
|
+
{ icon: ChevronsLeftRight, where: 'Timeline', label: "Drag a clip's edge to trim it" },
|
|
293
|
+
{ icon: MousePointerClick, where: 'Timeline', label: 'Double-click a clip to inspect it' },
|
|
294
|
+
{ icon: Timer, where: 'Timeline', label: 'Click the time readout to go to a timecode' },
|
|
120
295
|
],
|
|
121
296
|
},
|
|
122
297
|
{
|
|
298
|
+
// Icons here are the very buttons in the track-controls bar, in the order
|
|
299
|
+
// they sit there. Keep the two in step: a row whose glyph no longer matches
|
|
300
|
+
// its button is worse than no glyph at all.
|
|
123
301
|
heading: 'Toolbar',
|
|
124
302
|
entries: [
|
|
125
|
-
{ label: '
|
|
126
|
-
{ label: '
|
|
303
|
+
{ icon: SeparatorVertical, label: 'Preview axis: hover the timeline to preview that frame' },
|
|
304
|
+
{ icon: Magnet, label: 'Ripple: edits close the gap instead of leaving it' },
|
|
305
|
+
{ icon: Crop, label: 'Crop source: non-destructively crop the selected clip (greyed out until you select one)' },
|
|
127
306
|
],
|
|
128
307
|
},
|
|
129
308
|
{
|
|
130
309
|
heading: 'Keyboard',
|
|
131
310
|
entries: [
|
|
132
311
|
{ keys: ['S'], label: 'Split at the playhead' },
|
|
312
|
+
{ keys: ['⌘/Ctrl', 'A'], label: 'Toggle the preview axis' },
|
|
313
|
+
{ keys: ['⇧', 'Delete'], label: 'Ripple-delete the selection' },
|
|
133
314
|
{ keys: ['⌘/Ctrl', 'Z'], label: 'Undo' },
|
|
315
|
+
{ keys: ['⌘/Ctrl', '⇧', 'Z'], label: 'Redo' },
|
|
316
|
+
{ keys: ['⌘/Ctrl', 'C'], label: 'Copy the selection' },
|
|
317
|
+
{ keys: ['⌘/Ctrl', 'V'], label: 'Paste at the playhead' },
|
|
318
|
+
{ keys: ['⌘/Ctrl', 'D'], label: 'Duplicate the selection in place' },
|
|
319
|
+
{ keys: ['⌘/Ctrl', '⌥/Alt', 'V'], label: 'Paste attributes onto the selection' },
|
|
320
|
+
{ keys: ['⌘/Ctrl', 'K'], label: 'Command palette' },
|
|
321
|
+
{ keys: ['J', 'K', 'L'], label: 'Shuttle backward / stop / forward' },
|
|
322
|
+
{ keys: ['←', '→'], label: 'Step one frame (⇧ for ten frames)' },
|
|
323
|
+
{ keys: ['F'], label: 'Toggle fullscreen preview' },
|
|
134
324
|
],
|
|
135
325
|
},
|
|
136
326
|
]
|
|
@@ -141,9 +331,9 @@ export const CAROUSEL_CONTROLS: ControlSection[] = [
|
|
|
141
331
|
{
|
|
142
332
|
heading: 'Canvas',
|
|
143
333
|
entries: [
|
|
144
|
-
{ label: 'Drag an element to reposition it' },
|
|
145
|
-
{ label: 'Resize or rotate it via the handles' },
|
|
146
|
-
{ label: 'Double-click text to edit it' },
|
|
334
|
+
{ icon: Move, label: 'Drag an element to reposition it' },
|
|
335
|
+
{ icon: Maximize2, label: 'Resize or rotate it via the handles' },
|
|
336
|
+
{ icon: MousePointerClick, label: 'Double-click text to edit it' },
|
|
147
337
|
],
|
|
148
338
|
},
|
|
149
339
|
{
|
|
@@ -59,4 +59,47 @@ describe('ControlsInfoModal', () => {
|
|
|
59
59
|
expect(VIDEO_CONTROLS.length).toBeGreaterThan(0)
|
|
60
60
|
expect(CAROUSEL_CONTROLS.length).toBeGreaterThan(0)
|
|
61
61
|
})
|
|
62
|
+
|
|
63
|
+
it('renders an entry\'s `where` as a pill, not as a key chip', () => {
|
|
64
|
+
render(
|
|
65
|
+
<ControlsInfoModal
|
|
66
|
+
title="Editor controls"
|
|
67
|
+
sections={[{ heading: 'Mouse', entries: [{ where: 'Timeline', label: 'Drag a clip' }] }]}
|
|
68
|
+
onClose={vi.fn()}
|
|
69
|
+
/>,
|
|
70
|
+
)
|
|
71
|
+
// A surface name is not something you press, so it must not be a <kbd> —
|
|
72
|
+
// that element is the modal's promise that a row names a keystroke.
|
|
73
|
+
expect(screen.getByText('Timeline').tagName).not.toBe('KBD')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps every mouse gesture in ONE section, tagged by surface', () => {
|
|
77
|
+
// Preview and Timeline were two cards; merging them into "Mouse" moved the
|
|
78
|
+
// surface onto each row. Re-splitting is fine, but silently dropping the
|
|
79
|
+
// `where` tags would lose which surface a gesture applies to entirely.
|
|
80
|
+
const mouse = VIDEO_CONTROLS.filter((s) => s.entries.some((e) => e.where))
|
|
81
|
+
expect(mouse).toHaveLength(1)
|
|
82
|
+
expect(mouse[0].heading).toBe('Mouse')
|
|
83
|
+
expect(mouse[0].entries.every((e) => e.where && e.icon)).toBe(true)
|
|
84
|
+
expect(new Set(mouse[0].entries.map((e) => e.where))).toEqual(new Set(['Preview', 'Timeline']))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('gives every toolbar row the glyph of its button', () => {
|
|
88
|
+
const toolbar = VIDEO_CONTROLS.find((s) => s.heading === 'Toolbar')!
|
|
89
|
+
expect(toolbar.entries.every((e) => e.icon)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('lists the clipboard and fullscreen shortcuts under Keyboard', () => {
|
|
93
|
+
const keyboard = VIDEO_CONTROLS.find((s) => s.heading === 'Keyboard')!
|
|
94
|
+
const labels = keyboard.entries.map((e) => e.label)
|
|
95
|
+
expect(labels).toEqual(
|
|
96
|
+
expect.arrayContaining([
|
|
97
|
+
'Copy the selection',
|
|
98
|
+
'Paste at the playhead',
|
|
99
|
+
'Duplicate the selection in place',
|
|
100
|
+
'Paste attributes onto the selection',
|
|
101
|
+
'Toggle fullscreen preview',
|
|
102
|
+
]),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
62
105
|
})
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
MediaScope,
|
|
8
8
|
} from '../types'
|
|
9
9
|
import type { Project, ImageElement } from '../types'
|
|
10
|
-
import { defaultMontajTheme, applyTheme } from '../theme'
|
|
10
|
+
import { defaultMontajTheme, lightMontajTheme, applyTheme, isLightTheme } from '../theme'
|
|
11
11
|
|
|
12
12
|
// ── (a) A fake adapter literal must satisfy the EditorAdapter interface ──────
|
|
13
13
|
// This test is purely structural: it type-checks at build time. The runtime
|
|
@@ -197,4 +197,62 @@ describe('applyTheme', () => {
|
|
|
197
197
|
defaultMontajTheme.spacing[2],
|
|
198
198
|
)
|
|
199
199
|
})
|
|
200
|
+
|
|
201
|
+
it('writes editor CSS vars onto a div for lightMontajTheme', () => {
|
|
202
|
+
const el = document.createElement('div')
|
|
203
|
+
applyTheme(el, lightMontajTheme)
|
|
204
|
+
|
|
205
|
+
expect(el.style.getPropertyValue('--editor-bg')).toBe(
|
|
206
|
+
lightMontajTheme.colors.background,
|
|
207
|
+
)
|
|
208
|
+
expect(el.style.getPropertyValue('--editor-surface')).toBe(
|
|
209
|
+
lightMontajTheme.colors.surface,
|
|
210
|
+
)
|
|
211
|
+
expect(el.style.getPropertyValue('--editor-accent')).toBe(
|
|
212
|
+
lightMontajTheme.colors.accent,
|
|
213
|
+
)
|
|
214
|
+
expect(el.style.getPropertyValue('--editor-accent-foreground')).toBe(
|
|
215
|
+
lightMontajTheme.colors.accentForeground,
|
|
216
|
+
)
|
|
217
|
+
expect(el.style.getPropertyValue('--editor-text')).toBe(
|
|
218
|
+
lightMontajTheme.colors.text,
|
|
219
|
+
)
|
|
220
|
+
expect(el.style.getPropertyValue('--editor-border')).toBe(
|
|
221
|
+
lightMontajTheme.colors.border,
|
|
222
|
+
)
|
|
223
|
+
expect(el.style.getPropertyValue('--editor-selection')).toBe(
|
|
224
|
+
lightMontajTheme.colors.selection,
|
|
225
|
+
)
|
|
226
|
+
expect(el.style.getPropertyValue('--editor-font-sans')).toBe(
|
|
227
|
+
lightMontajTheme.fonts.sans,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
expect(lightMontajTheme.colors.background).not.toBe(defaultMontajTheme.colors.background)
|
|
231
|
+
expect(lightMontajTheme.colors.text).not.toBe(defaultMontajTheme.colors.text)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// ── (d) isLightTheme classifies a theme by its background's luminance ───────
|
|
236
|
+
describe('isLightTheme', () => {
|
|
237
|
+
it('returns true for lightMontajTheme', () => {
|
|
238
|
+
expect(isLightTheme(lightMontajTheme)).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('returns false for defaultMontajTheme', () => {
|
|
242
|
+
expect(isLightTheme(defaultMontajTheme)).toBe(false)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('falls back to false for an unparseable background color', () => {
|
|
246
|
+
expect(
|
|
247
|
+
isLightTheme({
|
|
248
|
+
...defaultMontajTheme,
|
|
249
|
+
colors: { ...defaultMontajTheme.colors, background: 'not-a-color' },
|
|
250
|
+
}),
|
|
251
|
+
).toBe(false)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('parses 3-digit hex backgrounds', () => {
|
|
255
|
+
expect(isLightTheme({ ...defaultMontajTheme, colors: { ...defaultMontajTheme.colors, background: '#fff' } })).toBe(true)
|
|
256
|
+
expect(isLightTheme({ ...defaultMontajTheme, colors: { ...defaultMontajTheme.colors, background: '#000' } })).toBe(false)
|
|
257
|
+
})
|
|
200
258
|
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { EASING_NAMES } from '@bycrux/timeline-core'
|
|
3
|
+
import type {
|
|
4
|
+
Keyframe as CoreKeyframe,
|
|
5
|
+
KeyframeTrack as CoreKeyframeTrack,
|
|
6
|
+
EasingName as CoreEasingName,
|
|
7
|
+
KeyframeProp as CoreKeyframeProp,
|
|
8
|
+
} from '@bycrux/timeline-core'
|
|
9
|
+
import type {
|
|
10
|
+
Keyframe as EditorKeyframe,
|
|
11
|
+
KeyframeTrack as EditorKeyframeTrack,
|
|
12
|
+
EasingName as EditorEasingName,
|
|
13
|
+
KeyframeProp as EditorKeyframeProp,
|
|
14
|
+
} from '../schema'
|
|
15
|
+
|
|
16
|
+
// schema.ts re-declares Keyframe/KeyframeTrack/EasingName/KeyframeProp
|
|
17
|
+
// instead of importing them — schema.ts is deliberately import-free (see its
|
|
18
|
+
// module header). The duplication is intentional but must stay structurally
|
|
19
|
+
// identical to the canonical definitions in @bycrux/timeline-core
|
|
20
|
+
// (src/curves.js, declared in its index.d.ts), which is where geometryAt/
|
|
21
|
+
// sampleTrack actually interpret these shapes for BOTH the preview and the
|
|
22
|
+
// render. This file is a compile-time tripwire: if either side's shape
|
|
23
|
+
// drifts, one of the assignments below fails to type-check — and thus fails
|
|
24
|
+
// `tsc`/the vitest build — long before it becomes a silent runtime bug.
|
|
25
|
+
//
|
|
26
|
+
// Mirrors montaj_assets/ui/src/lib/types/__tests__/schema-assignability.test.ts,
|
|
27
|
+
// extended to prove MUTUAL assignability (both directions) since neither side
|
|
28
|
+
// here is a superset of the other — they're meant to be the same shape twice.
|
|
29
|
+
function mutuallyAssignable<A, B>(_forward: (a: A) => B, _backward: (b: B) => A) {}
|
|
30
|
+
|
|
31
|
+
describe('editor schema Keyframe types <-> @bycrux/timeline-core Keyframe types', () => {
|
|
32
|
+
it('EasingName is mutually assignable', () => {
|
|
33
|
+
mutuallyAssignable<CoreEasingName, EditorEasingName>((a) => a, (b) => b)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('KeyframeProp is mutually assignable', () => {
|
|
37
|
+
mutuallyAssignable<CoreKeyframeProp, EditorKeyframeProp>((a) => a, (b) => b)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Keyframe is mutually assignable', () => {
|
|
41
|
+
mutuallyAssignable<CoreKeyframe, EditorKeyframe>((a) => a, (b) => b)
|
|
42
|
+
|
|
43
|
+
const sample: EditorKeyframe = { t: 0, value: 1, easing: 'ease-in-out' }
|
|
44
|
+
const asCore: CoreKeyframe = sample
|
|
45
|
+
const asEditor: EditorKeyframe = asCore
|
|
46
|
+
expect(asEditor.t).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('KeyframeTrack is mutually assignable', () => {
|
|
50
|
+
mutuallyAssignable<CoreKeyframeTrack, EditorKeyframeTrack>((a) => a, (b) => b)
|
|
51
|
+
|
|
52
|
+
const sample: EditorKeyframeTrack = { prop: 'scale', points: [{ t: 0, value: 1 }] }
|
|
53
|
+
const asCore: CoreKeyframeTrack = sample
|
|
54
|
+
const asEditor: EditorKeyframeTrack = asCore
|
|
55
|
+
expect(asEditor.prop).toBe('scale')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// The assignability checks above are type-level ONLY, so they fire under
|
|
60
|
+
// `tsc --noEmit` but NOT under `vitest run` — vitest's transform strips types
|
|
61
|
+
// without checking them, and the editor's `npm test` is `vitest run` with no
|
|
62
|
+
// typecheck step. That leaves the likeliest real drift unguarded in the command
|
|
63
|
+
// people actually run: someone adds an easing preset to timeline-core's
|
|
64
|
+
// curves.js and forgets schema.ts's `EasingName` union.
|
|
65
|
+
//
|
|
66
|
+
// This block closes that at runtime. EASING_NAMES is a real exported VALUE, so
|
|
67
|
+
// comparing it against the union's members catches the drift under vitest, and
|
|
68
|
+
// the exhaustiveness map makes the reverse direction (a member added to the
|
|
69
|
+
// union but not to the list below) a compile error.
|
|
70
|
+
describe('EasingName stays in sync with timeline-core at RUNTIME', () => {
|
|
71
|
+
// Adding a member to EditorEasingName without adding it here fails to compile:
|
|
72
|
+
// the Record must cover every member of the union exactly.
|
|
73
|
+
const EXHAUSTIVE: Record<EditorEasingName, true> = {
|
|
74
|
+
linear: true,
|
|
75
|
+
ease: true,
|
|
76
|
+
'ease-in': true,
|
|
77
|
+
'ease-out': true,
|
|
78
|
+
'ease-in-out': true,
|
|
79
|
+
hold: true,
|
|
80
|
+
}
|
|
81
|
+
const DECLARED = Object.keys(EXHAUSTIVE) as EditorEasingName[]
|
|
82
|
+
|
|
83
|
+
it('schema.ts declares exactly the easings timeline-core implements', () => {
|
|
84
|
+
expect([...DECLARED].sort()).toEqual([...EASING_NAMES].sort())
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('every name timeline-core exports is assignable to the editor union', () => {
|
|
88
|
+
for (const name of EASING_NAMES) {
|
|
89
|
+
const asEditor: EditorEasingName = name
|
|
90
|
+
expect(DECLARED).toContain(asEditor)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import type {
|
|
3
|
+
CaptionSegment,
|
|
3
4
|
EditorProject,
|
|
4
5
|
ImageElement,
|
|
5
6
|
OverlayElement,
|
|
6
7
|
Slide,
|
|
7
8
|
} from '../schema'
|
|
9
|
+
import { laneOf } from '../video/captionLanes'
|
|
8
10
|
|
|
9
11
|
// Compile-time assignability helper: if the argument doesn't satisfy T,
|
|
10
12
|
// this fails to type-check (and thus fails `tsc`/build, which vitest runs through).
|
|
@@ -101,4 +103,17 @@ describe('editor schema', () => {
|
|
|
101
103
|
expectAssignable<Slide>(slide)
|
|
102
104
|
expect(slide.elements).toHaveLength(2)
|
|
103
105
|
})
|
|
106
|
+
|
|
107
|
+
it('round-trips CaptionSegment.lane both present and absent', () => {
|
|
108
|
+
const withLane: CaptionSegment = { text: 'hi', start: 0, end: 1, lane: 2 }
|
|
109
|
+
const withoutLane: CaptionSegment = { text: 'hi', start: 0, end: 1 }
|
|
110
|
+
expectAssignable<CaptionSegment>(withLane)
|
|
111
|
+
expectAssignable<CaptionSegment>(withoutLane)
|
|
112
|
+
expect(withLane.lane).toBe(2)
|
|
113
|
+
expect(withoutLane.lane).toBeUndefined()
|
|
114
|
+
// Absent lane reads as lane 0 through the module every consumer goes
|
|
115
|
+
// through — not just `undefined` left for each caller to default itself.
|
|
116
|
+
expect(laneOf(withoutLane)).toBe(0)
|
|
117
|
+
expect(laneOf(withLane)).toBe(2)
|
|
118
|
+
})
|
|
104
119
|
})
|