@codellyson/framely-cli 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.
Files changed (40) hide show
  1. package/commands/compositions.js +135 -0
  2. package/commands/preview.js +889 -0
  3. package/commands/render.js +295 -0
  4. package/commands/still.js +165 -0
  5. package/index.js +93 -0
  6. package/package.json +60 -0
  7. package/studio/App.css +605 -0
  8. package/studio/App.jsx +185 -0
  9. package/studio/CompositionsView.css +399 -0
  10. package/studio/CompositionsView.jsx +327 -0
  11. package/studio/PropsEditor.css +195 -0
  12. package/studio/PropsEditor.tsx +176 -0
  13. package/studio/RenderDialog.tsx +476 -0
  14. package/studio/ShareDialog.tsx +200 -0
  15. package/studio/index.ts +19 -0
  16. package/studio/player/Player.css +199 -0
  17. package/studio/player/Player.jsx +355 -0
  18. package/studio/styles/design-system.css +592 -0
  19. package/studio/styles/dialogs.css +420 -0
  20. package/studio/templates/AnimatedGradient.jsx +99 -0
  21. package/studio/templates/InstagramStory.jsx +172 -0
  22. package/studio/templates/LowerThird.jsx +139 -0
  23. package/studio/templates/ProductShowcase.jsx +162 -0
  24. package/studio/templates/SlideTransition.jsx +211 -0
  25. package/studio/templates/SocialIntro.jsx +122 -0
  26. package/studio/templates/SubscribeAnimation.jsx +186 -0
  27. package/studio/templates/TemplateCard.tsx +58 -0
  28. package/studio/templates/TemplateFilters.tsx +97 -0
  29. package/studio/templates/TemplatePreviewDialog.tsx +196 -0
  30. package/studio/templates/TemplatesMarketplace.css +686 -0
  31. package/studio/templates/TemplatesMarketplace.tsx +172 -0
  32. package/studio/templates/TextReveal.jsx +134 -0
  33. package/studio/templates/UseTemplateDialog.tsx +154 -0
  34. package/studio/templates/index.ts +45 -0
  35. package/utils/browser.js +188 -0
  36. package/utils/codecs.js +200 -0
  37. package/utils/logger.js +35 -0
  38. package/utils/props.js +42 -0
  39. package/utils/render.js +447 -0
  40. package/utils/validate.js +148 -0
