@elah/editor 0.2.0 → 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,381 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState, } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { importFiles, importUrl, MEDIA_DRAG_MIME, mediaDragKindMime, useMediaLibrary, useMediaLibraryStore, } from '@elah/core';
5
+ import { ELEMENT_DRAG_MIME, cn } from '@elah/timeline';
6
+ import { isActivationKey, useAssetActivation, } from '../activation';
7
+ const KIND_ICONS = { video: '▶', audio: '♪', image: '◻' };
8
+ const KIND_TOKEN = {
9
+ video: { label: 'VIDEO' },
10
+ audio: { label: 'AUDIO' },
11
+ image: { label: 'IMAGE' },
12
+ };
13
+ const BADGE_CLASS = {
14
+ video: 'bg-tag-video-bg text-tag-video-fg',
15
+ audio: 'bg-tag-audio-bg text-tag-audio-fg',
16
+ image: 'bg-tag-image-bg text-tag-image-fg',
17
+ };
18
+ const TOAST_DISMISS_MS = 3000;
19
+ const THUMB_SIZE = 52;
20
+ function formatDuration(sec) {
21
+ if (!Number.isFinite(sec) || sec <= 0)
22
+ return '—';
23
+ const m = Math.floor(sec / 60);
24
+ const s = Math.floor(sec % 60);
25
+ return m > 0 ? `${m}:${s.toString().padStart(2, '0')}` : `${s}s`;
26
+ }
27
+ function formatBytes(bytes) {
28
+ if (!bytes)
29
+ return '—';
30
+ if (bytes < 1024 * 1024)
31
+ return `${(bytes / 1024).toFixed(0)} KB`;
32
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
33
+ }
34
+ function buildToast(skipped) {
35
+ if (skipped.length === 0)
36
+ return null;
37
+ const dups = skipped.filter((s) => s.reason === 'duplicate');
38
+ const bad = skipped.filter((s) => s.reason === 'unsupported');
39
+ const lines = [];
40
+ if (dups.length)
41
+ lines.push(`Skipped ${dups.length} duplicate${dups.length > 1 ? 's' : ''}`);
42
+ if (bad.length)
43
+ lines.push(`Skipped ${bad.length} unsupported file${bad.length > 1 ? 's' : ''}`);
44
+ return { message: lines.join('\n'), tone: bad.length ? 'warn' : 'info' };
45
+ }
46
+ function filterSort(assets, search, kind, sort) {
47
+ let out = assets;
48
+ if (kind !== 'all')
49
+ out = out.filter((a) => a.kind === kind);
50
+ if (search.trim()) {
51
+ const q = search.trim().toLowerCase();
52
+ out = out.filter((a) => a.name.toLowerCase().includes(q));
53
+ }
54
+ return [...out].sort((a, b) => {
55
+ if (sort === 'name')
56
+ return a.name.localeCompare(b.name);
57
+ if (sort === 'kind')
58
+ return a.kind.localeCompare(b.kind);
59
+ if (sort === 'duration')
60
+ return (a.durationSec ?? 0) - (b.durationSec ?? 0);
61
+ return 0;
62
+ });
63
+ }
64
+ const ShapeRect = () => (_jsx("svg", { viewBox: "0 0 24 24", width: 20, height: 20, fill: "currentColor", children: _jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }) }));
65
+ const ShapeCircle = () => (_jsx("svg", { viewBox: "0 0 24 24", width: 20, height: 20, fill: "currentColor", children: _jsx("circle", { cx: "12", cy: "12", r: "9" }) }));
66
+ const ShapeTriangle = () => (_jsx("svg", { viewBox: "0 0 24 24", width: 20, height: 20, fill: "currentColor", children: _jsx("polygon", { points: "12,3 22,21 2,21" }) }));
67
+ const FreehandIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", width: 20, height: 20, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M3 17c3-3 5-7 8-7s4 4 7 1" }), _jsx("path", { d: "M17 11l2 2-2 2" })] }));
68
+ const PALETTE_TILES = [
69
+ {
70
+ element: 'text',
71
+ label: 'Text',
72
+ icon: 'T',
73
+ iconStyle: {
74
+ background: 'var(--elah-tag-text-bg)',
75
+ border: '1px solid var(--elah-tag-text-border)',
76
+ color: 'var(--elah-tag-text-fg)',
77
+ fontFamily: 'Georgia, serif',
78
+ fontWeight: 700,
79
+ fontSize: 17,
80
+ },
81
+ },
82
+ {
83
+ element: 'shape',
84
+ shapeVariant: 'rect',
85
+ label: 'Rectangle',
86
+ icon: _jsx(ShapeRect, {}),
87
+ iconStyle: {
88
+ background: 'var(--elah-tag-shape-bg)',
89
+ border: '1px solid var(--elah-tag-shape-border)',
90
+ color: 'var(--elah-tag-shape-fg)',
91
+ },
92
+ },
93
+ {
94
+ element: 'shape',
95
+ shapeVariant: 'circle',
96
+ label: 'Circle',
97
+ icon: _jsx(ShapeCircle, {}),
98
+ iconStyle: {
99
+ background: 'var(--elah-tag-shape-bg)',
100
+ border: '1px solid var(--elah-tag-shape-border)',
101
+ color: 'var(--elah-tag-shape-fg)',
102
+ },
103
+ },
104
+ {
105
+ element: 'shape',
106
+ shapeVariant: 'triangle',
107
+ label: 'Triangle',
108
+ icon: _jsx(ShapeTriangle, {}),
109
+ iconStyle: {
110
+ background: 'var(--elah-tag-shape-bg)',
111
+ border: '1px solid var(--elah-tag-shape-border)',
112
+ color: 'var(--elah-tag-shape-fg)',
113
+ },
114
+ },
115
+ {
116
+ element: 'freehand',
117
+ label: 'Freehand',
118
+ icon: _jsx(FreehandIcon, {}),
119
+ iconStyle: {
120
+ background: 'var(--elah-tag-freehand-bg)',
121
+ border: '1px solid var(--elah-tag-freehand-border)',
122
+ color: 'var(--elah-tag-freehand-fg)',
123
+ },
124
+ },
125
+ ];
126
+ function ClipCard({ asset, viewMode, onDelete, onActivate, slots, }) {
127
+ const [ctxMenu, setCtxMenu] = useState(null);
128
+ const tag = KIND_TOKEN[asset.kind];
129
+ const kindBadgeSlot = asset.kind === 'video'
130
+ ? slots?.badgeVideo
131
+ : asset.kind === 'audio'
132
+ ? slots?.badgeAudio
133
+ : slots?.badgeImage;
134
+ const onDragStart = useCallback((e) => {
135
+ const payload = { kind: 'media-asset', assetId: asset.id };
136
+ e.dataTransfer.setData(MEDIA_DRAG_MIME, JSON.stringify(payload));
137
+ e.dataTransfer.setData(mediaDragKindMime(asset.kind), '');
138
+ e.dataTransfer.effectAllowed = 'copy';
139
+ }, [asset.id]);
140
+ const closeCtx = useCallback(() => setCtxMenu(null), []);
141
+ const handleDelete = useCallback(() => { onDelete(asset.id); setCtxMenu(null); }, [asset.id, onDelete]);
142
+ const handleActivate = useCallback(() => onActivate?.(asset), [asset, onActivate]);
143
+ const handleActivateKey = useCallback((e) => {
144
+ if (!onActivate || !isActivationKey(e.key))
145
+ return;
146
+ e.preventDefault();
147
+ onActivate(asset);
148
+ }, [asset, onActivate]);
149
+ if (viewMode === 'list') {
150
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { draggable: true, role: onActivate ? 'button' : undefined, tabIndex: onActivate ? 0 : undefined, className: cn('elah-media-card flex items-center gap-2 px-[10px] py-[6px] rounded-sm cursor-grab select-none bg-ed-card border border-ed-border transition-[background,border-color] duration-[120ms]', slots?.card), onDragStart: onDragStart, onClick: onActivate ? handleActivate : undefined, onKeyDown: onActivate ? handleActivateKey : undefined, onContextMenu: (e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY }); }, title: asset.name, children: [_jsx("span", { className: "text-sm text-ed-text-muted shrink-0 w-[18px] text-center", children: KIND_ICONS[asset.kind] }), _jsx("span", { className: cn('flex-1 text-[11px] text-ed-text overflow-hidden text-ellipsis whitespace-nowrap', slots?.cardTitle), children: asset.name }), _jsx("span", { className: cn('text-[9px] font-bold tracking-[0.06em] px-1 py-px rounded-sm shrink-0', BADGE_CLASS[asset.kind], slots?.badge, kindBadgeSlot), children: tag.label }), _jsx("span", { className: cn('text-[10px] text-ed-text-muted font-mono shrink-0', slots?.cardMeta), children: formatDuration(asset.durationSec) })] }), ctxMenu && _jsx(CtxMenu, { x: ctxMenu.x, y: ctxMenu.y, onClose: closeCtx, onDelete: handleDelete, errorClassName: slots?.error })] }));
151
+ }
152
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { draggable: true, role: onActivate ? 'button' : undefined, tabIndex: onActivate ? 0 : undefined, className: cn('elah-media-card flex items-center gap-[10px] px-[10px] py-2 rounded-md cursor-grab select-none bg-ed-card border border-ed-border transition-[background,border-color] duration-[150ms]', slots?.card), onDragStart: onDragStart, onClick: onActivate ? handleActivate : undefined, onKeyDown: onActivate ? handleActivateKey : undefined, onContextMenu: (e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY }); }, title: asset.name, children: [_jsx("div", { className: cn('relative shrink-0 bg-ed-bg rounded-sm border border-ed-border-subtle overflow-hidden', slots?.thumb), style: { width: THUMB_SIZE, height: THUMB_SIZE }, children: asset.thumbnailUrl ? (_jsx("img", { src: asset.thumbnailUrl, alt: "", draggable: false, className: "w-full h-full object-cover block" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-xl text-ed-text-muted", children: KIND_ICONS[asset.kind] })) }), _jsxs("div", { className: "flex flex-col gap-[3px] min-w-0", children: [_jsx("span", { className: cn('text-[11px] text-ed-text overflow-hidden text-ellipsis whitespace-nowrap', slots?.cardTitle), children: asset.name }), _jsxs("div", { className: "flex items-center gap-[6px]", children: [_jsx("span", { className: cn('text-[8px] font-bold tracking-[0.06em] px-[5px] py-[2px] rounded-sm', BADGE_CLASS[asset.kind], slots?.badge, kindBadgeSlot), children: tag.label }), _jsx("span", { className: cn('text-[10px] text-ed-text-muted font-mono', slots?.cardMeta), children: formatDuration(asset.durationSec) })] })] })] }), ctxMenu && _jsx(CtxMenu, { x: ctxMenu.x, y: ctxMenu.y, onClose: closeCtx, onDelete: handleDelete, errorClassName: slots?.error })] }));
153
+ }
154
+ function CtxMenu({ x, y, onClose, onDelete, errorClassName, }) {
155
+ return createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-[9998]", onMouseDown: onClose }), _jsx("div", { style: {
156
+ position: 'fixed',
157
+ top: y,
158
+ left: x,
159
+ zIndex: 9999,
160
+ background: 'var(--elah-bg-elevated)',
161
+ border: '1px solid var(--elah-outline)',
162
+ borderRadius: 'var(--elah-radius-sm)',
163
+ padding: '4px 0',
164
+ minWidth: 140,
165
+ boxShadow: 'var(--elah-menu-shadow, 0 8px 24px rgba(0,0,0,0.5))',
166
+ fontFamily: 'var(--elah-font-ui)',
167
+ }, children: _jsx("button", { type: "button", onMouseDown: (e) => e.stopPropagation(), onClick: onDelete, className: cn('block w-full px-[14px] py-[7px] text-left bg-transparent border-none text-ed-error text-[13px] cursor-pointer', errorClassName), onMouseEnter: (e) => { e.currentTarget.style.background = 'color-mix(in srgb, var(--elah-color-error) 12%, transparent)'; }, onMouseLeave: (e) => { e.currentTarget.style.background = 'none'; }, children: "Delete" }) })] }), document.body);
168
+ }
169
+ export function SourcePanel({ style, className, defaultLane = 'media', classNames, activateOnTap, onAssetActivate, }) {
170
+ const [lane, setLane] = useState(defaultLane);
171
+ const { assets } = useMediaLibrary();
172
+ const removeAsset = useMediaLibraryStore((s) => s.removeAsset);
173
+ const fileInputRef = useRef(null);
174
+ const [search, setSearch] = useState('');
175
+ const [kindFilter, setKindFilter] = useState('all');
176
+ const [sort, setSort] = useState('added');
177
+ const [viewMode, setViewMode] = useState('grid');
178
+ const [isDragOver, setIsDragOver] = useState(false);
179
+ const [importing, setImporting] = useState(false);
180
+ const [toast, setToast] = useState(null);
181
+ const [urlInputOpen, setUrlInputOpen] = useState(false);
182
+ const [urlValue, setUrlValue] = useState('');
183
+ const activationEnabled = activateOnTap === true || Boolean(onAssetActivate);
184
+ const activateAsset = useAssetActivation({
185
+ activateOnTap,
186
+ onAssetActivate,
187
+ setToast,
188
+ });
189
+ useEffect(() => {
190
+ if (!toast)
191
+ return;
192
+ const id = globalThis.setTimeout(() => setToast(null), TOAST_DISMISS_MS);
193
+ return () => globalThis.clearTimeout(id);
194
+ }, [toast]);
195
+ const handleDeleteAsset = useCallback((id) => removeAsset(id), [removeAsset]);
196
+ const handleAssetActivate = useCallback((asset) => {
197
+ void activateAsset({ kind: 'media-asset', asset });
198
+ }, [activateAsset]);
199
+ const handleElementActivate = useCallback((element, shapeVariant, label) => {
200
+ void activateAsset({ kind: 'element', element, shapeVariant, label });
201
+ }, [activateAsset]);
202
+ const handleFiles = useCallback(async (files) => {
203
+ const list = Array.from(files);
204
+ if (!list.length)
205
+ return;
206
+ setImporting(true);
207
+ try {
208
+ const { skipped } = await importFiles(list);
209
+ setToast(buildToast(skipped));
210
+ }
211
+ finally {
212
+ setImporting(false);
213
+ }
214
+ }, []);
215
+ const handleImportUrl = useCallback(async () => {
216
+ const url = urlValue.trim();
217
+ if (!url)
218
+ return;
219
+ setImporting(true);
220
+ try {
221
+ await importUrl(url);
222
+ setUrlValue('');
223
+ setUrlInputOpen(false);
224
+ }
225
+ catch (err) {
226
+ setToast({ message: `Failed: ${err instanceof Error ? err.message : String(err)}`, tone: 'warn' });
227
+ }
228
+ finally {
229
+ setImporting(false);
230
+ }
231
+ }, [urlValue]);
232
+ const onUrlKeyDown = useCallback((e) => {
233
+ if (e.key === 'Enter') {
234
+ e.preventDefault();
235
+ void handleImportUrl();
236
+ }
237
+ else if (e.key === 'Escape') {
238
+ setUrlInputOpen(false);
239
+ setUrlValue('');
240
+ }
241
+ }, [handleImportUrl]);
242
+ const onDragOver = useCallback((e) => {
243
+ if (e.dataTransfer.types.includes('Files')) {
244
+ e.preventDefault();
245
+ e.dataTransfer.dropEffect = 'copy';
246
+ setIsDragOver(true);
247
+ }
248
+ }, []);
249
+ const onDragLeave = useCallback((e) => {
250
+ if (!e.currentTarget.contains(e.relatedTarget))
251
+ setIsDragOver(false);
252
+ }, []);
253
+ const onDrop = useCallback((e) => {
254
+ e.preventDefault();
255
+ setIsDragOver(false);
256
+ if (e.dataTransfer.files.length)
257
+ void handleFiles(e.dataTransfer.files);
258
+ }, [handleFiles]);
259
+ const onFileChange = useCallback((e) => {
260
+ if (e.target.files?.length)
261
+ void handleFiles(e.target.files);
262
+ e.target.value = '';
263
+ }, [handleFiles]);
264
+ const makeDragStart = useCallback((element, shapeVariant) => (e) => {
265
+ const payload = { kind: 'element', element, shapeVariant };
266
+ e.dataTransfer.setData(ELEMENT_DRAG_MIME, JSON.stringify(payload));
267
+ e.dataTransfer.effectAllowed = 'copy';
268
+ }, []);
269
+ const cardSlots = {
270
+ card: classNames?.card,
271
+ cardTitle: classNames?.cardTitle,
272
+ cardMeta: classNames?.cardMeta,
273
+ thumb: classNames?.thumb,
274
+ badge: classNames?.badge,
275
+ badgeVideo: classNames?.badgeVideo,
276
+ badgeAudio: classNames?.badgeAudio,
277
+ badgeImage: classNames?.badgeImage,
278
+ error: classNames?.error,
279
+ };
280
+ const visibleAssets = filterSort(assets, search, kindFilter, sort);
281
+ const kindCounts = { all: assets.length };
282
+ for (const a of assets)
283
+ kindCounts[a.kind] = (kindCounts[a.kind] ?? 0) + 1;
284
+ const KIND_CHIPS = [
285
+ { value: 'all', label: `All ${kindCounts.all}` },
286
+ { value: 'video', label: `Video ${kindCounts.video ?? 0}` },
287
+ { value: 'audio', label: `Audio ${kindCounts.audio ?? 0}` },
288
+ { value: 'image', label: `Image ${kindCounts.image ?? 0}` },
289
+ ];
290
+ return (_jsxs("div", { className: cn('flex flex-col h-full bg-transparent', className, classNames?.root), style: style, children: [_jsx("div", { className: cn('flex items-center px-2 py-[6px] border-b border-ed-border gap-1 shrink-0', classNames?.tabBar), children: ['media', 'elements'].map((l) => (_jsx("button", { type: "button", onClick: () => setLane(l), className: cn(lane === l
291
+ ? cn('flex-1 py-[5px] text-[11px] font-semibold capitalize cursor-pointer transition-all duration-[120ms] rounded-sm bg-ed-elevated border border-ed-border text-ed-text', classNames?.tabActive)
292
+ : cn('flex-1 py-[5px] text-[11px] font-medium capitalize cursor-pointer transition-all duration-[120ms] rounded-sm bg-transparent border border-transparent text-ed-text-muted', classNames?.tab)), style: {
293
+ fontFamily: 'var(--elah-font-ui)',
294
+ }, children: l }, l))) }), lane === 'media' && (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: "video/*,audio/*,image/*", className: "hidden", onChange: onFileChange, "data-testid": "source-file-input" }), _jsxs("div", { className: cn('flex items-center gap-[6px] px-[10px] py-2 border-b border-ed-border shrink-0', classNames?.toolbar), children: [_jsx("button", { type: "button", onClick: () => setUrlInputOpen((o) => !o), disabled: importing, "data-testid": "source-url-toggle", className: ingestBtnClass(urlInputOpen, importing, classNames?.ingestButton), style: ingestBtnStyle(importing), children: "+ URL" }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: importing, className: ingestBtnClass(false, importing, classNames?.ingestButton), style: ingestBtnStyle(importing), children: importing ? '…' : '+ Add' }), _jsx("div", { className: "flex-1" }), ['grid', 'list'].map((v) => (_jsx("button", { type: "button", onClick: () => setViewMode(v), title: `${v} view`, style: {
295
+ width: 26,
296
+ height: 26,
297
+ display: 'flex',
298
+ alignItems: 'center',
299
+ justifyContent: 'center',
300
+ background: viewMode === v ? 'var(--elah-bg-elevated)' : 'transparent',
301
+ border: viewMode === v ? '1px solid var(--elah-border)' : '1px solid transparent',
302
+ borderRadius: 'var(--elah-radius-sm)',
303
+ cursor: 'pointer',
304
+ color: viewMode === v ? 'var(--elah-text)' : 'var(--elah-text-muted)',
305
+ fontSize: 12,
306
+ }, children: v === 'grid' ? '⊞' : '≡' }, v)))] }), urlInputOpen && (_jsxs("div", { className: cn('flex gap-[6px] px-[10px] py-2 border-b border-ed-border shrink-0', classNames?.toolbar), children: [_jsx("input", { type: "url", value: urlValue, onChange: (e) => setUrlValue(e.target.value), onKeyDown: onUrlKeyDown, placeholder: "https://\u2026/media.mp4", autoFocus: true, "data-testid": "source-url-input", style: {
307
+ flex: 1,
308
+ minWidth: 0,
309
+ padding: '5px 8px',
310
+ fontSize: 11,
311
+ fontFamily: 'var(--elah-font-mono)',
312
+ color: 'var(--elah-text)',
313
+ background: 'var(--elah-bg)',
314
+ border: '1px solid var(--elah-border)',
315
+ borderRadius: 'var(--elah-radius-sm)',
316
+ outline: 'none',
317
+ } }), _jsx("button", { type: "button", onClick: () => void handleImportUrl(), disabled: importing || !urlValue.trim(), className: ingestBtnClass(false, importing || !urlValue.trim(), classNames?.ingestButton), style: ingestBtnStyle(importing || !urlValue.trim()), children: "Add" })] })), _jsxs("div", { className: cn('flex items-center gap-[6px] px-[10px] py-[6px] border-b border-ed-border shrink-0', classNames?.toolbar), children: [_jsx("input", { type: "search", value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Search\u2026", className: classNames?.searchInput, style: {
318
+ flex: 1,
319
+ minWidth: 0,
320
+ padding: '4px 8px',
321
+ fontSize: 11,
322
+ color: 'var(--elah-text)',
323
+ background: 'var(--elah-bg)',
324
+ border: '1px solid var(--elah-border)',
325
+ borderRadius: 'var(--elah-radius-sm)',
326
+ outline: 'none',
327
+ fontFamily: 'var(--elah-font-ui)',
328
+ } }), _jsxs("select", { value: sort, onChange: (e) => setSort(e.target.value), style: {
329
+ padding: '4px 6px',
330
+ fontSize: 10,
331
+ color: 'var(--elah-text-muted)',
332
+ background: 'var(--elah-bg-card)',
333
+ border: '1px solid var(--elah-border)',
334
+ borderRadius: 'var(--elah-radius-sm)',
335
+ cursor: 'pointer',
336
+ outline: 'none',
337
+ }, children: [_jsx("option", { value: "added", children: "Added" }), _jsx("option", { value: "name", children: "Name" }), _jsx("option", { value: "kind", children: "Type" }), _jsx("option", { value: "duration", children: "Duration" })] })] }), _jsx("div", { className: cn('flex gap-1 px-[10px] py-[6px] border-b border-ed-border shrink-0 overflow-x-auto', classNames?.toolbar), children: KIND_CHIPS.map(({ value, label }) => (_jsx("button", { type: "button", onClick: () => setKindFilter(value), className: cn(kindFilter === value
338
+ ? cn('px-2 py-px text-[10px] font-bold rounded-full whitespace-nowrap cursor-pointer transition-all duration-[120ms] border bg-ed-accent-soft text-ed-accent-dim border-ed-accent', classNames?.sortChipActive)
339
+ : cn('px-2 py-px text-[10px] font-medium rounded-full whitespace-nowrap cursor-pointer transition-all duration-[120ms] border bg-ed-card text-ed-text-muted border-ed-border', classNames?.sortChip)), style: {
340
+ padding: '3px 8px',
341
+ }, children: label }, value))) }), _jsxs("div", { className: cn('flex-1 overflow-auto p-2 relative rounded', classNames?.dropzone), style: {
342
+ outline: isDragOver ? '2px dashed var(--elah-accent)' : 'none',
343
+ outlineOffset: -4,
344
+ }, children: [toast && (_jsx("div", { role: "status", className: cn('absolute top-2 left-2 right-2 z-[2] px-[10px] py-2 rounded-sm text-[10px] font-mono leading-[1.4] whitespace-pre-line', classNames?.toast), style: {
345
+ color: toast.tone === 'warn' ? 'var(--elah-danger-text, #f5d0a9)' : 'var(--elah-info-text, #c8d8f0)',
346
+ background: toast.tone === 'warn' ? 'var(--elah-danger-bg, #3a2418)' : 'var(--elah-info-bg, #1a2433)',
347
+ border: `1px solid ${toast.tone === 'warn' ? 'var(--elah-danger-border, #7a4a2a)' : 'var(--elah-info-border, #355070)'}`,
348
+ boxShadow: '0 4px 12px rgba(0,0,0,0.35)',
349
+ }, children: toast.message })), assets.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center min-h-[120px] p-4 text-center text-ed-text-muted text-[11px] border border-dashed border-ed-border rounded-md", children: [_jsx("span", { className: "mb-2 text-2xl opacity-50", children: "\u2193" }), "Drop files here", _jsx("br", {}), "or click Add"] })) : visibleAssets.length === 0 ? (_jsx("div", { className: "text-center text-ed-text-muted text-[11px] pt-8", children: "No matches" })) : (_jsx("div", { className: "flex flex-col gap-[6px]", children: visibleAssets.map((asset) => (_jsx(ClipCard, { asset: asset, viewMode: viewMode, onDelete: handleDeleteAsset, onActivate: activationEnabled ? handleAssetActivate : undefined, slots: cardSlots }, asset.id))) }))] })] })), lane === 'elements' && (_jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-auto", children: [_jsx("div", { className: cn('px-[10px] py-[6px] border-b border-ed-border shrink-0', classNames?.toolbar), children: _jsx("input", { type: "search", placeholder: "Search elements\u2026", className: classNames?.searchInput, style: {
350
+ width: '100%',
351
+ padding: '4px 8px',
352
+ fontSize: 11,
353
+ color: 'var(--elah-text)',
354
+ background: 'var(--elah-bg)',
355
+ border: '1px solid var(--elah-border)',
356
+ borderRadius: 'var(--elah-radius-sm)',
357
+ outline: 'none',
358
+ fontFamily: 'var(--elah-font-ui)',
359
+ boxSizing: 'border-box',
360
+ } }) }), _jsx("div", { className: "p-[10px] grid grid-cols-2 gap-[6px]", children: PALETTE_TILES.map(({ element, shapeVariant, label, icon, iconStyle }) => (_jsxs("div", { draggable: true, role: activationEnabled ? 'button' : undefined, tabIndex: activationEnabled ? 0 : undefined, className: cn('elah-element-card flex flex-col items-center justify-center gap-[6px] px-2 py-3 rounded-md cursor-grab select-none bg-ed-card border border-ed-border transition-[background,border-color] duration-[150ms] min-h-[72px]', classNames?.tile), onDragStart: makeDragStart(element, shapeVariant), onClick: activationEnabled ? () => handleElementActivate(element, shapeVariant, label) : undefined, onKeyDown: activationEnabled
361
+ ? (e) => {
362
+ if (!isActivationKey(e.key))
363
+ return;
364
+ e.preventDefault();
365
+ handleElementActivate(element, shapeVariant, label);
366
+ }
367
+ : undefined, title: `Drag ${label} onto the elements track`, children: [_jsx("span", { className: "w-8 h-8 flex items-center justify-center rounded-sm", style: iconStyle, children: icon }), _jsx("span", { className: cn('text-[11px] text-ed-text font-medium', classNames?.tileLabel), children: label })] }, shapeVariant ? `${element}-${shapeVariant}` : element))) })] }))] }));
368
+ }
369
+ function ingestBtnStyle(disabled) {
370
+ return {
371
+ padding: '4px 10px',
372
+ fontSize: 11,
373
+ fontWeight: 600,
374
+ fontFamily: 'var(--elah-font-ui)',
375
+ cursor: disabled ? 'wait' : 'pointer',
376
+ whiteSpace: 'nowrap',
377
+ };
378
+ }
379
+ function ingestBtnClass(active, disabled, slot) {
380
+ return cn('rounded-sm border border-ed-border', active ? 'bg-ed-elevated' : 'bg-ed-card', disabled ? 'text-ed-text-muted' : 'text-ed-accent', slot);
381
+ }
@@ -0,0 +1,2 @@
1
+ export { SourcePanel } from './SourcePanel';
2
+ export type { SourcePanelProps, SourcePanelClassNames } from './SourcePanel';
@@ -0,0 +1 @@
1
+ export { SourcePanel } from './SourcePanel';
@@ -0,0 +1,26 @@
1
+ import { type MediaAsset } from '@elah/core';
2
+ import { type ElementKind, type ShapeVariant } from '@elah/timeline';
3
+ export interface ActivationToast {
4
+ message: string;
5
+ tone: 'info' | 'warn';
6
+ }
7
+ export interface MediaActivationPayload {
8
+ kind: 'media-asset';
9
+ asset: MediaAsset;
10
+ }
11
+ export interface ElementActivationPayload {
12
+ kind: 'element';
13
+ element: ElementKind;
14
+ shapeVariant?: ShapeVariant;
15
+ label: string;
16
+ }
17
+ export type AssetActivationPayload = MediaActivationPayload | ElementActivationPayload;
18
+ export type AssetActivationHandler = (payload: AssetActivationPayload) => void | Promise<void>;
19
+ interface UseAssetActivationOptions {
20
+ activateOnTap?: boolean;
21
+ onAssetActivate?: AssetActivationHandler;
22
+ setToast: (toast: ActivationToast) => void;
23
+ }
24
+ export declare function isActivationKey(key: string): boolean;
25
+ export declare function useAssetActivation({ activateOnTap, onAssetActivate, setToast, }: UseAssetActivationOptions): (payload: AssetActivationPayload) => Promise<void>;
26
+ export {};
@@ -0,0 +1,53 @@
1
+ import { useCallback, useContext } from 'react';
2
+ import { EditorContext } from '@elah/core';
3
+ import { insertElement, insertMediaAsset, } from '@elah/timeline';
4
+ export function isActivationKey(key) {
5
+ return key === 'Enter' || key === ' ';
6
+ }
7
+ export function useAssetActivation({ activateOnTap, onAssetActivate, setToast, }) {
8
+ const editor = useContext(EditorContext);
9
+ const isActive = activateOnTap === true || Boolean(onAssetActivate);
10
+ return useCallback(async (payload) => {
11
+ if (!isActive)
12
+ return;
13
+ if (onAssetActivate) {
14
+ await onAssetActivate(payload);
15
+ return;
16
+ }
17
+ const engine = editor?.engine;
18
+ if (!engine) {
19
+ setToast({ message: 'Timeline unavailable', tone: 'warn' });
20
+ return;
21
+ }
22
+ const result = payload.kind === 'media-asset'
23
+ ? await insertMediaAsset(engine, payload.asset.id)
24
+ : insertElement(engine, elementPayload(payload));
25
+ showActivationToast(payload, result, setToast);
26
+ }, [editor, isActive, onAssetActivate, setToast]);
27
+ }
28
+ function elementPayload(payload) {
29
+ return {
30
+ kind: 'element',
31
+ element: payload.element,
32
+ shapeVariant: payload.shapeVariant,
33
+ };
34
+ }
35
+ function showActivationToast(payload, result, setToast) {
36
+ if (result.ok) {
37
+ setToast({ message: `Added ${payloadName(payload)}`, tone: 'info' });
38
+ return;
39
+ }
40
+ if (result.reason === 'cancelled')
41
+ return;
42
+ if (result.reason === 'missing-asset') {
43
+ setToast({ message: 'Asset unavailable', tone: 'warn' });
44
+ return;
45
+ }
46
+ setToast({ message: `No unlocked track for ${payloadKind(payload)}`, tone: 'warn' });
47
+ }
48
+ function payloadName(payload) {
49
+ return payload.kind === 'media-asset' ? payload.asset.name : payload.label;
50
+ }
51
+ function payloadKind(payload) {
52
+ return payload.kind === 'media-asset' ? payload.asset.kind : 'element';
53
+ }
package/dist/index.d.ts CHANGED
@@ -16,7 +16,9 @@ export type { VideoFrameProvider, VideoFrameProviderDeps } from '@elah/core';
16
16
  export { createVideoFrameProvider, MockVideoFrameProvider, SyntheticVideoFrameProvider } from '@elah/core';
