@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
package/studio/App.jsx ADDED
@@ -0,0 +1,185 @@
1
+ import { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { TimelineProvider, getCompositions } from '@codellyson/framely';
3
+ import { CompositionsView } from './CompositionsView';
4
+ import { getTemplateComponent } from './templates';
5
+ import './styles/design-system.css';
6
+ import './App.css';
7
+
8
+ /**
9
+ * Render mode — bare composition at native resolution.
10
+ * The renderer controls frames via window.__setFrame()
11
+ */
12
+ function RenderView({ composition, inputProps = {} }) {
13
+ const { component: Component, width, height, fps, durationInFrames, defaultProps } = composition;
14
+
15
+ // Merge defaultProps with inputProps (inputProps take priority)
16
+ const finalProps = { ...defaultProps, ...inputProps };
17
+
18
+ return (
19
+ <div
20
+ id="render-container"
21
+ style={{
22
+ width,
23
+ height,
24
+ position: 'relative',
25
+ overflow: 'hidden',
26
+ background: '#000',
27
+ }}
28
+ >
29
+ <TimelineProvider
30
+ fps={fps}
31
+ width={width}
32
+ height={height}
33
+ durationInFrames={durationInFrames}
34
+ renderMode={true}
35
+ >
36
+ <Component {...finalProps} />
37
+ </TimelineProvider>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Framely - Programmatic Video Framework
44
+ *
45
+ * Browse, preview, and render code-defined compositions.
46
+ */
47
+ function App() {
48
+ const params = new URLSearchParams(window.location.search);
49
+ const renderMode = params.get('renderMode') === 'true';
50
+ const initialCompositionId = params.get('composition') || 'sample-video';
51
+
52
+ // Parse props from URL for render mode
53
+ const urlProps = useMemo(() => {
54
+ const searchParams = new URLSearchParams(window.location.search);
55
+ const propsParam = searchParams.get('props');
56
+ if (propsParam) {
57
+ try {
58
+ return JSON.parse(decodeURIComponent(propsParam));
59
+ } catch {
60
+ return {};
61
+ }
62
+ }
63
+ return {};
64
+ }, []);
65
+
66
+ const [selectedCompositionId, setSelectedCompositionId] = useState(initialCompositionId);
67
+ const [templateCompositions, setTemplateCompositions] = useState({});
68
+
69
+ // Load all code-defined compositions
70
+ const codeCompositions = useMemo(() => {
71
+ const map = getCompositions();
72
+ const obj = {};
73
+ for (const [id, comp] of map) {
74
+ obj[id] = { ...comp };
75
+ }
76
+ return obj;
77
+ }, []);
78
+
79
+ // Merge code compositions with template compositions
80
+ const compositions = useMemo(() => ({
81
+ ...codeCompositions,
82
+ ...templateCompositions,
83
+ }), [codeCompositions, templateCompositions]);
84
+
85
+ // Handle using a template from the marketplace
86
+ const handleUseTemplate = useCallback((template, customId, customProps) => {
87
+ // Get the actual component from the template registry
88
+ const component = getTemplateComponent(template.id);
89
+
90
+ const newComposition = {
91
+ id: customId,
92
+ component: component,
93
+ width: template.width,
94
+ height: template.height,
95
+ fps: template.fps,
96
+ durationInFrames: template.durationInFrames,
97
+ defaultProps: customProps || { ...template.defaultProps },
98
+ folderPath: ['Templates'],
99
+ _isTemplate: true,
100
+ _templateId: template.id,
101
+ _templateName: template.name,
102
+ };
103
+
104
+ setTemplateCompositions(prev => ({
105
+ ...prev,
106
+ [customId]: newComposition,
107
+ }));
108
+
109
+ // Select the newly added composition
110
+ setSelectedCompositionId(customId);
111
+ }, []);
112
+
113
+ // Sync composition to URL
114
+ useEffect(() => {
115
+ if (renderMode) return;
116
+
117
+ const url = new URL(window.location.href);
118
+ if (selectedCompositionId) {
119
+ url.searchParams.set('composition', selectedCompositionId);
120
+ }
121
+ window.history.replaceState({}, '', url);
122
+ }, [selectedCompositionId, renderMode]);
123
+
124
+ const composition = compositions[selectedCompositionId];
125
+
126
+ // Render mode: bare composition at native resolution (for Playwright screenshots)
127
+ if (renderMode) {
128
+ // Check for template ID (used when rendering marketplace templates)
129
+ const templateId = params.get('templateId');
130
+
131
+ if (!composition) {
132
+ // Try to load as a template for render mode
133
+ // Use templateId if provided, otherwise try the composition ID
134
+ const lookupId = templateId || selectedCompositionId;
135
+ const templateComponent = getTemplateComponent(lookupId);
136
+ if (templateComponent) {
137
+ const templateComp = {
138
+ id: selectedCompositionId,
139
+ component: templateComponent,
140
+ width: parseInt(params.get('width') || '1920', 10),
141
+ height: parseInt(params.get('height') || '1080', 10),
142
+ fps: parseInt(params.get('fps') || '30', 10),
143
+ durationInFrames: parseInt(params.get('durationInFrames') || '150', 10),
144
+ defaultProps: urlProps,
145
+ };
146
+ return <RenderView composition={templateComp} inputProps={urlProps} />;
147
+ }
148
+ return <div style={{ color: 'white', padding: 40 }}>Composition "{selectedCompositionId}" not found.</div>;
149
+ }
150
+ return <RenderView composition={composition} inputProps={urlProps} />;
151
+ }
152
+
153
+ // Studio mode: browse and preview compositions
154
+ return (
155
+ <div className="framely-editor">
156
+ <header className="framely-header">
157
+ <div className="framely-logo">
158
+ <svg className="framely-logo-icon" width="28" height="28" viewBox="0 0 32 32" fill="none">
159
+ <defs>
160
+ <linearGradient id="logo-bg" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
161
+ <stop offset="0%" stopColor="#6366f1"/>
162
+ <stop offset="100%" stopColor="#7c3aed"/>
163
+ </linearGradient>
164
+ </defs>
165
+ <rect width="32" height="32" rx="8" fill="url(#logo-bg)"/>
166
+ <polygon points="12 8 24 16 12 24" fill="white"/>
167
+ </svg>
168
+ <span className="framely-logo-text">Framely</span>
169
+ </div>
170
+ <div className="framely-header-tagline">
171
+ Programmatic Video Creation
172
+ </div>
173
+ </header>
174
+
175
+ <CompositionsView
176
+ compositions={compositions}
177
+ selectedId={selectedCompositionId}
178
+ onSelectComposition={setSelectedCompositionId}
179
+ onUseTemplate={handleUseTemplate}
180
+ />
181
+ </div>
182
+ );
183
+ }
184
+
185
+ export default App;
@@ -0,0 +1,399 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ Framely Studio - CompositionsView
3
+ Uses design system from design-system.css
4
+ ═══════════════════════════════════════════════════════════════════════════ */
5
+
6
+ /* CompositionsView Layout */
7
+ .compositions-view {
8
+ display: flex;
9
+ flex: 1;
10
+ overflow: hidden;
11
+ background: var(--zinc-950, #0a0a0f);
12
+ font-family: var(--font-sans);
13
+ }
14
+
15
+ /* Sidebar */
16
+ .compositions-sidebar {
17
+ width: 260px;
18
+ background: var(--zinc-900, #111113);
19
+ border-right: 1px solid var(--framely-border, rgba(255, 255, 255, 0.06));
20
+ display: flex;
21
+ flex-direction: column;
22
+ overflow: hidden;
23
+ }
24
+
25
+ /* Tab Switcher */
26
+ .compositions-tabs {
27
+ display: flex;
28
+ padding: var(--space-2, 8px);
29
+ gap: var(--space-1, 4px);
30
+ border-bottom: 1px solid var(--framely-border, rgba(255, 255, 255, 0.06));
31
+ }
32
+
33
+ .compositions-tab {
34
+ flex: 1;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ gap: var(--space-2, 6px);
39
+ padding: var(--space-2, 8px) var(--space-3, 12px);
40
+ background: transparent;
41
+ border: none;
42
+ border-radius: var(--radius-md, 6px);
43
+ color: var(--zinc-500, rgba(255, 255, 255, 0.5));
44
+ font-family: var(--font-sans);
45
+ font-size: var(--text-xs, 12px);
46
+ font-weight: 500;
47
+ cursor: pointer;
48
+ transition: all var(--transition-fast, 0.15s ease);
49
+ }
50
+
51
+ .compositions-tab:hover {
52
+ background: rgba(255, 255, 255, 0.04);
53
+ color: var(--zinc-300, rgba(255, 255, 255, 0.7));
54
+ }
55
+
56
+ .compositions-tab.active {
57
+ background: var(--indigo-500, #6366f1);
58
+ color: white;
59
+ }
60
+
61
+ .compositions-tab svg {
62
+ opacity: 0.7;
63
+ }
64
+
65
+ .compositions-tab.active svg {
66
+ opacity: 1;
67
+ }
68
+
69
+ .compositions-sidebar-marketplace {
70
+ flex: 1;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ padding: var(--space-5, 20px);
75
+ text-align: center;
76
+ }
77
+
78
+ .compositions-sidebar-marketplace p {
79
+ margin: 0;
80
+ font-size: var(--text-xs, 12px);
81
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
82
+ }
83
+
84
+ .compositions-sidebar-header {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ padding: var(--space-4, 16px);
89
+ border-bottom: 1px solid var(--framely-border, rgba(255, 255, 255, 0.06));
90
+ }
91
+
92
+ .compositions-sidebar-header h3 {
93
+ margin: 0;
94
+ font-size: var(--text-sm, 13px);
95
+ font-weight: 600;
96
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
97
+ letter-spacing: -0.01em;
98
+ }
99
+
100
+ .compositions-count {
101
+ background: rgba(255, 255, 255, 0.08);
102
+ padding: 2px var(--space-2, 8px);
103
+ border-radius: var(--radius-full, 10px);
104
+ font-size: 11px;
105
+ font-family: var(--font-mono);
106
+ color: var(--zinc-500, rgba(255, 255, 255, 0.5));
107
+ }
108
+
109
+ .compositions-list {
110
+ flex: 1;
111
+ overflow-y: auto;
112
+ padding: var(--space-2, 8px);
113
+ }
114
+
115
+ /* Composition Items */
116
+ .compositions-item {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: var(--space-3, 10px);
120
+ width: 100%;
121
+ padding: var(--space-3, 10px) var(--space-3, 12px);
122
+ background: transparent;
123
+ border: none;
124
+ border-radius: var(--radius-lg, 8px);
125
+ cursor: pointer;
126
+ text-align: left;
127
+ transition: background var(--transition-fast, 0.15s);
128
+ }
129
+
130
+ .compositions-item:hover {
131
+ background: rgba(255, 255, 255, 0.04);
132
+ }
133
+
134
+ .compositions-item.active {
135
+ background: rgba(99, 102, 241, 0.15);
136
+ }
137
+
138
+ .compositions-item-icon {
139
+ font-size: 16px;
140
+ flex-shrink: 0;
141
+ }
142
+
143
+ .compositions-item-info {
144
+ flex: 1;
145
+ min-width: 0;
146
+ }
147
+
148
+ .compositions-item-name {
149
+ font-size: var(--text-sm, 13px);
150
+ font-weight: 500;
151
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
152
+ white-space: nowrap;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ }
156
+
157
+ .compositions-item-meta {
158
+ font-size: 11px;
159
+ font-family: var(--font-mono);
160
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
161
+ margin-top: 2px;
162
+ }
163
+
164
+ /* Folders */
165
+ .compositions-folder {
166
+ margin-bottom: var(--space-1, 4px);
167
+ }
168
+
169
+ .compositions-folder-header {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: var(--space-2, 8px);
173
+ width: 100%;
174
+ padding: var(--space-2, 8px) var(--space-3, 12px);
175
+ background: transparent;
176
+ border: none;
177
+ border-radius: var(--radius-md, 6px);
178
+ cursor: pointer;
179
+ font-family: var(--font-sans);
180
+ font-size: var(--text-xs, 12px);
181
+ color: var(--zinc-400, rgba(255, 255, 255, 0.6));
182
+ transition: background var(--transition-fast, 0.15s);
183
+ }
184
+
185
+ .compositions-folder-header:hover {
186
+ background: rgba(255, 255, 255, 0.04);
187
+ }
188
+
189
+ .compositions-folder-icon {
190
+ font-size: 10px;
191
+ width: 12px;
192
+ }
193
+
194
+ .compositions-folder-name {
195
+ flex: 1;
196
+ text-align: left;
197
+ font-weight: 500;
198
+ }
199
+
200
+ .compositions-folder-count {
201
+ font-size: 10px;
202
+ font-family: var(--font-mono);
203
+ color: var(--zinc-600, rgba(255, 255, 255, 0.3));
204
+ }
205
+
206
+ .compositions-folder-items {
207
+ padding-left: var(--space-3, 12px);
208
+ }
209
+
210
+ /* Main Preview Area */
211
+ .compositions-main {
212
+ flex: 1;
213
+ display: flex;
214
+ flex-direction: column;
215
+ min-width: 0;
216
+ }
217
+
218
+ .compositions-empty {
219
+ flex: 1;
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
224
+ font-size: var(--text-sm, 14px);
225
+ }
226
+
227
+ /* Info Panel */
228
+ .compositions-info {
229
+ width: 280px;
230
+ background: var(--zinc-900, #111113);
231
+ border-left: 1px solid var(--framely-border, rgba(255, 255, 255, 0.06));
232
+ padding: var(--space-5, 20px);
233
+ overflow-y: auto;
234
+ }
235
+
236
+ .compositions-info-header {
237
+ margin-bottom: var(--space-6, 24px);
238
+ }
239
+
240
+ .compositions-info-header h2 {
241
+ margin: 0;
242
+ font-size: var(--text-base, 16px);
243
+ font-weight: 600;
244
+ color: var(--zinc-100, rgba(255, 255, 255, 0.95));
245
+ word-break: break-word;
246
+ letter-spacing: -0.01em;
247
+ }
248
+
249
+ .compositions-info-folder {
250
+ display: block;
251
+ margin-top: var(--space-2, 6px);
252
+ font-size: var(--text-xs, 12px);
253
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
254
+ }
255
+
256
+ .compositions-info-section {
257
+ margin-bottom: var(--space-5, 20px);
258
+ }
259
+
260
+ .compositions-info-section h4 {
261
+ margin: 0 0 var(--space-3, 10px);
262
+ font-size: 11px;
263
+ font-weight: 600;
264
+ text-transform: uppercase;
265
+ letter-spacing: 0.05em;
266
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
267
+ }
268
+
269
+ .compositions-info-grid {
270
+ display: grid;
271
+ grid-template-columns: 1fr 1fr;
272
+ gap: var(--space-3, 12px);
273
+ }
274
+
275
+ .compositions-info-item {
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: 2px;
279
+ }
280
+
281
+ .compositions-info-item.full-width {
282
+ grid-column: 1 / -1;
283
+ }
284
+
285
+ .compositions-info-item .label {
286
+ font-size: 11px;
287
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
288
+ }
289
+
290
+ .compositions-info-item .value {
291
+ font-size: var(--text-sm, 14px);
292
+ font-weight: 500;
293
+ font-family: var(--font-mono);
294
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
295
+ }
296
+
297
+ .compositions-info-actions {
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: var(--space-2, 8px);
301
+ margin-top: var(--space-6, 24px);
302
+ padding-top: var(--space-5, 20px);
303
+ border-top: 1px solid var(--framely-border, rgba(255, 255, 255, 0.06));
304
+ }
305
+
306
+ .compositions-action-btn {
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ gap: var(--space-2, 8px);
311
+ padding: var(--space-3, 10px) var(--space-4, 16px);
312
+ background: rgba(255, 255, 255, 0.06);
313
+ border: 1px solid rgba(255, 255, 255, 0.08);
314
+ border-radius: var(--radius-lg, 8px);
315
+ font-family: var(--font-sans);
316
+ font-size: var(--text-sm, 13px);
317
+ font-weight: 500;
318
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
319
+ cursor: pointer;
320
+ transition: all var(--transition-fast, 0.15s);
321
+ }
322
+
323
+ .compositions-action-btn:hover {
324
+ background: rgba(255, 255, 255, 0.1);
325
+ }
326
+
327
+ .compositions-action-btn.primary {
328
+ background: var(--indigo-500, #6366f1);
329
+ border-color: var(--indigo-500, #6366f1);
330
+ }
331
+
332
+ .compositions-action-btn.primary:hover {
333
+ background: var(--indigo-600, #5558e3);
334
+ filter: brightness(1.1);
335
+ }
336
+
337
+ .compositions-info-empty {
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ height: 100%;
342
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
343
+ font-size: var(--text-sm, 13px);
344
+ }
345
+
346
+ /* Template Placeholder */
347
+ .compositions-template-placeholder {
348
+ flex: 1;
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
353
+ }
354
+
355
+ .template-placeholder-content {
356
+ text-align: center;
357
+ padding: var(--space-10, 40px);
358
+ }
359
+
360
+ .template-placeholder-icon {
361
+ font-size: 48px;
362
+ margin-bottom: var(--space-4, 16px);
363
+ }
364
+
365
+ .template-placeholder-content h3 {
366
+ margin: 0 0 var(--space-2, 8px);
367
+ font-size: var(--text-xl, 20px);
368
+ font-weight: 600;
369
+ color: var(--zinc-100, rgba(255, 255, 255, 0.9));
370
+ letter-spacing: -0.01em;
371
+ }
372
+
373
+ .template-placeholder-content > p {
374
+ margin: 0 0 var(--space-4, 16px);
375
+ font-size: var(--text-sm, 14px);
376
+ color: var(--zinc-500, rgba(255, 255, 255, 0.5));
377
+ }
378
+
379
+ .template-placeholder-info {
380
+ display: flex;
381
+ justify-content: center;
382
+ gap: var(--space-4, 16px);
383
+ margin-bottom: var(--space-5, 20px);
384
+ }
385
+
386
+ .template-placeholder-info span {
387
+ padding: var(--space-1, 4px) var(--space-3, 12px);
388
+ background: rgba(255, 255, 255, 0.08);
389
+ border-radius: var(--radius-full, 12px);
390
+ font-size: var(--text-xs, 12px);
391
+ font-family: var(--font-mono);
392
+ color: var(--zinc-300, rgba(255, 255, 255, 0.7));
393
+ }
394
+
395
+ .template-placeholder-note {
396
+ font-size: var(--text-xs, 12px);
397
+ color: var(--zinc-500, rgba(255, 255, 255, 0.4));
398
+ font-style: italic;
399
+ }