@@ -0,0 +1,199 @@
1
+ /* ─── Player Container ─── */
2
+ .framely-player {
3
+ display: flex;
4
+ flex-direction: column;
5
+ background: var(--zinc-950, #09090b);
6
+ border-radius: var(--radius-xl, 12px);
7
+ overflow: hidden;
8
+ border: 1px solid rgba(255, 255, 255, 0.06);
9
+ box-shadow:
10
+ 0 4px 24px rgba(0, 0, 0, 0.4),
11
+ 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
12
+ user-select: none;
13
+ font-family: var(--font-mono, 'JetBrains Mono', 'Fira Code', monospace);
14
+ }
15
+
16
+ /* ─── Viewport ─── */
17
+ .framely-viewport {
18
+ position: relative;
19
+ flex: 1;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ background: var(--zinc-900, #18181b);
24
+ background-image:
25
+ radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.01) 0%, transparent 70%);
26
+ overflow: hidden;
27
+ min-height: 300px;
28
+ }
29
+
30
+ .framely-canvas {
31
+ position: relative;
32
+ overflow: hidden;
33
+ background: #000;
34
+ box-shadow: 0 2px 40px rgba(0, 0, 0, 0.6);
35
+ }
36
+
37
+ /* ─── Controls Section ─── */
38
+ .framely-controls {
39
+ background: var(--zinc-900, #18181b);
40
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
41
+ padding: 0;
42
+ }
43
+
44
+ /* ─── Timeline Track ─── */
45
+ .framely-timeline-track {
46
+ position: relative;
47
+ height: 28px;
48
+ background: var(--zinc-800, #27272a);
49
+ cursor: pointer;
50
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
51
+ }
52
+
53
+ .framely-timeline-track:hover {
54
+ background: var(--zinc-700, #3f3f46);
55
+ }
56
+
57
+ .framely-timeline-progress {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ height: 100%;
62
+ background: linear-gradient(90deg, var(--indigo-500, #6366f1), var(--indigo-400, #818cf8));
63
+ opacity: 0.3;
64
+ transition: width 0.03s linear;
65
+ pointer-events: none;
66
+ }
67
+
68
+ .framely-timeline-thumb {
69
+ position: absolute;
70
+ top: 0;
71
+ width: 2px;
72
+ height: 100%;
73
+ background: var(--indigo-400, #818cf8);
74
+ transform: translateX(-1px);
75
+ pointer-events: none;
76
+ box-shadow: 0 0 8px rgba(129, 140, 248, 0.5);
77
+ }
78
+
79
+ .framely-timeline-thumb::after {
80
+ content: '';
81
+ position: absolute;
82
+ top: -2px;
83
+ left: 50%;
84
+ transform: translateX(-50%);
85
+ width: 10px;
86
+ height: 10px;
87
+ background: var(--indigo-400, #818cf8);
88
+ border-radius: 2px;
89
+ clip-path: polygon(0% 0%, 100% 0%, 100% 60%, 50% 100%, 0% 60%);
90
+ }
91
+
92
+ .framely-timeline-markers {
93
+ position: absolute;
94
+ top: 0;
95
+ left: 0;
96
+ right: 0;
97
+ bottom: 0;
98
+ pointer-events: none;
99
+ }
100
+
101
+ .framely-timeline-marker {
102
+ position: absolute;
103
+ top: 0;
104
+ width: 1px;
105
+ height: 6px;
106
+ background: rgba(255, 255, 255, 0.1);
107
+ }
108
+
109
+ /* ─── Transport Controls ─── */
110
+ .framely-transport {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ padding: var(--space-2, 8px) var(--space-4, 16px);
115
+ gap: var(--space-3, 12px);
116
+ }
117
+
118
+ .framely-transport-left {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: var(--space-1, 4px);
122
+ }
123
+
124
+ .framely-transport-right {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: var(--space-4, 16px);
128
+ }
129
+
130
+ /* ─── Buttons ─── */
131
+ .framely-btn {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ border: none;
136
+ background: transparent;
137
+ color: var(--zinc-400, rgba(255, 255, 255, 0.6));
138
+ cursor: pointer;
139
+ border-radius: var(--radius-md, 6px);
140
+ transition: all var(--transition-fast, 0.15s ease);
141
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
142
+ }
143
+
144
+ .framely-btn:hover {
145
+ background: rgba(255, 255, 255, 0.08);
146
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
147
+ }
148
+
149
+ .framely-btn:active {
150
+ background: rgba(255, 255, 255, 0.12);
151
+ transform: scale(0.95);
152
+ }
153
+
154
+ .framely-btn-transport {
155
+ width: 32px;
156
+ height: 32px;
157
+ padding: 0;
158
+ }
159
+
160
+ .framely-btn-play {
161
+ width: 40px;
162
+ height: 40px;
163
+ padding: 0;
164
+ background: rgba(99, 102, 241, 0.15);
165
+ color: var(--indigo-400, #818cf8);
166
+ margin: 0 var(--space-1, 4px);
167
+ }
168
+
169
+ .framely-btn-play:hover {
170
+ background: rgba(99, 102, 241, 0.25);
171
+ color: var(--indigo-300, #a5b4fc);
172
+ }
173
+
174
+ /* ─── Info Labels ─── */
175
+ .framely-time {
176
+ color: var(--zinc-200, rgba(255, 255, 255, 0.8));
177
+ font-size: var(--text-xs, 12px);
178
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
179
+ font-variant-numeric: tabular-nums;
180
+ letter-spacing: 0.02em;
181
+ }
182
+
183
+ .framely-frame-info {
184
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
185
+ font-size: 11px;
186
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
187
+ font-variant-numeric: tabular-nums;
188
+ }
189
+
190
+ .framely-fps-badge {
191
+ font-size: 10px;
192
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
193
+ color: var(--zinc-600, rgba(255, 255, 255, 0.3));
194
+ background: rgba(255, 255, 255, 0.05);
195
+ padding: 2px var(--space-2, 8px);
196
+ border-radius: var(--radius-sm, 4px);
197
+ letter-spacing: 0.05em;
198
+ text-transform: uppercase;
199
+ }
@@ -0,0 +1,355 @@
1
+ import { useRef, useEffect, useCallback, useState } from 'react';
2
+ import { TimelineProvider, useTimeline, Sequence, Video, Img } from '@codellyson/framely';
3
+ import './Player.css';
4
+
5
+ /**
6
+ * Player renders a composition inside a preview viewport with
7
+ * transport controls (play, pause, seek, scrub).
8
+ */
9
+ export function Player({
10
+ component: Component,
11
+ compositionWidth = 1920,
12
+ compositionHeight = 1080,
13
+ fps = 30,
14
+ durationInFrames = 300,
15
+ inputProps = {},
16
+ sequences = [],
17
+ style,
18
+ className,
19
+ }) {
20
+ return (
21
+ <TimelineProvider
22
+ fps={fps}
23
+ width={compositionWidth}
24
+ height={compositionHeight}
25
+ durationInFrames={durationInFrames}
26
+ >
27
+ <PlayerView
28
+ component={Component}
29
+ compositionWidth={compositionWidth}
30
+ compositionHeight={compositionHeight}
31
+ inputProps={inputProps}
32
+ sequences={sequences}
33
+ style={style}
34
+ className={className}
35
+ />
36
+ </TimelineProvider>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * PlayerView - Renders either a composition component or timeline sequences.
42
+ *
43
+ * If `component` is provided, renders the composition component.
44
+ * If `sequences` is provided (and no component), renders sequences as timeline clips.
45
+ */
46
+ export function PlayerView({
47
+ component: Component,
48
+ compositionWidth,
49
+ compositionHeight,
50
+ inputProps,
51
+ sequences = [],
52
+ style,
53
+ className,
54
+ }) {
55
+ const {
56
+ frame,
57
+ fps,
58
+ durationInFrames,
59
+ playing,
60
+ setFrame,
61
+ toggle,
62
+ play: _play,
63
+ pause: _pause,
64
+ } = useTimeline();
65
+
66
+ const containerRef = useRef(null);
67
+ const [scale, setScale] = useState(1);
68
+ const [isDragging, setIsDragging] = useState(false);
69
+
70
+ // Fit the composition into the available space
71
+ useEffect(() => {
72
+ const container = containerRef.current;
73
+ if (!container) return;
74
+
75
+ const observer = new ResizeObserver((entries) => {
76
+ const { width, height } = entries[0].contentRect;
77
+ const scaleX = width / compositionWidth;
78
+ const scaleY = height / compositionHeight;
79
+ setScale(Math.min(scaleX, scaleY));
80
+ });
81
+
82
+ observer.observe(container);
83
+ return () => observer.disconnect();
84
+ }, [compositionWidth, compositionHeight]);
85
+
86
+ // Keyboard shortcuts
87
+ useEffect(() => {
88
+ const handleKey = (e) => {
89
+ if (e.target.tagName === 'INPUT') return;
90
+ switch (e.code) {
91
+ case 'Space':
92
+ e.preventDefault();
93
+ toggle();
94
+ break;
95
+ case 'ArrowLeft':
96
+ e.preventDefault();
97
+ setFrame(Math.max(0, frame - (e.shiftKey ? 10 : 1)));
98
+ break;
99
+ case 'ArrowRight':
100
+ e.preventDefault();
101
+ setFrame(Math.min(durationInFrames - 1, frame + (e.shiftKey ? 10 : 1)));
102
+ break;
103
+ case 'Home':
104
+ e.preventDefault();
105
+ setFrame(0);
106
+ break;
107
+ case 'End':
108
+ e.preventDefault();
109
+ setFrame(durationInFrames - 1);
110
+ break;
111
+ }
112
+ };
113
+ window.addEventListener('keydown', handleKey);
114
+ return () => window.removeEventListener('keydown', handleKey);
115
+ }, [frame, durationInFrames, toggle, setFrame]);
116
+
117
+ // Timeline scrubbing
118
+ const handleTimelineInteraction = useCallback(
119
+ (e) => {
120
+ const rect = e.currentTarget.getBoundingClientRect();
121
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
122
+ const progress = x / rect.width;
123
+ setFrame(Math.round(progress * (durationInFrames - 1)));
124
+ },
125
+ [durationInFrames, setFrame]
126
+ );
127
+
128
+ const handleMouseDown = useCallback(
129
+ (e) => {
130
+ setIsDragging(true);
131
+ handleTimelineInteraction(e);
132
+ },
133
+ [handleTimelineInteraction]
134
+ );
135
+
136
+ useEffect(() => {
137
+ if (!isDragging) return;
138
+ const handleMove = (e) => {
139
+ const timeline = document.querySelector('.framely-timeline-track');
140
+ if (!timeline) return;
141
+ const rect = timeline.getBoundingClientRect();
142
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
143
+ const progress = x / rect.width;
144
+ setFrame(Math.round(progress * (durationInFrames - 1)));
145
+ };
146
+ const handleUp = () => setIsDragging(false);
147
+ window.addEventListener('mousemove', handleMove);
148
+ window.addEventListener('mouseup', handleUp);
149
+ return () => {
150
+ window.removeEventListener('mousemove', handleMove);
151
+ window.removeEventListener('mouseup', handleUp);
152
+ };
153
+ }, [isDragging, durationInFrames, setFrame]);
154
+
155
+ const currentTime = (frame / fps).toFixed(2);
156
+ const totalTime = ((durationInFrames - 1) / fps).toFixed(2);
157
+ const progress = durationInFrames > 1 ? (frame / (durationInFrames - 1)) * 100 : 0;
158
+
159
+ return (
160
+ <div className={`framely-player ${className || ''}`} style={style}>
161
+ {/* Viewport */}
162
+ <div className="framely-viewport" ref={containerRef}>
163
+ <div
164
+ className="framely-canvas"
165
+ style={{
166
+ width: compositionWidth,
167
+ height: compositionHeight,
168
+ transform: `scale(${scale})`,
169
+ transformOrigin: 'center center',
170
+ position: 'relative',
171
+ background: '#000',
172
+ }}
173
+ >
174
+ {/* Render composition component if provided */}
175
+ {Component && <Component {...inputProps} />}
176
+
177
+ {/* Render timeline sequences if no component */}
178
+ {!Component && sequences.map((seq, index) => (
179
+ <Sequence
180
+ key={seq.id || `seq-${index}`}
181
+ from={seq.from}
182
+ durationInFrames={seq.durationInFrames}
183
+ name={seq.name}
184
+ style={{ zIndex: index + 1 }}
185
+ >
186
+ {/* Video asset */}
187
+ {seq.assetType === 'video' && seq.assetPath && (
188
+ <Video
189
+ src={seq.assetPath}
190
+ style={{
191
+ width: '100%',
192
+ height: '100%',
193
+ objectFit: 'cover',
194
+ }}
195
+ />
196
+ )}
197
+ {/* Image asset */}
198
+ {seq.assetType === 'image' && seq.assetPath && (
199
+ <Img
200
+ src={seq.assetPath}
201
+ style={{
202
+ width: '100%',
203
+ height: '100%',
204
+ objectFit: 'cover',
205
+ }}
206
+ />
207
+ )}
208
+ {/* Solid color or default scene */}
209
+ {!seq.assetPath && (
210
+ <div
211
+ style={{
212
+ width: '100%',
213
+ height: '100%',
214
+ background: seq.color || '#1a1a2e',
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ color: 'white',
219
+ fontSize: 48,
220
+ fontFamily: 'system-ui, sans-serif',
221
+ fontWeight: 700,
222
+ }}
223
+ >
224
+ {seq.name || `Scene ${index + 1}`}
225
+ </div>
226
+ )}
227
+ </Sequence>
228
+ ))}
229
+
230
+ {/* Empty state for editor mode */}
231
+ {!Component && sequences.length === 0 && (
232
+ <div
233
+ style={{
234
+ position: 'absolute',
235
+ inset: 0,
236
+ display: 'flex',
237
+ alignItems: 'center',
238
+ justifyContent: 'center',
239
+ color: 'rgba(255,255,255,0.4)',
240
+ fontSize: 16,
241
+ fontFamily: 'system-ui, sans-serif',
242
+ textAlign: 'center',
243
+ padding: 40,
244
+ }}
245
+ >
246
+ Add assets from the panel to start creating your video
247
+ </div>
248
+ )}
249
+ </div>
250
+ </div>
251
+
252
+ {/* Controls */}
253
+ <div className="framely-controls">
254
+ {/* Timeline */}
255
+ <div
256
+ className="framely-timeline-track"
257
+ onMouseDown={handleMouseDown}
258
+ >
259
+ <div
260
+ className="framely-timeline-progress"
261
+ style={{ width: `${progress}%` }}
262
+ />
263
+ <div
264
+ className="framely-timeline-thumb"
265
+ style={{ left: `${progress}%` }}
266
+ />
267
+ {/* Frame markers */}
268
+ <div className="framely-timeline-markers">
269
+ {Array.from({ length: 11 }, (_, i) => (
270
+ <div
271
+ key={i}
272
+ className="framely-timeline-marker"
273
+ style={{ left: `${i * 10}%` }}
274
+ />
275
+ ))}
276
+ </div>
277
+ </div>
278
+
279
+ {/* Transport */}
280
+ <div className="framely-transport">
281
+ <div className="framely-transport-left">
282
+ <button
283
+ className="framely-btn framely-btn-transport"
284
+ onClick={() => setFrame(0)}
285
+ title="Go to start (Home)"
286
+ >
287
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
288
+ <rect x="2" y="3" width="2" height="10" />
289
+ <path d="M13 3L6 8l7 5V3z" />
290
+ </svg>
291
+ </button>
292
+
293
+ <button
294
+ className="framely-btn framely-btn-transport"
295
+ onClick={() => setFrame(Math.max(0, frame - 1))}
296
+ title="Previous frame (←)"
297
+ >
298
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
299
+ <path d="M12 3L5 8l7 5V3z" />
300
+ </svg>
301
+ </button>
302
+
303
+ <button
304
+ className="framely-btn framely-btn-play"
305
+ onClick={toggle}
306
+ title="Play/Pause (Space)"
307
+ >
308
+ {playing ? (
309
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
310
+ <rect x="3" y="2" width="4" height="12" rx="1" />
311
+ <rect x="9" y="2" width="4" height="12" rx="1" />
312
+ </svg>
313
+ ) : (
314
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
315
+ <path d="M4 2l10 6-10 6V2z" />
316
+ </svg>
317
+ )}
318
+ </button>
319
+
320
+ <button
321
+ className="framely-btn framely-btn-transport"
322
+ onClick={() => setFrame(Math.min(durationInFrames - 1, frame + 1))}
323
+ title="Next frame (→)"
324
+ >
325
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
326
+ <path d="M4 3l7 5-7 5V3z" />
327
+ </svg>
328
+ </button>
329
+
330
+ <button
331
+ className="framely-btn framely-btn-transport"
332
+ onClick={() => setFrame(durationInFrames - 1)}
333
+ title="Go to end (End)"
334
+ >
335
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
336
+ <path d="M3 3l7 5-7 5V3z" />
337
+ <rect x="12" y="3" width="2" height="10" />
338
+ </svg>
339
+ </button>
340
+ </div>
341
+
342
+ <div className="framely-transport-right">
343
+ <span className="framely-time">
344
+ {currentTime}s / {totalTime}s
345
+ </span>
346
+ <span className="framely-frame-info">
347
+ Frame {frame} / {durationInFrames - 1}
348
+ </span>
349
+ <span className="framely-fps-badge">{fps} fps</span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ );
355
+ }