@mevdragon/vidfarm-devcli 0.15.0 → 0.16.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.
@@ -1,4 +1,201 @@
1
1
  export function isVideoPreview(url) {
2
2
  return Boolean(url && /\.(mp4|webm|ogg|mov)(\?|#|$)/i.test(url));
3
3
  }
4
+ export const POPULAR_INSPIRATION_GROUPS = [
5
+ {
6
+ label: "Talking Head & Personal",
7
+ items: [
8
+ { title: "Vlog style", url: "https://vt.tiktok.com/ZSCWTFBSe/" },
9
+ { title: "Talking head monologue", url: "https://vt.tiktok.com/ZSCWw9wqf/" },
10
+ { title: "AI Avatar presenter", url: "https://vt.tiktok.com/ZSC7RVW1o/" },
11
+ { title: "Day in the life", url: "https://www.tiktok.com/@rubygrifiths/video/7657529386131868950" },
12
+ { title: "GRWM", url: "https://vt.tiktok.com/ZSCWKv8Xb/" },
13
+ { title: "Reaction video", url: "https://vt.tiktok.com/ZSCWEF8Rv/" },
14
+ { title: "Q&A", url: "https://vt.tiktok.com/ZSCWExEeV/" },
15
+ { title: "Behind the Scenes", url: "https://vt.tiktok.com/ZSCWTFYew/" },
16
+ { title: "Story time narrated", url: "https://vt.tiktok.com/ZSCW3kAT5/" },
17
+ { title: "VTuber", url: "https://vt.tiktok.com/ZSC7TQyVM/" },
18
+ { title: "Female Thirst Trap", url: "https://vt.tiktok.com/ZSCvRLyMf/" },
19
+ { title: "Male Thirst Trap", url: "https://vt.tiktok.com/ZSCvdUUE2/" },
20
+ { title: "Masked Host", url: "https://vt.tiktok.com/ZSCvdNvxG/" },
21
+ { title: "Hot Take", url: "https://vt.tiktok.com/ZSCvewSbB/" }
22
+ ]
23
+ },
24
+ {
25
+ label: "Text / Story-Driven",
26
+ items: [
27
+ { title: "Reddit story + gameplay", url: "https://vt.tiktok.com/ZSCWpeSRy/" },
28
+ { title: "Fake text message story", url: "https://vt.tiktok.com/ZSCWov56s/" },
29
+ { title: "Twitter/X Thread Video", url: "https://vt.tiktok.com/ZSC7199q6/" },
30
+ { title: "Quote / motivational card", url: "https://vt.tiktok.com/ZSC71D9QD/" },
31
+ { title: "Facts / did you know", url: "https://vt.tiktok.com/ZSCW3QrKA/" },
32
+ { title: "Bible verse / spiritual", url: "https://vt.tiktok.com/ZSCWsP8r7/" },
33
+ { title: "Horoscope / Astrology", url: "https://vt.tiktok.com/ZSC71497u/" },
34
+ { title: "Minecraft Reddit Brainrot", url: "https://youtube.com/shorts/AmdI56dl1z4" }
35
+ ]
36
+ },
37
+ {
38
+ label: "Educational / Explainer",
39
+ items: [
40
+ { title: "Educational explainer", url: "https://vt.tiktok.com/ZSC7e5fbr/" },
41
+ { title: "Whiteboard animation", url: "https://vt.tiktok.com/ZSCWpLa4p/" },
42
+ { title: "Kinetic typography", url: "https://vt.tiktok.com/ZSC7dJnJP/" },
43
+ { title: "Data viz / Infographic", url: "https://vt.tiktok.com/ZSC7RvdWR/" },
44
+ { title: "Science explainer", url: "https://youtu.be/WxmSJbG134k" },
45
+ { title: "Vox News Style", url: "https://www.tiktok.com/@vox/video/7619108001424723231" },
46
+ { title: "Low Poly Animation", url: "https://vt.tiktok.com/ZSC7RwmGR/" },
47
+ { title: "Math tutorial", url: "https://youtu.be/6yYnr8FL1fk" },
48
+ { title: "Historic retelling", url: "https://vt.tiktok.com/ZSC7N6Y68/" },
49
+ { title: "Book Summary", url: "https://vt.tiktok.com/ZSC7Fmemb/" },
50
+ { title: "Documentary Style", url: "https://vt.tiktok.com/ZSC7YBXm9/" },
51
+ { title: "Course lecture", url: "https://youtube.com/shorts/QoQUfVEp-d0" },
52
+ { title: "Scientific Selling", url: "https://www.tiktok.com/@maxcern/video/7604237124036037919" }
53
+ ]
54
+ },
55
+ {
56
+ label: "Product / Marketing",
57
+ items: [
58
+ { title: "Software product demo", url: "https://vt.tiktok.com/ZSC7rbNcV/" },
59
+ { title: "Product commercial", url: "https://vt.tiktok.com/ZSCv6C1Ms/" },
60
+ { title: "UGC Creator Ad", url: "https://vt.tiktok.com/ZSC7hwraf/" },
61
+ { title: "Product Spin Show", url: "https://vt.tiktok.com/ZSCWsuxxp/" },
62
+ { title: "Testimonial", url: "https://vt.tiktok.com/ZSC7kRvxk/" },
63
+ { title: "Explainer commercial", url: "https://vt.tiktok.com/ZSC7SEeHA/" },
64
+ { title: "Case Study", url: "https://www.tiktok.com/@juliejonesdesigns/video/7298542413948243243" },
65
+ { title: "Brand story", url: "https://vt.tiktok.com/ZSC7a7wvb/" },
66
+ { title: "Real Estate tour", url: "https://www.instagram.com/reel/DY19EdHGcDk/" },
67
+ { title: "Sales pitch", url: "https://www.tiktok.com/@mindfullallie/video/7633917841774742815" },
68
+ { title: "Logo reveal / brand intro", url: "https://vt.tiktok.com/ZSC7avGmH/" }
69
+ ]
70
+ },
71
+ {
72
+ label: "News / Journalism",
73
+ items: [
74
+ { title: "News anchor style", url: "https://vt.tiktok.com/ZSC7VtHNS/" },
75
+ { title: "Breaking news", url: "https://vt.tiktok.com/ZSCWWUCqS/" },
76
+ { title: "Weather Report", url: "https://vt.tiktok.com/ZSCWWBEEY/" },
77
+ { title: "Financial / stock update", url: "https://vt.tiktok.com/ZSC7Vt6rP/" },
78
+ { title: "Sports scores/recap", url: "https://vt.tiktok.com/ZSC7V7Pqf/" }
79
+ ]
80
+ },
81
+ {
82
+ label: "Entertainment / Creative",
83
+ items: [
84
+ { title: "Anime / manga", url: "https://vt.tiktok.com/ZSC7qnwWn/" },
85
+ { title: "Cartoon Animation", url: "https://vt.tiktok.com/ZSC7qvEfe/" },
86
+ { title: "Music Video", url: "https://vt.tiktok.com/ZSC7beC8P/" },
87
+ { title: "Lyric Video", url: "https://vt.tiktok.com/ZSC7bP5x7/" },
88
+ { title: "Meme video", url: "https://vt.tiktok.com/ZSC7g8sEn/" },
89
+ { title: "Meme recaptioning", url: "https://vt.tiktok.com/ZSCv6eeKD/" },
90
+ { title: "Hype Edit", url: "https://www.tiktok.com/@goldenhype0/video/7645035734901984543" },
91
+ { title: "Fantasy Hype Edit", url: "https://www.tiktok.com/@hype.edits/video/7538337237453344013" },
92
+ { title: "UGC Comedy", url: "https://www.tiktok.com/@very.manipulative/video/7644074719737646366" },
93
+ { title: "Parody", url: "https://vt.tiktok.com/ZSC7g4aa8/" },
94
+ { title: "ASMR", url: "https://vt.tiktok.com/ZSCvJvseN/" },
95
+ { title: "Cinematic short film", url: "https://vt.tiktok.com/ZSCvJV2mM/" },
96
+ { title: "Oddly satisfying", url: "https://vt.tiktok.com/ZSC7gqn9o/" },
97
+ { title: "POV video", url: "https://vt.tiktok.com/ZSCveMjMp/" },
98
+ { title: "Nostalgia", url: "https://vt.tiktok.com/ZSCv2DALg/" },
99
+ { title: "Musical", url: "https://vt.tiktok.com/ZSCvYwKBQ/" },
100
+ { title: "Gossip", url: "https://vt.tiktok.com/ZSCvYvrh7/" }
101
+ ]
102
+ },
103
+ {
104
+ label: "Data / Analytical",
105
+ items: [
106
+ { title: "Comparison \"X vs Y\"", url: "https://youtube.com/shorts/jFvemXPxayg" },
107
+ { title: "Ranking / Tier List", url: "https://youtube.com/shorts/Ku5xNHs7sAE" },
108
+ { title: "Countdown / top 10", url: "https://vt.tiktok.com/ZSC7psUDB/" },
109
+ { title: "Poll Results Reveal", url: "https://www.tiktok.com/@carterjstockman/video/7477653012874267947" },
110
+ { title: "Stats Comparison", url: "https://www.tiktok.com/@everythingecon/video/7650833928726564118" },
111
+ { title: "Leaderboard Reveal", url: "https://vt.tiktok.com/ZSC7sseno/" },
112
+ { title: "Sports stat breakdown", url: "https://vt.tiktok.com/ZSCvFUTHN/" },
113
+ { title: "Before/after Transformation", url: "https://vt.tiktok.com/ZSC7GRLT2/" }
114
+ ]
115
+ },
116
+ {
117
+ label: "Gaming",
118
+ items: [
119
+ { title: "Gameplay walkthrough", url: "https://vt.tiktok.com/ZSCWWE5X5/" },
120
+ { title: "Game review", url: "https://youtube.com/shorts/nrb0t9cRw3g" },
121
+ { title: "Game trailer", url: "https://vt.tiktok.com/ZSCW7LqKV/" },
122
+ { title: "Esports highlights", url: "https://vt.tiktok.com/ZSC7y4BEF/" }
123
+ ]
124
+ },
125
+ {
126
+ label: "Kids / Educational",
127
+ items: [
128
+ { title: "Nursery Rhyme", url: "https://vt.tiktok.com/ZSC7SWUUr/" },
129
+ { title: "Alphabet / counting", url: "https://vt.tiktok.com/ZSCWvhRNd/" },
130
+ { title: "Cartoon lesson", url: "https://vt.tiktok.com/ZSC7y9wHp/" },
131
+ { title: "Storybook read-along", url: "https://vt.tiktok.com/ZSC7yrnN8/" }
132
+ ]
133
+ },
134
+ {
135
+ label: "Ambient / Background",
136
+ items: [
137
+ { title: "Lo-fi study loop", url: "https://vt.tiktok.com/ZSCWcFgAh/" },
138
+ { title: "Nature + Music", url: "https://vt.tiktok.com/ZSCWc4Egr/" },
139
+ { title: "Fireplace / rain loop", url: "https://vt.tiktok.com/ZSCWcrF7v/" },
140
+ { title: "Aesthetic vibe/mood", url: "https://vt.tiktok.com/ZSCWcK2uQ/" }
141
+ ]
142
+ },
143
+ {
144
+ label: "Fitness / Wellness",
145
+ items: [
146
+ { title: "Workout tutorial", url: "https://vt.tiktok.com/ZSC7fLGUL/" },
147
+ { title: "Yoga / meditation", url: "https://vt.tiktok.com/ZSC7fTkJr/" },
148
+ { title: "Recipe / cooking", url: "https://vt.tiktok.com/ZSC7ffpcX/" },
149
+ { title: "Health tips", url: "https://vt.tiktok.com/ZSC7P6T8y/" }
150
+ ]
151
+ },
152
+ {
153
+ label: "Travel / Location",
154
+ items: [
155
+ { title: "Travel vlog", url: "https://vt.tiktok.com/ZSCWthBcp/" },
156
+ { title: "Destination showcase", url: "https://vt.tiktok.com/ZSC7aVDUv/" },
157
+ { title: "Review Guides", url: "https://vt.tiktok.com/ZSC7PpXUR/" },
158
+ { title: "City tour", url: "https://vt.tiktok.com/ZSC7ajj14/" }
159
+ ]
160
+ },
161
+ {
162
+ label: "Interview / Conversation",
163
+ items: [
164
+ { title: "Interview format", url: "https://vt.tiktok.com/ZSC74xE8A/" },
165
+ { title: "Panel discussion", url: "https://vt.tiktok.com/ZSC74xYML/" },
166
+ { title: "Debate", url: "https://vt.tiktok.com/ZSCvRpCW1/" },
167
+ { title: "Podcast clip", url: "https://www.tiktok.com/@rogantimes04/video/7659533957049961750" },
168
+ { title: "Audiogram", url: "https://vt.tiktok.com/ZSCWs6uMm/" },
169
+ { title: "Diary Confession", url: "https://vt.tiktok.com/ZSCvRcFAn/" }
170
+ ]
171
+ },
172
+ {
173
+ label: "Motion Design",
174
+ items: [
175
+ { title: "Motion graphics", url: "https://vt.tiktok.com/ZSC7u4VD8/" },
176
+ { title: "2D character animation", url: "https://vt.tiktok.com/ZSC7uCayM/" },
177
+ { title: "Stop motion", url: "https://vt.tiktok.com/ZSCWseh2r/" },
178
+ { title: "Static Fast Forward", url: "https://vt.tiktok.com/ZSC7HRUVa/" },
179
+ { title: "Montage Video", url: "https://vt.tiktok.com/ZSCv8QPRY/" },
180
+ { title: "Memoirs", url: "https://vt.tiktok.com/ZSCv89bXC/" }
181
+ ]
182
+ },
183
+ {
184
+ label: "Hype / Trailer",
185
+ items: [
186
+ { title: "Trailer / hype video", url: "https://vt.tiktok.com/ZSC7uHkTc/" },
187
+ { title: "Event promo", url: "https://vt.tiktok.com/ZSC7uMvBA/" },
188
+ { title: "Reveal video", url: "https://vt.tiktok.com/ZSCvNxK7d/" }
189
+ ]
190
+ },
191
+ {
192
+ label: "Pure AI",
193
+ items: [
194
+ { title: "AI Fruit Dramas", url: "https://vt.tiktok.com/ZSCv89Xck/" },
195
+ { title: "Surreal AI Characters", url: "https://vt.tiktok.com/ZSCv8nN2K/" },
196
+ { title: "Claymation Story", url: "https://vt.tiktok.com/ZSCvLJhAc/" },
197
+ { title: "Paper Mache Story", url: "https://www.youtube.com/watch?v=0JKu9TQCLMI" }
198
+ ]
199
+ }
200
+ ];
4
201
  //# sourceMappingURL=homepage-shared.js.map
@@ -7,6 +7,11 @@ export function createHomepageStore(boot) {
7
7
  bookmarks: new Set(),
8
8
  loading: boot.templates.length === 0,
9
9
  error: null,
10
+ view: "feed",
11
+ popularTemplates: [],
12
+ popularLoading: false,
13
+ popularLoaded: false,
14
+ popularError: null,
10
15
  setTemplates: (templates) => set({ templates, loading: false, error: null }),
11
16
  setSearchQuery: (searchQuery) => set({ searchQuery }),
12
17
  setBookmarksOnly: (bookmarksOnly) => set({ bookmarksOnly }),
@@ -22,7 +27,11 @@ export function createHomepageStore(boot) {
22
27
  return { bookmarks: next };
23
28
  }),
24
29
  setLoading: (loading) => set({ loading }),
25
- setError: (error) => set({ error, loading: false })
30
+ setError: (error) => set({ error, loading: false }),
31
+ setView: (view) => set({ view }),
32
+ setPopularTemplates: (popularTemplates) => set({ popularTemplates, popularLoading: false, popularLoaded: true, popularError: null }),
33
+ setPopularLoading: (popularLoading) => set({ popularLoading }),
34
+ setPopularError: (popularError) => set({ popularError, popularLoading: false })
26
35
  }));
