@elah/editor 0.2.1 → 0.3.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.
@@ -0,0 +1,178 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
+ import { useTimelineEngine, useSelectionStore, usePlaybackStore, computeContainViewport, } from '@elah/core';
4
+ import { useResolvedScene } from '../useResolvedScene';
5
+ const DEFAULT_SCALE = 0.5;
6
+ const DEFAULT_CENTER = 0.5;
7
+ const MIN_BOX_PX = 28;
8
+ const MIN_SCALE = 0.05;
9
+ const MAX_SCALE = 4;
10
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
11
+ const clamp01 = (v) => clamp(v, 0, 1);
12
+ function baseTransform(t) {
13
+ return {
14
+ x: t?.x ?? DEFAULT_CENTER,
15
+ y: t?.y ?? DEFAULT_CENTER,
16
+ scale: t?.scale ?? DEFAULT_SCALE,
17
+ rotation: t?.rotation ?? 0,
18
+ anchor: t?.anchor ?? { x: 0.5, y: 0.5 },
19
+ };
20
+ }
21
+ export function ShapeOverlay() {
22
+ const engine = useTimelineEngine();
23
+ const scene = useResolvedScene();
24
+ const isPlaying = usePlaybackStore((s) => s.isPlaying);
25
+ const selectedClipIds = useSelectionStore((s) => s.selectedClipIds);
26
+ const selectClip = useSelectionStore((s) => s.selectClip);
27
+ const clearSelection = useSelectionStore((s) => s.clearSelection);
28
+ const rootRef = useRef(null);
29
+ const [size, setSize] = useState({ width: 0, height: 0 });
30
+ const gestureRef = useRef(null);
31
+ useLayoutEffect(() => {
32
+ const el = rootRef.current;
33
+ if (!el)
34
+ return;
35
+ const apply = () => setSize({ width: el.clientWidth, height: el.clientHeight });
36
+ apply();
37
+ const obs = new ResizeObserver(apply);
38
+ obs.observe(el);
39
+ return () => obs.disconnect();
40
+ }, []);
41
+ const stage = scene.stage;
42
+ const fit = useMemo(() => computeContainViewport(size.width, size.height, stage.width, stage.height), [size.width, size.height, stage.width, stage.height]);
43
+ const scale = stage.width > 0 ? fit.width / stage.width : 1;
44
+ const items = useMemo(() => {
45
+ if (fit.width <= 0)
46
+ return [];
47
+ const shortSide = Math.min(stage.width, stage.height);
48
+ return scene.shapes.map((clip) => {
49
+ const t = baseTransform(clip.transform);
50
+ const sideStage = t.scale * shortSide;
51
+ const cxStage = t.x * stage.width;
52
+ const cyStage = t.y * stage.height;
53
+ const centerScreenX = fit.x + cxStage * scale;
54
+ const centerScreenY = fit.y + cyStage * scale;
55
+ const rawSide = sideStage * scale;
56
+ const w = Math.max(rawSide, MIN_BOX_PX);
57
+ const h = Math.max(rawSide, MIN_BOX_PX);
58
+ const left = centerScreenX - w / 2;
59
+ const top = centerScreenY - h / 2;
60
+ return {
61
+ clip,
62
+ rect: { left, top, width: w, height: h },
63
+ centerScreenX,
64
+ centerScreenY,
65
+ };
66
+ });
67
+ }, [scene.shapes, stage.width, stage.height, fit, scale]);
68
+ const ownsSelection = useMemo(() => items.some((it) => selectedClipIds.has(it.clip.id)), [items, selectedClipIds]);
69
+ const handlePointerMove = useCallback((e) => {
70
+ const g = gestureRef.current;
71
+ if (!g)
72
+ return;
73
+ if (g.type === 'move') {
74
+ const dx = (e.clientX - g.startClientX) / scale;
75
+ const dy = (e.clientY - g.startClientY) / scale;
76
+ const x = clamp01(g.base.x + dx / g.stageW);
77
+ const y = clamp01(g.base.y + dy / g.stageH);
78
+ engine.previewClip(g.id, g.trackId, { transform: { ...g.base, x, y } });
79
+ }
80
+ else {
81
+ const dist = Math.hypot(e.clientX - g.centerClientX, e.clientY - g.centerClientY);
82
+ const ratio = g.startDist > 0 ? dist / g.startDist : 1;
83
+ const next = clamp(g.base.scale * ratio, MIN_SCALE, MAX_SCALE);
84
+ engine.previewClip(g.id, g.trackId, { transform: { ...g.base, scale: next } });
85
+ }
86
+ }, [engine, scale]);
87
+ const endGesture = useCallback((e) => {
88
+ const g = gestureRef.current;
89
+ if (!g)
90
+ return;
91
+ gestureRef.current = null;
92
+ const target = e.currentTarget;
93
+ if (target.hasPointerCapture?.(e.pointerId)) {
94
+ target.releasePointerCapture(e.pointerId);
95
+ }
96
+ engine.commitInteraction(g.type === 'move' ? 'Move shape' : 'Resize shape');
97
+ }, [engine]);
98
+ const beginMove = useCallback((e, item) => {
99
+ e.stopPropagation();
100
+ if (!selectedClipIds.has(item.clip.id))
101
+ selectClip(item.clip.id);
102
+ e.currentTarget.setPointerCapture(e.pointerId);
103
+ gestureRef.current = {
104
+ type: 'move',
105
+ id: item.clip.id,
106
+ trackId: item.clip.trackId,
107
+ base: baseTransform(item.clip.transform),
108
+ stageW: stage.width,
109
+ stageH: stage.height,
110
+ startClientX: e.clientX,
111
+ startClientY: e.clientY,
112
+ centerClientX: 0,
113
+ centerClientY: 0,
114
+ startDist: 0,
115
+ };
116
+ }, [selectedClipIds, selectClip, stage.width, stage.height]);
117
+ const beginResize = useCallback((e, item) => {
118
+ e.stopPropagation();
119
+ if (!selectedClipIds.has(item.clip.id))
120
+ selectClip(item.clip.id);
121
+ e.currentTarget.setPointerCapture(e.pointerId);
122
+ const rootRect = rootRef.current?.getBoundingClientRect();
123
+ const centerClientX = (rootRect?.left ?? 0) + item.centerScreenX;
124
+ const centerClientY = (rootRect?.top ?? 0) + item.centerScreenY;
125
+ gestureRef.current = {
126
+ type: 'resize',
127
+ id: item.clip.id,
128
+ trackId: item.clip.trackId,
129
+ base: baseTransform(item.clip.transform),
130
+ stageW: stage.width,
131
+ stageH: stage.height,
132
+ startClientX: e.clientX,
133
+ startClientY: e.clientY,
134
+ centerClientX,
135
+ centerClientY,
136
+ startDist: Math.hypot(e.clientX - centerClientX, e.clientY - centerClientY),
137
+ };
138
+ }, [selectedClipIds, selectClip, stage.width, stage.height]);
139
+ return (_jsxs("div", { ref: rootRef, className: "absolute inset-0 z-[3] pointer-events-none overflow-hidden", children: [!isPlaying && ownsSelection && (_jsx("div", { className: "absolute inset-0 pointer-events-auto", onPointerDown: () => clearSelection() })), !isPlaying &&
140
+ items.map((item) => {
141
+ const { clip, rect } = item;
142
+ const selected = selectedClipIds.has(clip.id);
143
+ const boxStyle = {
144
+ position: 'absolute',
145
+ left: rect.left,
146
+ top: rect.top,
147
+ width: rect.width,
148
+ height: rect.height,
149
+ boxSizing: 'border-box',
150
+ border: selected
151
+ ? '1px solid var(--elah-selection-color, #4c9aff)'
152
+ : '1px solid transparent',
153
+ borderRadius: 2,
154
+ cursor: 'move',
155
+ pointerEvents: 'auto',
156
+ touchAction: 'none',
157
+ };
158
+ return (_jsx("div", { style: boxStyle, onPointerDown: (e) => beginMove(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, children: selected &&
159
+ CORNERS.map((corner) => (_jsx("div", { onPointerDown: (e) => beginResize(e, item), onPointerMove: handlePointerMove, onPointerUp: endGesture, onPointerCancel: endGesture, style: {
160
+ position: 'absolute',
161
+ ...corner.pos,
162
+ width: 10,
163
+ height: 10,
164
+ background: 'var(--elah-selection-handle, #fff)',
165
+ border: '1px solid var(--elah-selection-color, #4c9aff)',
166
+ borderRadius: 2,
167
+ cursor: corner.cursor,
168
+ pointerEvents: 'auto',
169
+ touchAction: 'none',
170
+ } }, corner.key))) }, clip.id));
171
+ })] }));
172
+ }
173
+ const CORNERS = [
174
+ { key: 'nw', pos: { left: -5, top: -5 }, cursor: 'nwse-resize' },
175
+ { key: 'ne', pos: { right: -5, top: -5 }, cursor: 'nesw-resize' },
176
+ { key: 'sw', pos: { left: -5, bottom: -5 }, cursor: 'nesw-resize' },
177
+ { key: 'se', pos: { right: -5, bottom: -5 }, cursor: 'nwse-resize' },
178
+ ];
@@ -16,14 +16,14 @@ export function StageBorder() {
16
16
  return () => obs.disconnect();
17
17
  }, []);
18
18
  const fit = useMemo(() => computeContainViewport(size.width, size.height, stage.width, stage.height), [size.width, size.height, stage.width, stage.height]);
19
- return (_jsx("div", { ref: rootRef, style: { position: 'absolute', inset: 0, zIndex: 1, pointerEvents: 'none', overflow: 'hidden' }, children: fit.width > 0 && fit.height > 0 && (_jsx("div", { style: {
19
+ return (_jsx("div", { ref: rootRef, className: "absolute inset-0 z-[1] pointer-events-none overflow-hidden", children: fit.width > 0 && fit.height > 0 && (_jsx("div", { style: {
20
20
  position: 'absolute',
21
21
  left: fit.x,
22
22
  top: fit.y,
23
23
  width: fit.width,
24
24
  height: fit.height,
25
- border: '1px solid rgba(225, 29, 72, 0.45)',
26
- boxShadow: '0 0 20px rgba(225, 29, 72, 0.08)',
25
+ border: '1px solid var(--elah-stage-border, rgba(225, 29, 72, 0.45))',
26
+ boxShadow: 'var(--elah-stage-glow, 0 0 20px rgba(225, 29, 72, 0.08))',
27
27
  boxSizing: 'border-box',
28
28
  } })) }));
29
29
  }
@@ -163,7 +163,7 @@ export function TextOverlay() {
163
163
  commitEditing();
164
164
  }, [isPlaying]);
165
165
  const editingItem = editingId ? items.find((it) => it.clip.id === editingId) : undefined;
166
- return (_jsxs("div", { ref: rootRef, style: { position: 'absolute', inset: 0, zIndex: 3, pointerEvents: 'none', overflow: 'hidden' }, children: [!isPlaying && (ownsSelection || editingId) && (_jsx("div", { style: { position: 'absolute', inset: 0, pointerEvents: 'auto' }, onPointerDown: () => {
166
+ return (_jsxs("div", { ref: rootRef, className: "absolute inset-0 z-[4] pointer-events-none overflow-hidden", children: [!isPlaying && (ownsSelection || editingId) && (_jsx("div", { className: "absolute inset-0 pointer-events-auto", onPointerDown: () => {
167
167
  if (editingId)
168
168
  commitEditing();
169
169
  clearSelection();
@@ -179,7 +179,7 @@ export function TextOverlay() {
179
179
  width: rect.width + BOX_PAD * 2,
180
180
  height: rect.height + BOX_PAD * 2,
181
181
  boxSizing: 'border-box',
182
- border: selected ? '1px solid #4c9aff' : '1px solid transparent',
182
+ border: selected ? '1px solid var(--elah-selection-color, #4c9aff)' : '1px solid transparent',
183
183
  borderRadius: 2,
184
184
  cursor: isEditing ? 'text' : 'move',
185
185
  pointerEvents: isEditing ? 'none' : 'auto',
@@ -193,8 +193,8 @@ export function TextOverlay() {
193
193
  ...corner.pos,
194
194
  width: 10,
195
195
  height: 10,
196
- background: '#fff',
197
- border: '1px solid #4c9aff',
196
+ background: 'var(--elah-selection-handle, #fff)',
197
+ border: '1px solid var(--elah-selection-color, #4c9aff)',
198
198
  borderRadius: 2,
199
199
  cursor: corner.cursor,
200
200
  pointerEvents: 'auto',
@@ -227,7 +227,7 @@ export function TextOverlay() {
227
227
  font: `${editingItem.layout.style.fontWeight} ${editingItem.layout.style.fontSize * scale}px ${editingItem.layout.style.fontFamily}`,
228
228
  lineHeight: `${editingItem.layout.lineAdvance * scale}px`,
229
229
  textAlign: editingItem.layout.style.textAlign,
230
- border: '1px solid #4c9aff',
230
+ border: '1px solid var(--elah-selection-color, #4c9aff)',
231
231
  outline: 'none',
232
232
  resize: 'none',
233
233
  padding: 0,
@@ -57,11 +57,7 @@ export const TransitionOverlay = forwardRef(function TransitionOverlay(_, ref) {
57
57
  }
58
58
  },
59
59
  }), []);
60
- return (_jsx("div", { ref: rootRef, style: {
61
- position: 'absolute',
62
- inset: 0,
60
+ return (_jsx("div", { ref: rootRef, className: "absolute inset-0 pointer-events-none overflow-hidden", style: {
63
61
  zIndex: 1,
64
- pointerEvents: 'none',
65
- overflow: 'hidden',
66
62
  } }));
67
63
  });
@@ -0,0 +1,37 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { type AssetActivationHandler } from '../activation';
3
+ export interface SourcePanelClassNames {
4
+ root?: string;
5
+ tabBar?: string;
6
+ tab?: string;
7
+ tabActive?: string;
8
+ toolbar?: string;
9
+ ingestButton?: string;
10
+ searchInput?: string;
11
+ sortChip?: string;
12
+ sortChipActive?: string;
13
+ card?: string;
14
+ cardTitle?: string;
15
+ cardMeta?: string;
16
+ thumb?: string;
17
+ badge?: string;
18
+ badgeVideo?: string;
19
+ badgeAudio?: string;
20
+ badgeImage?: string;
21
+ tile?: string;
22
+ tileLabel?: string;
23
+ dropzone?: string;
24
+ toast?: string;
25
+ error?: string;
26
+ }
27
+ export interface SourcePanelProps {
28
+ style?: CSSProperties;
29
+ className?: string;
30
+ defaultLane?: Lane;
31
+ classNames?: SourcePanelClassNames;
32
+ activateOnTap?: boolean;
33
+ onAssetActivate?: AssetActivationHandler;
34
+ }
35
+ type Lane = 'media' | 'elements';
36
+ export declare function SourcePanel({ style, className, defaultLane, classNames, activateOnTap, onAssetActivate, }: SourcePanelProps): import("react/jsx-runtime").JSX.Element;
37
+ export {};