@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.
- package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -1
- package/.agents/skills/vidfarm-media/references/tts.md +20 -1
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +74 -74
- package/dist/src/account-pages-legacy.js +2 -2
- package/dist/src/app.js +682 -12
- package/dist/src/config.js +13 -0
- package/dist/src/editor-chat.js +3 -2
- package/dist/src/frontend/homepage-client.js +211 -2
- package/dist/src/frontend/homepage-shared.js +197 -0
- package/dist/src/frontend/homepage-store.js +39 -1
- package/dist/src/frontend/homepage-view.js +150 -5
- package/dist/src/homepage.js +234 -18
- package/dist/src/landing-page.js +367 -0
- package/dist/src/primitive-registry.js +278 -2
- package/dist/src/reskin/agency-page.js +255 -0
- package/dist/src/reskin/calendar-page.js +252 -0
- package/dist/src/reskin/chat-page.js +414 -0
- package/dist/src/reskin/discover-page.js +380 -0
- package/dist/src/reskin/document.js +74 -0
- package/dist/src/reskin/help-page.js +318 -0
- package/dist/src/reskin/index-page.js +62 -0
- package/dist/src/reskin/job-runs-page.js +249 -0
- package/dist/src/reskin/library-page.js +515 -0
- package/dist/src/reskin/login-page.js +225 -0
- package/dist/src/reskin/pricing-page.js +359 -0
- package/dist/src/reskin/settings-page.js +353 -0
- package/dist/src/reskin/theme.js +265 -0
- package/dist/src/services/serverless-records.js +63 -0
- package/dist/src/services/swipe-customize.js +434 -0
- package/package.json +1 -1
- 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 (
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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" }))
|
|
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%",
|
package/dist/src/homepage.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
146
|
+
justify-content: center;
|
|
147
|
+
width: 44px;
|
|
148
|
+
height: 44px;
|
|
138
149
|
min-height: 44px;
|
|
139
|
-
padding: 0
|
|
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;
|