27
36
  }
28
37
  //# sourceMappingURL=homepage-store.js.map
@@ -263,7 +263,7 @@ function LazyVideo({ src, controls = true }) {
263
263
  }
264
264
  function DiscoverMuteToggle() {
265
265
  const muted = useDiscoverMuted();
266
- return (_jsxs("button", { type: "button", className: "discover-mute-toggle", "aria-pressed": muted, "aria-label": muted ? "Unmute video previews" : "Mute video previews", title: muted ? "Unmute previews" : "Mute previews", onClick: () => setDiscoverMuted(!muted), children: [muted ? _jsx(SpeakerOffIcon, {}) : _jsx(SpeakerOnIcon, {}), _jsx("span", { className: "discover-mute-toggle-label", children: muted ? "Muted" : "Sound on" })] }));
266
+ return (_jsx("button", { type: "button", className: "discover-mute-toggle", "aria-pressed": muted, "aria-label": muted ? "Unmute video previews" : "Mute video previews", title: muted ? "Unmute previews" : "Mute previews", onClick: () => setDiscoverMuted(!muted), children: muted ? _jsx(SpeakerOffIcon, {}) : _jsx(SpeakerOnIcon, {}) }));
267
267
  }
268
268
  function CopyInlineButton({ text, label }) {
269
269
  const [status, setStatus] = useState("");
@@ -479,13 +479,51 @@ function AddTemplateModal(input) {
479
479
  }
480
480
  } }))] }), file ? (_jsxs("div", { className: "discover-add-template-field", children: [_jsxs("label", { className: "label", htmlFor: "add-template-title", children: ["Title ", _jsx("span", { className: "discover-add-template-optional", children: "optional \u2014 defaults to the template id" })] }), _jsx("input", { id: "add-template-title", type: "text", placeholder: "Name this template\u2026", autoComplete: "off", maxLength: 140, value: title, onChange: (event) => setTitle(event.target.value) })] })) : null, _jsxs("div", { className: "discover-add-template-field", children: [_jsxs("label", { className: "label", htmlFor: "add-template-tagline", children: ["Tagline ", _jsx("span", { className: "discover-add-template-optional", children: "optional" })] }), _jsx("input", { id: "add-template-tagline", type: "text", placeholder: "What makes this video work?", autoComplete: "off", maxLength: 140, value: tagline, onChange: (event) => setTagline(event.target.value) })] }), _jsxs("div", { className: "discover-add-template-field", children: [_jsxs("label", { className: "label", htmlFor: "add-template-notes", children: ["Notes ", _jsx("span", { className: "discover-add-template-optional", children: "optional" })] }), _jsx("textarea", { id: "add-template-notes", rows: 4, placeholder: "Ideas for how you want to remix this\u2026", maxLength: 2000, value: notes, onChange: (event) => setNotes(event.target.value) })] }), error ? _jsx("p", { className: "discover-add-template-error", role: "alert", children: error }) : null, _jsxs("div", { className: "toolbar discover-add-template-actions", children: [_jsx("button", { type: "button", className: "secondary", onClick: onClose, disabled: submitting, children: "Cancel" }), _jsx("button", { type: "submit", className: "cta-button discover-add-template-submit", disabled: submitting, children: submitting ? (file ? "Uploading…" : "Adding…") : "Add Template" })] })] })] }) }));