17
17
  export { AudioPlaybackController } from '@elah/core';
18
18
  export type { AudioPlaybackControllerOptions } from '@elah/core';
19
- export { useMediaLibrary, useMediaLibraryStore, MEDIA_DRAG_MIME, importFiles } from '@elah/core';
19
+ export { defaultAudioResolver } from '@elah/core';
20
+ export type { AudioResolver } from '@elah/core';
21
+ export { useMediaLibrary, useMediaLibraryStore, MEDIA_DRAG_MIME, mediaDragKindMime, importFiles, importUrl } from '@elah/core';
20
22
  export type { MediaAsset, MediaKind, DragMediaPayload, ImportFilesOptions, ImportFilesResult, SkippedImport } from '@elah/core';
21
23
  export { useTracksStore } from '@elah/core';
22
24
  export { usePlaybackStore } from '@elah/core';
@@ -30,6 +32,7 @@ export type { SplitAtPlayheadData } from '@elah/core';
30
32
  export type { ActionResult, ActionFailureReason } from '@elah/core';
31
33
  export { framesToTimecode, secondsToFrames, framesToSeconds, getTotalFrames } from '@elah/core';
32
34
  export { generateId } from '@elah/core';
35
+ export { transformFromCoverRect } from '@elah/core';
33
36
  export { exportVideo } from '@elah/core';
