@dailies-studio/shot-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-B7notge7.css +1 -0
- package/dist/assets/index-BBE6Ta9L.js +52 -0
- package/dist/index.html +13 -0
- package/package.json +45 -0
- package/src/api.ts +289 -0
- package/src/app/App.tsx +394 -0
- package/src/audio/index.tsx +74 -0
- package/src/audio/preview.ts +364 -0
- package/src/bridge.ts +45 -0
- package/src/bridgeTypes.ts +582 -0
- package/src/channels.ts +40 -0
- package/src/main.tsx +15 -0
- package/src/panels/ResizeHandle.tsx +113 -0
- package/src/panels/agent/AgentPanel.tsx +478 -0
- package/src/panels/agent/agent.css +322 -0
- package/src/panels/export/ExportDialog.tsx +284 -0
- package/src/panels/files/FilePanel.tsx +564 -0
- package/src/panels/files/McpSection.tsx +66 -0
- package/src/panels/files/icons.tsx +118 -0
- package/src/panels/header/DocSwitcher.tsx +85 -0
- package/src/panels/header/Header.tsx +132 -0
- package/src/panels/header/HistoryControls.tsx +20 -0
- package/src/panels/header/StatusBadges.tsx +49 -0
- package/src/panels/header/TransportBar.tsx +86 -0
- package/src/panels/header/ZoomControls.tsx +38 -0
- package/src/panels/help/HelpSection.tsx +190 -0
- package/src/panels/inspector/FramingRows.tsx +69 -0
- package/src/panels/inspector/InspectorPanel.tsx +178 -0
- package/src/panels/inspector/MarkerSection.tsx +63 -0
- package/src/panels/inspector/ParamRow.tsx +71 -0
- package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
- package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
- package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
- package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
- package/src/panels/inspector/animate/ShotSection.tsx +126 -0
- package/src/panels/inspector/animate/useBlocks.ts +43 -0
- package/src/panels/inspector/design/AlignRow.tsx +63 -0
- package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
- package/src/panels/inspector/design/ClonerSection.tsx +142 -0
- package/src/panels/inspector/design/ComponentSection.tsx +94 -0
- package/src/panels/inspector/design/CornerSection.tsx +52 -0
- package/src/panels/inspector/design/FillSection.tsx +89 -0
- package/src/panels/inspector/design/HtmlSection.tsx +50 -0
- package/src/panels/inspector/design/InstanceSection.tsx +302 -0
- package/src/panels/inspector/design/MediaSection.tsx +251 -0
- package/src/panels/inspector/design/MeshSection.tsx +172 -0
- package/src/panels/inspector/design/ShadowSection.tsx +73 -0
- package/src/panels/inspector/design/StrokeSection.tsx +85 -0
- package/src/panels/inspector/design/TransformSection.tsx +91 -0
- package/src/panels/inspector/design/TypographySection.tsx +170 -0
- package/src/panels/inspector/scene/CameraSection.tsx +59 -0
- package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
- package/src/panels/inspector/scene/SceneSection.tsx +71 -0
- package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
- package/src/panels/inspector/useDocWrite.ts +268 -0
- package/src/panels/material/MaterialPicker.tsx +172 -0
- package/src/panels/stage/StageMenu.tsx +38 -0
- package/src/panels/stage/StagePanel.tsx +131 -0
- package/src/panels/timeline/AudioLane.tsx +340 -0
- package/src/panels/timeline/BlockRow.tsx +242 -0
- package/src/panels/timeline/ChannelRow.tsx +266 -0
- package/src/panels/timeline/LifespanRow.tsx +165 -0
- package/src/panels/timeline/MarkerLane.tsx +146 -0
- package/src/panels/timeline/SceneStrip.tsx +145 -0
- package/src/panels/timeline/TimelineDock.tsx +488 -0
- package/src/panels/timeline/model.ts +641 -0
- package/src/panels/timeline/snap.ts +191 -0
- package/src/panels/toolbar/AddMenu.tsx +230 -0
- package/src/panels/toolbar/CameraViewBar.tsx +70 -0
- package/src/panels/toolbar/ShadingBar.tsx +42 -0
- package/src/panels/toolbar/ToolDockBar.tsx +110 -0
- package/src/panels/toolbar/ViewModeBar.tsx +49 -0
- package/src/shotTokens.css +131 -0
- package/src/stage/FrustumGizmo.tsx +184 -0
- package/src/stage/GridPlane.tsx +274 -0
- package/src/stage/MoveGizmo.tsx +411 -0
- package/src/stage/PerfHud.tsx +206 -0
- package/src/stage/SplitRenderer.tsx +511 -0
- package/src/stage/StageApp.tsx +3423 -0
- package/src/stage/StageTools.tsx +581 -0
- package/src/stage/bridgeClient.ts +278 -0
- package/src/stage/feed.ts +243 -0
- package/src/stage/frustumGeometry.ts +172 -0
- package/src/stage/gizmoMath.ts +494 -0
- package/src/stage/handles.ts +151 -0
- package/src/stage/instanceModules.ts +152 -0
- package/src/stage/interactions/move.ts +172 -0
- package/src/stage/interactions/panZoom.ts +60 -0
- package/src/stage/isolation.ts +155 -0
- package/src/stage/main.tsx +110 -0
- package/src/stage/modalState.ts +427 -0
- package/src/stage/overlays/Overlays.tsx +1046 -0
- package/src/stage/panes.ts +121 -0
- package/src/stage/penState.ts +112 -0
- package/src/stage/project.ts +402 -0
- package/src/stage/shading.ts +55 -0
- package/src/stage/tokenColor.ts +26 -0
- package/src/stage/view.ts +182 -0
- package/src/stage/workingView.ts +636 -0
- package/src/state/agentActions.ts +203 -0
- package/src/state/clientId.ts +25 -0
- package/src/state/context.tsx +33 -0
- package/src/state/insert.ts +507 -0
- package/src/state/keymap.ts +560 -0
- package/src/state/menu.ts +141 -0
- package/src/state/panelSize.ts +26 -0
- package/src/state/renderEvents.ts +100 -0
- package/src/state/store.ts +4094 -0
- package/src/state/triangles.ts +70 -0
- package/src/state/types.ts +384 -0
- package/src/styles.css +2948 -0
- package/src/tools.ts +181 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The marker strip: one diamond per marker, at its MASTER frame.
|
|
2
|
+
//
|
|
3
|
+
// ── WHY THE STRIP SITS ABOVE THE AUDIO AND BELOW THE TRACKS ─────────────
|
|
4
|
+
//
|
|
5
|
+
// A marker is a note about the WHOLE video — `MarkerRef.frame` is a master
|
|
6
|
+
// frame and nothing scene-local can address one — so it cannot live inside a
|
|
7
|
+
// scene group. It sits directly over the audio lane because the marks are
|
|
8
|
+
// almost always beats in that audio, and reading a diamond against the waveform
|
|
9
|
+
// under it is the entire reason the strip is drawn rather than listed.
|
|
10
|
+
//
|
|
11
|
+
// ── ANCHORING: THE MARKS FOLLOW THE MUSIC ───────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// A beat marker that stays put when the music moves is the most-filed complaint
|
|
14
|
+
// against Premiere's sequence markers, and it is why Final Cut made clip
|
|
15
|
+
// ownership a fiat. The behaviour lives in `writeAudioDrag`, not here: a body
|
|
16
|
+
// drag on an audio clip carries every marker inside the clip's window, in the
|
|
17
|
+
// same scrub capture, so it is one undo entry and Escape restores the clip and
|
|
18
|
+
// its marks together.
|
|
19
|
+
//
|
|
20
|
+
// It is DERIVED CONTAINMENT and not a stored `marker.clipId`, and that choice is
|
|
21
|
+
// argued in model.ts's header. The short version: a stored owner needs an answer
|
|
22
|
+
// for clip-deleted, clip-trimmed-past-the-marker, two-clips-overlapping and
|
|
23
|
+
// marker-dragged-out, and each answer is a rule the author cannot see. What is
|
|
24
|
+
// on screen is the rule: the marks under the bar move with the bar.
|
|
25
|
+
//
|
|
26
|
+
// So this lane's own drag is the simple one. It moves ONE marker to a frame,
|
|
27
|
+
// through the dock's shared scrub and the shared `snap.ts` — there is no second
|
|
28
|
+
// snapper here either.
|
|
29
|
+
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
30
|
+
import { frameToPx, startDrag } from "@dailies-studio/timeline";
|
|
31
|
+
import type { Rational } from "@dailies-studio/engine";
|
|
32
|
+
import type { MarkerRef } from "@dailies-studio/shot/doc";
|
|
33
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
34
|
+
import { frameDelta, timecode } from "./model.js";
|
|
35
|
+
import { isSnapSuppressed } from "./snap.js";
|
|
36
|
+
import type { SnapPress } from "./snap.js";
|
|
37
|
+
|
|
38
|
+
export function MarkerLane(props: { ppf: number; labelWidth: number; beginSnap?: SnapPress }) {
|
|
39
|
+
const { ppf, labelWidth, beginSnap } = props;
|
|
40
|
+
const store = useStore();
|
|
41
|
+
const doc = useShotEditor((s) => s.doc);
|
|
42
|
+
const selected = useShotEditor((s) => s.selectedMarker);
|
|
43
|
+
if (doc === null) return null;
|
|
44
|
+
const markers = doc.markers ?? [];
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="marker-lane" data-testid="marker-lane">
|
|
48
|
+
<div className="marker-label-cell" style={{ width: labelWidth }}>
|
|
49
|
+
<span className="marker-lane-name">Markers</span>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className="marker-add"
|
|
53
|
+
title="Add a marker at the playhead (M)"
|
|
54
|
+
aria-label="Add a marker at the playhead"
|
|
55
|
+
onClick={() => store.addMarkerAtPlayhead()}
|
|
56
|
+
>
|
|
57
|
+
+
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="marker-track">
|
|
61
|
+
{markers.length === 0 ? (
|
|
62
|
+
<span className="marker-empty">Press M to mark this frame</span>
|
|
63
|
+
) : (
|
|
64
|
+
markers.map((marker) => (
|
|
65
|
+
<MarkerPin
|
|
66
|
+
key={marker.id}
|
|
67
|
+
marker={marker}
|
|
68
|
+
selected={marker.id === selected}
|
|
69
|
+
ppf={ppf}
|
|
70
|
+
fps={doc.fps}
|
|
71
|
+
beginSnap={beginSnap}
|
|
72
|
+
/>
|
|
73
|
+
))
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function MarkerPin(props: {
|
|
81
|
+
marker: MarkerRef;
|
|
82
|
+
selected: boolean;
|
|
83
|
+
ppf: number;
|
|
84
|
+
fps: Rational;
|
|
85
|
+
beginSnap: SnapPress | undefined;
|
|
86
|
+
}) {
|
|
87
|
+
const { marker, selected, ppf, fps, beginSnap } = props;
|
|
88
|
+
const store = useStore();
|
|
89
|
+
// Subscribed to the BOOLEAN, not the frame: useSyncExternalStore only
|
|
90
|
+
// notifies when the selected value changes, so a pin re-renders exactly
|
|
91
|
+
// twice per playhead pass-through instead of once per playback frame — and
|
|
92
|
+
// the lane above it never re-renders at all.
|
|
93
|
+
const atPlayhead = useShotEditor((s) => s.frame === marker.frame);
|
|
94
|
+
|
|
95
|
+
const drag = (e: ReactPointerEvent<HTMLElement>): void => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
// Selecting on the PRESS and not on a click: the press is what opens the
|
|
99
|
+
// inspector on the thing about to move, and a drag never fires a click.
|
|
100
|
+
store.selectMarker(marker.id);
|
|
101
|
+
const startX = e.clientX;
|
|
102
|
+
// Its OWN frame is excluded, or the diamond sits on its own snap target and
|
|
103
|
+
// the drag never leaves the ground.
|
|
104
|
+
const snap = beginSnap?.({ markerId: marker.id }, isSnapSuppressed(e));
|
|
105
|
+
store.beginBlockDrag();
|
|
106
|
+
startDrag(e, {
|
|
107
|
+
onMove(ev) {
|
|
108
|
+
// Measured from the PRESS, never accumulated: every capture re-derives
|
|
109
|
+
// from the scrub's base, so a coalesced move or a snap that bites and
|
|
110
|
+
// un-bites cannot make the mark drift.
|
|
111
|
+
const raw = marker.frame + frameDelta(ev.clientX - startX, ppf);
|
|
112
|
+
store.dragMarker(marker.id, snap === undefined ? raw : snap(raw));
|
|
113
|
+
},
|
|
114
|
+
onEnd: (commit) => store.endBlockDrag(commit),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const label = marker.label === undefined || marker.label === "" ? "Marker" : marker.label;
|
|
119
|
+
return (
|
|
120
|
+
<span
|
|
121
|
+
className={`marker-pin${selected ? " is-selected" : ""}${atPlayhead ? " is-at-playhead" : ""}`}
|
|
122
|
+
data-marker={marker.id}
|
|
123
|
+
style={{ left: frameToPx(marker.frame, ppf) }}
|
|
124
|
+
role="button"
|
|
125
|
+
tabIndex={-1}
|
|
126
|
+
aria-label={`${label} at ${timecode(marker.frame, fps)}`}
|
|
127
|
+
aria-pressed={selected}
|
|
128
|
+
title={`${label} · ${timecode(marker.frame, fps)} · drag to move it, ⌘drag to ignore snapping`}
|
|
129
|
+
onPointerDown={drag}
|
|
130
|
+
>
|
|
131
|
+
{/* The diamond is a rotated square rather than an SVG: it is 8px of
|
|
132
|
+
decoration on a lane that may hold a hundred of them, and a rotation
|
|
133
|
+
on a span costs no node the browser has to lay out separately. Its own
|
|
134
|
+
`color` overrides the lane's default, which is the only thing
|
|
135
|
+
`MarkerRef.color` is for. */}
|
|
136
|
+
<span
|
|
137
|
+
className="marker-diamond"
|
|
138
|
+
style={marker.color === undefined ? undefined : { background: marker.color }}
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
/>
|
|
141
|
+
{/* Hover (and selection) only, in CSS. Always-on labels turn a strip of
|
|
142
|
+
beats into a wall of overlapping text at any useful zoom. */}
|
|
143
|
+
<span className="marker-pin-label">{label}</span>
|
|
144
|
+
</span>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The SCENE LANE: scenes back to back over the master strip.
|
|
2
|
+
//
|
|
3
|
+
// Slim and secondary, and the ONLY home for the three verbs that are about
|
|
4
|
+
// scenes rather than about time — create, reorder, trim. The tracks below show
|
|
5
|
+
// every scene at once now, so this lane no longer decides what is on screen; it
|
|
6
|
+
// is the handle on the document's coarsest structure, sitting directly above the
|
|
7
|
+
// boundaries it draws.
|
|
8
|
+
//
|
|
9
|
+
// A bar's WIDTH is `scene.durationFrames` and its POSITION is a prefix sum over
|
|
10
|
+
// `doc.scenes` — derived, never stored. So trimming a bar writes one field and
|
|
11
|
+
// every bar after it moves for free, and reordering is a splice. Storing
|
|
12
|
+
// per-scene offsets would desync on both, and the desync only shows up in an
|
|
13
|
+
// export.
|
|
14
|
+
import { frameToPx, startDrag } from "@dailies-studio/timeline";
|
|
15
|
+
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
16
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
17
|
+
import { frameDelta, sceneBars } from "./model.js";
|
|
18
|
+
|
|
19
|
+
/** Its own component so the per-frame tick re-renders one absolutely
|
|
20
|
+
* positioned div, not every scene bar above it. */
|
|
21
|
+
function ScenePlayhead({ ppf }: { ppf: number }) {
|
|
22
|
+
const frame = useShotEditor((s) => s.frame);
|
|
23
|
+
return <div className="scene-playhead" style={{ left: frameToPx(frame, ppf) }} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SceneStrip({ ppf, labelWidth }: { ppf: number; labelWidth: number }) {
|
|
27
|
+
const store = useStore();
|
|
28
|
+
const doc = useShotEditor((s) => s.doc);
|
|
29
|
+
const sceneId = useShotEditor((s) => s.sceneId);
|
|
30
|
+
if (doc === null) return null;
|
|
31
|
+
const bars = sceneBars(doc);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="scene-strip" data-testid="scene-strip">
|
|
35
|
+
<div className="scene-strip-label" style={{ width: labelWidth }}>
|
|
36
|
+
<span>Shots</span>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="scene-strip-add"
|
|
40
|
+
aria-label="Add shot"
|
|
41
|
+
title="Add a shot at the end, and go to it"
|
|
42
|
+
onClick={() => store.addScene()}
|
|
43
|
+
>
|
|
44
|
+
+
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="scene-strip-lane">
|
|
48
|
+
{bars.map((bar, index) => (
|
|
49
|
+
<div
|
|
50
|
+
key={bar.id}
|
|
51
|
+
className={`scene-bar${bar.id === sceneId ? " is-current" : ""}`}
|
|
52
|
+
style={{ left: frameToPx(bar.start, ppf), width: Math.max(8, frameToPx(bar.end - bar.start, ppf)) }}
|
|
53
|
+
data-scene={bar.id}
|
|
54
|
+
draggable
|
|
55
|
+
onDragStart={(e) => e.dataTransfer.setData("text/plain", bar.id)}
|
|
56
|
+
onDragOver={(e) => e.preventDefault()}
|
|
57
|
+
onDrop={(e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
const from = e.dataTransfer.getData("text/plain");
|
|
60
|
+
if (from.length > 0 && from !== bar.id) store.reorderScene(from, index);
|
|
61
|
+
}}
|
|
62
|
+
onClick={() => {
|
|
63
|
+
// Clicking a scene both SELECTS it and moves the playhead into
|
|
64
|
+
// it: the micro lane follows the playhead, so selecting without
|
|
65
|
+
// seeking would show one scene's tracks under another's ruler.
|
|
66
|
+
// The frame is read AT THE CLICK, not subscribed: a handler
|
|
67
|
+
// needs the current value once, and the subscription was what
|
|
68
|
+
// re-rendered every bar on every playback frame.
|
|
69
|
+
store.setScene(bar.id);
|
|
70
|
+
const frame = store.getState().frame;
|
|
71
|
+
if (frame < bar.start || frame >= bar.end) store.seek(bar.start);
|
|
72
|
+
}}
|
|
73
|
+
// Zoom the dock to this shot. A COMPOUND on purpose: the single
|
|
74
|
+
// click above fires first on both clicks, so the shot is selected
|
|
75
|
+
// and the playhead is inside it before the lane reframes around
|
|
76
|
+
// it. `\` (fit) is the way back out.
|
|
77
|
+
onDoubleClick={() => store.fitDock(bar.id)}
|
|
78
|
+
title="Double-click to zoom the timeline to this shot"
|
|
79
|
+
>
|
|
80
|
+
<span className="scene-bar-name">{bar.name}</span>
|
|
81
|
+
<SceneTrimHandle
|
|
82
|
+
onDrag={(deltaPx) => store.trimScene(bar.id, bar.end - bar.start + frameDelta(deltaPx, ppf))}
|
|
83
|
+
onStart={() => store.beginBlockDrag()}
|
|
84
|
+
onEnd={(commit) => store.endBlockDrag(commit)}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
{/* The boundaries themselves. A diamond per cut, filled when the shot
|
|
89
|
+
before it blends into the one after — strip geometry rather than bar
|
|
90
|
+
geometry, because a transition is about the SEAM and the bars stay
|
|
91
|
+
scene-local. Nothing after the last bar: there is no seam there. */}
|
|
92
|
+
{bars.slice(0, -1).map((bar) => (
|
|
93
|
+
<button
|
|
94
|
+
key={`cut-${bar.id}`}
|
|
95
|
+
type="button"
|
|
96
|
+
className={`scene-cut${doc.scenesById[bar.id]?.transition !== undefined ? " is-blend" : ""}`}
|
|
97
|
+
style={{ left: frameToPx(bar.end, ppf) }}
|
|
98
|
+
aria-label={`Transition after ${bar.name}`}
|
|
99
|
+
title={
|
|
100
|
+
doc.scenesById[bar.id]?.transition === undefined
|
|
101
|
+
? `Cut after ${bar.name}`
|
|
102
|
+
: `${doc.scenesById[bar.id]!.transition!.kind} after ${bar.name}`
|
|
103
|
+
}
|
|
104
|
+
onClick={(e) => {
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
// Select the OUTGOING shot: that is where the transition is
|
|
107
|
+
// stored and where the inspector edits it. The playhead goes to
|
|
108
|
+
// the last frame it owns, so the stage shows the frame the blend
|
|
109
|
+
// starts from.
|
|
110
|
+
store.setScene(bar.id);
|
|
111
|
+
store.seek(Math.max(bar.start, bar.end - 1));
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
{/* One playhead across the whole master timeline, so the macro lane
|
|
116
|
+
always shows where the export is. */}
|
|
117
|
+
<ScenePlayhead ppf={ppf} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function SceneTrimHandle({
|
|
124
|
+
onStart,
|
|
125
|
+
onDrag,
|
|
126
|
+
onEnd,
|
|
127
|
+
}: {
|
|
128
|
+
onStart(): void;
|
|
129
|
+
onDrag(deltaPx: number): void;
|
|
130
|
+
onEnd(commit: boolean): void;
|
|
131
|
+
}) {
|
|
132
|
+
const onPointerDown = (e: ReactPointerEvent<HTMLSpanElement>): void => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
const startX = e.clientX;
|
|
136
|
+
onStart();
|
|
137
|
+
// Absolute from the PRESS, never cumulative — the same rule every drag in
|
|
138
|
+
// this lane follows, and the reason a coalesced move cannot make it drift.
|
|
139
|
+
startDrag(e, {
|
|
140
|
+
onMove: (ev) => onDrag(ev.clientX - startX),
|
|
141
|
+
onEnd: (commit) => onEnd(commit),
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
return <span className="scene-trim" data-handle="scene-out" onPointerDown={onPointerDown} aria-hidden="true" />;
|
|
145
|
+
}
|