481
481
  }
482
+ // The curated "Popular" picks. They are real public templates (seeded once from
483
+ // POPULAR_INSPIRATION_GROUPS), so they render with the exact same TemplateCard
484
+ // as the Feed — inline autoplay video that obeys the page mute toggle — just
485
+ // grouped under their editorial category headings and ordered by the server.
486
+ function PopularTemplates(input) {
487
+ if (input.loading) {
488
+ return _jsx("ul", { className: "discover-list", children: _jsx("li", { className: "empty-state discover-loading-card", children: "Loading popular formats\u2026" }) });
489
+ }
490
+ if (input.error) {
491
+ return _jsx("ul", { className: "discover-list", children: _jsx("li", { className: "empty-state discover-loading-card", children: input.error }) });
492
+ }
493
+ const query = input.searchQuery.trim().toLowerCase();
494
+ const filtered = query
495
+ ? input.templates.filter((template) => `${template.title} ${template.templateId} ${template.popularGroup ?? ""}`.toLowerCase().includes(query))
496
+ : input.templates;
497
+ if (filtered.length === 0) {
498
+ return (_jsx("ul", { className: "discover-list", children: _jsx("li", { className: "empty-state discover-loading-card", children: query ? "No popular formats match your search." : "No popular formats yet." }) }));
499
+ }
500
+ // Group by popularGroup, preserving the server's rank order (already sorted).
501
+ const groups = [];
502
+ for (const template of filtered) {
503
+ const label = (template.popularGroup || "Popular").trim() || "Popular";
504
+ let group = groups.find((entry) => entry.label === label);
505
+ if (!group) {
506
+ group = { label, items: [] };
507
+ groups.push(group);
508
+ }
509
+ group.items.push(template);
510
+ }
511
+ return (_jsx("div", { className: "discover-popular", children: groups.map((group) => (_jsxs("section", { className: "discover-popular-group", children: [_jsx("h3", { className: "discover-popular-group-label", children: group.label }), _jsx("ul", { className: "discover-list", children: group.items.map((template) => (_jsx(TemplateCard, { template: template, isBookmarked: input.bookmarks.has(template.templateId), onToggleBookmark: input.onToggleBookmark, onOpenPreview: input.onOpenPreview, onDeleteTemplate: input.onDeleteTemplate, accountUserId: input.accountUserId }, template.templateId))) })] }, group.label))) }));
512
+ }
482
513
  export function HomepageShell(input) {
483
514
  const showEmpty = !input.loading && !input.error && input.templates.length > 0 && input.visibleTemplates.length === 0;
484
515
  const [previewTemplate, setPreviewTemplate] = useState(null);
485
516
  const [isAddTemplateOpen, setIsAddTemplateOpen] = useState(false);
486
- const searchPlaceholder = input.loading || input.error
487
- ? "Search Templates"
488
- : `Search ${input.visibleTemplates.length} Template${input.visibleTemplates.length === 1 ? "" : "s"}`;
517
+ // "feed" = the live inspirations catalog (continuously grown); "popular" = the
518
+ // curated static picks fetched from /discover/popular. View state lives in the
519
+ // store so the client can lazily load the Popular set on first open.
520
+ const isPopular = input.view === "popular";
521
+ const setView = (value) => input.onViewChange?.(value);
522
+ const searchPlaceholder = isPopular
523
+ ? "Search popular formats"
524
+ : input.loading || input.error
525
+ ? "Search Templates"
526
+ : `Search ${input.visibleTemplates.length} Template${input.visibleTemplates.length === 1 ? "" : "s"}`;
489
527
  // Chat rail is always visible on /discover so logged-out visitors see the copilot
490
528
  // affordance and understand what unlocks after login. When logged in, the real chat
491
529
  // React tree portals into the [data-template-editor-chat-root] element (see
@@ -493,7 +531,7 @@ export function HomepageShell(input) {
493
531
  // disabled textarea and a "Log in to chat" CTA in the same slot.
494
532
  const hasChatRail = true;
495
533
  const isChatEnabled = input.account.isLoggedIn;
496
- return (_jsxs("section", { className: `frame discover-frame${hasChatRail ? " has-chat-rail" : ""}`, children: [_jsxs("header", { className: "topbar", children: [_jsx(BrandLockup, { account: input.account }), _jsx("div", { className: "topbar-actions", children: _jsx(PrimaryNav, { account: input.account }) })] }), _jsx("div", { className: "frame-body", children: _jsxs("section", { className: "discover-shell", children: [_jsxs("div", { className: "discover-filter-bar", role: "toolbar", "aria-label": "Composition filters", children: [_jsxs("div", { className: "discover-filter-search search-field", children: [_jsx("span", { className: "search-icon", "aria-hidden": "true", children: _jsx(SearchIcon, {}) }), _jsx("input", { id: "search", type: "search", placeholder: searchPlaceholder, autoComplete: "off", value: input.searchQuery, onChange: (event) => input.onSearchQueryChange?.(event.target.value) })] }), _jsxs("label", { className: "discover-filter-toggle", htmlFor: "bookmarks-only", "data-active": input.bookmarksOnly ? "true" : "false", children: [_jsx("span", { className: "discover-filter-toggle-label", children: "Saved" }), _jsxs("span", { className: "switch-shell", "data-active": input.bookmarksOnly ? "true" : "false", children: [_jsx("input", { id: "bookmarks-only", className: "switch-input", type: "checkbox", checked: input.bookmarksOnly, onChange: (event) => input.onBookmarksOnlyChange?.(event.target.checked) }), _jsx("span", { className: "switch-knob" })] })] }), input.account.isLoggedIn ? (_jsx("button", { type: "button", className: "discover-add-template-button", onClick: () => setIsAddTemplateOpen(true), children: "Add Template" })) : (_jsx("a", { className: "discover-add-template-button", href: "/login", children: "Add Template" })), _jsx(DiscoverMuteToggle, {})] }), _jsx("section", { className: "discover-results", children: _jsx("div", { className: "discover-results-scroll", children: _jsxs("div", { className: "discover-list-scroll", children: [_jsx("ul", { className: "discover-list", id: "template-list", children: input.loading ? (_jsx("li", { className: "empty-state discover-loading-card", children: "Loading compositions..." })) : input.error ? (_jsx("li", { className: "empty-state discover-loading-card", children: "Unable to load compositions." })) : input.templates.length === 0 ? (_jsx("li", { className: "empty-state discover-loading-card", children: "No compositions available." })) : (input.visibleTemplates.map((template) => (_jsx(TemplateCard, { template: template, isBookmarked: input.bookmarks.has(template.templateId), onToggleBookmark: input.onToggleBookmark, onOpenPreview: setPreviewTemplate, onDeleteTemplate: input.onDeleteTemplate, accountUserId: input.account.userId }, template.templateId)))) }), _jsx("div", { className: "empty-state empty", id: "empty-state", "data-visible": showEmpty ? "true" : "false", children: "No compositions match the current search and filter combination." })] }) }) })] }) }), hasChatRail ? (_jsx("aside", { className: "editor-right-rail", "aria-label": "Discover copilot", children: isChatEnabled ? (_jsx("div", { className: "editor-chat-panel", "data-template-editor-chat-root": true })) : (_jsxs(_Fragment, { children: [_jsx("a", { className: "vf-editor-chat-mobile-fab", href: "/login", "aria-label": "Log in to chat with Vidfarm", children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M20 11.5a7.5 7.5 0 0 1-9.8 7.1L4 20l1.5-5.3A7.5 7.5 0 1 1 20 11.5Z" }) }) }), _jsxs("div", { className: "editor-chat-panel editor-chat-panel--logged-out", style: {
534
+ return (_jsxs("section", { className: `frame discover-frame${hasChatRail ? " has-chat-rail" : ""}`, children: [_jsxs("header", { className: "topbar", children: [_jsx(BrandLockup, { account: input.account }), _jsx("div", { className: "topbar-actions", children: _jsx(PrimaryNav, { account: input.account }) })] }), _jsx("div", { className: "frame-body", children: _jsxs("section", { className: "discover-shell", children: [_jsxs("div", { className: "discover-filter-bar", role: "toolbar", "aria-label": "Composition filters", children: [_jsxs("div", { className: "view-toggle discover-view-toggle", role: "tablist", "aria-label": "Discover view", children: [_jsx("button", { type: "button", className: `view-toggle-tab${!isPopular ? " is-active" : ""}`, role: "tab", "aria-selected": !isPopular, onClick: () => setView("feed"), children: "Feed" }), _jsx("button", { type: "button", className: `view-toggle-tab${isPopular ? " is-active" : ""}`, role: "tab", "aria-selected": isPopular, onClick: () => setView("popular"), children: "Popular" })] }), _jsx(DiscoverMuteToggle, {}), _jsxs("div", { className: "discover-filter-search search-field", children: [_jsx("span", { className: "search-icon", "aria-hidden": "true", children: _jsx(SearchIcon, {}) }), _jsx("input", { id: "search", type: "search", placeholder: searchPlaceholder, autoComplete: "off", value: input.searchQuery, onChange: (event) => input.onSearchQueryChange?.(event.target.value) })] }), !isPopular ? (_jsxs("label", { className: "discover-filter-toggle", htmlFor: "bookmarks-only", "data-active": input.bookmarksOnly ? "true" : "false", children: [_jsx("span", { className: "discover-filter-toggle-label", children: "Saved" }), _jsxs("span", { className: "switch-shell", "data-active": input.bookmarksOnly ? "true" : "false", children: [_jsx("input", { id: "bookmarks-only", className: "switch-input", type: "checkbox", checked: input.bookmarksOnly, onChange: (event) => input.onBookmarksOnlyChange?.(event.target.checked) }), _jsx("span", { className: "switch-knob" })] })] })) : null, input.account.isLoggedIn ? (_jsx("button", { type: "button", className: "discover-add-template-button", onClick: () => setIsAddTemplateOpen(true), children: "Add Template" })) : (_jsx("a", { className: "discover-add-template-button", href: "/login", children: "Add Template" }))] }), _jsx("section", { className: "discover-results", children: _jsx("div", { className: "discover-results-scroll", children: _jsx("div", { className: "discover-list-scroll", children: isPopular ? (_jsx(PopularTemplates, { templates: input.popularTemplates, loading: input.popularLoading, error: input.popularError, searchQuery: input.searchQuery, bookmarks: input.bookmarks, onToggleBookmark: input.onToggleBookmark, onOpenPreview: setPreviewTemplate, onDeleteTemplate: input.onDeleteTemplate, accountUserId: input.account.userId })) : (_jsxs(_Fragment, { children: [_jsx("ul", { className: "discover-list", id: "template-list", children: input.loading ? (_jsx("li", { className: "empty-state discover-loading-card", children: "Loading compositions..." })) : input.error ? (_jsx("li", { className: "empty-state discover-loading-card", children: "Unable to load compositions." })) : input.templates.length === 0 ? (_jsx("li", { className: "empty-state discover-loading-card", children: "No compositions available." })) : (input.visibleTemplates.map((template) => (_jsx(TemplateCard, { template: template, isBookmarked: input.bookmarks.has(template.templateId), onToggleBookmark: input.onToggleBookmark, onOpenPreview: setPreviewTemplate, onDeleteTemplate: input.onDeleteTemplate, accountUserId: input.account.userId }, template.templateId)))) }), _jsx("div", { className: "empty-state empty", id: "empty-state", "data-visible": showEmpty ? "true" : "false", children: "No compositions match the current search and filter combination." })] })) }) }) })] }) }), hasChatRail ? (_jsx("aside", { className: "editor-right-rail", "aria-label": "Discover copilot", children: isChatEnabled ? (_jsx("div", { className: "editor-chat-panel", "data-template-editor-chat-root": true })) : (_jsxs(_Fragment, { children: [_jsx("a", { className: "vf-editor-chat-mobile-fab", href: "/login", "aria-label": "Log in to chat with Vidfarm", children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M20 11.5a7.5 7.5 0 0 1-9.8 7.1L4 20l1.5-5.3A7.5 7.5 0 1 1 20 11.5Z" }) }) }), _jsxs("div", { className: "editor-chat-panel editor-chat-panel--logged-out", style: {
497
535
  display: "flex",
498
536
  flexDirection: "column",
499
537
  height: "100%",
@@ -25,7 +25,7 @@ export function renderHomepage(input) {
25
25
  }
26
26
  : undefined,
27
27
  body: `
28
- <div id="homepage-root">${renderToString(_jsx(HomepageShell, { account: input.account, templates: input.templates, visibleTemplates: input.templates, searchQuery: "", bookmarksOnly: false, bookmarks: new Set(), loading: input.templates.length === 0, error: null, onSearchQueryChange: undefined, onBookmarksOnlyChange: undefined, onToggleBookmark: undefined, onAddTemplate: undefined, onDeleteTemplate: undefined, skillEndpointPrefix: boot.skillEndpointPrefix }))}</div>
28
+ <div id="homepage-root">${renderToString(_jsx(HomepageShell, { account: input.account, templates: input.templates, visibleTemplates: input.templates, searchQuery: "", bookmarksOnly: false, bookmarks: new Set(), loading: input.templates.length === 0, error: null, view: "feed", popularTemplates: [], popularLoading: false, popularError: null, onSearchQueryChange: undefined, onBookmarksOnlyChange: undefined, onViewChange: undefined, onToggleBookmark: undefined, onAddTemplate: undefined, onDeleteTemplate: undefined, skillEndpointPrefix: boot.skillEndpointPrefix }))}</div>
29
29
  <script type="application/json" id="homepage-boot">${escapeJsonForHtml(boot)}</script>
30
30
  <script type="module" src="${HOMEPAGE_CLIENT_ASSET}"></script>
31
31
  `,
@@ -70,8 +70,7 @@ export function renderHomepage(input) {
70
70
  position: sticky;
71
71
  top: 0;
72
72
  z-index: 5;
73
- display: grid;
74
- grid-template-columns: minmax(220px, 1fr) auto auto auto;
73
+ display: flex;
75
74
  align-items: center;
76
75
  gap: 12px;
77
76
  padding: 14px 22px;
@@ -80,6 +79,16 @@ export function renderHomepage(input) {
80
79
  backdrop-filter: blur(8px);
81
80
  }
82
81
 
82
+ .discover-filter-search {
83
+ flex: 1 1 auto;
84
+ min-width: 200px;
85
+ }
86
+
87
+ /* Feed/Popular segmented switch reuses the shared page-shell .view-toggle
88
+ look; height-match the 44px filter-bar controls. */
89
+ .discover-view-toggle { flex: 0 0 auto; }
90
+ .discover-view-toggle .view-toggle-tab { min-height: 36px; padding: 0 16px; }
91
+
83
92
  .discover-filter-search input {
84
93
  min-height: 44px;
85
94
  padding-left: 46px;
@@ -134,17 +143,16 @@ export function renderHomepage(input) {
134
143
  .discover-mute-toggle {
135
144
  display: inline-flex;
136
145
  align-items: center;
137
- gap: 8px;
146
+ justify-content: center;
147
+ width: 44px;
148
+ height: 44px;
138
149
  min-height: 44px;
139
- padding: 0 16px;
150
+ padding: 0;
140
151
  border: 1px solid rgba(209, 219, 233, 0.96);
141
152
  border-radius: 999px;
142
153
  background: rgba(255, 255, 255, 0.9);
143
154
  color: #55637b;
144
155
  cursor: pointer;
145
- font-size: 0.86rem;
146
- font-weight: 700;
147
- white-space: nowrap;
148
156
  }
149
157
 
150
158
  .discover-mute-toggle svg {
@@ -541,6 +549,27 @@ export function renderHomepage(input) {
541
549
  gap: 22px;
542
550
  }
543
551
 
552
+ /* Popular view: the curated picks render with the same autoplay
553
+ TemplateCard rows as the Feed, just grouped under category headings. */
554
+ .discover-popular {
555
+ display: grid;
556
+ gap: 34px;
557
+ }
558
+
559
+ .discover-popular-group {
560
+ display: grid;
561
+ gap: 22px;
562
+ }
563
+
564
+ .discover-popular-group-label {
565
+ margin: 0;
566
+ font-size: 0.78rem;
567
+ font-weight: 800;
568
+ letter-spacing: 0.14em;
569
+ text-transform: uppercase;
570
+ color: #a9884a;
571
+ }
572
+
544
573
  .discover-card {
545
574
  display: grid;
546
575
  grid-template-columns: minmax(220px, 340px) minmax(0, 1fr);
@@ -919,14 +948,6 @@ export function renderHomepage(input) {
919
948
  @media (max-width: 1180px) {
920
949
  .discover-frame.has-chat-rail { padding-left: 0; }
921
950
 
922
- .discover-mute-toggle-label {
923
- display: none;
924
- }
925
-
926
- .discover-mute-toggle {
927
- padding: 0 12px;
928
- }
929
-
930
951
  .discover-card {
931
952
  grid-template-columns: minmax(200px, 300px) minmax(0, 1fr);
932
953
  gap: 20px;
@@ -1209,12 +1230,24 @@ export function renderHomepage(input) {
1209
1230
  }
1210
1231
 
1211
1232
  .discover-filter-toggle,
1212
- .discover-add-template-button,
1213
- .discover-mute-toggle {
1233
+ .discover-add-template-button {
1214
1234
  flex: 1 1 auto;
1215
1235
  justify-content: center;
1216
1236
  }
1217
1237
 
1238
+ .discover-mute-toggle {
1239
+ flex: 0 0 auto;
1240
+ }
1241
+
1242
+ /* Feed/Popular leads its own full-width row on phones. */
1243
+ .discover-view-toggle {
1244
+ flex: 1 1 100%;
1245
+ }
1246
+
1247
+ .discover-view-toggle .view-toggle-tab {
1248
+ flex: 1 1 auto;
1249
+ }
1250
+
1218
1251
  .discover-media-modal-backdrop {
1219
1252
  align-items: stretch;
1220
1253
  padding: 12px;
@@ -591,6 +591,15 @@ class ServerlessRecordsServiceImpl {
591
591
  // so the coarse keyword filter never fights the TTL cache.
592
592
  return filterAndRankBySearch(this.templatesFeedCache.records, input.query).slice(0, limit);
593
593
  }
594
+ // Curated "Popular" picks: the handful of public templates flagged with a
595
+ // popularGroup. Drawn from the same cached feed as listTemplatesFeed (the set
596
+ // is tiny), ordered by popularRank so the editorial sequence is preserved.
597
+ async listPopularTemplates() {
598
+ const feed = await this.listTemplatesFeed({ limit: ServerlessRecordsServiceImpl.TEMPLATES_FEED_MAX });
599
+ return feed
600
+ .filter((record) => typeof record.popularGroup === "string" && record.popularGroup.trim().length > 0)
601
+ .sort((a, b) => (a.popularRank ?? 0) - (b.popularRank ?? 0));
602
+ }
594
603
  async queryTemplatesFeed() {
595
604
  const response = await dynamodb.send(new QueryCommand({
596
605
  TableName: requireMainTableName(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {