@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,186 @@
1
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from '@codellyson/framely';
2
+
3
+ /**
4
+ * YouTube Subscribe Animation Template
5
+ */
6
+ export function SubscribeAnimation({
7
+ channelName = 'Your Channel',
8
+ buttonColor = '#FF0000',
9
+ showBell = true,
10
+ }) {
11
+ const frame = useCurrentFrame();
12
+ const { fps } = useVideoConfig();
13
+
14
+ // Button entrance
15
+ const buttonSpring = spring({ frame, fps, config: { damping: 12, stiffness: 150 } });
16
+ const buttonScale = interpolate(buttonSpring, [0, 1], [0, 1]);
17
+ const buttonY = interpolate(buttonSpring, [0, 1], [50, 0]);
18
+
19
+ // Subscribe text morph
20
+ const textProgress = interpolate(frame, [40, 50], [0, 1], { extrapolateRight: 'clamp' });
21
+ const showSubscribed = textProgress > 0.5;
22
+
23
+ // Bell animation
24
+ const bellDelay = 60;
25
+ const bellSpring = spring({ frame: frame - bellDelay, fps, config: { damping: 8, stiffness: 200 } });
26
+ const bellScale = interpolate(bellSpring, [0, 1], [0, 1]);
27
+ const bellRotate = interpolate(
28
+ frame - bellDelay,
29
+ [0, 10, 20, 30, 40],
30
+ [0, -20, 20, -10, 0],
31
+ { extrapolateRight: 'clamp' }
32
+ );
33
+
34
+ // Cursor animation
35
+ const cursorX = interpolate(frame, [0, 30, 35], [400, 0, 0], { extrapolateRight: 'clamp' });
36
+ const cursorY = interpolate(frame, [0, 30, 35], [200, 0, 5], { extrapolateRight: 'clamp' });
37
+ const cursorOpacity = interpolate(frame, [0, 10, 100, 120], [0, 1, 1, 0], { extrapolateRight: 'clamp' });
38
+ const cursorClick = frame >= 35 && frame <= 45;
39
+
40
+ // Channel name
41
+ const nameOpacity = interpolate(frame, [70, 90], [0, 1], { extrapolateRight: 'clamp' });
42
+ const nameY = interpolate(frame, [70, 90], [20, 0], { extrapolateRight: 'clamp' });
43
+
44
+ return (
45
+ <AbsoluteFill
46
+ style={{
47
+ background: 'linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%)',
48
+ display: 'flex',
49
+ flexDirection: 'column',
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ gap: 30,
53
+ }}
54
+ >
55
+ {/* Subscribe Button */}
56
+ <div
57
+ style={{
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ gap: 20,
61
+ transform: `scale(${buttonScale}) translateY(${buttonY}px)`,
62
+ }}
63
+ >
64
+ <button
65
+ style={{
66
+ padding: '16px 32px',
67
+ fontSize: 24,
68
+ fontWeight: 600,
69
+ color: '#fff',
70
+ background: showSubscribed ? '#666' : buttonColor,
71
+ border: 'none',
72
+ borderRadius: 4,
73
+ cursor: 'pointer',
74
+ transition: 'background 0.2s',
75
+ minWidth: 200,
76
+ }}
77
+ >
78
+ {showSubscribed ? 'SUBSCRIBED' : 'SUBSCRIBE'}
79
+ </button>
80
+
81
+ {/* Bell */}
82
+ {showBell && (
83
+ <div
84
+ style={{
85
+ width: 50,
86
+ height: 50,
87
+ background: 'rgba(255, 255, 255, 0.1)',
88
+ borderRadius: '50%',
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ transform: `scale(${bellScale}) rotate(${bellRotate}deg)`,
93
+ }}
94
+ >
95
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="#fff">
96
+ <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z" />
97
+ </svg>
98
+ </div>
99
+ )}
100
+ </div>
101
+
102
+ {/* Channel Name */}
103
+ <div
104
+ style={{
105
+ opacity: nameOpacity,
106
+ transform: `translateY(${nameY}px)`,
107
+ textAlign: 'center',
108
+ }}
109
+ >
110
+ <p
111
+ style={{
112
+ fontSize: 18,
113
+ color: 'rgba(255, 255, 255, 0.6)',
114
+ margin: 0,
115
+ }}
116
+ >
117
+ Thanks for subscribing to
118
+ </p>
119
+ <p
120
+ style={{
121
+ fontSize: 28,
122
+ fontWeight: 600,
123
+ color: '#fff',
124
+ margin: '8px 0 0',
125
+ }}
126
+ >
127
+ {channelName}
128
+ </p>
129
+ </div>
130
+
131
+ {/* Cursor */}
132
+ <div
133
+ style={{
134
+ position: 'absolute',
135
+ left: '50%',
136
+ top: '50%',
137
+ transform: `translate(${cursorX}px, ${cursorY}px)`,
138
+ opacity: cursorOpacity,
139
+ pointerEvents: 'none',
140
+ }}
141
+ >
142
+ <svg
143
+ width="32"
144
+ height="32"
145
+ viewBox="0 0 24 24"
146
+ style={{
147
+ transform: cursorClick ? 'scale(0.9)' : 'scale(1)',
148
+ transition: 'transform 0.1s',
149
+ }}
150
+ >
151
+ <path
152
+ d="M4 4l16 12-6.5 1.5L12 24 4 4z"
153
+ fill="#fff"
154
+ stroke="#000"
155
+ strokeWidth="1"
156
+ />
157
+ </svg>
158
+ </div>
159
+
160
+ {/* Particles on subscribe */}
161
+ {showSubscribed && [...Array(12)].map((_, i) => {
162
+ const angle = (i / 12) * Math.PI * 2;
163
+ const distance = interpolate(frame - 45, [0, 30], [0, 150], { extrapolateRight: 'clamp' });
164
+ const particleOpacity = interpolate(frame - 45, [0, 20, 30], [0, 1, 0], { extrapolateRight: 'clamp' });
165
+ return (
166
+ <div
167
+ key={i}
168
+ style={{
169
+ position: 'absolute',
170
+ left: '50%',
171
+ top: '45%',
172
+ width: 8,
173
+ height: 8,
174
+ background: buttonColor,
175
+ borderRadius: '50%',
176
+ transform: `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px)`,
177
+ opacity: particleOpacity,
178
+ }}
179
+ />
180
+ );
181
+ })}
182
+ </AbsoluteFill>
183
+ );
184
+ }
185
+
186
+ export default SubscribeAnimation;
@@ -0,0 +1,58 @@
1
+ import type { Template } from '@codellyson/framely';
2
+
3
+ export interface TemplateCardProps {
4
+ template: Template;
5
+ onClick: () => void;
6
+ }
7
+
8
+ /**
9
+ * Template card component for the marketplace grid
10
+ */
11
+ export function TemplateCard({ template, onClick }: TemplateCardProps) {
12
+ return (
13
+ <button className="template-card" onClick={onClick} type="button">
14
+ <div className="template-card-preview">
15
+ <img
16
+ src={template.preview.thumbnail}
17
+ alt={template.name}
18
+ loading="lazy"
19
+ />
20
+ {template.featured && (
21
+ <span className="template-card-badge">Featured</span>
22
+ )}
23
+ </div>
24
+ <div className="template-card-info">
25
+ <h3 className="template-card-name">{template.name}</h3>
26
+ <p className="template-card-meta">
27
+ {template.width}x{template.height} &bull; {template.fps}fps
28
+ </p>
29
+ <div className="template-card-footer">
30
+ <span className="template-card-author">
31
+ {template.author.verified && (
32
+ <svg
33
+ className="template-verified-icon"
34
+ width="12"
35
+ height="12"
36
+ viewBox="0 0 24 24"
37
+ fill="currentColor"
38
+ >
39
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
40
+ </svg>
41
+ )}
42
+ {template.author.name}
43
+ </span>
44
+ {template.rating && (
45
+ <span className="template-card-rating">
46
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
47
+ <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
48
+ </svg>
49
+ {template.rating.toFixed(1)}
50
+ </span>
51
+ )}
52
+ </div>
53
+ </div>
54
+ </button>
55
+ );
56
+ }
57
+
58
+ export default TemplateCard;
@@ -0,0 +1,97 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { templatesApi, CATEGORY_LABELS } from '@codellyson/framely';
3
+ import type { TemplateCategory, TemplatesFilterParams, CategoryCount } from '@codellyson/framely';
4
+
5
+ export interface TemplateFiltersProps {
6
+ filters: TemplatesFilterParams;
7
+ onChange: (filters: Partial<TemplatesFilterParams>) => void;
8
+ }
9
+
10
+ /**
11
+ * Filter controls for the templates marketplace
12
+ */
13
+ export function TemplateFilters({ filters, onChange }: TemplateFiltersProps) {
14
+ const [categories, setCategories] = useState<CategoryCount[]>([]);
15
+ const [searchValue, setSearchValue] = useState(filters.search || '');
16
+
17
+ // Fetch categories on mount
18
+ useEffect(() => {
19
+ templatesApi.getCategories().then(setCategories).catch(console.error);
20
+ }, []);
21
+
22
+ // Debounced search
23
+ useEffect(() => {
24
+ const timeout = setTimeout(() => {
25
+ if (searchValue !== filters.search) {
26
+ onChange({ search: searchValue || undefined });
27
+ }
28
+ }, 300);
29
+ return () => clearTimeout(timeout);
30
+ }, [searchValue, filters.search, onChange]);
31
+
32
+ return (
33
+ <div className="templates-filters">
34
+ {/* Search */}
35
+ <div className="filter-search">
36
+ <svg
37
+ className="filter-search-icon"
38
+ width="16"
39
+ height="16"
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ strokeWidth="2"
44
+ >
45
+ <circle cx="11" cy="11" r="8" />
46
+ <path d="M21 21l-4.35-4.35" />
47
+ </svg>
48
+ <input
49
+ type="text"
50
+ placeholder="Search templates..."
51
+ value={searchValue}
52
+ onChange={(e) => setSearchValue(e.target.value)}
53
+ />
54
+ </div>
55
+
56
+ {/* Categories */}
57
+ <div className="filter-categories">
58
+ <button
59
+ type="button"
60
+ className={`filter-category ${!filters.category ? 'active' : ''}`}
61
+ onClick={() => onChange({ category: undefined })}
62
+ >
63
+ All
64
+ </button>
65
+ {categories.map(({ category, count }) => (
66
+ <button
67
+ key={category}
68
+ type="button"
69
+ className={`filter-category ${filters.category === category ? 'active' : ''}`}
70
+ onClick={() =>
71
+ onChange({ category: filters.category === category ? undefined : category })
72
+ }
73
+ >
74
+ {CATEGORY_LABELS[category as TemplateCategory] || category}
75
+ <span className="filter-category-count">{count}</span>
76
+ </button>
77
+ ))}
78
+ </div>
79
+
80
+ {/* Sort */}
81
+ <div className="filter-sort">
82
+ <select
83
+ value={filters.sortBy || 'newest'}
84
+ onChange={(e) =>
85
+ onChange({ sortBy: e.target.value as TemplatesFilterParams['sortBy'] })
86
+ }
87
+ >
88
+ <option value="newest">Newest</option>
89
+ <option value="popular">Most Popular</option>
90
+ <option value="rating">Highest Rated</option>
91
+ </select>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export default TemplateFilters;
@@ -0,0 +1,196 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { Template } from '@codellyson/framely';
3
+ import { CATEGORY_LABELS } from '@codellyson/framely';
4
+
5
+ export interface TemplatePreviewDialogProps {
6
+ open: boolean;
7
+ template: Template | null;
8
+ onClose: () => void;
9
+ onUseTemplate: () => void;
10
+ }
11
+
12
+ /**
13
+ * Dialog for previewing a template before using it
14
+ */
15
+ export function TemplatePreviewDialog({
16
+ open,
17
+ template,
18
+ onClose,
19
+ onUseTemplate,
20
+ }: TemplatePreviewDialogProps) {
21
+ const dialogRef = useRef<HTMLDivElement>(null);
22
+
23
+ // Escape key handler
24
+ useEffect(() => {
25
+ if (!open) return;
26
+ const handleKeyDown = (e: KeyboardEvent) => {
27
+ if (e.key === 'Escape') onClose();
28
+ };
29
+ document.addEventListener('keydown', handleKeyDown);
30
+ return () => document.removeEventListener('keydown', handleKeyDown);
31
+ }, [open, onClose]);
32
+
33
+ // Focus trap
34
+ useEffect(() => {
35
+ if (!open || !dialogRef.current) return;
36
+ const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
37
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
38
+ );
39
+ if (focusable.length > 0) {
40
+ focusable[0].focus();
41
+ }
42
+ }, [open]);
43
+
44
+ if (!open || !template) return null;
45
+
46
+ const duration = template.durationInFrames / template.fps;
47
+
48
+ return (
49
+ <div
50
+ className="template-preview-overlay"
51
+ role="dialog"
52
+ aria-modal="true"
53
+ aria-labelledby="preview-dialog-title"
54
+ onClick={(e) => {
55
+ if (e.target === e.currentTarget) onClose();
56
+ }}
57
+ >
58
+ <div ref={dialogRef} className="template-preview-dialog">
59
+ {/* Header */}
60
+ <div className="template-preview-header">
61
+ <h2 id="preview-dialog-title">{template.name}</h2>
62
+ <button
63
+ type="button"
64
+ onClick={onClose}
65
+ aria-label="Close"
66
+ className="template-dialog-close"
67
+ >
68
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
69
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
70
+ </svg>
71
+ </button>
72
+ </div>
73
+
74
+ {/* Content */}
75
+ <div className="template-preview-content">
76
+ {/* Preview Area */}
77
+ <div className="template-preview-media">
78
+ {template.preview.video ? (
79
+ <video
80
+ src={template.preview.video}
81
+ autoPlay
82
+ loop
83
+ muted
84
+ playsInline
85
+ />
86
+ ) : template.preview.preview ? (
87
+ <img src={template.preview.preview} alt={template.name} />
88
+ ) : (
89
+ <img src={template.preview.thumbnail} alt={template.name} />
90
+ )}
91
+ </div>
92
+
93
+ {/* Details Panel */}
94
+ <div className="template-preview-details">
95
+ <p className="template-preview-description">{template.description}</p>
96
+
97
+ <div className="template-preview-info-grid">
98
+ <div className="template-preview-info-item">
99
+ <span className="label">Resolution</span>
100
+ <span className="value">
101
+ {template.width} x {template.height}
102
+ </span>
103
+ </div>
104
+ <div className="template-preview-info-item">
105
+ <span className="label">FPS</span>
106
+ <span className="value">{template.fps}</span>
107
+ </div>
108
+ <div className="template-preview-info-item">
109
+ <span className="label">Duration</span>
110
+ <span className="value">{duration.toFixed(1)}s</span>
111
+ </div>
112
+ <div className="template-preview-info-item">
113
+ <span className="label">Category</span>
114
+ <span className="value">
115
+ {CATEGORY_LABELS[template.category] || template.category}
116
+ </span>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="template-preview-author">
121
+ <span className="label">Created by</span>
122
+ <div className="template-preview-author-info">
123
+ {template.author.avatar && (
124
+ <img
125
+ src={template.author.avatar}
126
+ alt=""
127
+ className="template-preview-author-avatar"
128
+ />
129
+ )}
130
+ <span className="template-preview-author-name">
131
+ {template.author.verified && (
132
+ <svg
133
+ className="template-verified-icon"
134
+ width="14"
135
+ height="14"
136
+ viewBox="0 0 24 24"
137
+ fill="currentColor"
138
+ >
139
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
140
+ </svg>
141
+ )}
142
+ {template.author.name}
143
+ </span>
144
+ </div>
145
+ </div>
146
+
147
+ {template.tags.length > 0 && (
148
+ <div className="template-preview-tags">
149
+ {template.tags.map((tag) => (
150
+ <span key={tag} className="template-preview-tag">
151
+ {tag}
152
+ </span>
153
+ ))}
154
+ </div>
155
+ )}
156
+
157
+ {template.downloads !== undefined && (
158
+ <div className="template-preview-stats">
159
+ <span className="template-preview-stat">
160
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
161
+ <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
162
+ </svg>
163
+ {template.downloads.toLocaleString()} downloads
164
+ </span>
165
+ {template.rating && (
166
+ <span className="template-preview-stat">
167
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
168
+ <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
169
+ </svg>
170
+ {template.rating.toFixed(1)}
171
+ </span>
172
+ )}
173
+ </div>
174
+ )}
175
+ </div>
176
+ </div>
177
+
178
+ {/* Footer */}
179
+ <div className="template-preview-footer">
180
+ <button type="button" onClick={onClose} className="template-btn-secondary">
181
+ Cancel
182
+ </button>
183
+ <button
184
+ type="button"
185
+ onClick={onUseTemplate}
186
+ className="template-btn-primary"
187
+ >
188
+ Use Template
189
+ </button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ export default TemplatePreviewDialog;