@mevdragon/vidfarm-devcli 0.15.0 → 0.17.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 (37) hide show
  1. package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
  2. package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
  3. package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
  4. package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
  5. package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
  6. package/.agents/skills/vidfarm-media/SKILL.md +2 -1
  7. package/.agents/skills/vidfarm-media/references/tts.md +20 -1
  8. package/demo/dist/app.css +1 -1
  9. package/demo/dist/app.js +74 -74
  10. package/dist/src/account-pages-legacy.js +2 -2
  11. package/dist/src/app.js +682 -12
  12. package/dist/src/config.js +13 -0
  13. package/dist/src/editor-chat.js +3 -2
  14. package/dist/src/frontend/homepage-client.js +211 -2
  15. package/dist/src/frontend/homepage-shared.js +197 -0
  16. package/dist/src/frontend/homepage-store.js +39 -1
  17. package/dist/src/frontend/homepage-view.js +150 -5
  18. package/dist/src/homepage.js +234 -18
  19. package/dist/src/landing-page.js +367 -0
  20. package/dist/src/primitive-registry.js +278 -2
  21. package/dist/src/reskin/agency-page.js +255 -0
  22. package/dist/src/reskin/calendar-page.js +252 -0
  23. package/dist/src/reskin/chat-page.js +414 -0
  24. package/dist/src/reskin/discover-page.js +380 -0
  25. package/dist/src/reskin/document.js +74 -0
  26. package/dist/src/reskin/help-page.js +318 -0
  27. package/dist/src/reskin/index-page.js +62 -0
  28. package/dist/src/reskin/job-runs-page.js +249 -0
  29. package/dist/src/reskin/library-page.js +515 -0
  30. package/dist/src/reskin/login-page.js +225 -0
  31. package/dist/src/reskin/pricing-page.js +359 -0
  32. package/dist/src/reskin/settings-page.js +353 -0
  33. package/dist/src/reskin/theme.js +265 -0
  34. package/dist/src/services/serverless-records.js +63 -0
  35. package/dist/src/services/swipe-customize.js +434 -0
  36. package/package.json +1 -1
  37. package/public/assets/homepage-client-app.js +23 -23
@@ -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,158 @@ 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 categories\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 categories match your search." : "No categories 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
+ }
513
+ // Tinder-style deck of the customer's own product, auto-recut from decomposed
514
+ // templates. Previews are LIVE HyperFrames compositions in an iframe (no baked
515
+ // MP4) — only the top two cards mount an iframe (the concurrent-decoder ceiling);
516
+ // the rest are static placeholders. Drag or tap the buttons to approve/reject.
517
+ function SwipeDeck(input) {
518
+ const [drag, setDrag] = useState(null);
519
+ const [flyOut, setFlyOut] = useState(null);
520
+ const [busy, setBusy] = useState(false);
521
+ const [actionError, setActionError] = useState(null);
522
+ // Deck previews start muted (the only autoplay browsers allow); this toggle
523
+ // is the user gesture that unlocks audio. The same-origin preview iframes
524
+ // poll these window flags (see injectSwipePreviewFit) — the top-card id gate
525
+ // keeps the preloaded second iframe silent so audio never doubles.
526
+ const [soundOn, setSoundOn] = useState(false);
527
+ const topCardId = input.stack.length ? input.stack[0].id : null;
528
+ useEffect(() => {
529
+ window.__VF_SWIPE_SOUND__ = soundOn;
530
+ window.__VF_SWIPE_SOUND_TOP__ = topCardId;
531
+ return () => {
532
+ window.__VF_SWIPE_SOUND__ = false;
533
+ window.__VF_SWIPE_SOUND_TOP__ = null;
534
+ };
535
+ }, [soundOn, topCardId]);
536
+ const startRef = useRef(null);
537
+ const THRESHOLD = 110;
538
+ const decide = (card, dir) => {
539
+ if (!card || busy)
540
+ return;
541
+ setBusy(true);
542
+ setActionError(null);
543
+ setFlyOut({ id: card.id, dir });
544
+ // Let the fly-out animation play, then commit (parent removes the card and
545
+ // refills the deck).
546
+ window.setTimeout(async () => {
547
+ const action = dir === "right" ? input.onApprove : input.onReject;
548
+ try {
549
+ const result = await action?.(card.id);
550
+ if (result && !result.ok)
551
+ setActionError(result.error ?? "Action failed.");
552
+ }
553
+ finally {
554
+ setFlyOut(null);
555
+ setDrag(null);
556
+ startRef.current = null;
557
+ setBusy(false);
558
+ }
559
+ }, 260);
560
+ };
561
+ if (!input.isLoggedIn) {
562
+ return (_jsxs("div", { className: "swipe-empty", children: [_jsx("p", { className: "swipe-empty-title", children: "Swipe is for your product" }), _jsx("p", { className: "swipe-empty-sub", children: "Log in to auto-generate ads from viral templates and swipe to approve." }), _jsx("a", { className: "swipe-empty-cta", href: "/login", children: "Log in" })] }));
563
+ }
564
+ if (!input.productSet) {
565
+ return (_jsxs("div", { className: "swipe-empty", children: [_jsx("p", { className: "swipe-empty-title", children: "Tell us about your product" }), _jsx("p", { className: "swipe-empty-sub", children: "Swipe auto-recuts viral templates into ads for your product. Add a product / offer description in Settings to start the deck." }), _jsx("a", { className: "swipe-empty-cta", href: "/settings", children: "Open Settings" })] }));
566
+ }
567
+ if (input.error) {
568
+ return (_jsxs("div", { className: "swipe-empty", children: [_jsx("p", { className: "swipe-empty-title", children: "Couldn't load your deck" }), _jsx("p", { className: "swipe-empty-sub", children: input.error })] }));
569
+ }
570
+ if (input.stack.length === 0) {
571
+ if (input.loading || input.customizing > 0 || input.refilling) {
572
+ return (_jsxs("div", { className: "swipe-empty", children: [_jsx("div", { className: "swipe-spinner", "aria-hidden": "true" }), _jsx("p", { className: "swipe-empty-title", children: "Customizing fresh clips\u2026" }), _jsx("p", { className: "swipe-empty-sub", children: "Recutting viral templates into ads for your product." })] }));
573
+ }
574
+ return (_jsxs("div", { className: "swipe-empty", children: [_jsx("p", { className: "swipe-empty-title", children: "You're all caught up" }), _jsx("p", { className: "swipe-empty-sub", children: "No more templates to customize right now. Check back as new ones are added." })] }));
575
+ }
576
+ const visible = input.stack.slice(0, 4);
577
+ const top = visible[0];
578
+ return (_jsxs("div", { className: "swipe-deck-wrap", children: [_jsxs("div", { className: "swipe-deck-row", children: [_jsx("button", { type: "button", className: "swipe-action swipe-action-reject", disabled: busy, onClick: () => decide(top, "left"), "aria-label": "Reject", children: "\u2715" }), _jsxs("div", { className: "swipe-deck", children: [_jsx("button", { type: "button", className: "swipe-sound", onClick: () => setSoundOn((value) => !value), "aria-label": soundOn ? "Mute preview" : "Play preview with sound", "aria-pressed": soundOn, title: soundOn ? "Mute" : "Sound on", children: soundOn ? "🔊" : "🔇" }), visible.map((card, index) => {
579
+ const isTop = index === 0;
580
+ const isFlying = flyOut?.id === card.id;
581
+ const isDragging = isTop && drag?.id === card.id;
582
+ const useIframe = index <= 1 && Boolean(card.previewUrl);
583
+ const dx = isDragging ? drag.dx : 0;
584
+ const dy = isDragging ? drag.dy : 0;
585
+ let transform;
586
+ let transition;
587
+ if (isFlying) {
588
+ transform = `translate(${flyOut.dir === "right" ? 130 : -130}%, ${dy}px) rotate(${flyOut.dir === "right" ? 22 : -22}deg)`;
589
+ transition = "transform 0.26s ease-in";
590
+ }
591
+ else if (isTop) {
592
+ transform = `translate(${dx}px, ${dy * 0.25}px) rotate(${dx / 22}deg)`;
593
+ transition = isDragging ? "none" : "transform 0.2s ease";
594
+ }
595
+ else {
596
+ transform = `translateY(${index * 12}px) scale(${1 - index * 0.04})`;
597
+ transition = "transform 0.2s ease";
598
+ }
599
+ const hint = isDragging ? (drag.dx > 40 ? "approve" : drag.dx < -40 ? "reject" : null) : null;
600
+ return (_jsxs("div", { className: "swipe-card", style: { transform, transition, zIndex: 100 - index }, onPointerDown: isTop && !busy ? (event) => {
601
+ startRef.current = { x: event.clientX, y: event.clientY };
602
+ event.currentTarget.setPointerCapture?.(event.pointerId);
603
+ } : undefined, onPointerMove: isTop && !busy ? (event) => {
604
+ if (!startRef.current)
605
+ return;
606
+ setDrag({ id: card.id, dx: event.clientX - startRef.current.x, dy: event.clientY - startRef.current.y });
607
+ } : undefined, onPointerUp: isTop && !busy ? () => {
608
+ const moved = drag?.id === card.id ? drag.dx : 0;
609
+ startRef.current = null;
610
+ if (moved > THRESHOLD)
611
+ decide(card, "right");
612
+ else if (moved < -THRESHOLD)
613
+ decide(card, "left");
614
+ else
615
+ setDrag(null);
616
+ } : undefined, onPointerCancel: isTop ? () => { startRef.current = null; setDrag(null); } : undefined, children: [useIframe ? (_jsx("iframe", { className: "swipe-card-preview", src: card.previewUrl ?? undefined, title: card.title, loading: "lazy", scrolling: "no", allow: "autoplay", style: { pointerEvents: "none" } })) : (_jsx("div", { className: "swipe-card-fallback", children: _jsx("span", { children: card.title }) })), _jsxs("div", { className: "swipe-card-overlay", children: [_jsx("div", { className: "swipe-card-title", children: card.title }), card.caption ? _jsx("div", { className: "swipe-card-caption", children: card.caption }) : null] }), hint === "approve" ? _jsx("div", { className: "swipe-stamp swipe-stamp-yes", children: "Approve" }) : null, hint === "reject" ? _jsx("div", { className: "swipe-stamp swipe-stamp-no", children: "Reject" }) : null] }, card.id));
617
+ })] }), _jsx("button", { type: "button", className: "swipe-action swipe-action-approve", disabled: busy, onClick: () => decide(top, "right"), "aria-label": "Approve", children: "\u2713" })] }), _jsx("div", { className: "swipe-status", children: input.customizing > 0 || input.refilling ? "Customizing more…" : `${input.stack.length} ready` }), actionError ? _jsx("div", { className: "swipe-action-error", children: actionError }) : null] }));
618
+ }
482
619
  export function HomepageShell(input) {
483
620
  const showEmpty = !input.loading && !input.error && input.templates.length > 0 && input.visibleTemplates.length === 0;
484
621
  const [previewTemplate, setPreviewTemplate] = useState(null);
485
622
  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"}`;
623
+ // "feed" = the live inspirations catalog (continuously grown); "popular" = the
624
+ // curated static picks fetched from /discover/popular. View state lives in the
625
+ // store so the client can lazily load the Popular set on first open.
626
+ const isPopular = input.view === "popular";
627
+ const isSwipe = input.view === "swipe";
628
+ const setView = (value) => input.onViewChange?.(value);
629
+ const searchPlaceholder = isPopular
630
+ ? "Search categories"
631
+ : input.loading || input.error
632
+ ? "Search Templates"
633
+ : `Search ${input.visibleTemplates.length} Template${input.visibleTemplates.length === 1 ? "" : "s"}`;
489
634
  // Chat rail is always visible on /discover so logged-out visitors see the copilot
490
635
  // affordance and understand what unlocks after login. When logged in, the real chat
491
636
  // React tree portals into the [data-template-editor-chat-root] element (see
@@ -493,7 +638,7 @@ export function HomepageShell(input) {
493
638
  // disabled textarea and a "Log in to chat" CTA in the same slot.
494
639
  const hasChatRail = true;
495
640
  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: {
641
+ 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 && !isSwipe ? " is-active" : ""}`, role: "tab", "aria-selected": !isPopular && !isSwipe, onClick: () => setView("feed"), children: "Feed" }), _jsx("button", { type: "button", className: `view-toggle-tab${isSwipe ? " is-active" : ""}`, role: "tab", "aria-selected": isSwipe, onClick: () => setView("swipe"), children: "Swipe" }), _jsx("button", { type: "button", className: `view-toggle-tab${isPopular ? " is-active" : ""}`, role: "tab", "aria-selected": isPopular, onClick: () => setView("popular"), children: "Category" })] }), _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 && !isSwipe ? (_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: isSwipe ? (_jsx(SwipeDeck, { stack: input.swipeStack, loading: input.swipeLoading, loaded: input.swipeLoaded, error: input.swipeError, customizing: input.swipeCustomizing, targetDepth: input.swipeTargetDepth, productSet: input.swipeProductSet, refilling: input.swipeRefilling, isLoggedIn: input.account.isLoggedIn, onApprove: input.onSwipeApprove, onReject: input.onSwipeReject })) : 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
642
  display: "flex",
498
643
  flexDirection: "column",
499
644
  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, swipeStack: [], swipeLoading: false, swipeLoaded: false, swipeError: null, swipeCustomizing: 0, swipeTargetDepth: 5, swipeProductSet: true, swipeRefilling: false, 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 {
@@ -519,6 +527,189 @@ export function renderHomepage(input) {
519
527
  padding-right: 6px;
520
528
  }
521
529
 
530
+ /* ---- Swipe deck ---- */
531
+ .swipe-deck-wrap {
532
+ display: flex;
533
+ flex-direction: column;
534
+ align-items: center;
535
+ justify-content: center;
536
+ gap: 18px;
537
+ height: 100%;
538
+ min-height: 0;
539
+ padding: 8px 0 4px;
540
+ }
541
+ .swipe-deck {
542
+ position: relative;
543
+ width: min(320px, 62vw);
544
+ aspect-ratio: 9 / 16;
545
+ max-height: 66vh;
546
+ flex: 0 0 auto;
547
+ }
548
+ .swipe-card {
549
+ position: absolute;
550
+ inset: 0;
551
+ border-radius: 22px;
552
+ overflow: hidden;
553
+ background: #0d0b10;
554
+ box-shadow: 0 18px 44px rgba(31, 20, 8, 0.24), 0 2px 8px rgba(31, 20, 8, 0.12);
555
+ touch-action: none;
556
+ will-change: transform;
557
+ user-select: none;
558
+ }
559
+ .swipe-card:first-child { cursor: grab; }
560
+ .swipe-card:first-child:active { cursor: grabbing; }
561
+ .swipe-sound {
562
+ position: absolute;
563
+ top: 10px;
564
+ right: 10px;
565
+ z-index: 200;
566
+ width: 40px;
567
+ height: 40px;
568
+ border-radius: 50%;
569
+ border: 0;
570
+ background: rgba(0, 0, 0, 0.45);
571
+ color: #fff;
572
+ font-size: 17px;
573
+ line-height: 1;
574
+ cursor: pointer;
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: center;
578
+ backdrop-filter: blur(4px);
579
+ transition: background 0.15s ease, transform 0.15s ease;
580
+ }
581
+ .swipe-sound:hover { background: rgba(0, 0, 0, 0.68); transform: scale(1.06); }
582
+ .swipe-card-preview,
583
+ .swipe-card-fallback {
584
+ position: absolute;
585
+ inset: 0;
586
+ width: 100%;
587
+ height: 100%;
588
+ border: 0;
589
+ display: block;
590
+ }
591
+ .swipe-card-fallback {
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ text-align: center;
596
+ padding: 22px;
597
+ color: rgba(255, 255, 255, 0.72);
598
+ font-size: 15px;
599
+ font-weight: 600;
600
+ background: linear-gradient(160deg, #2a2431, #16121c);
601
+ }
602
+ .swipe-card-overlay {
603
+ position: absolute;
604
+ left: 0;
605
+ right: 0;
606
+ bottom: 0;
607
+ padding: 40px 16px 16px;
608
+ background: linear-gradient(0deg, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
609
+ color: #fff;
610
+ pointer-events: none;
611
+ }
612
+ .swipe-card-title { font-size: 16px; font-weight: 800; line-height: 1.25; }
613
+ .swipe-card-caption {
614
+ margin-top: 4px;
615
+ font-size: 12.5px;
616
+ line-height: 1.35;
617
+ color: rgba(255, 255, 255, 0.86);
618
+ display: -webkit-box;
619
+ -webkit-line-clamp: 2;
620
+ -webkit-box-orient: vertical;
621
+ overflow: hidden;
622
+ }
623
+ .swipe-stamp {
624
+ position: absolute;
625
+ top: 20px;
626
+ padding: 6px 14px;
627
+ border-radius: 10px;
628
+ font-weight: 900;
629
+ font-size: 20px;
630
+ letter-spacing: 0.06em;
631
+ text-transform: uppercase;
632
+ border: 3px solid currentColor;
633
+ pointer-events: none;
634
+ }
635
+ .swipe-stamp-yes { left: 18px; color: #22c55e; transform: rotate(-14deg); }
636
+ .swipe-stamp-no { right: 18px; color: #ef4444; transform: rotate(14deg); }
637
+ /* Buttons flank the deck, filling the empty side space. */
638
+ .swipe-deck-row {
639
+ display: flex;
640
+ align-items: center;
641
+ justify-content: center;
642
+ gap: clamp(10px, 3vw, 56px);
643
+ width: 100%;
644
+ flex: 1 1 auto;
645
+ min-height: 0;
646
+ }
647
+ .swipe-action {
648
+ width: 60px;
649
+ height: 60px;
650
+ border-radius: 50%;
651
+ border: none;
652
+ font-size: 26px;
653
+ font-weight: 800;
654
+ cursor: pointer;
655
+ display: flex;
656
+ align-items: center;
657
+ justify-content: center;
658
+ flex: 0 0 auto;
659
+ box-shadow: 0 8px 20px rgba(31, 20, 8, 0.18);
660
+ transition: transform 0.12s ease, filter 0.12s ease;
661
+ }
662
+ .swipe-action:hover:not(:disabled) { transform: translateY(-3px); }
663
+ .swipe-action:disabled { opacity: 0.5; cursor: default; }
664
+ .swipe-action-reject { background: #fff; color: #ef4444; border: 1.5px solid rgba(239, 68, 68, 0.35); }
665
+ .swipe-action-approve { background: #22c55e; color: #fff; }
666
+ .swipe-status {
667
+ margin-top: 14px;
668
+ text-align: center;
669
+ font-size: 13px;
670
+ font-weight: 700;
671
+ color: #7a6a52;
672
+ flex: 0 0 auto;
673
+ }
674
+ .swipe-action-error {
675
+ color: #b91c1c;
676
+ font-size: 13px;
677
+ font-weight: 600;
678
+ text-align: center;
679
+ }
680
+ .swipe-empty {
681
+ display: flex;
682
+ flex-direction: column;
683
+ align-items: center;
684
+ justify-content: center;
685
+ text-align: center;
686
+ gap: 8px;
687
+ height: 100%;
688
+ padding: 24px;
689
+ color: #6f6047;
690
+ }
691
+ .swipe-empty-title { font-size: 18px; font-weight: 800; color: #3a2f1e; }
692
+ .swipe-empty-sub { font-size: 14px; max-width: 340px; line-height: 1.45; }
693
+ .swipe-empty-cta {
694
+ margin-top: 10px;
695
+ padding: 10px 20px;
696
+ border-radius: 999px;
697
+ background: #f5a524;
698
+ color: #2a1c00;
699
+ font-weight: 800;
700
+ text-decoration: none;
701
+ }
702
+ .swipe-spinner {
703
+ width: 34px;
704
+ height: 34px;
705
+ border-radius: 50%;
706
+ border: 3px solid rgba(245, 165, 36, 0.28);
707
+ border-top-color: #f5a524;
708
+ animation: swipe-spin 0.8s linear infinite;
709
+ margin-bottom: 4px;
710
+ }
711
+ @keyframes swipe-spin { to { transform: rotate(360deg); } }
712
+
522
713
  .discover-count {
523
714
  position: sticky;
524
715
  top: 0;
@@ -541,6 +732,27 @@ export function renderHomepage(input) {
541
732
  gap: 22px;
542
733
  }
543
734
 
735
+ /* Popular view: the curated picks render with the same autoplay
736
+ TemplateCard rows as the Feed, just grouped under category headings. */
737
+ .discover-popular {
738
+ display: grid;
739
+ gap: 34px;
740
+ }
741
+
742
+ .discover-popular-group {
743
+ display: grid;
744
+ gap: 22px;
745
+ }
746
+
747
+ .discover-popular-group-label {
748
+ margin: 0;
749
+ font-size: 0.78rem;
750
+ font-weight: 800;
751
+ letter-spacing: 0.14em;
752
+ text-transform: uppercase;
753
+ color: #a9884a;
754
+ }
755
+
544
756
  .discover-card {
545
757
  display: grid;
546
758
  grid-template-columns: minmax(220px, 340px) minmax(0, 1fr);
@@ -919,14 +1131,6 @@ export function renderHomepage(input) {
919
1131
  @media (max-width: 1180px) {
920
1132
  .discover-frame.has-chat-rail { padding-left: 0; }
921
1133
 
922
- .discover-mute-toggle-label {
923
- display: none;
924
- }
925
-
926
- .discover-mute-toggle {
927
- padding: 0 12px;
928
- }
929
-
930
1134
  .discover-card {
931
1135
  grid-template-columns: minmax(200px, 300px) minmax(0, 1fr);
932
1136
  gap: 20px;
@@ -1209,12 +1413,24 @@ export function renderHomepage(input) {
1209
1413
  }
1210
1414
 
1211
1415
  .discover-filter-toggle,
1212
- .discover-add-template-button,
1213
- .discover-mute-toggle {
1416
+ .discover-add-template-button {
1214
1417
  flex: 1 1 auto;
1215
1418
  justify-content: center;
1216
1419
  }
1217
1420
 
1421
+ .discover-mute-toggle {
1422
+ flex: 0 0 auto;
1423
+ }
1424
+
1425
+ /* Feed/Popular leads its own full-width row on phones. */
1426
+ .discover-view-toggle {
1427
+ flex: 1 1 100%;
1428
+ }
1429
+
1430
+ .discover-view-toggle .view-toggle-tab {
1431
+ flex: 1 1 auto;
1432
+ }
1433
+
1218
1434
  .discover-media-modal-backdrop {
1219
1435
  align-items: stretch;
1220
1436
  padding: 12px;