@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,705 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useSyncExternalStore } from "react";
3
- import { resolveAccountDisplayName, withAccountQuery } from "../page-shell.js";
4
- import { isVideoPreview } from "./homepage-shared.js";
5
- function DiscoverIcon() {
6
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M10 3v4" }), _jsx("path", { d: "M10 13v4" }), _jsx("path", { d: "M3 10h4" }), _jsx("path", { d: "M13 10h4" }), _jsx("path", { d: "m5.8 5.8 2.3 2.3" }), _jsx("path", { d: "m11.9 11.9 2.3 2.3" }), _jsx("path", { d: "m14.2 5.8-2.3 2.3" }), _jsx("path", { d: "m8.1 11.9-2.3 2.3" })] });
7
- }
8
- function LibraryIcon() {
9
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M4.5 4.5h3v11h-3z" }), _jsx("path", { d: "M8.7 4.5h3v11h-3z" }), _jsx("path", { d: "m13.4 5.1 2.3-.6 2.2 10.7-2.3.6z" })] });
10
- }
11
- function CalendarIcon() {
12
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "10", cy: "10", r: "7.2" }), _jsx("path", { d: "M10 6.3v4.1l2.8 1.7" })] });
13
- }
14
- function ChatIcon() {
15
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M4.2 5.2a2 2 0 0 1 2-2h7.6a2 2 0 0 1 2 2v5.6a2 2 0 0 1-2 2H9.2l-4.1 3.1v-3.1a2 2 0 0 1-.9-1.7Z" }), _jsx("path", { d: "M7.3 7.1h5.4" }), _jsx("path", { d: "M7.3 9.6h3.7" })] });
16
- }
17
- function SettingsIcon() {
18
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M14.8 15.2a5.4 5.4 0 1 0-9.6 0" }), _jsx("circle", { cx: "10", cy: "6.8", r: "3.1" })] });
19
- }
20
- function MoreIcon() {
21
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: [_jsx("circle", { cx: "4.2", cy: "10", r: "1.45" }), _jsx("circle", { cx: "10", cy: "10", r: "1.45" }), _jsx("circle", { cx: "15.8", cy: "10", r: "1.45" })] });
22
- }
23
- function MenuIcon() {
24
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", children: [_jsx("path", { d: "M3.5 5.5h13" }), _jsx("path", { d: "M3.5 10h13" }), _jsx("path", { d: "M3.5 14.5h13" })] });
25
- }
26
- function SearchIcon() {
27
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "8.5", cy: "8.5", r: "5.8" }), _jsx("path", { d: "m13 13 4.2 4.2" })] });
28
- }
29
- function BookmarkIcon() {
30
- return _jsx("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M6 3.8h8a1 1 0 0 1 1 1v11.4l-5-2.9-5 2.9V4.8a1 1 0 0 1 1-1Z" }) });
31
- }
32
- function clearLogoutAccountCache(accountId) {
33
- const account = accountId?.trim();
34
- if (!account || typeof window === "undefined") {
35
- return;
36
- }
37
- try {
38
- const registryIndexKey = "vidfarm:agency:accounts:index";
39
- const rawIds = window.localStorage.getItem(registryIndexKey);
40
- const ids = rawIds ? JSON.parse(rawIds) : [];
41
- if (Array.isArray(ids)) {
42
- window.localStorage.setItem(registryIndexKey, JSON.stringify(ids.filter((value) => typeof value === "string" && value !== account)));
43
- }
44
- window.localStorage.removeItem(`vidfarm:agency:account_${account}`);
45
- window.localStorage.removeItem(`vidfarm:flockposter-cache:v1_${account}`);
46
- window.localStorage.removeItem(`vidfarm:discover-bookmarks:v1_${account}`);
47
- }
48
- catch {
49
- // Ignore storage failures.
50
- }
51
- }
52
- function PrimaryNav({ account }) {
53
- const logoutDialogRef = useRef(null);
54
- const items = account.isLoggedIn
55
- ? [
56
- { id: "discover", href: withAccountQuery("/discover", account.userId), label: "Discover", icon: _jsx(DiscoverIcon, {}) },
57
- { id: "library", href: withAccountQuery("/library", account.userId), label: "Library", icon: _jsx(LibraryIcon, {}) },
58
- { id: "calendar", href: withAccountQuery("/calendar", account.userId), label: "Calendar", icon: _jsx(CalendarIcon, {}) },
59
- { id: "settings", href: withAccountQuery("/settings", account.userId), label: "Settings", icon: _jsx(SettingsIcon, {}) }
60
- ]
61
- : [
62
- { id: "discover", href: "/discover", label: "Discover", icon: _jsx(DiscoverIcon, {}) },
63
- { id: "login", href: "/login", label: "Login", icon: _jsx(SettingsIcon, {}) }
64
- ];
65
- const menuItems = account.isLoggedIn
66
- ? [
67
- { id: "chat", href: withAccountQuery("/chat", account.userId), label: "Chat" },
68
- { id: "job-runs", href: withAccountQuery("/job-runs", account.userId), label: "Run History" },
69
- { id: "tools", href: withAccountQuery("/tools/image", account.userId), label: "Tools" },
70
- { id: "help", href: "/help", label: "Help" },
71
- { id: "logout", action: "logout", label: "Log Out" }
72
- ]
73
- : [
74
- { id: "help", href: "/help", label: "Help" },
75
- { id: "login", href: "/login", label: "Login" }
76
- ];
77
- const mobileMenuPrimaryItems = items.filter((item) => !menuItems.some((menuItem) => menuItem.id === item.id));
78
- return (_jsxs(_Fragment, { children: [_jsxs("nav", { className: "primary-nav", "aria-label": "Primary", children: [items.map((item) => (_jsxs("a", { className: `nav-link${item.id === "discover" ? " is-active" : ""}`, href: item.href, children: [_jsx("span", { className: "nav-icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { children: item.label })] }, item.id))), menuItems.map((item) => ("href" in item ? (_jsx("a", { className: "nav-link nav-mobile-extra", href: item.href, children: _jsx("span", { children: item.label }) }, `mobile-${item.id}`)) : (_jsx("button", { type: "button", className: "nav-link nav-mobile-extra nav-action-link", onClick: () => {
79
- if (!logoutDialogRef.current?.open) {
80
- logoutDialogRef.current?.showModal();
81
- }
82
- }, children: _jsx("span", { children: item.label }) }, `mobile-${item.id}`)))), menuItems.length ? (_jsxs("details", { className: "nav-menu", children: [_jsxs("summary", { className: "nav-link nav-menu-trigger", "aria-label": "Open navigation menu", children: [_jsx("span", { className: "nav-icon nav-menu-desktop-icon", "aria-hidden": "true", children: _jsx(MoreIcon, {}) }), _jsx("span", { className: "nav-icon nav-menu-mobile-icon", "aria-hidden": "true", children: _jsx(MenuIcon, {}) }), _jsx("span", { className: "nav-menu-label", children: "Menu" })] }), _jsxs("div", { className: "nav-menu-panel", role: "menu", children: [mobileMenuPrimaryItems.map((item) => (_jsx("a", { className: `nav-menu-item nav-menu-mobile-only${item.id === "discover" ? " is-active" : ""}`, role: "menuitem", href: item.href, children: item.label }, `menu-mobile-${item.id}`))), menuItems.map((item) => ("href" in item ? (_jsx("a", { className: "nav-menu-item", role: "menuitem", href: item.href, children: item.label }, item.id)) : (_jsx("button", { type: "button", className: "nav-menu-item nav-menu-button", role: "menuitem", onClick: () => {
83
- if (!logoutDialogRef.current?.open) {
84
- logoutDialogRef.current?.showModal();
85
- }
86
- }, children: item.label }, item.id))))] })] })) : null] }), account.isLoggedIn ? (_jsxs("dialog", { className: "logout-dialog", ref: logoutDialogRef, onClick: (event) => {
87
- const dialog = logoutDialogRef.current;
88
- if (!dialog) {
89
- return;
90
- }
91
- const rect = dialog.getBoundingClientRect();
92
- const isBackdropClick = event.clientX < rect.left ||
93
- event.clientX > rect.right ||
94
- event.clientY < rect.top ||
95
- event.clientY > rect.bottom;
96
- if (isBackdropClick) {
97
- dialog.close();
98
- }
99
- }, children: [_jsxs("div", { className: "logout-dialog-head", children: [_jsx("div", { className: "label logout-dialog-kicker", children: "Log out" }), _jsx("h3", { children: "Log out of this browser?" }), _jsx("p", { children: "This clears the stored developer key, workspace access, and local session state for the current browser." })] }), _jsxs("div", { className: "toolbar logout-dialog-actions", children: [_jsx("button", { type: "button", className: "secondary", onClick: () => logoutDialogRef.current?.close(), children: "Cancel" }), _jsxs("form", { method: "post", action: "/logout", onSubmit: () => clearLogoutAccountCache(account.userId), children: [_jsx("input", { type: "hidden", name: "account", value: account.userId ?? "" }), _jsx("button", { type: "submit", className: "danger", children: "Yes, log out" })] })] })] })) : null] }));
100
- }
101
- function BrandLockup({ account }) {
102
- const email = account.email?.trim() || "";
103
- const derivedName = resolveAccountDisplayName({ displayName: account.displayName, email: account.email });
104
- const href = withAccountQuery("/discover", account.userId);
105
- return (_jsxs("a", { className: "brand-lockup", href: href, children: [_jsx("img", { className: "brand-logo-image", src: "/assets/logo-vidfarm.png", alt: "VidFarm AI" }), _jsxs("div", { className: "brand-copy", children: [_jsxs("div", { className: "brand-title brand-title-static", children: ["VidFarm", _jsx("span", { className: "brand-title-mobile-hidden", children: " AI" })] }), email ? (_jsxs("div", { className: "brand-account-meta", children: [derivedName, " ", _jsxs("span", { children: ["(", email, ")"] })] })) : null] })] }));
106
- }
107
- function CloseIcon() {
108
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", children: [_jsx("path", { d: "m5 5 10 10" }), _jsx("path", { d: "m15 5-10 10" })] });
109
- }
110
- function DownloadIcon() {
111
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M10 3v9" }), _jsx("path", { d: "m6.5 8.5 3.5 3.5 3.5-3.5" }), _jsx("path", { d: "M4 16h12" })] });
112
- }
113
- function LinkIcon() {
114
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1" }), _jsx("path", { d: "M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2" })] });
115
- }
116
- function SpeakerOnIcon() {
117
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M3.5 8v4h2.8l3.5 3V5l-3.5 3Z" }), _jsx("path", { d: "M12.4 7.6a3.4 3.4 0 0 1 0 4.8" }), _jsx("path", { d: "M14.6 5.4a6.5 6.5 0 0 1 0 9.2" })] });
118
- }
119
- function SpeakerOffIcon() {
120
- return _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M3.5 8v4h2.8l3.5 3V5l-3.5 3Z" }), _jsx("path", { d: "m12.6 8 4 4" }), _jsx("path", { d: "m16.6 8-4 4" })] });
121
- }
122
- const DISCOVER_MUTE_STORAGE_KEY = "vidfarm:discover-muted";
123
- let discoverMuted = typeof window !== "undefined" && window.localStorage?.getItem(DISCOVER_MUTE_STORAGE_KEY) === "1";
124
- const discoverMuteListeners = new Set();
125
- function setDiscoverMuted(muted) {
126
- discoverMuted = muted;
127
- try {
128
- window.localStorage.setItem(DISCOVER_MUTE_STORAGE_KEY, muted ? "1" : "0");
129
- }
130
- catch {
131
- // storage unavailable (private mode); the toggle still works for this page view
132
- }
133
- for (const listener of discoverMuteListeners) {
134
- listener();
135
- }
136
- }
137
- function subscribeDiscoverMuted(listener) {
138
- discoverMuteListeners.add(listener);
139
- return () => {
140
- discoverMuteListeners.delete(listener);
141
- };
142
- }
143
- function useDiscoverMuted() {
144
- return useSyncExternalStore(subscribeDiscoverMuted, () => discoverMuted, () => false);
145
- }
146
- // While the preview modal is open, feed videos pause so their audio does not
147
- // compete with the modal player.
148
- let discoverPlaybackSuspended = false;
149
- const discoverSuspendListeners = new Set();
150
- function setDiscoverPlaybackSuspended(suspended) {
151
- discoverPlaybackSuspended = suspended;
152
- for (const listener of discoverSuspendListeners) {
153
- listener();
154
- }
155
- }
156
- function LazyVideo({ src, controls = true }) {
157
- const ref = useRef(null);
158
- useEffect(() => {
159
- const video = ref.current;
160
- if (!video) {
161
- return;
162
- }
163
- const load = () => {
164
- if (!video.dataset.src || video.getAttribute("src")) {
165
- return;
166
- }
167
- video.setAttribute("src", video.dataset.src);
168
- video.load();
169
- };
170
- let inView = false;
171
- const attemptPlay = () => {
172
- if (discoverPlaybackSuspended) {
173
- return;
174
- }
175
- load();
176
- video.muted = discoverMuted;
177
- video.play().then(() => {
178
- delete video.dataset.mutedFallback;
179
- }).catch(() => {
180
- if (video.muted) {
181
- return;
182
- }
183
- // Unmuted autoplay is blocked until the user interacts with the page.
184
- video.dataset.mutedFallback = "1";
185
- video.muted = true;
186
- video.play().catch(() => { });
187
- });
188
- };
189
- const unmuteOnGesture = () => {
190
- if (!discoverMuted && video.dataset.mutedFallback === "1" && !video.paused) {
191
- video.muted = false;
192
- delete video.dataset.mutedFallback;
193
- }
194
- };
195
- window.addEventListener("pointerdown", unmuteOnGesture);
196
- window.addEventListener("keydown", unmuteOnGesture);
197
- const applyGlobalMute = () => {
198
- if (discoverMuted) {
199
- delete video.dataset.mutedFallback;
200
- video.muted = true;
201
- }
202
- else if (!video.paused) {
203
- // The toggle click is a user gesture, so unmuting live playback is allowed.
204
- video.muted = false;
205
- }
206
- };
207
- const unsubscribeMute = subscribeDiscoverMuted(applyGlobalMute);
208
- const applySuspend = () => {
209
- if (discoverPlaybackSuspended) {
210
- if (!video.paused) {
211
- video.pause();
212
- }
213
- }
214
- else if (inView) {
215
- attemptPlay();
216
- }
217
- };
218
- discoverSuspendListeners.add(applySuspend);
219
- const removeListeners = () => {
220
- window.removeEventListener("pointerdown", unmuteOnGesture);
221
- window.removeEventListener("keydown", unmuteOnGesture);
222
- unsubscribeMute();
223
- discoverSuspendListeners.delete(applySuspend);
224
- };
225
- if (!("IntersectionObserver" in window)) {
226
- inView = true;
227
- attemptPlay();
228
- return removeListeners;
229
- }
230
- const preloadObserver = new IntersectionObserver((entries) => {
231
- for (const entry of entries) {
232
- if (!entry.isIntersecting) {
233
- continue;
234
- }
235
- load();
236
- preloadObserver.unobserve(entry.target);
237
- }
238
- }, {
239
- rootMargin: "240px 0px"
240
- });
241
- const playbackObserver = new IntersectionObserver((entries) => {
242
- for (const entry of entries) {
243
- inView = entry.isIntersecting;
244
- if (entry.isIntersecting) {
245
- attemptPlay();
246
- }
247
- else if (!video.paused) {
248
- video.pause();
249
- }
250
- }
251
- }, {
252
- threshold: 0.5
253
- });
254
- preloadObserver.observe(video);
255
- playbackObserver.observe(video);
256
- return () => {
257
- preloadObserver.disconnect();
258
- playbackObserver.disconnect();
259
- removeListeners();
260
- };
261
- }, []);
262
- return _jsx("video", { ref: ref, className: "preview", "data-src": src, preload: "metadata", playsInline: true, loop: true, controls: controls });
263
- }
264
- function DiscoverMuteToggle() {
265
- const muted = useDiscoverMuted();
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
- }
268
- function CopyInlineButton({ text, label }) {
269
- const [status, setStatus] = useState("");
270
- const handleClick = async () => {
271
- try {
272
- await navigator.clipboard.writeText(text);
273
- setStatus("done");
274
- }
275
- catch {
276
- setStatus("error");
277
- }
278
- window.setTimeout(() => setStatus(""), 1600);
279
- };
280
- return (_jsxs("button", { className: "pill-tag copy-inline", type: "button", "data-state": status, "aria-label": status === "done" ? `${label} copied` : label, onClick: () => void handleClick(), children: [_jsx("span", { className: "copy-inline-label", children: label }), _jsx("span", { className: "copy-inline-feedback", "aria-hidden": "true", children: status === "error" ? "Couldn’t copy" : "Copied" })] }));
281
- }
282
- function editorHref(templateId, accountUserId) {
283
- const usesNewEditor = templateId.startsWith("hyperframes:") || templateId.startsWith("template_");
284
- return usesNewEditor
285
- ? `/editor/${encodeURIComponent(templateId.replace(/^hyperframes:/, ""))}${accountUserId ? `?account=${encodeURIComponent(accountUserId)}` : ""}`
286
- : withAccountQuery(`/templates/${encodeURIComponent(templateId)}/editor/docs`, accountUserId);
287
- }
288
- function CreateButton({ templateId, accountUserId }) {
289
- return _jsx("a", { className: "cta-button", href: editorHref(templateId, accountUserId), target: "_blank", rel: "noopener", children: "Editor" });
290
- }
291
- function CardMenu(input) {
292
- const { template } = input;
293
- const [isOpen, setIsOpen] = useState(false);
294
- const [copied, setCopied] = useState(false);
295
- const [deleting, setDeleting] = useState(false);
296
- const menuRef = useRef(null);
297
- const isPending = template.status === "processing" || template.status === "failed";
298
- const canDelete = template.visibility === "private" && Boolean(input.onDeleteTemplate);
299
- useEffect(() => {
300
- if (!isOpen) {
301
- return;
302
- }
303
- const handleClickOutside = (event) => {
304
- if (menuRef.current && !menuRef.current.contains(event.target)) {
305
- setIsOpen(false);
306
- }
307
- };
308
- const handleKey = (event) => {
309
- if (event.key === "Escape")
310
- setIsOpen(false);
311
- };
312
- window.addEventListener("mousedown", handleClickOutside);
313
- window.addEventListener("keydown", handleKey);
314
- return () => {
315
- window.removeEventListener("mousedown", handleClickOutside);
316
- window.removeEventListener("keydown", handleKey);
317
- };
318
- }, [isOpen]);
319
- const handleCopyId = async () => {
320
- try {
321
- await navigator.clipboard.writeText(template.templateId);
322
- setCopied(true);
323
- window.setTimeout(() => {
324
- setCopied(false);
325
- setIsOpen(false);
326
- }, 900);
327
- }
328
- catch {
329
- setIsOpen(false);
330
- }
331
- };
332
- const handleDelete = async () => {
333
- if (!window.confirm(`Delete "${template.title}"? This removes it from your Discover feed permanently.`)) {
334
- return;
335
- }
336
- setDeleting(true);
337
- const result = await (input.onDeleteTemplate?.(template.templateId) ?? Promise.resolve({ ok: false, error: "Delete unavailable." }));
338
- setDeleting(false);
339
- if (result.ok) {
340
- setIsOpen(false);
341
- }
342
- else {
343
- window.alert(result.error || "Unable to delete this template.");
344
- }
345
- };
346
- return (_jsxs("div", { className: "discover-card-menu", ref: menuRef, children: [_jsx("button", { type: "button", className: "discover-card-menu-trigger", "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-label": `More actions for ${template.title}`, title: "More actions", onClick: () => setIsOpen((prev) => !prev), children: _jsx(MoreIcon, {}) }), isOpen ? (_jsxs("div", { className: "discover-card-menu-panel", role: "menu", children: [_jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => void handleCopyId(), children: copied ? "Copied!" : "Copy ID" }), isPending ? (_jsx("span", { className: "discover-card-menu-item is-disabled", role: "menuitem", "aria-disabled": "true", children: "Open in Editor" })) : (_jsx("a", { className: "discover-card-menu-item", role: "menuitem", href: editorHref(template.templateId, input.accountUserId), target: "_blank", rel: "noopener", children: "Open in Editor" })), _jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => {
347
- input.onToggleBookmark?.(template.templateId);
348
- setIsOpen(false);
349
- }, children: input.isBookmarked ? "Remove Bookmark" : "Bookmark" }), canDelete ? (_jsx("button", { type: "button", className: "discover-card-menu-item is-danger", role: "menuitem", disabled: deleting, onClick: () => void handleDelete(), children: deleting ? "Deleting…" : "Delete" })) : null] })) : null] }));
350
- }
351
- function BookmarkButton(input) {
352
- const label = `${input.isBookmarked ? "Remove bookmark for " : "Bookmark "}${input.title}`;
353
- return (_jsx("button", { className: "bookmark-button", type: "button", "data-active": input.isBookmarked ? "true" : "false", "aria-label": label, "aria-pressed": input.isBookmarked, title: label, onClick: () => input.onToggleBookmark?.(input.templateId), children: _jsx(BookmarkIcon, {}) }));
354
- }
355
- function formatDifficulty(difficulty) {
356
- return difficulty.charAt(0).toUpperCase() + difficulty.slice(1);
357
- }
358
- function formatTemplateMeta(template) {
359
- const layerCount = typeof template.layerCount === "number" ? template.layerCount : null;
360
- const sourceCount = typeof template.sourceAssetCount === "number" ? template.sourceAssetCount : null;
361
- const duration = typeof template.durationSeconds === "number" && Number.isFinite(template.durationSeconds)
362
- ? template.durationSeconds >= 60
363
- ? `${Math.floor(template.durationSeconds / 60)}:${String(Math.round(template.durationSeconds % 60)).padStart(2, "0")}`
364
- : `${template.durationSeconds.toFixed(template.durationSeconds >= 10 ? 1 : 2)}s`
365
- : null;
366
- const parts = [
367
- layerCount === null ? null : `${layerCount} layer${layerCount === 1 ? "" : "s"}`,
368
- sourceCount === null ? null : `${sourceCount} source${sourceCount === 1 ? "" : "s"}`,
369
- duration
370
- ].filter(Boolean);
371
- return parts.length ? parts.join(" / ") : template.previewUrl ? "Draft composition with editable layers." : "Layer recipe with editable timeline.";
372
- }
373
- function TemplateCard(input) {
374
- const { template } = input;
375
- const isPending = template.status === "processing" || template.status === "failed";
376
- const media = template.previewUrl
377
- ? isVideoPreview(template.previewUrl)
378
- ? _jsx(LazyVideo, { src: template.previewUrl, controls: false })
379
- : _jsx("img", { className: "preview", src: template.previewUrl, alt: `${template.title} preview`, loading: "lazy", decoding: "async" })
380
- : _jsx("div", { className: "preview preview-empty", children: template.status === "processing" ? "Downloading…" : "No preview" });
381
- return (_jsxs("li", { className: "discover-card row", "data-template-id": template.templateId, children: [template.previewUrl ? (_jsxs("button", { className: "discover-card-media discover-card-media-button", type: "button", "aria-label": `Open ${template.title} preview`, onClick: () => input.onOpenPreview?.(template), children: [media, _jsx("span", { className: "discover-card-media-affordance", "aria-hidden": "true", children: "View" })] })) : (_jsx("div", { className: "discover-card-media", children: media })), _jsxs("div", { className: "discover-card-copy", children: [_jsxs("div", { className: "discover-card-head", children: [_jsxs("div", { className: "discover-card-title-wrap", children: [_jsx("h2", { children: template.title }), _jsx("p", { className: "discover-card-meta", children: formatTemplateMeta(template) })] }), _jsxs("div", { className: "discover-card-actions", children: [_jsx(BookmarkButton, { templateId: template.templateId, title: template.title, isBookmarked: input.isBookmarked, onToggleBookmark: input.onToggleBookmark }), isPending ? (_jsx("span", { className: "cta-button discover-card-cta-disabled", "aria-disabled": "true", children: template.status === "failed" ? "Failed" : "Preparing…" })) : (_jsx(CreateButton, { templateId: template.templateId, accountUserId: input.accountUserId })), _jsx(CardMenu, { template: template, isBookmarked: input.isBookmarked, onToggleBookmark: input.onToggleBookmark, onDeleteTemplate: input.onDeleteTemplate, accountUserId: input.accountUserId })] })] }), _jsx("div", { className: "discover-card-body", children: _jsx("p", { children: String(template.viralDna || "").trim() || "No layer strategy provided for this composition yet." }) }), _jsxs("div", { className: "discover-card-tags", children: [template.origin === "raw" ? (_jsx("div", { className: "pill-tag origin-pill", title: "Your own project \u2014 created from a prompt or your raw clips, not an imported inspiration", children: "Your project" })) : null, template.visibility === "private" ? (_jsx("div", { className: "pill-tag private-pill", title: "Only visible to your account", children: "Only you" })) : null, template.status === "processing" ? (_jsx("div", { className: "pill-tag processing-pill", children: "Processing" })) : template.status === "failed" ? (_jsx("div", { className: "pill-tag failed-pill", children: "Download failed" })) : null, _jsx("div", { className: "pill-tag difficulty-pill", "data-difficulty": template.difficulty, children: formatDifficulty(template.difficulty) }), _jsx("div", { className: "pill-tag", children: template.sourceType || (isVideoPreview(template.previewUrl) ? "Video source" : "Image sequence") }), _jsx(CopyInlineButton, { text: template.templateId, label: template.templateId })] })] })] }));
382
- }
383
- function PreviewModal(input) {
384
- const { onClose, template } = input;
385
- const previewUrl = template.previewUrl || "";
386
- const isVideo = isVideoPreview(previewUrl);
387
- const [copyStatus, setCopyStatus] = useState("");
388
- const muted = useDiscoverMuted();
389
- useEffect(() => {
390
- setDiscoverPlaybackSuspended(true);
391
- return () => setDiscoverPlaybackSuspended(false);
392
- }, []);
393
- const handleCopyLink = async () => {
394
- try {
395
- await navigator.clipboard.writeText(previewUrl);
396
- setCopyStatus("done");
397
- }
398
- catch {
399
- setCopyStatus("error");
400
- }
401
- window.setTimeout(() => setCopyStatus(""), 1600);
402
- };
403
- useEffect(() => {
404
- const handleKeyDown = (event) => {
405
- if (event.key === "Escape") {
406
- onClose();
407
- }
408
- };
409
- const previousOverflow = document.body.style.overflow;
410
- document.body.style.overflow = "hidden";
411
- window.addEventListener("keydown", handleKeyDown);
412
- return () => {
413
- document.body.style.overflow = previousOverflow;
414
- window.removeEventListener("keydown", handleKeyDown);
415
- };
416
- }, [onClose]);
417
- return (_jsx("div", { className: "discover-media-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "discover-media-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "discover-media-modal-title", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "discover-media-modal-head", children: [_jsx("h2", { id: "discover-media-modal-title", children: template.title }), _jsxs("div", { className: "discover-media-modal-actions", children: [_jsx("a", { className: "discover-media-modal-action", href: previewUrl, download: true, "aria-label": `Download ${template.title} preview`, title: "Download", onClick: (event) => event.stopPropagation(), children: _jsx(DownloadIcon, {}) }), _jsx("button", { className: "discover-media-modal-action", type: "button", "data-state": copyStatus, "aria-label": copyStatus === "done" ? "Media URL copied" : copyStatus === "error" ? "Unable to copy media URL" : `Copy ${template.title} preview URL`, title: copyStatus === "done" ? "Copied" : copyStatus === "error" ? "Copy failed" : "Copy link", onClick: () => void handleCopyLink(), children: _jsx(LinkIcon, {}) }), _jsx("button", { className: "discover-media-modal-action", type: "button", "aria-label": "Close preview", title: "Close", onClick: onClose, children: _jsx(CloseIcon, {}) })] })] }), _jsx("div", { className: "discover-media-modal-stage", children: isVideo ? (_jsx("video", { src: previewUrl, controls: true, autoPlay: true, playsInline: true, preload: "metadata", muted: muted, onVolumeChange: (event) => {
418
- // The modal player's mute button doubles as the page-wide toggle.
419
- const nowMuted = event.currentTarget.muted;
420
- if (nowMuted !== discoverMuted) {
421
- setDiscoverMuted(nowMuted);
422
- }
423
- } })) : (_jsx("img", { src: previewUrl, alt: `${template.title} preview` })) })] }) }));
424
- }
425
- function AddTemplateModal(input) {
426
- const { onClose } = input;
427
- const [sourceUrl, setSourceUrl] = useState("");
428
- const [file, setFile] = useState(null);
429
- const [title, setTitle] = useState("");
430
- const [tagline, setTagline] = useState("");
431
- const [notes, setNotes] = useState("");
432
- const [submitting, setSubmitting] = useState(false);
433
- const [error, setError] = useState(null);
434
- useEffect(() => {
435
- const handleKeyDown = (event) => {
436
- if (event.key === "Escape") {
437
- onClose();
438
- }
439
- };
440
- const previousOverflow = document.body.style.overflow;
441
- document.body.style.overflow = "hidden";
442
- window.addEventListener("keydown", handleKeyDown);
443
- return () => {
444
- document.body.style.overflow = previousOverflow;
445
- window.removeEventListener("keydown", handleKeyDown);
446
- };
447
- }, [onClose]);
448
- const handleSubmit = async () => {
449
- const trimmedUrl = sourceUrl.trim();
450
- if (!trimmedUrl && !file) {
451
- setError("Paste a TikTok, YouTube, Twitter/X, or Instagram video URL — or upload a video file.");
452
- return;
453
- }
454
- if (file && file.size > 200 * 1024 * 1024) {
455
- setError("Video uploads can be at most 200 MB.");
456
- return;
457
- }
458
- setSubmitting(true);
459
- setError(null);
460
- const result = await (input.onAddTemplate?.({ sourceUrl: file ? "" : trimmedUrl, file, title: title.trim(), tagline: tagline.trim(), notes: notes.trim() })
461
- ?? Promise.resolve({ ok: false, error: "Adding templates is unavailable right now." }));
462
- setSubmitting(false);
463
- if (result.ok) {
464
- onClose();
465
- }
466
- else {
467
- setError(result.error || "Unable to add that video. Try again.");
468
- }
469
- };
470
- return (_jsx("div", { className: "discover-media-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "discover-media-modal discover-add-template-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "discover-add-template-title", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "discover-media-modal-head", children: [_jsx("h2", { id: "discover-add-template-title", children: "Add Template" }), _jsx("div", { className: "discover-media-modal-actions", children: _jsx("button", { className: "discover-media-modal-action", type: "button", "aria-label": "Close", title: "Close", onClick: onClose, children: _jsx(CloseIcon, {}) }) })] }), _jsxs("form", { className: "discover-add-template-form", onSubmit: (event) => {
471
- event.preventDefault();
472
- void handleSubmit();
473
- }, children: [_jsx("p", { className: "discover-add-template-hint", children: "Paste a link to a TikTok, YouTube, Twitter/X, or Instagram video \u2014 or upload a video file from your computer. We add it to your Discover feed, visible only to your account." }), _jsxs("div", { className: "discover-add-template-field", children: [_jsx("label", { className: "label", htmlFor: "add-template-url", children: "Video URL" }), _jsx("input", { id: "add-template-url", type: "url", required: !file, disabled: Boolean(file), placeholder: "https://www.tiktok.com/@creator/video/\u2026", autoComplete: "off", value: sourceUrl, onChange: (event) => setSourceUrl(event.target.value) })] }), _jsxs("div", { className: "discover-add-template-field", children: [_jsxs("label", { className: "label", htmlFor: "add-template-file", children: ["Or upload a video ", _jsx("span", { className: "discover-add-template-optional", children: "MP4 / MOV / WebM, up to 200 MB" })] }), file ? (_jsxs("div", { className: "discover-add-template-file-chip", children: [_jsx("span", { className: "discover-add-template-file-name", children: file.name }), _jsxs("span", { className: "discover-add-template-file-size", children: [(file.size / (1024 * 1024)).toFixed(1), " MB"] }), _jsx("button", { type: "button", className: "discover-add-template-file-clear", "aria-label": "Remove selected file", onClick: () => setFile(null), children: "\u2715" })] })) : (_jsx("input", { id: "add-template-file", type: "file", accept: "video/mp4,video/quicktime,video/webm,.mp4,.m4v,.mov,.webm", onChange: (event) => {
474
- const selected = event.target.files?.[0] ?? null;
475
- if (selected) {
476
- setFile(selected);
477
- setSourceUrl("");
478
- setError(null);
479
- }
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
- }
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
- }
619
- export function HomepageShell(input) {
620
- const showEmpty = !input.loading && !input.error && input.templates.length > 0 && input.visibleTemplates.length === 0;
621
- const [previewTemplate, setPreviewTemplate] = useState(null);
622
- const [isAddTemplateOpen, setIsAddTemplateOpen] = useState(false);
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"}`;
634
- // Chat rail is always visible on /discover so logged-out visitors see the copilot
635
- // affordance and understand what unlocks after login. When logged in, the real chat
636
- // React tree portals into the [data-template-editor-chat-root] element (see
637
- // page-runtime-client). When logged out, we render an inert placeholder with a
638
- // disabled textarea and a "Log in to chat" CTA in the same slot.
639
- const hasChatRail = true;
640
- const isChatEnabled = input.account.isLoggedIn;
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: {
642
- display: "flex",
643
- flexDirection: "column",
644
- height: "100%",
645
- padding: "20px",
646
- gap: "16px",
647
- boxSizing: "border-box"
648
- }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [_jsx("span", { className: "label", style: {
649
- fontSize: "0.68rem",
650
- fontWeight: 800,
651
- letterSpacing: "0.14em",
652
- textTransform: "uppercase",
653
- color: "#a9884a"
654
- }, children: "Vidfarm copilot" }), _jsx("h3", { style: { margin: 0, fontSize: "1.05rem", color: "#2f3748" }, children: "Chat with Vidfarm" }), _jsx("p", { style: { margin: 0, fontSize: "0.9rem", color: "#74829c" }, children: "Ask for hooks, angles, and remixes tailored to compositions in your library." })] }), _jsx("div", { style: {
655
- display: "flex",
656
- flexDirection: "column",
657
- gap: "8px",
658
- marginTop: "14px"
659
- }, "aria-hidden": "true", children: [
660
- "Give me 10 hooks for this coffee-shop template",
661
- "Which awareness stage fits my product?",
662
- "Remix this into an ad for my brand"
663
- ].map((prompt) => (_jsxs("span", { style: {
664
- display: "inline-flex",
665
- alignItems: "center",
666
- gap: "8px",
667
- padding: "10px 14px",
668
- borderRadius: "14px",
669
- border: "1px dashed rgba(191, 164, 109, 0.45)",
670
- background: "rgba(250, 245, 234, 0.65)",
671
- color: "#74829c",
672
- fontSize: "0.85rem",
673
- lineHeight: 1.35
674
- }, children: [_jsx("span", { style: { color: "#bfa46d", fontWeight: 700 }, children: "\u201C" }), prompt] }, prompt))) }), _jsxs("div", { style: {
675
- marginTop: "auto",
676
- display: "flex",
677
- flexDirection: "column",
678
- gap: "10px"
679
- }, children: [_jsx("textarea", { rows: 3, placeholder: "Log in to chat with Vidfarm\u2026", disabled: true, "aria-label": "Chat with Vidfarm (log in to enable)", style: {
680
- width: "100%",
681
- resize: "none",
682
- padding: "10px 12px",
683
- borderRadius: "10px",
684
- border: "1px solid rgba(191, 164, 109, 0.36)",
685
- background: "rgba(250, 245, 234, 0.9)",
686
- color: "#74829c",
687
- fontFamily: "inherit",
688
- fontSize: "0.9rem",
689
- cursor: "not-allowed",
690
- boxSizing: "border-box"
691
- } }), _jsx("a", { href: "/login", style: {
692
- display: "inline-flex",
693
- alignItems: "center",
694
- justifyContent: "center",
695
- minHeight: "38px",
696
- padding: "0 16px",
697
- borderRadius: "999px",
698
- background: "#bfa46d",
699
- color: "#1a1409",
700
- fontWeight: 700,
701
- fontSize: "0.9rem",
702
- textDecoration: "none"
703
- }, children: "Log in to chat" })] })] })] })) })) : null, previewTemplate ? _jsx(PreviewModal, { template: previewTemplate, onClose: () => setPreviewTemplate(null) }) : null, isAddTemplateOpen ? (_jsx(AddTemplateModal, { onClose: () => setIsAddTemplateOpen(false), onAddTemplate: input.onAddTemplate })) : null] }));
704
- }
705
- //# sourceMappingURL=homepage-view.js.map