@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8

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 (154) 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 +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -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 +265 -0
  13. package/SKILL.director.md +476 -260
  14. package/dist/src/cli.js +362 -96
  15. package/dist/src/devcli/clips.js +15 -10
  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 +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21603
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -828
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3654
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. 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