34
37
  export { lazyExportVideo } from '@elah/core';
35
38
  export type { ExportOptions, ExportProgress, ExportVideoCodec, ExportAudioCodec } from '@elah/core';
@@ -40,8 +43,9 @@ export { useTracks } from '@elah/timeline';
40
43
  export { usePlayback } from '@elah/timeline';
41
44
  export { useSelection } from '@elah/timeline';
42
45
  export { useTimelineDrop } from '@elah/timeline';
46
+ export { insertMediaAsset, insertElement } from '@elah/timeline';
43
47
  export { ELEMENT_DRAG_MIME } from '@elah/timeline';
44
- export type { DragElementPayload, ElementKind } from '@elah/timeline';
48
+ export type { DragElementPayload, ElementKind, ShapeVariant, InsertAssetOptions, InsertAssetResult, InsertAssetFailureReason, InsertedKind, } from '@elah/timeline';
45
49
  export { EditorProvider } from './editor/EditorProvider';
46
50
  export type { EditorProviderProps } from './editor/EditorProvider';
47
51
  export { useEditor, useTimelineEngine, usePlaybackEngine } from '@elah/core';
@@ -50,5 +54,8 @@ export { AssetPanel } from './editor/AssetPanel';
50
54
  export type { AssetPanelProps } from './editor/AssetPanel';
