@vfrey/vfrey 0.0.1 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (603) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +282 -0
  40. package/src/api/commands.ts +330 -0
  41. package/src/api/config.ts +34 -0
  42. package/src/api/creators.ts +463 -0
  43. package/src/api/credentials.ts +152 -0
  44. package/src/api/mail.ts +333 -0
  45. package/src/api/topics.ts +207 -0
  46. package/src/api/types.ts +205 -0
  47. package/src/cli.ts +467 -0
  48. package/src/cloud/executor.ts +281 -0
  49. package/src/cloud/job.ts +106 -0
  50. package/src/cloud/render-job.ts +187 -0
  51. package/src/config.ts +309 -0
  52. package/src/dashboard-link.ts +180 -0
  53. package/src/dashboard-start.ts +103 -0
  54. package/src/desk/channel.ts +64 -0
  55. package/src/desk/cli.ts +216 -0
  56. package/src/desk/creators-route.ts +129 -0
  57. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  58. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  59. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  60. package/src/desk/http.ts +460 -0
  61. package/src/desk/mail-route.ts +103 -0
  62. package/src/desk/overlay.ts +112 -0
  63. package/src/desk/preview.ts +422 -0
  64. package/src/desk/screens.ts +149 -0
  65. package/src/desk/secrets.ts +143 -0
  66. package/src/desk/series.ts +127 -0
  67. package/src/desk/state.ts +77 -0
  68. package/src/desk/studio-route.ts +119 -0
  69. package/src/desk/tick-runner.ts +66 -0
  70. package/src/desk/tick.ts +56 -0
  71. package/src/desk/topics-route.ts +41 -0
  72. package/src/desk/ui/app.css +505 -0
  73. package/src/desk/ui/app.js +185 -0
  74. package/src/desk/ui/apple-icon.png +0 -0
  75. package/src/desk/ui/components.js +79 -0
  76. package/src/desk/ui/core.js +156 -0
  77. package/src/desk/ui/favicon.ico +0 -0
  78. package/src/desk/ui/icon.svg +1 -0
  79. package/src/desk/ui/icons.js +49 -0
  80. package/src/desk/ui/index.html +41 -0
  81. package/src/desk/ui/metrics-math.d.ts +22 -0
  82. package/src/desk/ui/metrics-math.js +135 -0
  83. package/src/desk/ui/theme.js +5 -0
  84. package/src/desk/ui/views-creators.js +209 -0
  85. package/src/desk/ui/views-keys.js +114 -0
  86. package/src/desk/ui/views-mail.js +188 -0
  87. package/src/desk/ui/views-misc.js +123 -0
  88. package/src/desk/ui/views-review.js +119 -0
  89. package/src/desk/ui/views-runs.js +350 -0
  90. package/src/desk/ui/views-screens.js +163 -0
  91. package/src/desk/ui/views-series.js +318 -0
  92. package/src/desk/ui/views-topics.js +87 -0
  93. package/src/desk/verbs.ts +361 -0
  94. package/src/doctor.ts +212 -0
  95. package/src/drain.ts +14 -0
  96. package/src/engage/index.ts +217 -0
  97. package/src/generate/index.ts +110 -0
  98. package/src/generate/script.ts +152 -0
  99. package/src/generate/title.ts +78 -0
  100. package/src/help.ts +233 -0
  101. package/src/home.ts +45 -0
  102. package/src/llm/client.ts +177 -0
  103. package/src/log.ts +7 -0
  104. package/src/oauth/tokens.ts +189 -0
  105. package/src/oauth/vault.ts +72 -0
  106. package/src/pipeline/review.ts +642 -0
  107. package/src/pipeline/run.ts +443 -0
  108. package/src/pipeline/series-guard.ts +26 -0
  109. package/src/pipeline/source-fetch.ts +69 -0
  110. package/src/pipeline/stages.ts +728 -0
  111. package/src/pipeline/topics-source.ts +90 -0
  112. package/src/product/commands.ts +296 -0
  113. package/src/product/config-file.ts +38 -0
  114. package/src/product/engine-choice.ts +89 -0
  115. package/src/product/engines.ts +103 -0
  116. package/src/product/insights.ts +50 -0
  117. package/src/product/keys.ts +67 -0
  118. package/src/product/paths.ts +18 -0
  119. package/src/product/probes.ts +88 -0
  120. package/src/product/push-flow.ts +90 -0
  121. package/src/product/render-video.ts +21 -0
  122. package/src/product/short.ts +62 -0
  123. package/src/product/update.ts +32 -0
  124. package/src/project/canonical.ts +14 -0
  125. package/src/project/commands.ts +177 -0
  126. package/src/project/dashboard-status.ts +28 -0
  127. package/src/project/discover.ts +19 -0
  128. package/src/project/index-file.ts +73 -0
  129. package/src/project/marker.ts +87 -0
  130. package/src/project/migrate.ts +201 -0
  131. package/src/project/names.ts +27 -0
  132. package/src/project/notice.ts +16 -0
  133. package/src/project/open.ts +88 -0
  134. package/src/project/scaffold.ts +112 -0
  135. package/src/project/templates.ts +42 -0
  136. package/src/project/videos.ts +64 -0
  137. package/src/publish/facebook.ts +91 -0
  138. package/src/publish/index.ts +12 -0
  139. package/src/publish/instagram.ts +97 -0
  140. package/src/publish/rate.ts +62 -0
  141. package/src/publish/session.ts +242 -0
  142. package/src/publish/tiktok.ts +202 -0
  143. package/src/publish/youtube.ts +177 -0
  144. package/src/queue/desk-ops.ts +1170 -0
  145. package/src/queue/seal.ts +3 -0
  146. package/src/queue/store.ts +1006 -0
  147. package/src/remotion/AiLabel.tsx +26 -0
  148. package/src/remotion/Root.tsx +73 -0
  149. package/src/remotion/defaults.ts +26 -0
  150. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  151. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  152. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  153. package/src/remotion/films/rea/drawings.tsx +907 -0
  154. package/src/remotion/films/rea/timing.ts +144 -0
  155. package/src/remotion/index.ts +4 -0
  156. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  157. package/src/remotion/templates/KineticType.tsx +39 -0
  158. package/src/remotion/templates/SignalBoard.tsx +50 -0
  159. package/src/remotion/timing.ts +22 -0
  160. package/src/render/chrome.ts +78 -0
  161. package/src/render/engine-mark.ts +29 -0
  162. package/src/render/framey-template.ts +130 -0
  163. package/src/render/html-engine.ts +149 -0
  164. package/src/render/node-crypto-stub.ts +11 -0
  165. package/src/render/probe.ts +29 -0
  166. package/src/render/props.ts +17 -0
  167. package/src/render/vertical-short.ts +35 -0
  168. package/src/render/webpack.ts +33 -0
  169. package/src/render/worker.ts +65 -0
  170. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  171. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  172. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  173. package/src/scene/assets/gsap/gsap.min.js +11 -0
  174. package/src/scene/audio.ts +54 -0
  175. package/src/scene/blocks/LICENSE +190 -0
  176. package/src/scene/blocks/NOTICE +5 -0
  177. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  178. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  179. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  180. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  181. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  182. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  183. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  184. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  185. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  186. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  187. package/src/scene/blocks/index.json +824 -0
  188. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  189. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  190. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  191. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  192. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  193. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  194. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  195. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  196. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  197. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  198. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  199. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  200. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  201. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  202. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  203. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  204. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  205. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  206. package/src/scene/blocks/snow/snow.html +205 -0
  207. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  208. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  209. package/src/scene/blocks/starfield/starfield.html +188 -0
  210. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  211. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  212. package/src/scene/blocks/x-post/x-post.html +437 -0
  213. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  214. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  215. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  216. package/src/scene/blocks.ts +229 -0
  217. package/src/scene/command.ts +272 -0
  218. package/src/scene/figure.ts +181 -0
  219. package/src/scene/glyphs.ts +107 -0
  220. package/src/scene/init.ts +86 -0
  221. package/src/scene/inspect/css-tree.d.ts +6 -0
  222. package/src/scene/inspect/page-scripts.ts +272 -0
  223. package/src/scene/inspect/report.ts +405 -0
  224. package/src/scene/inspect/run.ts +200 -0
  225. package/src/scene/inspect/source.ts +236 -0
  226. package/src/scene/label.ts +131 -0
  227. package/src/scene/net-guard.ts +74 -0
  228. package/src/scene/render.ts +292 -0
  229. package/src/scene/runtime/browser.ts +199 -0
  230. package/src/scene/runtime/core.ts +181 -0
  231. package/src/scene/runtime/react.tsx +115 -0
  232. package/src/scene/runtime/transitions.ts +133 -0
  233. package/src/scene/sanitize.ts +181 -0
  234. package/src/scene/scene.ts +152 -0
  235. package/src/scene/studio-command.ts +48 -0
  236. package/src/scene/studio-skin.ts +59 -0
  237. package/src/scene/studio.ts +337 -0
  238. package/src/scene/supply.ts +111 -0
  239. package/src/scene/timing.ts +288 -0
  240. package/src/scene/transitions/LICENSE +190 -0
  241. package/src/scene/transitions/NOTICE +5 -0
  242. package/src/scene/transitions/SOURCE.json +24 -0
  243. package/src/scene/transitions/transitions.global.js +38 -0
  244. package/src/scene/transitions.ts +26 -0
  245. package/src/service/all-projects.ts +153 -0
  246. package/src/service/commands.ts +256 -0
  247. package/src/service/due.ts +82 -0
  248. package/src/service/lock.ts +101 -0
  249. package/src/service/log-file.ts +56 -0
  250. package/src/service/state.ts +41 -0
  251. package/src/service/units.ts +90 -0
  252. package/src/service/worker.ts +51 -0
  253. package/src/skill-agents.ts +74 -0
  254. package/src/skill-install.ts +334 -0
  255. package/src/sync-write.ts +21 -0
  256. package/src/tick-report.ts +205 -0
  257. package/src/tts.ts +124 -0
  258. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  259. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  260. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  261. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  262. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  263. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  264. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  265. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  266. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  267. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  268. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  269. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  270. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  271. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  272. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  273. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  274. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  275. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  276. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  277. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  278. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  279. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  280. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  281. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  282. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  283. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  284. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  285. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  286. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  287. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  288. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  289. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  290. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  291. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  292. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  293. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  294. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  295. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  296. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  297. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  298. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  299. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  300. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  301. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  302. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  303. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  304. package/vendor/@vfrey/contracts/package.json +18 -0
  305. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  306. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  307. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  308. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  309. package/vendor/@vfrey/ingest/package.json +21 -0
  310. package/vendor/framey/LICENSE +190 -0
  311. package/vendor/framey/NOTICE +15 -0
  312. package/vendor/framey/SOURCE.json +24 -0
  313. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  314. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  315. package/vendor/framey/bin/framey.mjs +16 -0
  316. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  317. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  318. package/vendor/framey/dist/api.d.ts +211 -0
  319. package/vendor/framey/dist/api.js +76 -0
  320. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  321. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  322. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  323. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  324. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  325. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  326. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  327. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  328. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  329. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  330. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  331. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  332. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  333. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  334. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  335. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  336. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  337. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  338. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  339. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  340. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  341. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  342. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  343. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  344. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  345. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  346. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  347. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  348. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  349. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  350. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  351. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  352. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  353. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  354. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  355. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  356. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  357. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  358. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  359. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  360. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  361. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  362. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  363. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  364. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  365. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  366. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  367. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  368. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  369. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  370. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  371. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  372. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  373. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  374. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  375. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  376. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  377. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  378. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  379. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  380. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  381. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  382. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  383. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  384. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  385. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  386. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  387. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  388. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  389. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  390. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  391. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  392. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  393. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  394. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  395. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  396. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  397. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  398. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  399. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  400. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  401. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  402. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  403. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  404. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  405. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  406. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  407. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  408. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  409. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  410. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  411. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  412. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  413. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  414. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  415. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  416. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  417. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  418. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  419. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  420. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  421. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  422. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  423. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  424. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  425. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  426. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  427. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  428. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  429. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  430. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  431. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  432. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  433. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  434. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  435. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  436. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  437. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  438. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  439. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  440. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  441. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  442. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  443. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  444. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  445. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  446. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  447. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  448. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  449. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  450. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  451. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  452. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  453. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  454. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  455. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  456. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  457. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  458. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  459. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  460. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  461. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  462. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  463. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  464. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  465. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  466. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  467. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  468. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  469. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  470. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  471. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  472. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  473. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  474. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  475. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  476. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  477. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  478. package/vendor/framey/dist/cli.js +439 -0
  479. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  480. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  481. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  482. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  483. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  484. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  485. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  486. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  487. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  488. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  489. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  490. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  491. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  492. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  493. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  494. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  495. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  496. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  497. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  498. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  499. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  500. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  501. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  502. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  503. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  504. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  505. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  506. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  507. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  508. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  509. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  510. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  511. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  512. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  513. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  514. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  515. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  516. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  517. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  518. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  519. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  520. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  521. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  522. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  523. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  524. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  525. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  526. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  527. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  528. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  529. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  530. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  531. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  532. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  533. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  534. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  535. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  536. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  537. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  538. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  539. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  540. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  541. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  542. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  543. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  544. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  545. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  546. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  547. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  548. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  549. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  550. package/vendor/framey/dist/runtimeVersion.js +18 -0
  551. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  552. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  553. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  554. package/vendor/framey/dist/sherpaWorker.js +84 -0
  555. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  556. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  557. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  558. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  559. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  560. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  561. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  562. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  563. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  564. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  565. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  566. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  567. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  568. package/vendor/framey/dist/studio/favicon.svg +14 -0
  569. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  570. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  571. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  572. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  573. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  574. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  575. package/vendor/framey/dist/studio/index.html +23 -0
  576. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  577. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  578. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  579. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  580. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  581. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  582. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  583. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  584. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  585. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  586. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  587. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  588. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  589. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  590. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  591. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  592. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  593. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  594. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  595. package/vendor/framey/dist/warmCheck.js +220 -0
  596. package/vendor/framey/dist/warmCheckClient.js +139 -0
  597. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  598. package/vendor/framey-player/LICENSE +190 -0
  599. package/vendor/framey-player/NOTICE +16 -0
  600. package/vendor/framey-player/SOURCE.json +6 -0
  601. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  602. package/vendor/framey.pin +1 -0
  603. package/bin/index.js +0 -3
@@ -0,0 +1,318 @@
1
+ // Series list and series detail.
2
+ import { html, icon, setHtml, $, $$, api, toast, refusalToast, store, rel, abs, cap } from "./core.js";
3
+ import { statusPill, emptyState, platformChips, cadenceSummary, CADENCE, latestRunStatus, platformName } from "./components.js";
4
+
5
+ const csv = (s) => String(s || "").split(",").map((x) => x.trim()).filter(Boolean);
6
+ const SOURCE_KINDS = { rss: "RSS feed", subreddit: "Subreddit", youtubeChannel: "YouTube channel", youtubeTrending: "YouTube trending", "vfrey-topics": "vfrey Topics (niche id)" };
7
+ const TZ_HINT = "UTC";
8
+
9
+ // ---------------------------------------------------------------- list
10
+ export function seriesListView() {
11
+ let root, ctx, accountsKey = "";
12
+ return {
13
+ title: "Series",
14
+ mount(el, params, c) {
15
+ root = el; ctx = c;
16
+ setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Series</span><h1>Series</h1><p class="sub">Each series is a niche with its own cadence, sources, and platforms.</p></div>
17
+ <div class="actions"><button type="button" class="btn" id="new-btn" aria-expanded="false" aria-controls="new-form">${icon("plus")}New series</button></div></div>
18
+ <form class="panel inline-form" id="new-form" hidden>
19
+ <div class="panel-head"><h2>New series</h2></div>
20
+ <div class="panel-body"><div class="grid-3">
21
+ <div class="field"><label for="n-id">Series id</label><input type="text" id="n-id" name="id" required pattern="[a-z0-9][a-z0-9-]*" placeholder="desk" autocomplete="off"><span class="hint">Lowercase letters, digits, dashes.</span></div>
22
+ <div class="field"><label for="n-acc">Account</label><select id="n-acc" name="accountId" required></select></div>
23
+ <div class="field"><label for="n-lang">Language</label><input type="text" id="n-lang" name="language" value="en" autocomplete="off"></div>
24
+ <div class="field" style="grid-column: span 2"><label for="n-niche">Niche</label><input type="text" id="n-niche" name="niche" required placeholder="What this series is about" autocomplete="off"></div>
25
+ <div class="field"><label for="n-tz">Timezone</label><input type="text" id="n-tz" name="timezone" value="${TZ_HINT}" autocomplete="off"></div>
26
+ </div><div class="form-foot"><button type="submit" class="btn">Create series</button><button type="button" class="btn ghost" id="new-cancel">Cancel</button></div></div></form>
27
+ <div id="list"></div>`);
28
+ const form = $(el, "#new-form");
29
+ const toggle = (show) => { form.hidden = !show; $(el, "#new-btn").setAttribute("aria-expanded", String(show)); if (show) $(el, "#n-id").focus(); };
30
+ $(el, "#new-btn").addEventListener("click", () => toggle(form.hidden));
31
+ $(el, "#new-cancel").addEventListener("click", () => toggle(false));
32
+ form.addEventListener("submit", async (e) => {
33
+ e.preventDefault();
34
+ const d = Object.fromEntries(new FormData(form));
35
+ const r = await api("/api/series", { id: d.id.trim(), accountId: d.accountId, niche: d.niche.trim(), language: d.language.trim() || undefined, timezone: d.timezone.trim() || undefined });
36
+ if (!r.ok) return refusalToast(r);
37
+ toast(`Series ${d.id} created`);
38
+ store.refresh?.();
39
+ ctx.navigate(`/series/${encodeURIComponent(r.id || d.id)}`);
40
+ });
41
+ },
42
+ update(s) {
43
+ const accs = s.accounts || [];
44
+ const key = accs.map((a) => a.id).join("|");
45
+ if (key !== accountsKey) {
46
+ const sel = $(root, "#n-acc"); const keep = sel.value;
47
+ setHtml(sel, html`${accs.map((a) => html`<option value="${a.id}">${a.name}</option>`)}`);
48
+ if (keep) sel.value = keep; accountsKey = key;
49
+ }
50
+ const list = s.series || [];
51
+ if (!list.length) {
52
+ setHtml($(root, "#list"), html`<div class="panel">${emptyState("series", "No series yet", "A series is one niche that posts on a schedule. Create the first one.")}</div>`);
53
+ return;
54
+ }
55
+ setHtml($(root, "#list"), html`<div class="series-list">${list.map((x) => {
56
+ const cad = CADENCE[x.cadence?.label] || ["—", ""];
57
+ return html`<a class="panel series-row" href="/series/${x.id}" data-link data-series="${x.id}">
58
+ <div><p class="name">${x.id}</p><p class="niche">${x.niche}</p></div>
59
+ <div><div class="chips"><span class="chip ${x.paused ? "warn" : x.autopilot ? "ok" : ""}">${x.paused ? "Paused" : x.autopilot ? "Autopilot" : "Manual"}</span><span class="chip">${x.language}</span></div><p class="sub">${cadenceSummary(x)}</p></div>
60
+ <div>${platformChips(s, x.id)}<p class="sub"><span class="chip ${cad[1]}" style="height:20px;margin-top:6px">${cad[0]}</span> ${x.cadence?.postsLast7d ?? 0} of ${x.cadence?.target ?? 0} this week</p></div>
61
+ <div>${latestRunStatus(s, x)}</div></a>`;
62
+ })}</div>`);
63
+ },
64
+ };
65
+ }
66
+
67
+ // ---------------------------------------------------------------- detail
68
+ export function seriesDetailView() {
69
+ let root, ctx, id, built = false, listening = false, confirmDelete = false, goLiveOpen = false, liveNotice = null;
70
+ const dirty = new Set();
71
+ const cur = () => (store.state?.series || []).find((x) => x.id === id);
72
+
73
+ function setVal(form, name, v) {
74
+ const el = form.elements[name]; if (!el) return;
75
+ if (el.type === "checkbox") el.checked = !!v;
76
+ else if (document.activeElement !== el) el.value = Array.isArray(v) ? v.join(", ") : v ?? "";
77
+ }
78
+ function fill(s, only) {
79
+ const x = cur(); if (!x) return;
80
+ const f = (n) => $(root, `form[data-sec="${n}"]`);
81
+ const sections = {
82
+ basics: () => ["niche", "language", "voices", "hashtagPool", "autopilot", "allowSpicy", "stylePath", "engine"].forEach((k) => setVal(f("basics"), k, x[k])),
83
+ switches: () => ["publishEnabled", "engageEnabled", "reviewEnabled"].forEach((k) => setVal(f("switches"), k, k === "publishEnabled" ? x[k] !== false : x[k])),
84
+ cadence: () => ["timezone", "minHoursBetweenPosts", "maxPostsPerDay", "windows"].forEach((k) => setVal(f("cadence"), k, x[k])),
85
+ quota: () => ["youtubeDailyUploadCap", "maxRepliesPerDay", "minSecondsBetween"].forEach((k) => setVal(f("quota"), k, x[k])),
86
+ platforms: () => {
87
+ for (const p of ["youtube", "tiktok", "instagram", "facebook"]) {
88
+ const row = (s.platforms || []).find((r) => r.seriesId === id && r.platform === p);
89
+ setVal(f("platforms"), p, row?.enabled);
90
+ }
91
+ const yt = (s.platforms || []).find((r) => r.seriesId === id && r.platform === "youtube");
92
+ setVal(f("platforms"), "privacy", yt?.config?.privacy ?? "private");
93
+ },
94
+ };
95
+ for (const [name, fn] of Object.entries(sections)) if ((!only || only === name) && !dirty.has(name)) fn();
96
+ }
97
+
98
+ function head(s) {
99
+ const x = cur(); const cad = CADENCE[x.cadence?.label] || ["—", ""];
100
+ setHtml($(root, "#sd-head"), html`<a class="crumb" href="/series" data-link>${icon("chevronRight")}Series</a>
101
+ <div class="page-head"><div><span class="eyebrow">Series / Detail</span><h1>${x.id}</h1><p class="sub">${x.niche}</p>
102
+ <div class="chips" style="margin-top:10px"><span class="chip ${x.paused ? "warn" : x.autopilot ? "ok" : ""}">${x.paused ? "Paused" : x.autopilot ? "Autopilot" : "Manual"}</span><span class="chip ${cad[1]}">${cad[0]} · ${x.cadence?.postsLast7d ?? 0} of ${x.cadence?.target ?? 0} this week</span></div></div>
103
+ <div class="actions"><span class="sub">${cadenceSummary(x)}</span></div></div>`);
104
+ }
105
+
106
+ function quotaUsage(x) {
107
+ const capv = Number($(root, "[name=youtubeDailyUploadCap]").value);
108
+ setHtml($(root, "#q-yt"), html`<span>${x.quota?.uploadsToday ?? 0} uploaded today</span>${capv === 0 ? html`<span class="chip warn">Posting is off</span>` : ""}`);
109
+ setHtml($(root, "#q-rep"), html`<span>${x.quota?.repliesToday ?? 0} replies today</span>`);
110
+ setHtml($(root, "#q-gap"), html`<span>${x.quota?.lastReplyAt ? `Last reply ${rel(x.quota.lastReplyAt)}` : "No reply yet"}</span>`);
111
+ }
112
+
113
+ function renderLive(s) {
114
+ const x = cur();
115
+ const ids = (s.channelIds || {})[id] || {};
116
+ const on = (s.platforms || []).filter((r) => r.seriesId === id && r.enabled).map((r) => r.platform);
117
+ const known = on.filter((p) => ids[p]);
118
+ const notice = liveNotice ? html`<div class="notice" role="alert"><b>${liveNotice.refusal}</b> ${liveNotice.fix || ""}</div>` : "";
119
+ if (x.live) {
120
+ setHtml($(root, "#live-box"), html`<div class="live-state"><span class="chip ok" data-live-state>Live</span>
121
+ <span class="sub">Uploads and replies are real.</span>
122
+ <button type="button" class="btn secondary sm" data-act="dry-run">Back to dry-run</button></div>`);
123
+ } else if (!goLiveOpen) {
124
+ setHtml($(root, "#live-box"), html`<div class="live-state"><span class="chip warn" data-live-state>Dry-run</span>
125
+ <span class="sub">The whole pipeline runs and a dry-run publication is recorded. Nothing is uploaded and no reply is sent.</span>
126
+ <button type="button" class="btn sm" data-act="go-live">Go live</button></div>${notice}`);
127
+ } else {
128
+ setHtml($(root, "#live-box"), html`<form class="live-form" id="live-form"><span class="chip warn" data-live-state>Dry-run</span>
129
+ ${known.length ? html`<p class="sub">Channel ids on file:</p><ul class="ids">${known.map((p) => html`<li><span>${platformName(p)}</span> <code data-channel-id="${p}">${ids[p]}</code></li>`)}</ul>
130
+ <p class="sub">Type one of them to confirm this series posts to that channel. Going live makes uploads and replies real.</p>`
131
+ : html`<p class="sub">No channel id is on file for this series. Type the id of the channel or page it posts to (the YouTube channel id, for example). It is saved and used from now on.</p>`}
132
+ <div class="field"><label for="live-confirm">Channel id</label><input type="text" id="live-confirm" name="confirm" autocomplete="off" spellcheck="false" required></div>
133
+ <div class="form-foot"><button type="submit" class="btn">Go live</button><button type="button" class="btn ghost" data-act="go-live-cancel">Cancel</button></div>${notice}</form>`);
134
+ }
135
+ const events = x.events || [];
136
+ setHtml($(root, "#live-log"), events.length ? html`<p class="group-label">Recent changes</p>${events.map((e) => html`<div class="list-row" data-series-event="${e.code}"><span class="grow">${e.message}</span><span class="sub" title="${abs(e.at)}">${rel(e.at)}</span></div>`)}` : "");
137
+ }
138
+
139
+ function renderSources(s) {
140
+ const list = (s.sources || []).filter((x) => x.seriesId === id);
141
+ setHtml($(root, "#src-list"), list.length
142
+ ? html`${list.map((x) => html`<div class="list-row"><span class="chip">${SOURCE_KINDS[x.kind] || x.kind}</span><span class="grow">${x.value}</span>${x.lastStatus ? html`<span class="chip ${/ok|success|fetched/i.test(x.lastStatus) ? "ok" : "warn"}">${x.lastStatus}</span>` : ""}<button type="button" class="btn ghost sm" data-act="rm-source" data-id="${x.id}" aria-label="Remove ${x.value}">${icon("trash")}Remove</button></div>`)}`
143
+ : html`<p class="muted">No source yet. Add one so the series has something to write about.</p>`);
144
+ }
145
+
146
+ function renderSecretChips(s) {
147
+ for (const row of $$(root, "[data-secret]")) {
148
+ const [kind, owner, field] = row.dataset.secret.split("|");
149
+ const present = (s.credentials || []).some((c) => c.ownerKind === kind && c.ownerId === owner && c.field === field);
150
+ const chip = $(row, ".chip"); chip.className = `chip ${present ? "ok" : "warn"}`; chip.textContent = present ? "Present" : "Missing";
151
+ }
152
+ }
153
+
154
+ function renderControls() {
155
+ const x = cur();
156
+ setHtml($(root, "#controls"), html`<div class="actions">
157
+ <button type="button" class="btn secondary" data-act="${x.paused ? "resume" : "pause"}">${icon(x.paused ? "play" : "pause")}${x.paused ? "Resume" : "Pause"}</button>
158
+ ${confirmDelete ? html`<span class="confirm">Delete ${x.id}?<button type="button" class="btn danger solid sm" data-act="delete-yes">Yes, delete</button><button type="button" class="btn ghost sm" data-act="delete-no">Keep it</button></span>`
159
+ : html`<button type="button" class="btn danger" data-act="delete">${icon("trash")}Delete series</button>`}</div>
160
+ <p class="hint" style="margin-top:10px">Paused series keep their settings but never post. Delete removes the series and its sources.</p><div id="ctl-notice"></div>`);
161
+ }
162
+
163
+ async function save(section, path, body, ok) {
164
+ const r = await api(path, body);
165
+ if (!r.ok) { refusalToast(r); return r; }
166
+ dirty.delete(section);
167
+ toast(ok);
168
+ store.refresh?.();
169
+ return r;
170
+ }
171
+
172
+ const engineLabel = (s, id) => (s.engines || []).find((e) => e.id === id)?.label || id || "Remotion";
173
+
174
+ function build(s) {
175
+ const x = cur();
176
+ const sw = (n, l) => html`<label class="switch"><input type="checkbox" name="${n}"><span>${l}</span></label>`;
177
+ const num = (n, l) => html`<div class="field"><label for="f-${n}">${l}</label><input type="number" id="f-${n}" name="${n}" min="0" step="1"></div>`;
178
+ const acct = (s.accounts || []).find((a) => a.id === x.accountId);
179
+ const secretGroup = (kind, ownerId, fields, label) => html`<p class="group-label">${label}</p>${fields.map((field) => html`
180
+ <div class="sec-row" data-secret="${kind}|${ownerId}|${field}"><div class="sec-name"><span class="mono">${field}</span><span class="chip warn">Missing</span></div>
181
+ <div class="sec-form"><input type="password" autocomplete="new-password" spellcheck="false" aria-label="${field} value" placeholder="Paste new value"><button type="button" class="btn sm" data-act="secret-save">Save</button><button type="button" class="btn secondary sm" data-act="secret-clear">Clear</button></div></div>`)}`;
182
+ const secrets = s.secretFields || { account: [], series: [] };
183
+ setHtml(root, html`<div id="sd-head"></div><div class="stack">
184
+ <form class="panel" data-sec="basics"><div class="panel-head"><h2>Basics</h2></div><div class="panel-body">
185
+ <div class="grid-2"><div class="field"><label for="f-niche">Niche</label><input type="text" id="f-niche" name="niche" required></div>
186
+ <div class="field"><label for="f-language">Language</label><input type="text" id="f-language" name="language"></div>
187
+ <div class="field"><label for="f-voices">Voices</label><input type="text" id="f-voices" name="voices" placeholder="Comma separated"><span class="hint">Comma separated.</span></div>
188
+ <div class="field"><label for="f-hashtagPool">Hashtag pool</label><input type="text" id="f-hashtagPool" name="hashtagPool" placeholder="#shorts, #tech"><span class="hint">Comma separated.</span></div>
189
+ <div class="field" style="grid-column:1/-1"><label for="f-stylePath">Style path</label><input type="text" id="f-stylePath" name="stylePath" placeholder="channel-style/desk.json"></div>
190
+ <div class="field" style="grid-column:1/-1"><label for="f-engine">Engine</label><select id="f-engine" name="engine" aria-describedby="f-engine-hint"><option value="">Machine default (${engineLabel(s, s.defaultEngine)})</option>${(s.engines || []).map((e) => html`<option value="${e.id}" ${e.availability === "ready" ? "" : "disabled"} title="${e.reason || ""}">${e.label}${e.availability === "ready" ? "" : " (unavailable)"}</option>`)}</select>
191
+ <span class="hint" id="f-engine-hint">Which engine renders this series' videos. A run started with an engine named on the command uses that one instead.${(s.engines || []).filter((e) => e.availability !== "ready").map((e) => html` <span class="engine-why">${e.label}: ${e.reason}.</span>`)}</span></div></div>
192
+ <div class="switch-row" style="margin-top:20px">${sw("autopilot", "Autopilot")}${sw("allowSpicy", "Allow spicy takes")}</div>
193
+ <div class="form-foot"><button type="submit" class="btn">Save basics</button></div></div></form>
194
+
195
+ <section class="panel" data-sec="publishing" id="publishing"><div class="panel-head"><h2>Publishing</h2><span class="sub">A new series is a dry-run until you go live</span></div><div class="panel-body">
196
+ <div id="live-box"></div>
197
+ <form data-sec="switches" style="margin-top:20px"><p class="group-label">Switches</p>
198
+ <div class="switch-row">${sw("publishEnabled", "Publish")}${sw("engageEnabled", "Engage")}${sw("reviewEnabled", "Review before render")}</div>
199
+ <p class="hint" style="margin-top:8px">Publish off: the video is made and kept, and nothing uploads. Engage off: no replies. Review on: each video waits for your approval before it renders.</p>
200
+ <div class="form-foot"><button type="submit" class="btn secondary">Save switches</button></div></form>
201
+ <div id="live-log"></div></div></section>
202
+
203
+ <form class="panel" data-sec="cadence"><div class="panel-head"><h2>Cadence</h2><span class="sub">When posts may go out</span></div><div class="panel-body">
204
+ <div class="grid-2"><div class="field"><label for="f-timezone">Timezone</label><input type="text" id="f-timezone" name="timezone" placeholder="UTC"></div>
205
+ ${num("minHoursBetweenPosts", "Min hours between posts")}${num("maxPostsPerDay", "Max posts per day")}
206
+ <div class="field"><label for="f-windows">Windows</label><input type="text" id="f-windows" name="windows" placeholder="09:00-11:00, 18:00-20:00"><span class="hint">Comma separated.</span></div></div>
207
+ <div class="form-foot"><button type="submit" class="btn">Save cadence</button></div></div></form>
208
+
209
+ <form class="panel" data-sec="quota"><div class="panel-head"><h2>Quota</h2><span class="sub">Hard caps per day</span></div><div class="panel-body stack" style="gap:16px">
210
+ <div class="quota-row">${num("youtubeDailyUploadCap", "YouTube daily upload cap")}<div class="usage" id="q-yt"></div></div>
211
+ <div class="quota-row">${num("maxRepliesPerDay", "Max replies per day")}<div class="usage" id="q-rep"></div></div>
212
+ <div class="quota-row">${num("minSecondsBetween", "Min seconds between replies")}<div class="usage" id="q-gap"></div></div>
213
+ <div class="form-foot" style="margin-top:0"><button type="submit" class="btn">Save quota</button></div></div></form>
214
+
215
+ <form class="panel" data-sec="platforms"><div class="panel-head"><h2>Platforms</h2><span class="sub">Where finished videos go</span></div><div class="panel-body">
216
+ ${["youtube", "tiktok", "instagram", "facebook"].map((p) => html`<div class="plat"><div class="name">${sw(p, platformName(p))}</div>
217
+ ${p === "youtube" ? html`<div class="field"><label class="sr" for="f-privacy">YouTube privacy</label><select id="f-privacy" name="privacy"><option value="private">Private</option><option value="unlisted">Unlisted</option><option value="public">Public</option></select></div>` : ""}</div>`)}
218
+ <div class="form-foot"><button type="submit" class="btn">Save platforms</button></div></div></form>
219
+
220
+ <section class="panel" data-sec="sources"><div class="panel-head"><h2>Sources</h2><span class="sub">What the series reads for ideas</span></div><div class="panel-body">
221
+ <div id="src-list"></div>
222
+ <form class="add-row" id="src-form"><select name="kind" aria-label="Source kind">${Object.entries(SOURCE_KINDS).map(([k, l]) => html`<option value="${k}">${l}</option>`)}</select>
223
+ <input type="text" name="value" required aria-label="Source value" placeholder="https://… or r/subreddit or channel id or niche id" autocomplete="off"><button type="submit" class="btn">${icon("plus")}Add source</button></form></div></section>
224
+
225
+ <section class="panel" data-sec="secrets"><div class="panel-head"><h2>Secrets</h2><span class="sub">Write only. A saved value is never shown again.</span></div><div class="panel-body">
226
+ ${secrets.series.length ? secretGroup("series", id, secrets.series, `Series · ${id}`) : ""}
227
+ ${secrets.account.length ? secretGroup("account", x.accountId, secrets.account, `Account · ${acct?.name || x.accountId}`) : ""}
228
+ ${!secrets.series.length && !secrets.account.length ? html`<p class="muted">This setup has no secret fields.</p>` : ""}</div></section>
229
+
230
+ <section class="panel" data-sec="controls"><div class="panel-head"><h2>Controls</h2></div><div class="panel-body" id="controls"></div></section></div>`);
231
+ built = true;
232
+ if (listening) return;
233
+ listening = true;
234
+
235
+ root.addEventListener("input", (e) => { const f = e.target.closest("form[data-sec]"); if (f && e.target.type !== "password") dirty.add(f.dataset.sec); if (e.target.name === "youtubeDailyUploadCap") quotaUsage(cur()); });
236
+ root.addEventListener("submit", async (e) => {
237
+ e.preventDefault();
238
+ const form = e.target;
239
+ if (form.id === "src-form") {
240
+ const d = Object.fromEntries(new FormData(form));
241
+ const r = await api(`/api/series/${encodeURIComponent(id)}/sources`, { kind: d.kind, value: d.value.trim() });
242
+ if (!r.ok) return refusalToast(r);
243
+ form.reset(); toast("Source added"); store.refresh?.(); return;
244
+ }
245
+ if (form.id === "live-form") {
246
+ const r = await api(`/api/series/${encodeURIComponent(id)}/live`, { confirm: form.elements.confirm.value.trim() });
247
+ if (!r.ok) { liveNotice = r; renderLive(store.state); return refusalToast(r); }
248
+ goLiveOpen = false; liveNotice = null; toast("Series is live");
249
+ if (store.state) { const x = cur(); if (x) x.live = true; }
250
+ renderLive(store.state); store.refresh?.(); return;
251
+ }
252
+ const sec = form.dataset.sec; const v = (n) => form.elements[n];
253
+ const base = `/api/series/${encodeURIComponent(id)}`;
254
+ if (sec === "basics") await save(sec, base, { niche: v("niche").value.trim(), language: v("language").value.trim(), voices: csv(v("voices").value), hashtagPool: csv(v("hashtagPool").value), autopilot: v("autopilot").checked, allowSpicy: v("allowSpicy").checked, stylePath: v("stylePath").value.trim(), engine: v("engine").value }, "Basics saved");
255
+ if (sec === "switches") await save(sec, base, { publishEnabled: v("publishEnabled").checked, engageEnabled: v("engageEnabled").checked, reviewEnabled: v("reviewEnabled").checked }, "Switches saved");
256
+ if (sec === "cadence") await save(sec, `${base}/cadence`, { timezone: v("timezone").value.trim(), minHoursBetweenPosts: Number(v("minHoursBetweenPosts").value), maxPostsPerDay: Number(v("maxPostsPerDay").value), windows: csv(v("windows").value) }, "Cadence saved");
257
+ if (sec === "quota") await save(sec, `${base}/quota`, { youtubeDailyUploadCap: Number(v("youtubeDailyUploadCap").value), maxRepliesPerDay: Number(v("maxRepliesPerDay").value), minSecondsBetween: Number(v("minSecondsBetween").value) }, "Quota saved");
258
+ if (sec === "platforms") {
259
+ let n = 0;
260
+ for (const p of ["youtube", "tiktok", "instagram", "facebook"]) {
261
+ const row = (store.state.platforms || []).find((r) => r.seriesId === id && r.platform === p);
262
+ const enabled = v(p).checked;
263
+ const privacy = p === "youtube" ? v("privacy").value : null;
264
+ const changed = enabled !== !!row?.enabled || (p === "youtube" && privacy !== (row?.config?.privacy ?? "private"));
265
+ if (!changed) continue;
266
+ const body = { platform: p, enabled }; if (p === "youtube") body.config = { ...(row?.config || {}), privacy };
267
+ const r = await api(`${base}/platforms`, body);
268
+ if (!r.ok) return refusalToast(r);
269
+ n++;
270
+ }
271
+ dirty.delete(sec); toast(n ? "Platforms saved" : "Platforms already up to date"); store.refresh?.();
272
+ }
273
+ });
274
+ root.addEventListener("click", async (e) => {
275
+ const b = e.target.closest("[data-act]"); if (!b) return;
276
+ const act = b.dataset.act; const base = `/api/series/${encodeURIComponent(id)}`;
277
+ if (act === "rm-source") { const r = await api(`/api/sources/${encodeURIComponent(b.dataset.id)}/delete`, {}); if (!r.ok) return refusalToast(r); toast("Source removed"); store.refresh?.(); }
278
+ if (act === "pause" || act === "resume") { const r = await api(`${base}/${act}`, {}); if (!r.ok) return refusalToast(r); toast(act === "pause" ? "Series paused" : "Series resumed"); store.refresh?.(); }
279
+ if (act === "go-live") { goLiveOpen = true; liveNotice = null; renderLive(store.state); const f = $(root, "#live-confirm"); if (f) f.focus(); }
280
+ if (act === "go-live-cancel") { goLiveOpen = false; liveNotice = null; renderLive(store.state); }
281
+ if (act === "dry-run") { const r = await api(`${base}/dry-run`, {}); if (!r.ok) return refusalToast(r); toast("Back to dry-run"); store.refresh?.(); }
282
+ if (act === "delete") { confirmDelete = true; renderControls(); }
283
+ if (act === "delete-no") { confirmDelete = false; renderControls(); }
284
+ if (act === "delete-yes") {
285
+ confirmDelete = false;
286
+ const r = await api(`${base}/delete`, {});
287
+ if (!r.ok) { renderControls(); setHtml($(root, "#ctl-notice"), html`<div class="notice" role="alert"><b>${r.refusal}</b> ${r.fix || ""}</div>`); return refusalToast(r); }
288
+ toast(`Series ${id} deleted`); store.refresh?.(); ctx.navigate("/series");
289
+ }
290
+ if (act === "secret-save" || act === "secret-clear") {
291
+ const row = b.closest("[data-secret]"); const input = $(row, "input");
292
+ const [ownerKind, ownerId, field] = row.dataset.secret.split("|");
293
+ if (act === "secret-save" && !input.value) { toast("Type a value first", { kind: "error" }); return; }
294
+ const r = act === "secret-save" ? await api("/api/secrets", { ownerKind, ownerId, field, value: input.value }) : await api("/api/secrets/clear", { ownerKind, ownerId, field });
295
+ input.value = "";
296
+ if (!r.ok) return refusalToast(r);
297
+ toast(act === "secret-save" ? `${field} saved` : `${field} cleared`); store.refresh?.();
298
+ }
299
+ });
300
+ }
301
+
302
+ return {
303
+ title: "Series",
304
+ mount(el, params, c) { root = el; ctx = c; id = params.id; el.classList.add("narrow"); setHtml(el, html`<p class="muted">Loading…</p>`); },
305
+ update(s) {
306
+ if (!cur()) {
307
+ built = false;
308
+ setHtml(root, html`<a class="crumb" href="/series" data-link>${icon("chevronRight")}Series</a><div class="panel">${emptyState("series", "Series not found", `There is no series called ${id}.`, html`<a class="btn" href="/series" data-link>Back to series</a>`)}</div>`);
309
+ return;
310
+ }
311
+ if (!built) { build(s); fill(s); renderControls(); }
312
+ head(s); fill(s); quotaUsage(cur()); renderSources(s); renderSecretChips(s); renderLive(s);
313
+ const x = cur();
314
+ const pause = $(root, "[data-act=pause],[data-act=resume]");
315
+ if (pause && pause.dataset.act !== (x.paused ? "resume" : "pause") && !confirmDelete) renderControls();
316
+ },
317
+ };
318
+ }
@@ -0,0 +1,87 @@
1
+ // Topics: what is trending in a niche, with evidence, and "Make a video" for one of them.
2
+ // The browser talks only to this desk's own /api/topics routes. The vfrey token stays on the server.
3
+ import { html, icon, setHtml, $, $$, read, toast, refusalToast, store, num } from "./core.js";
4
+ import { emptyState, platformName } from "./components.js";
5
+ import { startTick } from "./views-runs.js";
6
+
7
+ const views = (n) => (typeof n === "number" ? (n >= 1e6 ? `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K` : num(n)) : "—");
8
+ const age = (h) => (typeof h !== "number" || h < 0 ? "—" : h < 1 ? "<1h" : h < 48 ? `${Math.round(h)}h` : `${Math.round(h / 24)}d`);
9
+ const safeUrl = (u) => (/^https?:\/\//i.test(String(u || "")) ? u : "");
10
+ const NICHE_KEY = "vfrey-topics-niche";
11
+ const remembered = () => { try { return localStorage.getItem(NICHE_KEY) || ""; } catch { return ""; } };
12
+ const remember = (v) => { try { localStorage.setItem(NICHE_KEY, v); } catch { /* ignore */ } };
13
+
14
+ export function topicsView() {
15
+ let root;
16
+ const m = { niches: null, nicheId: remembered(), topics: null, error: null, loading: false, busy: false, series: "" };
17
+
18
+ const draw = () => {
19
+ if (!root) return;
20
+ if (m.error) {
21
+ const out = m.error.code === "not_signed_in" || m.error.code === "unauthorized" || m.error.code === "not_live";
22
+ const cmd = out ? html`<p><code data-testid="login-command">npx vfrey login</code></p>` : "";
23
+ setHtml($(root, "#t-body"), html`<div class="panel" data-testid="topics-state" data-state="${m.error.code || "error"}">${emptyState("alert", out ? "Sign in to see topics" : "Topics are not available", m.error.refusal || "Something went wrong.", html`${cmd}<p class="hint">${m.error.fix || ""}</p>`)}</div>`);
24
+ return;
25
+ }
26
+ if (!m.niches) { setHtml($(root, "#t-body"), html`<div class="panel"><p class="muted" style="padding:20px">Loading niches…</p></div>`); return; }
27
+ const all = (store.state?.series || []);
28
+ if (!m.series && all.length) m.series = all[0].id;
29
+ const picker = html`<div class="add-row" style="grid-template-columns:minmax(0,1fr) minmax(0,1fr)">
30
+ <label class="field"><span class="muted">Niche</span><select id="t-niche" aria-label="Niche">${m.niches.map((n) => html`<option value="${n.id}" ${n.id === m.nicheId ? "selected" : ""}>${n.name}${n.custom ? " (yours)" : ""}</option>`)}</select></label>
31
+ <label class="field"><span class="muted">Make videos in series</span><select id="t-series" aria-label="Series">${all.length ? all.map((s) => html`<option value="${s.id}" ${s.id === m.series ? "selected" : ""}>${s.id}</option>`) : html`<option value="">No series yet</option>`}</select></label></div>`;
32
+ let list;
33
+ if (m.loading) list = html`<p class="muted" style="padding:20px">Loading topics…</p>`;
34
+ else if (!m.nicheId) list = html`<p class="muted" style="padding:20px">Pick a niche to see what is trending.</p>`;
35
+ else if (!m.topics) list = "";
36
+ else if (!m.topics.length) list = html`<p class="muted" style="padding:20px" data-testid="topics-empty">No topics yet for this niche. It is collected on a schedule; check back later.</p>`;
37
+ else list = m.topics.map((t, i) => html`<div class="list-row" data-topic="${t.id}" style="align-items:flex-start;flex-wrap:wrap">
38
+ <span class="chip">${i + 1}</span>
39
+ <div class="grow"><p class="t"><b>${t.title}</b></p>
40
+ <p class="sub">Score ${Number(t.score).toFixed(1)} · ${(t.platforms || []).map(platformName).join(", ")}</p>
41
+ <ul class="evidence" style="list-style:none;padding:0;margin:6px 0 0">${(t.evidence || []).map((e) => html`<li class="sub">${safeUrl(e.url) ? html`<a href="${safeUrl(e.url)}" target="_blank" rel="noopener noreferrer">${e.url}</a>` : "(no link)"} · ${views(e.views)} views · ${age(e.ageHours)}</li>`)}</ul></div>
42
+ <button type="button" class="btn sm" data-make="${t.id}" ${m.busy || !m.series ? "disabled" : ""}>${icon("play")}Make a video</button></div>`);
43
+ setHtml($(root, "#t-body"), html`<div class="stack"><section class="panel"><div class="panel-body">${picker}</div></section>
44
+ <section class="panel" id="t-list"><div class="panel-head"><h2>Ranked topics</h2><span class="sub">Topics use credits; see your balance with <code>npx vfrey whoami</code>.</span></div>${list}</section></div>`);
45
+ };
46
+
47
+ const loadTopics = async () => {
48
+ if (!m.nicheId) { m.topics = null; draw(); return; }
49
+ m.loading = true; m.error = null; draw();
50
+ const r = await read(`/api/topics?niche=${encodeURIComponent(m.nicheId)}&limit=20`).catch(() => ({ ok: false, refusal: "The desk could not be reached.", fix: "Check that `npx vfrey dashboard` is still running.", code: "offline" }));
51
+ m.loading = false;
52
+ if (!r.ok) { m.error = r; m.topics = null; } else m.topics = r.topics;
53
+ draw();
54
+ };
55
+
56
+ const loadNiches = async () => {
57
+ const r = await read("/api/topics/niches").catch(() => ({ ok: false, refusal: "The desk could not be reached.", fix: "Check that `npx vfrey dashboard` is still running.", code: "offline" }));
58
+ if (!r.ok) { m.error = r; draw(); return; }
59
+ m.niches = r.niches;
60
+ if (!m.niches.some((n) => n.id === m.nicheId)) m.nicheId = m.niches[0]?.id || "";
61
+ draw();
62
+ await loadTopics();
63
+ };
64
+
65
+ return {
66
+ title: "Topics",
67
+ mount(el) {
68
+ root = el;
69
+ setHtml(el, html`<div class="page-head"><div><span class="eyebrow">Desk / Topics</span><h1>Topics</h1><p class="sub">What is trending in a niche, with the posts that show it.</p></div></div><div id="t-body"></div>`);
70
+ el.addEventListener("change", (e) => {
71
+ if (e.target.id === "t-niche") { m.nicheId = e.target.value; remember(m.nicheId); loadTopics(); }
72
+ if (e.target.id === "t-series") m.series = e.target.value;
73
+ });
74
+ el.addEventListener("click", async (e) => {
75
+ const b = e.target.closest("[data-make]"); if (!b) return;
76
+ const topic = (m.topics || []).find((t) => t.id === b.dataset.make); if (!topic || !m.series) return;
77
+ m.busy = true; draw();
78
+ const r = await startTick({ now: true, series: m.series, about: topic.title });
79
+ m.busy = false; draw();
80
+ if (r.ok) toast("Making a video about this topic", { detail: topic.title });
81
+ });
82
+ draw();
83
+ loadNiches();
84
+ },
85
+ update() { if (m.niches && !m.series) draw(); },
86
+ };
87
+ }