51
55
  export { ElementsPanel } from './editor/ElementsPanel';
52
56
  export type { ElementsPanelProps } from './editor/ElementsPanel';
57
+ export { SourcePanel } from './editor/SourcePanel';
58
+ export type { SourcePanelProps, SourcePanelClassNames } from './editor/SourcePanel';
59
+ export type { AssetActivationPayload, AssetActivationHandler } from './editor/activation';
53
60
  export { Preview } from './editor/Preview';
54
61
  export type { PreviewProps, PreviewHandle } from './editor/Preview';
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ export { GpuDebugCounters } from '@elah/core';
6
6
  export { createDefaultDemuxerFactory, createMediabunnyBackend, isMediabunnyCompatible } from '@elah/core';
7
7
  export { createVideoFrameProvider, MockVideoFrameProvider, SyntheticVideoFrameProvider } from '@elah/core';
8
8
  export { AudioPlaybackController } from '@elah/core';
9
- export { useMediaLibrary, useMediaLibraryStore, MEDIA_DRAG_MIME, importFiles } from '@elah/core';
9
+ export { defaultAudioResolver } from '@elah/core';
10
+ export { useMediaLibrary, useMediaLibraryStore, MEDIA_DRAG_MIME, mediaDragKindMime, importFiles, importUrl } from '@elah/core';
10
11
  export { useTracksStore } from '@elah/core';
11
12
  export { usePlaybackStore } from '@elah/core';
12
13
  export { useSelectionStore } from '@elah/core';
@@ -17,6 +18,7 @@ export { createImageClip } from '@elah/core';
17
18
  export { splitClipAtPlayhead } from '@elah/core';
18
19
  export { framesToTimecode, secondsToFrames, framesToSeconds, getTotalFrames } from '@elah/core';
19
20
  export { generateId } from '@elah/core';
21
+ export { transformFromCoverRect } from '@elah/core';
20
22
  export { exportVideo } from '@elah/core';
21
23
  export { lazyExportVideo } from '@elah/core';
22
24
  export { Timeline } from '@elah/timeline';
@@ -25,10 +27,12 @@ export { useTracks } from '@elah/timeline';
25
27
  export { usePlayback } from '@elah/timeline';
26
28
  export { useSelection } from '@elah/timeline';
27
29
  export { useTimelineDrop } from '@elah/timeline';
30
+ export { insertMediaAsset, insertElement } from '@elah/timeline';
28
31
  export { ELEMENT_DRAG_MIME } from '@elah/timeline';
29
32
  export { EditorProvider } from './editor/EditorProvider';
30
33
  export { useEditor, useTimelineEngine, usePlaybackEngine } from '@elah/core';
31
34
  export { useResolvedScene } from './editor/useResolvedScene';
32
35
  export { AssetPanel } from './editor/AssetPanel';
33
36
  export { ElementsPanel } from './editor/ElementsPanel';
37
+ export { SourcePanel } from './editor/SourcePanel';
34
38
  export { Preview } from './editor/Preview';
@@ -0,0 +1 @@
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.left-2{left:.5rem}.right-2{right:.5rem}.top-2{top:.5rem}.isolate{isolation:isolate}.z-\[1\]{z-index:1}.z-\[2\]{z-index:2}.z-\[3\]{z-index:3}.z-\[4\]{z-index:4}.z-\[9998\]{z-index:9998}.mx-\[10px\]{margin-left:10px;margin-right:10px}.mb-2{margin-bottom:.5rem}.mt-\[10px\]{margin-top:10px}.block{display:block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-8{height:2rem}.h-full{height:100%}.min-h-0{min-height:0}.min-h-\[120px\]{min-height:120px}.min-h-\[72px\]{min-height:72px}.w-8{width:2rem}.w-\[18px\]{width:18px}.w-full{width:100%}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-\[10px\]{gap:10px}.gap-\[3px\]{gap:3px}.gap-\[6px\]{gap:6px}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.rounded{border-radius:4px}.rounded-full{border-radius:9999px}.rounded-md{border-radius:6px}.rounded-sm{border-radius:3px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-ed-accent{border-color:var(--elah-accent)}.border-ed-border{border-color:var(--elah-border)}.border-ed-border-subtle{border-color:var(--elah-border-subtle)}.border-transparent{border-color:transparent}.bg-\[var\(--elah-preview-bg\2c \#06070A\)\]{background-color:var(--elah-preview-bg,#06070a)}.bg-ed-accent-soft{background-color:var(--elah-accent-soft)}.bg-ed-bg{background-color:var(--elah-bg)}.bg-ed-card{background-color:var(--elah-bg-card)}.bg-ed-elevated{background-color:var(--elah-bg-elevated)}.bg-tag-audio-bg{background-color:var(--elah-tag-audio-bg)}.bg-tag-image-bg{background-color:var(--elah-tag-image-bg)}.bg-tag-video-bg{background-color:var(--elah-tag-video-bg)}.bg-transparent{background-color:transparent}.object-cover{-o-object-fit:cover;object-fit:cover}.p-2{padding:.5rem}.p-4{padding:1rem}.p-\[10px\]{padding:10px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-\[10px\]{padding-left:10px;padding-right:10px}.px-\[14px\]{padding-left:14px;padding-right:14px}.px-\[5px\]{padding-left:5px;padding-right:5px}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-\[10px\]{padding-top:10px;padding-bottom:10px}.py-\[2px\]{padding-top:2px;padding-bottom:2px}.py-\[5px\]{padding-top:5px;padding-bottom:5px}.py-\[6px\]{padding-top:6px;padding-bottom:6px}.py-\[7px\]{padding-top:7px;padding-bottom:7px}.py-px{padding-top:1px;padding-bottom:1px}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:var(--font-geist-mono),JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-\[1\.4\]{line-height:1.4}.tracking-\[0\.01em\]{letter-spacing:.01em}.tracking-\[0\.06em\]{letter-spacing:.06em}.tracking-\[0\.08em\]{letter-spacing:.08em}.text-ed-accent{color:var(--elah-accent)}.text-ed-accent-dim{color:var(--elah-accent-dim)}.text-ed-error{color:var(--elah-color-error)}.text-ed-text{color:var(--elah-text)}.text-ed-text-muted{color:var(--elah-text-muted)}.text-tag-audio-fg{color:var(--elah-tag-audio-fg)}.text-tag-image-fg{color:var(--elah-tag-image-fg)}.text-tag-video-fg{color:var(--elah-tag-video-fg)}.opacity-50{opacity:.5}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background\2c border-color\]{transition-property:background,border-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-\[120ms\]{transition-duration:.12s}.duration-\[150ms\]{transition-duration:.15s}