voxflow 1.15.0 → 1.15.2

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 (453) hide show
  1. package/README.md +34 -0
  2. package/bin/voxflow.js +27 -0
  3. package/dist/index.js +1 -1
  4. package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
  5. package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
  6. package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
  7. package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
  8. package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
  9. package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
  10. package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
  11. package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
  12. package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
  13. package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
  14. package/dist/remotion-bundle/112.bundle.js +11 -0
  15. package/dist/remotion-bundle/112.bundle.js.map +1 -0
  16. package/dist/remotion-bundle/113.bundle.js +11 -0
  17. package/dist/remotion-bundle/113.bundle.js.map +1 -0
  18. package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
  19. package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
  20. package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
  21. package/dist/remotion-bundle/163.bundle.js +14678 -0
  22. package/dist/remotion-bundle/163.bundle.js.map +1 -0
  23. package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
  24. package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
  25. package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
  26. package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
  27. package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
  28. package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
  29. package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
  30. package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
  31. package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
  32. package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
  33. package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
  34. package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
  35. package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
  36. package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
  37. package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
  38. package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
  39. package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
  40. package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
  41. package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
  42. package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
  43. package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
  44. package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
  45. package/dist/remotion-bundle/275.bundle.js +21 -0
  46. package/dist/remotion-bundle/275.bundle.js.map +1 -0
  47. package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
  48. package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
  49. package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
  50. package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
  51. package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
  52. package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
  53. package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
  54. package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
  55. package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
  56. package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
  57. package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
  58. package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
  59. package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
  60. package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
  61. package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
  62. package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
  63. package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
  64. package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
  65. package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
  66. package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
  67. package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
  68. package/dist/remotion-bundle/434.bundle.js +205 -0
  69. package/dist/remotion-bundle/434.bundle.js.map +1 -0
  70. package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
  71. package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
  72. package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
  73. package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
  74. package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
  75. package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
  76. package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
  77. package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
  78. package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
  79. package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
  80. package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
  81. package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
  82. package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
  83. package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
  84. package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
  85. package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
  86. package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
  87. package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
  88. package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
  89. package/dist/remotion-bundle/580.bundle.js +11 -0
  90. package/dist/remotion-bundle/580.bundle.js.map +1 -0
  91. package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
  92. package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
  93. package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
  94. package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
  95. package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
  96. package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
  97. package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
  98. package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
  99. package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
  100. package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
  101. package/dist/remotion-bundle/651.bundle.js +11 -0
  102. package/dist/remotion-bundle/651.bundle.js.map +1 -0
  103. package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
  104. package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
  105. package/dist/remotion-bundle/690.bundle.js +3479 -0
  106. package/dist/remotion-bundle/690.bundle.js.map +1 -0
  107. package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
  108. package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
  109. package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
  110. package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
  111. package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
  112. package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
  113. package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
  114. package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
  115. package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
  116. package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
  117. package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
  118. package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
  119. package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
  120. package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
  121. package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
  122. package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
  123. package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
  124. package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
  125. package/dist/remotion-bundle/813.bundle.js +117511 -0
  126. package/dist/remotion-bundle/813.bundle.js.map +1 -0
  127. package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
  128. package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
  129. package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
  130. package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
  131. package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
  132. package/dist/remotion-bundle/879.bundle.js +64 -0
  133. package/dist/remotion-bundle/879.bundle.js.map +1 -0
  134. package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
  135. package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
  136. package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
  137. package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
  138. package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
  139. package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
  140. package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
  141. package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
  142. package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
  143. package/dist/remotion-bundle/938.bundle.js +451 -0
  144. package/dist/remotion-bundle/938.bundle.js.map +1 -0
  145. package/dist/remotion-bundle/967.bundle.js +4462 -0
  146. package/dist/remotion-bundle/967.bundle.js.map +1 -0
  147. package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
  148. package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
  149. package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
  150. package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
  151. package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
  152. package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
  153. package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
  154. package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
  155. package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
  156. package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
  157. package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
  158. package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
  159. package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
  160. package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
  161. package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
  162. package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
  163. package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
  164. package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
  165. package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
  166. package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
  167. package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
  168. package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
  169. package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
  170. package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
  171. package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
  172. package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
  173. package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
  174. package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
  175. package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
  176. package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
  177. package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
  178. package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
  179. package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
  180. package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
  181. package/dist/remotion-bundle/bundle.js +83376 -0
  182. package/dist/remotion-bundle/bundle.js.map +1 -0
  183. package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
  184. package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
  185. package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
  186. package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
  187. package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
  188. package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
  189. package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
  190. package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
  191. package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
  192. package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
  193. package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
  194. package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
  195. package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
  196. package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
  197. package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
  198. package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
  199. package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
  200. package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
  201. package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
  202. package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
  203. package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
  204. package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
  205. package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
  206. package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
  207. package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
  208. package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
  209. package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
  210. package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
  211. package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
  212. package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
  213. package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
  214. package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
  215. package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
  216. package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
  217. package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
  218. package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
  219. package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
  220. package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
  221. package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
  222. package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
  223. package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
  224. package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
  225. package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
  226. package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
  227. package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
  228. package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
  229. package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
  230. package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
  231. package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
  232. package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
  233. package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
  234. package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
  235. package/dist/remotion-bundle/favicon.ico +0 -0
  236. package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
  237. package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
  238. package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
  239. package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
  240. package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
  241. package/dist/remotion-bundle/index.html +49 -0
  242. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
  243. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
  244. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
  245. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
  246. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
  247. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
  248. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
  249. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
  250. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
  251. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
  252. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
  253. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
  254. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
  255. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
  256. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
  257. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
  258. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
  259. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
  260. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
  261. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
  262. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
  263. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
  264. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
  265. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
  266. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
  267. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
  268. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
  269. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
  270. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
  271. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
  272. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
  273. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
  274. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
  275. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
  276. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
  277. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
  278. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
  279. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
  280. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
  281. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
  282. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
  283. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
  284. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
  285. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
  286. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
  287. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
  288. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
  289. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
  290. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
  291. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
  292. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
  293. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
  294. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
  295. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
  296. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
  297. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
  298. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
  299. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
  300. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
  301. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
  302. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
  303. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
  304. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
  305. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
  306. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
  307. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
  308. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
  309. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
  310. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
  311. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
  312. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
  313. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
  314. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
  315. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
  316. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
  317. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
  318. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
  319. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
  320. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
  321. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
  322. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
  323. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
  324. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
  325. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
  326. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
  327. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
  328. package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
  329. package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
  330. package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
  331. package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
  332. package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
  333. package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
  334. package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
  335. package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
  336. package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
  337. package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
  338. package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
  339. package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
  340. package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
  341. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
  342. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
  343. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
  344. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
  345. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
  346. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
  347. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
  348. package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
  349. package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
  350. package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
  351. package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
  352. package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
  353. package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
  354. package/dist/remotion-bundle/source-map-helper.wasm +0 -0
  355. package/lib/cli.js +270 -0
  356. package/lib/commands/_registry.js +48 -0
  357. package/lib/commands/add.js +242 -0
  358. package/lib/commands/asr/azure-transcribe.js +336 -0
  359. package/lib/commands/asr/cloud-transcribe.js +384 -0
  360. package/lib/commands/asr/helpers.js +76 -0
  361. package/lib/commands/asr/index.js +236 -0
  362. package/lib/commands/asr/local-transcribe.js +125 -0
  363. package/lib/commands/asr-jobs.js +257 -0
  364. package/lib/commands/asr.js +11 -0
  365. package/lib/commands/auth-cmds.js +358 -0
  366. package/lib/commands/dub.js +542 -0
  367. package/lib/commands/explain.js +512 -0
  368. package/lib/commands/feedback.js +152 -0
  369. package/lib/commands/image.js +207 -0
  370. package/lib/commands/mcp-key.js +166 -0
  371. package/lib/commands/narrate.js +639 -0
  372. package/lib/commands/picstory-templates.js +276 -0
  373. package/lib/commands/picstory.js +547 -0
  374. package/lib/commands/podcast/dialogue.js +109 -0
  375. package/lib/commands/podcast/generate.js +127 -0
  376. package/lib/commands/podcast/index.js +561 -0
  377. package/lib/commands/podcast/synthesize.js +188 -0
  378. package/lib/commands/podcast.js +11 -0
  379. package/lib/commands/present.js +519 -0
  380. package/lib/commands/publish.js +415 -0
  381. package/lib/commands/skills.js +473 -0
  382. package/lib/commands/slice-render.js +282 -0
  383. package/lib/commands/slice-stage.js +264 -0
  384. package/lib/commands/slice.js +346 -0
  385. package/lib/commands/slides/constants.js +108 -0
  386. package/lib/commands/slides/html-renderer.js +338 -0
  387. package/lib/commands/slides/index.js +345 -0
  388. package/lib/commands/slides.js +11 -0
  389. package/lib/commands/story.js +302 -0
  390. package/lib/commands/summarize.js +532 -0
  391. package/lib/commands/synthesize.js +261 -0
  392. package/lib/commands/translate.js +593 -0
  393. package/lib/commands/upgrade.js +249 -0
  394. package/lib/commands/video-translate.js +577 -0
  395. package/lib/commands/voices.js +292 -0
  396. package/lib/core/agent-env.js +104 -0
  397. package/lib/core/args.js +107 -0
  398. package/lib/core/asr-client.js +448 -0
  399. package/lib/core/asr-jobs-client.js +126 -0
  400. package/lib/core/asr-jobs-store.js +105 -0
  401. package/lib/core/asr-r2-upload.js +181 -0
  402. package/lib/core/asr-upload.js +132 -0
  403. package/lib/core/audio-extract.js +150 -0
  404. package/lib/core/audio.js +219 -0
  405. package/lib/core/auth.js +880 -0
  406. package/lib/core/config.js +197 -0
  407. package/lib/core/feedback.js +64 -0
  408. package/lib/core/ffmpeg.js +476 -0
  409. package/lib/core/http.js +188 -0
  410. package/lib/core/image-client.js +55 -0
  411. package/lib/core/intent-params.js +11 -0
  412. package/lib/core/llm-client.js +76 -0
  413. package/lib/core/logger.js +208 -0
  414. package/lib/core/mic-recorder.js +182 -0
  415. package/lib/core/pause-markers.js +94 -0
  416. package/lib/core/podcast-pacing.js +118 -0
  417. package/lib/core/spinner.js +33 -0
  418. package/lib/core/srt.js +394 -0
  419. package/lib/core/telemetry.js +100 -0
  420. package/lib/core/timeline.js +92 -0
  421. package/lib/core/tts-synthesizer.js +70 -0
  422. package/lib/core/update-check.js +185 -0
  423. package/lib/core/url-download.js +148 -0
  424. package/lib/core/whisper-local.js +279 -0
  425. package/lib/internal/deck-validator.js +488 -0
  426. package/lib/internal/slice-themes.json +370 -0
  427. package/lib/stage-core/cloud-render.js +170 -0
  428. package/lib/stage-core/deck-format.js +133 -0
  429. package/lib/stage-core/edit-prompt.js +104 -0
  430. package/lib/stage-core/event-bus.js +31 -0
  431. package/lib/stage-core/port.js +46 -0
  432. package/lib/stage-core/server.js +352 -0
  433. package/lib/stage-core/snapshot-store.js +198 -0
  434. package/lib/stage-core/watcher.js +106 -0
  435. package/lib/stage-ui/slice/template.js +1672 -0
  436. package/package.json +9 -4
  437. package/skills/.claude-plugin/marketplace.json +22 -0
  438. package/skills/.claude-plugin/plugin.json +25 -0
  439. package/skills/LICENSE +21 -0
  440. package/skills/README.md +120 -0
  441. package/skills/hub/SKILL.md +317 -0
  442. package/skills/podcast/SKILL.md +146 -0
  443. package/skills/slice/SKILL.md +205 -0
  444. package/skills/slice/agents/openai.yaml +4 -0
  445. package/skills/slice/references/deck-schema.md +183 -0
  446. package/skills/slice/references/example-decks.md +108 -0
  447. package/skills/slice/references/themes.md +172 -0
  448. package/skills/transcribe/SKILL.md +473 -0
  449. package/skills/video/SKILL.md +261 -0
  450. package/skills/voxflow-slice/SKILL.md +271 -0
  451. package/skills/voxflow-slice/examples/article.md +13 -0
  452. package/skills/voxflow-slice/examples/expected-deck.json +39 -0
  453. package/skills/voxflow-slice/examples/validate.mjs +46 -0
@@ -0,0 +1,1672 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage UI for `voxflow slice stage`.
5
+ *
6
+ * Inline HTML/JS string — embedded as a JS literal so ncc bundles it without
7
+ * triggering the asset-rewrite trap (see scripts/check-no-asset-rewrite.js).
8
+ *
9
+ * Responsibilities:
10
+ * - Subscribe to /events SSE; render the deck on the `deck` event.
11
+ * - Render each card as a 9:16 vertical preview with per-theme styling
12
+ * (best-effort approximation of the 6 Slice themes — pixel-perfect
13
+ * output still requires the cloud Remotion render).
14
+ * - Light/dark page chrome with auto / manual toggle, persisted in
15
+ * localStorage (key: `voxflow.stage.theme`).
16
+ *
17
+ * Card theme palettes are deliberately fixed (not viewer-overridable) — a
18
+ * `bold-poster` card stays bright yellow even in dark page chrome, because
19
+ * theme is a property of the deck content, not the viewer.
20
+ */
21
+
22
+ const { formatCardAsText, formatDeckAsMarkdown, suggestDeckFilename } = require('../../stage-core/deck-format');
23
+
24
+ function renderSliceStageHtml({ sourcePath, port }) {
25
+ const safePath = String(sourcePath || '').replace(/[<>&"']/g, (c) => ({
26
+ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;',
27
+ }[c]));
28
+
29
+ return `<!doctype html>
30
+ <html lang="en" data-theme="auto">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width,initial-scale=1">
34
+ <title>VoxFlow Stage — Slice</title>
35
+ <style>
36
+ /* ─── Page chrome: light + dark via prefers-color-scheme + manual override ── */
37
+ :root {
38
+ --bg: #ffffff;
39
+ --panel: #f7f7f9;
40
+ --panel-2: #ffffff;
41
+ --border: #e5e5ea;
42
+ --text: #1a1a1f;
43
+ --muted: #6b6b78;
44
+ --accent: #5851b8;
45
+ --good: #16a34a;
46
+ --warn: #d97706;
47
+ --bad: #dc2626;
48
+ --shadow: 0 1px 2px rgba(15, 15, 25, 0.04), 0 4px 12px rgba(15, 15, 25, 0.05);
49
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
50
+ color-scheme: light;
51
+ }
52
+ @media (prefers-color-scheme: dark) {
53
+ :root[data-theme="auto"] {
54
+ --bg: #0e0d14;
55
+ --panel: #16151d;
56
+ --panel-2: #1c1a25;
57
+ --border: #2a2735;
58
+ --text: #e8e6f0;
59
+ --muted: #8a8694;
60
+ --accent: #8b85e0;
61
+ --good: #3ad29f;
62
+ --warn: #f5a524;
63
+ --bad: #f56565;
64
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.5);
65
+ color-scheme: dark;
66
+ }
67
+ }
68
+ :root[data-theme="dark"] {
69
+ --bg: #0e0d14;
70
+ --panel: #16151d;
71
+ --panel-2: #1c1a25;
72
+ --border: #2a2735;
73
+ --text: #e8e6f0;
74
+ --muted: #8a8694;
75
+ --accent: #8b85e0;
76
+ --good: #3ad29f;
77
+ --warn: #f5a524;
78
+ --bad: #f56565;
79
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.5);
80
+ color-scheme: dark;
81
+ }
82
+ :root[data-theme="light"] {
83
+ color-scheme: light;
84
+ }
85
+
86
+ * { box-sizing: border-box; }
87
+ body {
88
+ margin: 0;
89
+ background: var(--bg);
90
+ color: var(--text);
91
+ min-height: 100vh;
92
+ transition: background 0.15s ease, color 0.15s ease;
93
+ }
94
+ header {
95
+ display: flex; align-items: center; gap: 12px;
96
+ padding: 12px 20px;
97
+ border-bottom: 1px solid var(--border);
98
+ background: var(--panel);
99
+ position: sticky; top: 0; z-index: 10;
100
+ backdrop-filter: saturate(140%) blur(8px);
101
+ }
102
+ header .brand {
103
+ font-weight: 600; letter-spacing: 0.02em;
104
+ }
105
+ header .brand .accent { color: var(--accent); }
106
+ header .src {
107
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
108
+ font-size: 12px; color: var(--muted);
109
+ max-width: 50ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
110
+ }
111
+ header .grow { flex: 1; }
112
+ header .status {
113
+ display: inline-flex; align-items: center; gap: 6px;
114
+ font-size: 12px; color: var(--muted);
115
+ }
116
+ .dot {
117
+ width: 8px; height: 8px; border-radius: 50%;
118
+ background: var(--muted);
119
+ transition: background 0.2s ease;
120
+ }
121
+ .dot.good { background: var(--good); }
122
+ .dot.warn { background: var(--warn); }
123
+ .dot.bad { background: var(--bad); }
124
+
125
+ .theme-toggle {
126
+ appearance: none; border: 1px solid var(--border);
127
+ background: var(--panel-2); color: var(--text);
128
+ font-size: 12px; font-family: inherit;
129
+ padding: 4px 10px; border-radius: 6px; cursor: pointer;
130
+ display: inline-flex; align-items: center; gap: 6px;
131
+ transition: background 0.15s ease, border-color 0.15s ease;
132
+ }
133
+ .theme-toggle:hover { border-color: var(--accent); }
134
+ .theme-toggle .icon { font-size: 13px; }
135
+
136
+ /* Versions button reuses theme-toggle visuals so the header looks like
137
+ one cohesive control cluster, not a bag of buttons. */
138
+ .versions-btn {
139
+ appearance: none; border: 1px solid var(--border);
140
+ background: var(--panel-2); color: var(--text);
141
+ font-size: 12px; font-family: inherit;
142
+ padding: 4px 10px; border-radius: 6px; cursor: pointer;
143
+ display: inline-flex; align-items: center; gap: 6px;
144
+ transition: background 0.15s ease, border-color 0.15s ease;
145
+ }
146
+ .versions-btn:hover { border-color: var(--accent); }
147
+ .versions-btn .count-pill {
148
+ background: var(--accent); color: white;
149
+ border-radius: 9px; padding: 0 6px; font-size: 10px;
150
+ font-weight: 600; min-width: 16px; text-align: center;
151
+ }
152
+
153
+ .versions-drawer {
154
+ position: fixed; top: 56px; right: 16px;
155
+ width: 320px; max-height: calc(100vh - 80px);
156
+ background: var(--panel); border: 1px solid var(--border);
157
+ border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.18);
158
+ display: none; flex-direction: column;
159
+ z-index: 50;
160
+ }
161
+ .versions-drawer.open { display: flex; }
162
+ .versions-drawer header.drawer-head {
163
+ display: flex; align-items: center; gap: 8px;
164
+ padding: 10px 12px; border-bottom: 1px solid var(--border);
165
+ font-size: 13px; font-weight: 600;
166
+ }
167
+ .versions-drawer .drawer-head .grow { flex: 1; }
168
+ .versions-drawer .drawer-close {
169
+ appearance: none; border: 0; background: transparent;
170
+ color: var(--muted); font-size: 18px; line-height: 1;
171
+ cursor: pointer; padding: 2px 6px; border-radius: 4px;
172
+ }
173
+ .versions-drawer .drawer-close:hover { background: var(--panel-2); color: var(--text); }
174
+ .versions-drawer .drawer-body {
175
+ overflow-y: auto; padding: 6px;
176
+ }
177
+ .versions-drawer .empty-msg {
178
+ padding: 16px 12px; color: var(--muted); font-size: 12px; text-align: center;
179
+ }
180
+ .version-row {
181
+ display: grid; grid-template-columns: 1fr auto;
182
+ gap: 8px; align-items: center;
183
+ padding: 8px 10px; border-radius: 6px;
184
+ border: 1px solid transparent;
185
+ }
186
+ .version-row + .version-row { margin-top: 2px; }
187
+ .version-row:hover { background: var(--panel-2); border-color: var(--border); }
188
+ .version-row.current {
189
+ background: var(--panel-2); border-color: var(--accent);
190
+ }
191
+ .version-row .ts {
192
+ font-size: 12px; font-weight: 500; color: var(--text);
193
+ font-variant-numeric: tabular-nums;
194
+ }
195
+ .version-row .meta {
196
+ font-size: 10px; color: var(--muted);
197
+ font-family: var(--mono);
198
+ }
199
+ .version-row .restore-btn {
200
+ appearance: none; border: 1px solid var(--border);
201
+ background: var(--panel); color: var(--text);
202
+ font-size: 11px; padding: 3px 9px; border-radius: 5px;
203
+ cursor: pointer;
204
+ }
205
+ .version-row.current .restore-btn { display: none; }
206
+ .version-row .restore-btn:hover { border-color: var(--accent); color: var(--accent); }
207
+ .version-row .restore-btn:disabled { opacity: 0.5; cursor: progress; }
208
+ .versions-drawer .store-path {
209
+ padding: 8px 12px; border-top: 1px solid var(--border);
210
+ font-size: 10px; color: var(--muted); font-family: var(--mono);
211
+ word-break: break-all;
212
+ }
213
+
214
+ /* Render button — primary action, sits between History and theme toggle.
215
+ Visually heavier than the other header pills since it's the "ship"
216
+ affordance (the one quota-charging action in the page). */
217
+ .render-btn {
218
+ appearance: none; border: 1px solid var(--accent);
219
+ background: var(--accent); color: white;
220
+ font-size: 12px; font-family: inherit; font-weight: 600;
221
+ padding: 4px 12px; border-radius: 6px; cursor: pointer;
222
+ display: inline-flex; align-items: center; gap: 6px;
223
+ transition: filter 0.15s ease;
224
+ }
225
+ .render-btn:hover:not(:disabled) { filter: brightness(1.08); }
226
+ .render-btn:disabled { opacity: 0.6; cursor: progress; }
227
+ .render-btn .icon { font-size: 13px; }
228
+ .render-btn.completed {
229
+ background: transparent; color: var(--accent);
230
+ text-decoration: none;
231
+ }
232
+ .render-btn.completed:hover { filter: none; text-decoration: underline; }
233
+
234
+ .render-modal-body { display: flex; flex-direction: column; gap: 8px; padding: 4px 0 6px; }
235
+ .render-modal-body .quota-line {
236
+ display: flex; justify-content: space-between; align-items: baseline;
237
+ padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px;
238
+ background: var(--panel-2);
239
+ }
240
+ .render-modal-body .quota-line .key { color: var(--muted); font-size: 11px; }
241
+ .render-modal-body .quota-line .val { font-family: var(--mono); font-size: 13px; font-weight: 600; }
242
+ .render-modal-body .quota-line.short .val { color: var(--bad); }
243
+ .render-modal-body .err-msg {
244
+ color: var(--bad); font-size: 12px; padding: 6px 10px;
245
+ border: 1px solid var(--bad); border-radius: 6px;
246
+ background: color-mix(in srgb, var(--bad) 8%, transparent);
247
+ }
248
+ .render-modal-body .progress-msg {
249
+ color: var(--muted); font-size: 12px; padding: 6px 10px;
250
+ }
251
+ .render-modal-body .progress-msg .stage-label { color: var(--text); font-weight: 600; }
252
+ .render-confirm-btn {
253
+ appearance: none; border: 1px solid var(--accent);
254
+ background: var(--accent); color: white;
255
+ font-size: 12px; font-family: inherit; font-weight: 600;
256
+ padding: 6px 14px; border-radius: 6px; cursor: pointer;
257
+ }
258
+ .render-confirm-btn:disabled { opacity: 0.6; cursor: progress; }
259
+ .render-confirm-btn:hover:not(:disabled) { filter: brightness(1.08); }
260
+
261
+ main {
262
+ display: grid;
263
+ grid-template-rows: auto auto;
264
+ gap: 16px;
265
+ padding: 20px;
266
+ max-width: 1400px; margin: 0 auto;
267
+ }
268
+ section {
269
+ background: var(--panel);
270
+ border: 1px solid var(--border);
271
+ border-radius: 12px;
272
+ padding: 18px;
273
+ box-shadow: var(--shadow);
274
+ }
275
+ section h2 {
276
+ margin: 0 0 14px;
277
+ font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
278
+ color: var(--muted); font-weight: 600;
279
+ display: flex; align-items: center; gap: 8px;
280
+ }
281
+ section h2 .count {
282
+ background: var(--panel-2); border: 1px solid var(--border);
283
+ color: var(--text);
284
+ padding: 1px 8px; border-radius: 999px;
285
+ font-size: 11px; font-weight: 500; letter-spacing: 0;
286
+ }
287
+ section h2 .preview-tag {
288
+ margin-left: auto;
289
+ background: transparent; color: var(--muted);
290
+ font-size: 10px; font-weight: 500; letter-spacing: 0.04em;
291
+ text-transform: none;
292
+ }
293
+
294
+ /* ─── Cards grid (9:16 vertical previews) ─────────────────────────────── */
295
+ .cards-grid {
296
+ display: grid;
297
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
298
+ gap: 18px;
299
+ }
300
+ .stage-card {
301
+ aspect-ratio: 9 / 16;
302
+ border-radius: 10px;
303
+ overflow: hidden;
304
+ position: relative;
305
+ box-shadow: var(--shadow);
306
+ display: flex; flex-direction: column;
307
+ transition: transform 0.15s ease;
308
+ cursor: default;
309
+ }
310
+ .stage-card:hover { transform: translateY(-2px); }
311
+ .stage-card .frame-meta {
312
+ position: absolute; top: 10px; left: 10px; right: 10px;
313
+ display: flex; align-items: center; gap: 6px;
314
+ font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
315
+ opacity: 0.55; pointer-events: none;
316
+ z-index: 2;
317
+ }
318
+ .stage-card .frame-meta .idx {
319
+ background: rgba(0,0,0,0.18); color: inherit;
320
+ padding: 1px 6px; border-radius: 4px;
321
+ font-weight: 600;
322
+ }
323
+ .stage-card[data-card-bg="dark"] .frame-meta .idx {
324
+ background: rgba(255,255,255,0.18);
325
+ }
326
+ .stage-card .figure-chip {
327
+ position: absolute; top: 32px; right: 10px;
328
+ font-size: 10px; padding: 2px 8px; border-radius: 4px;
329
+ background: rgba(0,0,0,0.08);
330
+ opacity: 0.7;
331
+ z-index: 2;
332
+ }
333
+ .stage-card[data-card-bg="dark"] .figure-chip {
334
+ background: rgba(255,255,255,0.12);
335
+ }
336
+ .stage-card .body {
337
+ flex: 1;
338
+ display: flex; flex-direction: column;
339
+ justify-content: center;
340
+ padding: 28px 22px 22px;
341
+ gap: 8px;
342
+ overflow: hidden;
343
+ }
344
+ .stage-card.kind-title .body { justify-content: center; text-align: left; }
345
+ /* Body cards: extra bottom padding so narration never sits underneath
346
+ the [Edit with AI] button that appears on hover. */
347
+ .stage-card.kind-body .body { justify-content: flex-end; padding-bottom: 46px; }
348
+ .stage-card .title-text {
349
+ font-size: clamp(18px, 2.4vw, 26px);
350
+ line-height: 1.18;
351
+ font-weight: var(--card-title-weight, 700);
352
+ word-break: keep-all;
353
+ }
354
+ .stage-card .title-text .line {
355
+ display: block;
356
+ }
357
+ .stage-card .caption {
358
+ font-size: clamp(13px, 1.6vw, 16px);
359
+ line-height: 1.35;
360
+ font-weight: 600;
361
+ }
362
+ .stage-card .narration {
363
+ font-size: 11px;
364
+ line-height: 1.5;
365
+ opacity: 0.7;
366
+ max-height: 4.5em; overflow: hidden;
367
+ display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
368
+ }
369
+ .stage-card .accent-bar {
370
+ position: absolute; left: 0; bottom: 0;
371
+ height: 4px; width: 32%;
372
+ background: var(--card-accent, currentColor);
373
+ }
374
+
375
+ /* ─── Edit-with-AI affordance: per-card button + floating selection btn ── */
376
+ .edit-btn {
377
+ position: absolute; right: 10px; bottom: 10px;
378
+ appearance: none; border: 0; cursor: pointer;
379
+ font: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
380
+ padding: 5px 10px; border-radius: 6px;
381
+ background: rgba(0,0,0,0.78); color: #fff;
382
+ opacity: 0; transform: translateY(4px);
383
+ transition: opacity 0.15s ease, transform 0.15s ease;
384
+ z-index: 3;
385
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
386
+ }
387
+ .stage-card[data-card-bg="dark"] .edit-btn {
388
+ background: rgba(255,255,255,0.92); color: #0a0a0a;
389
+ }
390
+ .stage-card:hover .edit-btn,
391
+ .edit-btn:focus-visible {
392
+ opacity: 1; transform: translateY(0);
393
+ }
394
+ .edit-btn:hover { filter: brightness(1.1); }
395
+ .edit-btn .icon { margin-right: 4px; }
396
+
397
+ /* ─── Text-export controls (Phase 1.4) — per-card copy + deck toolbar ── */
398
+ .stage-card .card-actions {
399
+ position: absolute; right: 10px; top: 10px;
400
+ display: flex; gap: 6px; z-index: 3;
401
+ opacity: 0; transform: translateY(-4px);
402
+ transition: opacity 0.15s ease, transform 0.15s ease;
403
+ }
404
+ .stage-card:hover .card-actions { opacity: 1; transform: translateY(0); }
405
+ .stage-card .card-actions button {
406
+ appearance: none; border: 0; cursor: pointer; font: inherit;
407
+ font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
408
+ padding: 4px 9px; border-radius: 6px;
409
+ background: rgba(0,0,0,0.78); color: #fff;
410
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
411
+ }
412
+ .stage-card[data-card-bg="dark"] .card-actions button {
413
+ background: rgba(255,255,255,0.92); color: #0a0a0a;
414
+ }
415
+ .stage-card .card-actions button.copied {
416
+ background: rgba(34,197,94,0.92); color: #fff;
417
+ }
418
+ .deck-toolbar {
419
+ display: flex; gap: 8px; flex-wrap: wrap;
420
+ margin: 0 0 14px;
421
+ }
422
+ .deck-toolbar button {
423
+ appearance: none; cursor: pointer; font: inherit;
424
+ background: transparent; color: var(--text);
425
+ border: 1px solid var(--border); border-radius: 8px;
426
+ padding: 6px 12px; font-size: 12px;
427
+ }
428
+ .deck-toolbar button:hover:not(:disabled) { border-color: var(--accent); }
429
+ .deck-toolbar button:disabled {
430
+ color: var(--muted); cursor: not-allowed; opacity: 0.65;
431
+ }
432
+ .deck-toolbar button.copied {
433
+ color: var(--good); border-color: var(--good);
434
+ }
435
+
436
+ .selection-fab {
437
+ position: fixed; z-index: 50;
438
+ appearance: none; border: 0; cursor: pointer;
439
+ font: inherit; font-size: 11px; font-weight: 600;
440
+ padding: 6px 12px; border-radius: 6px;
441
+ background: var(--accent); color: #fff;
442
+ box-shadow: 0 4px 14px rgba(0,0,0,0.3);
443
+ display: none;
444
+ }
445
+ .selection-fab.visible { display: inline-flex; align-items: center; gap: 6px; }
446
+
447
+ /* ─── Modal for showing the copy-pasteable AI prompt ──────────────────── */
448
+ .modal-backdrop {
449
+ position: fixed; inset: 0; z-index: 100;
450
+ background: rgba(8, 8, 14, 0.55);
451
+ backdrop-filter: blur(2px);
452
+ display: none; align-items: center; justify-content: center;
453
+ padding: 20px;
454
+ }
455
+ .modal-backdrop.open { display: flex; }
456
+ .modal {
457
+ width: 100%; max-width: 560px; max-height: 86vh;
458
+ background: var(--panel); color: var(--text);
459
+ border: 1px solid var(--border); border-radius: 12px;
460
+ box-shadow: var(--shadow);
461
+ display: flex; flex-direction: column;
462
+ overflow: hidden;
463
+ }
464
+ .modal-header {
465
+ display: flex; align-items: center; gap: 12px;
466
+ padding: 14px 18px;
467
+ border-bottom: 1px solid var(--border);
468
+ }
469
+ .modal-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
470
+ .modal-header .grow { flex: 1; }
471
+ .modal-close {
472
+ appearance: none; border: 0; background: transparent;
473
+ font-size: 18px; line-height: 1; cursor: pointer;
474
+ color: var(--muted); padding: 4px 8px; border-radius: 4px;
475
+ }
476
+ .modal-close:hover { background: var(--panel-2); color: var(--text); }
477
+ .modal-body {
478
+ padding: 16px 18px; overflow-y: auto;
479
+ display: flex; flex-direction: column; gap: 12px;
480
+ }
481
+ .modal-body label {
482
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
483
+ color: var(--muted); font-weight: 600;
484
+ }
485
+ .modal-body textarea.instruction {
486
+ width: 100%; min-height: 56px; resize: vertical;
487
+ padding: 10px 12px; border-radius: 6px;
488
+ border: 1px solid var(--border); background: var(--panel-2);
489
+ color: var(--text); font: inherit; font-size: 13px;
490
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
491
+ }
492
+ .modal-body textarea.instruction:focus {
493
+ outline: none;
494
+ border-color: var(--accent);
495
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
496
+ }
497
+ .modal-body pre.prompt {
498
+ margin: 0; max-height: 320px; overflow: auto;
499
+ padding: 12px; border-radius: 6px;
500
+ background: var(--panel-2); border: 1px solid var(--border);
501
+ font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
502
+ color: var(--text); white-space: pre-wrap; word-break: break-word;
503
+ }
504
+ .modal-footer {
505
+ display: flex; align-items: center; gap: 12px;
506
+ padding: 12px 18px;
507
+ border-top: 1px solid var(--border);
508
+ background: var(--panel-2);
509
+ }
510
+ .modal-footer .hint { color: var(--muted); font-size: 11px; flex: 1; }
511
+ .copy-btn {
512
+ appearance: none; border: 0; cursor: pointer;
513
+ font: inherit; font-size: 13px; font-weight: 600;
514
+ padding: 8px 16px; border-radius: 6px;
515
+ background: var(--accent); color: #fff;
516
+ transition: background 0.15s ease, opacity 0.15s ease;
517
+ }
518
+ .copy-btn:hover { filter: brightness(1.08); }
519
+ .copy-btn.copied { background: var(--good); }
520
+
521
+ /* ─── Parse-error banner: shown when deck.json is invalid JSON ────────── */
522
+ .parse-error-banner {
523
+ display: none;
524
+ margin: 0 0 14px;
525
+ padding: 10px 14px; border-radius: 8px;
526
+ background: color-mix(in srgb, var(--bad) 12%, transparent);
527
+ border: 1px solid color-mix(in srgb, var(--bad) 30%, transparent);
528
+ color: var(--text);
529
+ font-size: 12px; line-height: 1.5;
530
+ }
531
+ .parse-error-banner.visible { display: block; }
532
+ .parse-error-banner strong {
533
+ color: var(--bad); font-weight: 600;
534
+ display: block; margin-bottom: 2px;
535
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
536
+ }
537
+ .parse-error-banner code {
538
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
539
+ font-size: 12px;
540
+ background: var(--panel-2);
541
+ padding: 1px 6px; border-radius: 4px;
542
+ word-break: break-all;
543
+ }
544
+
545
+ /* ─── Just-changed flash: highlights cards that changed on hot-reload ── */
546
+ @keyframes vfStageJustChanged {
547
+ 0% { box-shadow: 0 0 0 3px var(--accent), var(--shadow); }
548
+ 60% { box-shadow: 0 0 0 3px var(--accent), var(--shadow); }
549
+ 100% { box-shadow: 0 0 0 0 transparent, var(--shadow); }
550
+ }
551
+ .stage-card.just-changed {
552
+ animation: vfStageJustChanged 1500ms ease-out;
553
+ }
554
+
555
+ pre {
556
+ margin: 0;
557
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
558
+ font-size: 12px; line-height: 1.55;
559
+ color: var(--text); background: var(--panel-2);
560
+ border: 1px solid var(--border); border-radius: 8px;
561
+ padding: 14px; overflow: auto; max-height: 60vh;
562
+ }
563
+ .empty {
564
+ color: var(--muted);
565
+ font-size: 13px;
566
+ padding: 48px 12px;
567
+ text-align: center;
568
+ }
569
+ .footer-hint {
570
+ padding: 10px 20px 20px;
571
+ font-size: 11px; color: var(--muted);
572
+ text-align: center;
573
+ }
574
+ code.kbd {
575
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 4px;
576
+ padding: 1px 6px; font-family: ui-monospace, monospace; font-size: 11px;
577
+ }
578
+
579
+ details.json-section { margin-top: 0; }
580
+ details.json-section > summary {
581
+ cursor: pointer; list-style: none;
582
+ font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
583
+ color: var(--muted); font-weight: 600;
584
+ padding: 6px 0;
585
+ }
586
+ details.json-section > summary::-webkit-details-marker { display: none; }
587
+ details.json-section > summary::before {
588
+ content: '▸'; display: inline-block; margin-right: 6px;
589
+ transition: transform 0.15s ease;
590
+ }
591
+ details.json-section[open] > summary::before { transform: rotate(90deg); }
592
+ </style>
593
+ </head>
594
+ <body>
595
+ <header>
596
+ <div class="brand">Vox<span class="accent">Flow</span> Stage <span style="color:var(--muted)">/ slice</span></div>
597
+ <div class="src" id="src" title="${safePath}">${safePath}</div>
598
+ <div class="grow"></div>
599
+ <div class="status">
600
+ <span class="dot" id="dot"></span>
601
+ <span id="status-label">connecting…</span>
602
+ </div>
603
+ <button class="render-btn" id="render-btn" type="button" aria-label="Render this deck to mp4 (cloud)">
604
+ <span class="icon">▶</span>
605
+ <span id="render-btn-label">Render mp4</span>
606
+ </button>
607
+ <button class="versions-btn" id="versions-btn" type="button" aria-label="Show version history" aria-expanded="false">
608
+ <span class="icon">📚</span>
609
+ <span>History</span>
610
+ <span class="count-pill" id="versions-count" hidden>0</span>
611
+ </button>
612
+ <button class="theme-toggle" id="theme-toggle" type="button" aria-label="Toggle color theme">
613
+ <span class="icon" id="theme-icon">◐</span>
614
+ <span id="theme-label">auto</span>
615
+ </button>
616
+ </header>
617
+
618
+ <aside class="versions-drawer" id="versions-drawer" role="dialog" aria-modal="false" aria-labelledby="versions-drawer-title">
619
+ <header class="drawer-head">
620
+ <span id="versions-drawer-title">Version history</span>
621
+ <span class="grow"></span>
622
+ <button class="drawer-close" id="versions-close" type="button" aria-label="Close history">×</button>
623
+ </header>
624
+ <div class="drawer-body" id="versions-body">
625
+ <div class="empty-msg">No history yet — edits will show up here.</div>
626
+ </div>
627
+ <div class="store-path" id="versions-store-path" hidden></div>
628
+ </aside>
629
+
630
+ <main>
631
+ <section>
632
+ <h2>
633
+ Cards
634
+ <span class="count" id="card-count" hidden>0</span>
635
+ <span class="preview-tag">preview · final mp4 from voxflow.studio/apps/slice</span>
636
+ </h2>
637
+ <div class="parse-error-banner" id="parse-error-banner" role="status" aria-live="polite">
638
+ <strong>Deck JSON is invalid — preview shows last good version</strong>
639
+ <span id="parse-error-detail"></span>
640
+ </div>
641
+ <div class="deck-toolbar" id="deck-toolbar">
642
+ <button id="copy-json-btn" type="button" disabled title="Copy raw deck.json to clipboard">Copy JSON</button>
643
+ <button id="download-json-btn" type="button" disabled title="Save deck.json to disk">Download .json</button>
644
+ <button id="copy-md-btn" type="button" disabled title="Copy as Markdown — paste into Notion / blog / 飞书">Copy as Markdown</button>
645
+ </div>
646
+ <div id="cards-pane" class="empty">Waiting for deck…</div>
647
+ </section>
648
+ <section>
649
+ <details class="json-section">
650
+ <summary>Deck JSON</summary>
651
+ <pre id="json-pane">// no deck loaded yet</pre>
652
+ </details>
653
+ </section>
654
+ </main>
655
+
656
+ <div class="footer-hint">
657
+ Edit <code class="kbd">${safePath}</code> in your editor — this page hot-reloads on save.
658
+ &nbsp;·&nbsp; Listening on <code class="kbd">http://127.0.0.1:${Number(port) || 5180}</code>
659
+ </div>
660
+
661
+ <button class="selection-fab" id="selection-fab" type="button" aria-label="Edit selection with AI">
662
+ <span>✏</span><span>Edit selection with AI</span>
663
+ </button>
664
+
665
+ <div class="modal-backdrop" id="render-modal" role="dialog" aria-modal="true" aria-labelledby="render-modal-title" hidden>
666
+ <div class="modal">
667
+ <div class="modal-header">
668
+ <h3 id="render-modal-title">Render this deck to mp4</h3>
669
+ <div class="grow"></div>
670
+ <button class="modal-close" id="render-modal-close" type="button" aria-label="Close">×</button>
671
+ </div>
672
+ <div class="modal-body render-modal-body" id="render-modal-body">
673
+ <div class="quota-line">
674
+ <span class="key">Cost</span>
675
+ <span class="val">500 quota</span>
676
+ </div>
677
+ <div class="quota-line" id="render-quota-line">
678
+ <span class="key">Your remaining</span>
679
+ <span class="val" id="render-quota-val">loading…</span>
680
+ </div>
681
+ <div class="progress-msg" id="render-progress-msg" hidden>
682
+ <span class="stage-label" id="render-progress-stage">Submitting…</span>
683
+ <span id="render-progress-detail"></span>
684
+ </div>
685
+ <div class="err-msg" id="render-err" hidden></div>
686
+ </div>
687
+ <div class="modal-footer">
688
+ <span class="hint">
689
+ Cloud rendered at voxflow.studio/apps/slice. The mp4 link opens in a new tab when ready.
690
+ </span>
691
+ <button class="render-confirm-btn" id="render-confirm" type="button" disabled>Render now</button>
692
+ </div>
693
+ </div>
694
+ </div>
695
+
696
+ <div class="modal-backdrop" id="edit-modal" role="dialog" aria-modal="true" aria-labelledby="edit-modal-title" hidden>
697
+ <div class="modal">
698
+ <div class="modal-header">
699
+ <h3 id="edit-modal-title">Edit with AI</h3>
700
+ <div class="grow"></div>
701
+ <button class="modal-close" id="modal-close" type="button" aria-label="Close">×</button>
702
+ </div>
703
+ <div class="modal-body">
704
+ <label for="edit-instruction">Tell the AI what to change</label>
705
+ <textarea
706
+ id="edit-instruction"
707
+ class="instruction"
708
+ placeholder="e.g. shorten the narration to 30 chars and make it punchier"
709
+ autocomplete="off"
710
+ spellcheck="false"
711
+ ></textarea>
712
+ <label>Prompt to send (auto-generated)</label>
713
+ <pre class="prompt" id="edit-prompt-preview"></pre>
714
+ </div>
715
+ <div class="modal-footer">
716
+ <span class="hint">
717
+ Paste into Claude Code / Cursor / ChatGPT — the file edit will hot-reload here.
718
+ </span>
719
+ <button class="copy-btn" id="copy-prompt" type="button">Copy prompt</button>
720
+ </div>
721
+ </div>
722
+ </div>
723
+
724
+ <script>
725
+ (function () {
726
+ // ─── Theme toggle (auto → light → dark → auto) ───────────────────────
727
+ var THEME_KEY = 'voxflow.stage.theme';
728
+ var html = document.documentElement;
729
+ var toggleBtn = document.getElementById('theme-toggle');
730
+ var iconEl = document.getElementById('theme-icon');
731
+ var labelEl = document.getElementById('theme-label');
732
+
733
+ function applyTheme(mode) {
734
+ html.setAttribute('data-theme', mode);
735
+ if (mode === 'light') { iconEl.textContent = '☀'; labelEl.textContent = 'light'; }
736
+ else if (mode === 'dark') { iconEl.textContent = '☾'; labelEl.textContent = 'dark'; }
737
+ else { iconEl.textContent = '◐'; labelEl.textContent = 'auto'; }
738
+ }
739
+
740
+ var stored;
741
+ try { stored = localStorage.getItem(THEME_KEY); } catch (_) { stored = null; }
742
+ applyTheme(stored === 'light' || stored === 'dark' ? stored : 'auto');
743
+
744
+ toggleBtn.addEventListener('click', function () {
745
+ var next = ({ auto: 'light', light: 'dark', dark: 'auto' })[
746
+ html.getAttribute('data-theme') || 'auto'
747
+ ];
748
+ try { localStorage.setItem(THEME_KEY, next); } catch (_) {}
749
+ applyTheme(next);
750
+ });
751
+
752
+ // ─── Card-theme palettes (best-effort approximation of the 6 Slice
753
+ // themes — final pixel-accurate render lives in the cloud). ──────
754
+ var CARD_THEMES = {
755
+ 'paper-slide': {
756
+ bg: '#f4ecd8', fg: '#2d2417', accent: '#8b6f47',
757
+ font: 'Georgia, "Times New Roman", serif',
758
+ titleWeight: 600, surface: 'light',
759
+ },
760
+ 'editorial-mag': {
761
+ bg: '#ffffff', fg: '#0a0a0a', accent: '#c41e3a',
762
+ font: 'Georgia, "Times New Roman", serif',
763
+ titleWeight: 700, surface: 'light',
764
+ },
765
+ 'bold-poster': {
766
+ bg: '#fde047', fg: '#0a0a0a', accent: '#dc2626',
767
+ font: 'system-ui, -apple-system, sans-serif',
768
+ titleWeight: 900, surface: 'light',
769
+ },
770
+ 'notion-card': {
771
+ bg: '#ffffff', fg: '#37352f', accent: '#787774',
772
+ font: 'system-ui, "Segoe UI", -apple-system, sans-serif',
773
+ titleWeight: 600, surface: 'light',
774
+ border: '1px solid #e9e9e7',
775
+ },
776
+ 'brutalist': {
777
+ bg: '#ffffff', fg: '#000000', accent: '#000000',
778
+ font: 'ui-monospace, "Courier New", monospace',
779
+ titleWeight: 900, surface: 'light',
780
+ border: '4px solid #000',
781
+ },
782
+ 'glass-dark': {
783
+ bg: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
784
+ fg: '#e8e6f0', accent: '#a78bfa',
785
+ font: 'system-ui, -apple-system, sans-serif',
786
+ titleWeight: 700, surface: 'dark',
787
+ border: '1px solid rgba(255,255,255,0.12)',
788
+ },
789
+ };
790
+ var DEFAULT_THEME = 'paper-slide';
791
+
792
+ // ─── Element refs ────────────────────────────────────────────────────
793
+ var dot = document.getElementById('dot');
794
+ var statusLabel = document.getElementById('status-label');
795
+ var cardsPane = document.getElementById('cards-pane');
796
+ var jsonPane = document.getElementById('json-pane');
797
+ var countBadge = document.getElementById('card-count');
798
+ var modal = document.getElementById('edit-modal');
799
+ var modalTitle = document.getElementById('edit-modal-title');
800
+ var modalClose = document.getElementById('modal-close');
801
+ var instructionInput = document.getElementById('edit-instruction');
802
+ var promptPreview = document.getElementById('edit-prompt-preview');
803
+ var copyBtn = document.getElementById('copy-prompt');
804
+ var selectionFab = document.getElementById('selection-fab');
805
+ var errorBanner = document.getElementById('parse-error-banner');
806
+ var errorDetail = document.getElementById('parse-error-detail');
807
+
808
+ // ─── Text-export (Phase 1.4) — refs + formatter source embed ────────
809
+ var copyJsonBtn = document.getElementById('copy-json-btn');
810
+ var downloadJsonBtn = document.getElementById('download-json-btn');
811
+ var copyMdBtn = document.getElementById('copy-md-btn');
812
+
813
+ // Pure formatters lifted verbatim from lib/stage-core/deck-format.js
814
+ // via Function.prototype.toString — single source of truth, the
815
+ // node:test units cover the same source that runs in the browser.
816
+ var formatCardAsText = ${formatCardAsText.toString()};
817
+ var formatDeckAsMarkdown = ${formatDeckAsMarkdown.toString()};
818
+ var suggestDeckFilename = ${suggestDeckFilename.toString()};
819
+
820
+ function flashCopied(btn) {
821
+ var orig = btn.textContent;
822
+ btn.textContent = 'Copied!';
823
+ btn.classList.add('copied');
824
+ setTimeout(function () {
825
+ btn.textContent = orig;
826
+ btn.classList.remove('copied');
827
+ }, 1200);
828
+ }
829
+ function copyTextToClipboard(text, btn) {
830
+ if (navigator.clipboard && window.isSecureContext) {
831
+ navigator.clipboard.writeText(text).then(function () { flashCopied(btn); });
832
+ return;
833
+ }
834
+ // Fallback for non-secure contexts: hidden textarea + execCommand
835
+ var ta = document.createElement('textarea');
836
+ ta.value = text;
837
+ ta.style.position = 'fixed';
838
+ ta.style.opacity = '0';
839
+ document.body.appendChild(ta);
840
+ ta.select();
841
+ try { document.execCommand('copy'); flashCopied(btn); } catch (_) {}
842
+ document.body.removeChild(ta);
843
+ }
844
+ function downloadJson(deck, btn) {
845
+ var blob = new Blob([JSON.stringify(deck, null, 2)], { type: 'application/json' });
846
+ var url = URL.createObjectURL(blob);
847
+ var a = document.createElement('a');
848
+ a.href = url;
849
+ a.download = suggestDeckFilename(deck);
850
+ document.body.appendChild(a);
851
+ a.click();
852
+ document.body.removeChild(a);
853
+ URL.revokeObjectURL(url);
854
+ flashCopied(btn);
855
+ }
856
+
857
+ copyJsonBtn.addEventListener('click', function () {
858
+ if (!currentDeck) return;
859
+ copyTextToClipboard(JSON.stringify(currentDeck, null, 2), copyJsonBtn);
860
+ });
861
+ downloadJsonBtn.addEventListener('click', function () {
862
+ if (!currentDeck) return;
863
+ downloadJson(currentDeck, downloadJsonBtn);
864
+ });
865
+ copyMdBtn.addEventListener('click', function () {
866
+ if (!currentDeck) return;
867
+ copyTextToClipboard(formatDeckAsMarkdown(currentDeck), copyMdBtn);
868
+ });
869
+
870
+ // Per-card "Copy text" — event delegation on cardsPane so we don't
871
+ // re-bind on every hot-reload re-render.
872
+ cardsPane.addEventListener('click', function (ev) {
873
+ var btn = ev.target.closest && ev.target.closest('[data-action="copy-card"]');
874
+ if (!btn || !currentDeck) return;
875
+ var idx = parseInt(btn.getAttribute('data-card-index'), 10);
876
+ if (!Number.isFinite(idx)) return;
877
+ var cards = Array.isArray(currentDeck.cards) ? currentDeck.cards : [];
878
+ var card = cards[idx];
879
+ if (!card) return;
880
+ copyTextToClipboard(formatCardAsText(card), btn);
881
+ });
882
+
883
+ // For highlighting cards that just changed on hot-reload, we keep a
884
+ // hash of each card's stringified JSON. On the next deck event we
885
+ // diff per-index and add the just-changed CSS class to whichever
886
+ // DOM card moved.
887
+ var prevCardKeys = [];
888
+ // Whole-deck de-dup hash: editors that save atomically (write new,
889
+ // rename over old) cause fs.watch to fire 2-3 events for one save,
890
+ // which produced duplicate 'deck' SSE messages. Without this guard
891
+ // each duplicate would re-write innerHTML and wipe the
892
+ // .just-changed animation class added by the first render.
893
+ var lastRenderedDeckHash = null;
894
+
895
+ // Latest deck snapshot — populated by SSE. Edit-prompt builder reads
896
+ // current card data from here so the prompt always reflects what's
897
+ // on screen, not what was on screen when the modal first opened.
898
+ var currentDeck = null;
899
+ var currentDeckPath = '';
900
+
901
+ // Set when a deck event arrives with restoredFrom — the next
902
+ // renderDeck call should NOT flash diff highlights, because the user
903
+ // intentionally rewound history (every card is "different from prev"
904
+ // but that's the wrong story to tell visually).
905
+ var suppressDiffOnce = false;
906
+
907
+ function setStatus(state, label) {
908
+ dot.className = 'dot ' + state;
909
+ statusLabel.textContent = label;
910
+ }
911
+
912
+ function escapeHtml(s) {
913
+ return String(s == null ? '' : s).replace(/[<>&"']/g, function (c) {
914
+ return ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;' })[c];
915
+ });
916
+ }
917
+
918
+ function renderCard(card, i, themeId) {
919
+ var t = CARD_THEMES[themeId] || CARD_THEMES[DEFAULT_THEME];
920
+ var kind = card.kind === 'title' ? 'title' : 'body';
921
+ var titleHtml = '';
922
+ if (kind === 'title') {
923
+ var titleArr;
924
+ if (Array.isArray(card.title)) titleArr = card.title;
925
+ else if (typeof card.title === 'string') titleArr = [card.title];
926
+ else titleArr = ['(card ' + (i + 1) + ')'];
927
+ titleHtml = '<div class="title-text">'
928
+ + titleArr.map(function (line) {
929
+ return '<span class="line">' + escapeHtml(line) + '</span>';
930
+ }).join('')
931
+ + '</div>';
932
+ } else {
933
+ var caption = typeof card.caption === 'string' ? card.caption : ('Card ' + (i + 1));
934
+ var narration = typeof card.narration === 'string' ? card.narration : '';
935
+ titleHtml = '<div class="caption">' + escapeHtml(caption) + '</div>'
936
+ + (narration ? '<div class="narration">' + escapeHtml(narration) + '</div>' : '');
937
+ }
938
+
939
+ var figureChip = card.figureKeyword
940
+ ? '<div class="figure-chip">' + escapeHtml(card.figureKeyword) + '</div>'
941
+ : '';
942
+
943
+ var styleParts = [
944
+ 'background:' + t.bg,
945
+ 'color:' + t.fg,
946
+ 'font-family:' + t.font,
947
+ '--card-title-weight:' + t.titleWeight,
948
+ '--card-accent:' + t.accent,
949
+ ];
950
+ if (t.border) styleParts.push('border:' + t.border);
951
+
952
+ return '<div class="stage-card kind-' + kind + '"'
953
+ + ' data-card-bg="' + (t.surface === 'dark' ? 'dark' : 'light') + '"'
954
+ + ' data-card-idx="' + i + '"'
955
+ + ' style="' + styleParts.join(';') + '">'
956
+ + '<div class="frame-meta">'
957
+ + '<span class="idx">#' + (i + 1) + '</span>'
958
+ + '<span>' + escapeHtml(themeId) + ' · ' + escapeHtml(kind) + '</span>'
959
+ + '</div>'
960
+ + figureChip
961
+ + '<div class="body">' + titleHtml + '</div>'
962
+ + '<div class="accent-bar"></div>'
963
+ + '<div class="card-actions">'
964
+ + '<button type="button" data-action="copy-card" data-card-index="' + i + '"'
965
+ + ' aria-label="Copy card ' + (i + 1) + ' as text">Copy text</button>'
966
+ + '</div>'
967
+ + '<button class="edit-btn" type="button" data-edit-card-idx="' + i + '"'
968
+ + ' aria-label="Edit card ' + (i + 1) + ' with AI">'
969
+ + '<span class="icon">✏</span>Edit with AI'
970
+ + '</button>'
971
+ + '</div>';
972
+ }
973
+
974
+ function renderDeck(deck) {
975
+ currentDeck = deck;
976
+ // A successful render means the file is parseable now; clear any
977
+ // stale error banner from a previous broken-JSON state.
978
+ hideParseError();
979
+ var hasDeck = !!(deck && Array.isArray(deck.cards) && deck.cards.length > 0);
980
+ copyJsonBtn.disabled = !hasDeck;
981
+ downloadJsonBtn.disabled = !hasDeck;
982
+ copyMdBtn.disabled = !hasDeck;
983
+ if (!deck) {
984
+ cardsPane.className = 'empty';
985
+ cardsPane.textContent = 'Deck unavailable.';
986
+ jsonPane.textContent = '// no deck';
987
+ countBadge.hidden = true;
988
+ prevCardKeys = [];
989
+ lastRenderedDeckHash = null;
990
+ return;
991
+ }
992
+ // Skip identical-content re-renders (atomic save → duplicate SSE).
993
+ // This both saves work and preserves the .just-changed animation
994
+ // applied on the first render of the new content.
995
+ var deckHash;
996
+ try { deckHash = JSON.stringify(deck); } catch (_) { deckHash = null; }
997
+ if (deckHash !== null && deckHash === lastRenderedDeckHash) return;
998
+ lastRenderedDeckHash = deckHash;
999
+ var cards = Array.isArray(deck.cards) ? deck.cards : [];
1000
+ var themeId = (typeof deck.theme === 'string' && CARD_THEMES[deck.theme])
1001
+ ? deck.theme : DEFAULT_THEME;
1002
+
1003
+ // Compute per-card keys + which indices changed since the last
1004
+ // render. We diff BEFORE replacing innerHTML so the prev hash list
1005
+ // reflects the cards currently on screen.
1006
+ var newKeys = cards.map(function (c) {
1007
+ try { return JSON.stringify(c); } catch (_) { return Math.random().toString(36); }
1008
+ });
1009
+ var changedIdxs = [];
1010
+ for (var k = 0; k < newKeys.length; k++) {
1011
+ if (newKeys[k] !== prevCardKeys[k]) changedIdxs.push(k);
1012
+ }
1013
+ var firstRender = prevCardKeys.length === 0;
1014
+ prevCardKeys = newKeys;
1015
+
1016
+ if (cards.length === 0) {
1017
+ cardsPane.className = 'empty';
1018
+ cardsPane.textContent = 'Deck has no cards yet.';
1019
+ countBadge.hidden = true;
1020
+ } else {
1021
+ cardsPane.className = 'cards-grid';
1022
+ var html = '';
1023
+ for (var i = 0; i < cards.length; i++) {
1024
+ html += renderCard(cards[i] || {}, i, themeId);
1025
+ }
1026
+ cardsPane.innerHTML = html;
1027
+ countBadge.textContent = cards.length + ' · ' + themeId;
1028
+ countBadge.hidden = false;
1029
+
1030
+ // Highlight changed cards on hot-reload — but skip the very first
1031
+ // render so we don't flash every card just because the page loaded.
1032
+ // Also skip after a restore: the user just rewound history on
1033
+ // purpose; flashing every card as "just changed" would be noise.
1034
+ var skipDiffFlash = firstRender || suppressDiffOnce;
1035
+ suppressDiffOnce = false;
1036
+ if (!skipDiffFlash && changedIdxs.length > 0 && changedIdxs.length < cards.length) {
1037
+ for (var c2 = 0; c2 < changedIdxs.length; c2++) {
1038
+ var el = cardsPane.querySelector('[data-card-idx="' + changedIdxs[c2] + '"]');
1039
+ if (el) el.classList.add('just-changed');
1040
+ }
1041
+ }
1042
+ }
1043
+ try {
1044
+ jsonPane.textContent = JSON.stringify(deck, null, 2);
1045
+ } catch (e) {
1046
+ jsonPane.textContent = '// could not stringify deck: ' + e.message;
1047
+ }
1048
+ }
1049
+
1050
+ function showParseError(detailMsg) {
1051
+ errorDetail.innerHTML = '<code>' + escapeHtml(String(detailMsg || 'unknown')) + '</code>';
1052
+ errorBanner.classList.add('visible');
1053
+ }
1054
+ function hideParseError() {
1055
+ errorBanner.classList.remove('visible');
1056
+ errorDetail.textContent = '';
1057
+ }
1058
+
1059
+ function connect() {
1060
+ setStatus('warn', 'connecting…');
1061
+ var es = new EventSource('/events');
1062
+ es.addEventListener('deck', function (e) {
1063
+ setStatus('good', 'live');
1064
+ try {
1065
+ var msg = JSON.parse(e.data);
1066
+ if (msg && typeof msg.sourcePath === 'string') currentDeckPath = msg.sourcePath;
1067
+ if (msg && msg.restoredFrom) suppressDiffOnce = true;
1068
+ renderDeck(msg.deck);
1069
+ // Refresh the version drawer count + (if open) the row list. We
1070
+ // call this on every deck event so users always see the latest
1071
+ // count without having to re-open the drawer.
1072
+ refreshVersions();
1073
+ } catch (err) {
1074
+ setStatus('bad', 'parse error');
1075
+ }
1076
+ });
1077
+ // The server bus emits { type: 'error', error: 'JSON parse failed: …' }
1078
+ // when the deck file becomes unparseable; that arrives here as an
1079
+ // SSE message with name=error AND data set. The connection-level
1080
+ // EventSource error (lost socket, etc.) arrives WITHOUT data — we
1081
+ // discriminate on e.data so neither swallows the other.
1082
+ es.addEventListener('error', function (e) {
1083
+ if (e && typeof e.data === 'string' && e.data.length > 0) {
1084
+ try {
1085
+ var msg = JSON.parse(e.data);
1086
+ if (msg && msg.type === 'error') {
1087
+ showParseError(msg.error || 'invalid JSON');
1088
+ setStatus('warn', 'deck error');
1089
+ return;
1090
+ }
1091
+ } catch (_) { /* fall through */ }
1092
+ }
1093
+ if (es.readyState === EventSource.CLOSED) {
1094
+ setStatus('bad', 'disconnected');
1095
+ } else {
1096
+ setStatus('warn', 'reconnecting…');
1097
+ }
1098
+ });
1099
+ es.addEventListener('open', function () {
1100
+ setStatus('good', 'live');
1101
+ });
1102
+ }
1103
+ connect();
1104
+
1105
+ // ─── Version history drawer ─────────────────────────────────────────
1106
+ var versionsBtn = document.getElementById('versions-btn');
1107
+ var versionsCount = document.getElementById('versions-count');
1108
+ var versionsDrawer = document.getElementById('versions-drawer');
1109
+ var versionsBody = document.getElementById('versions-body');
1110
+ var versionsClose = document.getElementById('versions-close');
1111
+ var versionsStorePath = document.getElementById('versions-store-path');
1112
+
1113
+ // Cached version list — rendered into the drawer when it opens.
1114
+ // We always poll on deck events to keep the count pill fresh, even
1115
+ // when the drawer is closed.
1116
+ var lastVersions = [];
1117
+ var lastStorePath = '';
1118
+ var restoringId = null;
1119
+
1120
+ function fmtRelativeTime(ts) {
1121
+ var diffSec = Math.floor((Date.now() - ts) / 1000);
1122
+ if (diffSec < 5) return 'just now';
1123
+ if (diffSec < 60) return diffSec + 's ago';
1124
+ if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
1125
+ if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
1126
+ return Math.floor(diffSec / 86400) + 'd ago';
1127
+ }
1128
+
1129
+ function refreshVersions() {
1130
+ fetch('/api/snapshots', { credentials: 'omit' })
1131
+ .then(function (r) { return r.ok ? r.json() : null; })
1132
+ .then(function (data) {
1133
+ if (!data || !Array.isArray(data.versions)) return;
1134
+ lastVersions = data.versions;
1135
+ lastStorePath = data.storePath || '';
1136
+ versionsCount.textContent = String(lastVersions.length);
1137
+ versionsCount.hidden = lastVersions.length === 0;
1138
+ if (versionsDrawer.classList.contains('open')) renderVersionsList();
1139
+ })
1140
+ .catch(function () { /* drawer feature is best-effort */ });
1141
+ }
1142
+
1143
+ function renderVersionsList() {
1144
+ if (lastVersions.length === 0) {
1145
+ versionsBody.innerHTML = '<div class="empty-msg">No history yet — edits will show up here.</div>';
1146
+ versionsStorePath.hidden = true;
1147
+ return;
1148
+ }
1149
+ // The current deck = the newest snapshot (since every parseable
1150
+ // change is snapshotted before publish). Tag its row.
1151
+ var html = '';
1152
+ for (var i = 0; i < lastVersions.length; i++) {
1153
+ var v = lastVersions[i];
1154
+ var isCurrent = (i === 0);
1155
+ var label = isCurrent ? 'current' : fmtRelativeTime(v.ts);
1156
+ var meta = v.cardCount + ' cards · ' + (v.sizeBytes >= 1024
1157
+ ? (v.sizeBytes / 1024).toFixed(1) + 'KB' : v.sizeBytes + 'B');
1158
+ var btn = isCurrent ? '' :
1159
+ '<button class="restore-btn" type="button" data-restore-id="' + escapeHtml(v.id) + '"'
1160
+ + (restoringId === v.id ? ' disabled' : '')
1161
+ + '>' + (restoringId === v.id ? 'restoring…' : 'Restore') + '</button>';
1162
+ html += '<div class="version-row ' + (isCurrent ? 'current' : '') + '">'
1163
+ + '<div>'
1164
+ + '<div class="ts">' + escapeHtml(label) + '</div>'
1165
+ + '<div class="meta">' + escapeHtml(meta) + '</div>'
1166
+ + '</div>'
1167
+ + btn
1168
+ + '</div>';
1169
+ }
1170
+ versionsBody.innerHTML = html;
1171
+ if (lastStorePath) {
1172
+ versionsStorePath.textContent = lastStorePath;
1173
+ versionsStorePath.hidden = false;
1174
+ } else {
1175
+ versionsStorePath.hidden = true;
1176
+ }
1177
+ }
1178
+
1179
+ function openVersionsDrawer() {
1180
+ versionsDrawer.classList.add('open');
1181
+ versionsBtn.setAttribute('aria-expanded', 'true');
1182
+ renderVersionsList();
1183
+ }
1184
+ function closeVersionsDrawer() {
1185
+ versionsDrawer.classList.remove('open');
1186
+ versionsBtn.setAttribute('aria-expanded', 'false');
1187
+ }
1188
+
1189
+ versionsBtn.addEventListener('click', function () {
1190
+ if (versionsDrawer.classList.contains('open')) closeVersionsDrawer();
1191
+ else openVersionsDrawer();
1192
+ });
1193
+ versionsClose.addEventListener('click', closeVersionsDrawer);
1194
+ // Esc closes the drawer (mirrors modal behavior).
1195
+ document.addEventListener('keydown', function (e) {
1196
+ if (e.key === 'Escape' && versionsDrawer.classList.contains('open')) {
1197
+ closeVersionsDrawer();
1198
+ }
1199
+ });
1200
+
1201
+ versionsBody.addEventListener('click', function (e) {
1202
+ var target = e.target.closest('[data-restore-id]');
1203
+ if (!target) return;
1204
+ var id = target.getAttribute('data-restore-id');
1205
+ if (!id || restoringId) return;
1206
+ restoringId = id;
1207
+ renderVersionsList();
1208
+ fetch('/api/snapshots/restore', {
1209
+ method: 'POST',
1210
+ headers: { 'Content-Type': 'application/json' },
1211
+ credentials: 'omit',
1212
+ body: JSON.stringify({ id: id }),
1213
+ })
1214
+ .then(function (r) {
1215
+ if (!r.ok) throw new Error('restore failed: ' + r.status);
1216
+ // Watcher → SSE deck event will fire shortly with restoredFrom set;
1217
+ // the diff-highlight suppression and refreshVersions both run there.
1218
+ })
1219
+ .catch(function (err) {
1220
+ // Surface the failure inline in the drawer so the user isn't
1221
+ // left wondering why the page didn't update.
1222
+ versionsBody.innerHTML = '<div class="empty-msg" style="color:var(--bad)">'
1223
+ + escapeHtml('Restore failed: ' + err.message) + '</div>';
1224
+ })
1225
+ .finally(function () {
1226
+ restoringId = null;
1227
+ // Re-render after the deck event lands, but also after a short
1228
+ // tick so the disabled button visually clears even if the deck
1229
+ // event somehow doesn't arrive.
1230
+ setTimeout(function () {
1231
+ if (versionsDrawer.classList.contains('open')) renderVersionsList();
1232
+ }, 250);
1233
+ });
1234
+ });
1235
+
1236
+ // First load (the initial deck SSE event also calls this, but we want
1237
+ // the count pill populated even if the deck event arrives late).
1238
+ refreshVersions();
1239
+
1240
+ // ─── Render to MP4 (cloud) ──────────────────────────────────────────
1241
+ // State machine:
1242
+ // idle → button "Render mp4", click opens modal
1243
+ // confirming → modal open, fetching quota, "Render now" enables
1244
+ // once quota ≥ 500 (or 401/network error shown)
1245
+ // submitting → POST /api/render-mp4, button disabled
1246
+ // polling → modal hidden, button shows "Rendering…", poll every 3s
1247
+ // completed → button becomes "Open mp4 ↗" link to video URL
1248
+ // failed → button reverts to idle, modal shows error on next click
1249
+ var renderBtn = document.getElementById('render-btn');
1250
+ var renderBtnLabel = document.getElementById('render-btn-label');
1251
+ var renderModal = document.getElementById('render-modal');
1252
+ var renderModalClose = document.getElementById('render-modal-close');
1253
+ var renderConfirm = document.getElementById('render-confirm');
1254
+ var renderQuotaLine = document.getElementById('render-quota-line');
1255
+ var renderQuotaVal = document.getElementById('render-quota-val');
1256
+ var renderProgressMsg = document.getElementById('render-progress-msg');
1257
+ var renderProgressStage = document.getElementById('render-progress-stage');
1258
+ var renderProgressDetail = document.getElementById('render-progress-detail');
1259
+ var renderErr = document.getElementById('render-err');
1260
+
1261
+ var RENDER_COST = 500;
1262
+ var renderState = 'idle';
1263
+ var renderJobId = null;
1264
+ var renderVideoUrl = null;
1265
+ var renderPollTimer = null;
1266
+ var renderLastError = null;
1267
+
1268
+ function setRenderError(msg) {
1269
+ renderLastError = msg;
1270
+ renderErr.textContent = msg;
1271
+ renderErr.hidden = false;
1272
+ }
1273
+ function clearRenderError() {
1274
+ renderLastError = null;
1275
+ renderErr.hidden = true;
1276
+ renderErr.textContent = '';
1277
+ }
1278
+
1279
+ function refreshRenderBtn() {
1280
+ renderBtn.classList.remove('completed');
1281
+ if (renderState === 'idle' || renderState === 'failed') {
1282
+ renderBtn.disabled = false;
1283
+ renderBtnLabel.textContent = 'Render mp4';
1284
+ renderBtn.setAttribute('aria-label', 'Render this deck to mp4 (cloud)');
1285
+ } else if (renderState === 'submitting') {
1286
+ renderBtn.disabled = true;
1287
+ renderBtnLabel.textContent = 'Submitting…';
1288
+ } else if (renderState === 'polling') {
1289
+ renderBtn.disabled = false; // click reopens modal to show progress
1290
+ renderBtnLabel.textContent = 'Rendering…';
1291
+ } else if (renderState === 'completed') {
1292
+ renderBtn.disabled = false;
1293
+ renderBtn.classList.add('completed');
1294
+ renderBtnLabel.textContent = 'Open mp4 ↗';
1295
+ }
1296
+ }
1297
+
1298
+ function openRenderModal() {
1299
+ renderModal.hidden = false;
1300
+ renderModal.classList.add('open');
1301
+ clearRenderError();
1302
+ if (renderState === 'polling') {
1303
+ // Reopen during polling: show progress, hide confirm.
1304
+ renderProgressMsg.hidden = false;
1305
+ renderQuotaLine.style.display = 'none';
1306
+ renderConfirm.hidden = true;
1307
+ return;
1308
+ }
1309
+ renderProgressMsg.hidden = true;
1310
+ renderQuotaLine.style.display = '';
1311
+ renderConfirm.hidden = false;
1312
+ renderConfirm.disabled = true;
1313
+ renderConfirm.textContent = 'Render now';
1314
+ // Pull quota balance.
1315
+ renderQuotaVal.textContent = 'loading…';
1316
+ renderQuotaLine.classList.remove('short');
1317
+ fetch('/api/quota-balance', { credentials: 'omit' })
1318
+ .then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
1319
+ .then(function (resp) {
1320
+ if (resp.status === 401 || resp.json.code === 'not_logged_in') {
1321
+ renderQuotaVal.textContent = 'not logged in';
1322
+ setRenderError('Run \\u0060voxflow login\\u0060 in your terminal, then refresh this page.');
1323
+ return;
1324
+ }
1325
+ if (resp.status !== 200 || resp.json.code !== 'success') {
1326
+ renderQuotaVal.textContent = '?';
1327
+ setRenderError('Could not read quota: ' + (resp.json.message || 'HTTP ' + resp.status));
1328
+ return;
1329
+ }
1330
+ var rem = resp.json.remaining == null ? null : Number(resp.json.remaining);
1331
+ renderQuotaVal.textContent = rem == null ? '?' : rem.toLocaleString();
1332
+ if (rem !== null && rem < RENDER_COST) {
1333
+ renderQuotaLine.classList.add('short');
1334
+ setRenderError('Insufficient quota — need ' + RENDER_COST + ', have ' + rem + '.');
1335
+ renderConfirm.disabled = true;
1336
+ } else {
1337
+ renderConfirm.disabled = false;
1338
+ }
1339
+ })
1340
+ .catch(function (err) {
1341
+ renderQuotaVal.textContent = '?';
1342
+ setRenderError('Quota lookup failed: ' + err.message);
1343
+ });
1344
+ }
1345
+ function closeRenderModal() {
1346
+ renderModal.classList.remove('open');
1347
+ renderModal.hidden = true;
1348
+ }
1349
+
1350
+ function startPolling() {
1351
+ function tick() {
1352
+ fetch('/api/render-status/' + encodeURIComponent(renderJobId), { credentials: 'omit' })
1353
+ .then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
1354
+ .then(function (resp) {
1355
+ if (resp.status !== 200 || resp.json.code !== 'success') {
1356
+ // Transient errors — keep polling. The user can give up by
1357
+ // refreshing the page; we don't auto-cancel because the job
1358
+ // itself is still running on the backend.
1359
+ renderProgressDetail.textContent = ' · ' + (resp.json.message || 'retrying…');
1360
+ return;
1361
+ }
1362
+ var st = resp.json.status || 'unknown';
1363
+ renderProgressStage.textContent = st;
1364
+ renderProgressDetail.textContent = '';
1365
+ if (st === 'completed') {
1366
+ renderState = 'completed';
1367
+ renderVideoUrl = resp.json.videoUrl || null;
1368
+ clearInterval(renderPollTimer);
1369
+ renderPollTimer = null;
1370
+ refreshRenderBtn();
1371
+ closeRenderModal();
1372
+ if (renderVideoUrl) {
1373
+ // Auto-open the mp4 — same gesture as a "Render → Open"
1374
+ // single click would be elsewhere. Future click on the
1375
+ // button still re-opens (browser may block the auto-open
1376
+ // but the button is the recovery affordance).
1377
+ try { window.open(renderVideoUrl, '_blank', 'noopener'); } catch (_) { /* popup blocked */ }
1378
+ }
1379
+ } else if (st === 'failed' || st === 'error') {
1380
+ renderState = 'failed';
1381
+ clearInterval(renderPollTimer);
1382
+ renderPollTimer = null;
1383
+ refreshRenderBtn();
1384
+ setRenderError('Render failed. Check \\u0060voxflow status\\u0060 or retry.');
1385
+ }
1386
+ })
1387
+ .catch(function (err) {
1388
+ renderProgressDetail.textContent = ' · ' + err.message;
1389
+ });
1390
+ }
1391
+ tick();
1392
+ renderPollTimer = setInterval(tick, 3000);
1393
+ }
1394
+
1395
+ renderBtn.addEventListener('click', function () {
1396
+ if (renderState === 'completed' && renderVideoUrl) {
1397
+ window.open(renderVideoUrl, '_blank', 'noopener');
1398
+ return;
1399
+ }
1400
+ openRenderModal();
1401
+ });
1402
+ renderModalClose.addEventListener('click', closeRenderModal);
1403
+ // Click outside the inner modal closes (mirrors edit-modal).
1404
+ renderModal.addEventListener('click', function (e) {
1405
+ if (e.target === renderModal) closeRenderModal();
1406
+ });
1407
+ // Esc closes (mirrors edit-modal + drawer).
1408
+ document.addEventListener('keydown', function (e) {
1409
+ if (e.key === 'Escape' && renderModal.classList.contains('open')) {
1410
+ closeRenderModal();
1411
+ }
1412
+ });
1413
+
1414
+ renderConfirm.addEventListener('click', function () {
1415
+ if (!currentDeck || !Array.isArray(currentDeck.cards)) {
1416
+ setRenderError('No deck loaded — fix the JSON parse error first.');
1417
+ return;
1418
+ }
1419
+ renderConfirm.disabled = true;
1420
+ renderConfirm.textContent = 'Submitting…';
1421
+ clearRenderError();
1422
+ renderState = 'submitting';
1423
+ refreshRenderBtn();
1424
+ fetch('/api/render-mp4', {
1425
+ method: 'POST',
1426
+ headers: { 'Content-Type': 'application/json' },
1427
+ credentials: 'omit',
1428
+ body: JSON.stringify({ deck: currentDeck }),
1429
+ })
1430
+ .then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
1431
+ .then(function (resp) {
1432
+ if (resp.status !== 200 || resp.json.code !== 'success') {
1433
+ renderState = 'failed';
1434
+ refreshRenderBtn();
1435
+ setRenderError(resp.json.message || ('Render submit failed (' + resp.status + ')'));
1436
+ renderConfirm.disabled = false;
1437
+ renderConfirm.textContent = 'Render now';
1438
+ return;
1439
+ }
1440
+ renderJobId = resp.json.jobId;
1441
+ renderState = 'polling';
1442
+ refreshRenderBtn();
1443
+ // Switch the modal into "rendering…" mode so the user sees the
1444
+ // job pick up. They can close the modal — polling continues in
1445
+ // the background and the button label tracks state.
1446
+ renderQuotaLine.style.display = 'none';
1447
+ renderConfirm.hidden = true;
1448
+ renderProgressMsg.hidden = false;
1449
+ renderProgressStage.textContent = 'pending';
1450
+ renderProgressDetail.textContent = ' · job ' + renderJobId;
1451
+ startPolling();
1452
+ })
1453
+ .catch(function (err) {
1454
+ renderState = 'failed';
1455
+ refreshRenderBtn();
1456
+ setRenderError('Network error: ' + err.message);
1457
+ renderConfirm.disabled = false;
1458
+ renderConfirm.textContent = 'Render now';
1459
+ });
1460
+ });
1461
+
1462
+ refreshRenderBtn();
1463
+
1464
+ // ─── Edit-with-AI: prompt builder (mirrors stage-core/edit-prompt.js) ─
1465
+ // Kept as a small in-browser duplicate because the template is an
1466
+ // inline string — pulling the Node module across the boundary would
1467
+ // need a build step we deliberately don't have. The Node module is
1468
+ // unit-tested; the template version follows the same contract.
1469
+ function buildEditPrompt(args) {
1470
+ var deckPath = String(args.deckPath || '').trim();
1471
+ var cardIdx = (typeof args.cardIdx === 'number') ? args.cardIdx : 0;
1472
+ var card = (args.card && typeof args.card === 'object') ? args.card : {};
1473
+ var selection = (typeof args.selectedText === 'string') ? args.selectedText.trim() : '';
1474
+ var instruction = (typeof args.instruction === 'string') ? args.instruction.trim() : '';
1475
+ var kind = (typeof card.kind === 'string' && card.kind) ? card.kind : 'unknown';
1476
+
1477
+ var cardJson;
1478
+ try {
1479
+ cardJson = JSON.stringify(card, null, 2);
1480
+ } catch (_) {
1481
+ cardJson = JSON.stringify({ kind: card.kind || null, _note: 'card not stringifiable' }, null, 2);
1482
+ }
1483
+
1484
+ var instructionLine = instruction
1485
+ ? ' ' + instruction
1486
+ : ' <REPLACE THIS WITH YOUR INSTRUCTION — e.g. "make it punchier"; "shorten to 12 chars">';
1487
+
1488
+ var lines = [
1489
+ "I'm iterating on a Slice deck (vertical 1080x1920 card video — narrated, theme-styled).",
1490
+ '',
1491
+ 'File to edit:',
1492
+ ' ' + deckPath,
1493
+ '',
1494
+ 'Card to change: cards[' + cardIdx + '] (kind: ' + kind + ')',
1495
+ ];
1496
+
1497
+ if (selection) {
1498
+ lines.push('');
1499
+ lines.push('Specifically, this exact substring needs to change:');
1500
+ lines.push(' ' + JSON.stringify(selection));
1501
+ lines.push('');
1502
+ lines.push('Replace only that substring; keep everything else in the card identical unless the change requires touching adjacent text.');
1503
+ }
1504
+
1505
+ lines.push('');
1506
+ lines.push('Current card value:');
1507
+ lines.push('\`\`\`json');
1508
+ lines.push(cardJson);
1509
+ lines.push('\`\`\`');
1510
+ lines.push('');
1511
+ lines.push('Change request:');
1512
+ lines.push(instructionLine);
1513
+ lines.push('');
1514
+ lines.push('When you finish, save the file in place — preserve every other field and the JSON structure.');
1515
+ lines.push('The local stage server is watching this file and will hot-reload the browser preview within ~150ms.');
1516
+
1517
+ return lines.join('\\n');
1518
+ }
1519
+
1520
+ // ─── Edit-with-AI: modal state + handlers ────────────────────────────
1521
+ var modalCtx = null; // { cardIdx, selectedText }
1522
+
1523
+ function refreshPromptPreview() {
1524
+ if (!modalCtx) return;
1525
+ var card = (currentDeck && Array.isArray(currentDeck.cards))
1526
+ ? currentDeck.cards[modalCtx.cardIdx] : null;
1527
+ if (!card) {
1528
+ promptPreview.textContent = '// card no longer exists in deck — close and pick another';
1529
+ return;
1530
+ }
1531
+ promptPreview.textContent = buildEditPrompt({
1532
+ deckPath: currentDeckPath,
1533
+ cardIdx: modalCtx.cardIdx,
1534
+ card: card,
1535
+ selectedText: modalCtx.selectedText,
1536
+ instruction: instructionInput.value,
1537
+ });
1538
+ }
1539
+
1540
+ function openModal(cardIdx, selectedText) {
1541
+ modalCtx = { cardIdx: cardIdx, selectedText: selectedText || '' };
1542
+ modalTitle.textContent = selectedText
1543
+ ? ('Edit selection in card #' + (cardIdx + 1))
1544
+ : ('Edit card #' + (cardIdx + 1) + ' with AI');
1545
+ instructionInput.value = '';
1546
+ copyBtn.classList.remove('copied');
1547
+ copyBtn.textContent = 'Copy prompt';
1548
+ refreshPromptPreview();
1549
+ modal.classList.add('open');
1550
+ modal.removeAttribute('hidden');
1551
+ // Focus the instruction box so the user can type immediately.
1552
+ setTimeout(function () { instructionInput.focus(); }, 30);
1553
+ }
1554
+
1555
+ function closeModal() {
1556
+ modal.classList.remove('open');
1557
+ modal.setAttribute('hidden', '');
1558
+ modalCtx = null;
1559
+ hideSelectionFab();
1560
+ }
1561
+
1562
+ modalClose.addEventListener('click', closeModal);
1563
+ modal.addEventListener('click', function (e) {
1564
+ // Click on backdrop (not the inner .modal) closes.
1565
+ if (e.target === modal) closeModal();
1566
+ });
1567
+ document.addEventListener('keydown', function (e) {
1568
+ if (e.key === 'Escape' && modal.classList.contains('open')) closeModal();
1569
+ });
1570
+ instructionInput.addEventListener('input', refreshPromptPreview);
1571
+
1572
+ copyBtn.addEventListener('click', function () {
1573
+ var text = promptPreview.textContent;
1574
+ var done = function () {
1575
+ copyBtn.classList.add('copied');
1576
+ copyBtn.textContent = '✓ Copied — paste in your AI';
1577
+ // Auto-close the modal so the user can switch straight to their
1578
+ // AI tool. 700ms gives them time to read the "Copied" state.
1579
+ setTimeout(function () {
1580
+ if (modal.classList.contains('open')) closeModal();
1581
+ copyBtn.classList.remove('copied');
1582
+ copyBtn.textContent = 'Copy prompt';
1583
+ }, 700);
1584
+ };
1585
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1586
+ navigator.clipboard.writeText(text).then(done, function () {
1587
+ // Fallback for non-secure contexts (file://, very old browsers)
1588
+ fallbackCopy(text); done();
1589
+ });
1590
+ } else {
1591
+ fallbackCopy(text); done();
1592
+ }
1593
+ });
1594
+
1595
+ function fallbackCopy(text) {
1596
+ var ta = document.createElement('textarea');
1597
+ ta.value = text;
1598
+ ta.style.position = 'fixed'; ta.style.opacity = '0';
1599
+ document.body.appendChild(ta);
1600
+ ta.select();
1601
+ try { document.execCommand('copy'); } catch (_) { /* give up */ }
1602
+ document.body.removeChild(ta);
1603
+ }
1604
+
1605
+ // ─── Per-card "Edit with AI" button (event delegation) ───────────────
1606
+ cardsPane.addEventListener('click', function (e) {
1607
+ var btn = e.target.closest && e.target.closest('.edit-btn');
1608
+ if (!btn) return;
1609
+ var idx = parseInt(btn.getAttribute('data-edit-card-idx'), 10);
1610
+ if (Number.isFinite(idx)) openModal(idx, '');
1611
+ });
1612
+
1613
+ // ─── Text-selection floating button ──────────────────────────────────
1614
+ function hideSelectionFab() {
1615
+ selectionFab.classList.remove('visible');
1616
+ selectionFab._cardIdx = null;
1617
+ selectionFab._text = '';
1618
+ }
1619
+
1620
+ function maybeShowSelectionFab() {
1621
+ var sel = window.getSelection();
1622
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
1623
+ hideSelectionFab();
1624
+ return;
1625
+ }
1626
+ var text = sel.toString().trim();
1627
+ if (!text) { hideSelectionFab(); return; }
1628
+ // Anchor must be inside a stage card.
1629
+ var anchor = sel.anchorNode;
1630
+ var cardEl = (anchor && anchor.nodeType === 1)
1631
+ ? anchor.closest && anchor.closest('.stage-card')
1632
+ : (anchor && anchor.parentElement && anchor.parentElement.closest('.stage-card'));
1633
+ if (!cardEl) { hideSelectionFab(); return; }
1634
+ var idxStr = cardEl.getAttribute('data-card-idx');
1635
+ var idx = parseInt(idxStr, 10);
1636
+ if (!Number.isFinite(idx)) { hideSelectionFab(); return; }
1637
+
1638
+ var rect = sel.getRangeAt(0).getBoundingClientRect();
1639
+ // Position just below the selection, clamped inside viewport.
1640
+ var x = Math.min(window.innerWidth - 220, Math.max(8, rect.left));
1641
+ var y = Math.min(window.innerHeight - 40, rect.bottom + 6);
1642
+ selectionFab.style.left = x + 'px';
1643
+ selectionFab.style.top = y + 'px';
1644
+ selectionFab._cardIdx = idx;
1645
+ selectionFab._text = text;
1646
+ selectionFab.classList.add('visible');
1647
+ }
1648
+
1649
+ document.addEventListener('mouseup', function () {
1650
+ // Defer so the selection settles after click-release.
1651
+ setTimeout(maybeShowSelectionFab, 10);
1652
+ });
1653
+ document.addEventListener('selectionchange', function () {
1654
+ var sel = window.getSelection();
1655
+ if (!sel || sel.isCollapsed || !sel.toString().trim()) hideSelectionFab();
1656
+ });
1657
+ selectionFab.addEventListener('mousedown', function (e) {
1658
+ // Prevent the mousedown from clearing the selection before our click handler runs.
1659
+ e.preventDefault();
1660
+ });
1661
+ selectionFab.addEventListener('click', function () {
1662
+ if (Number.isFinite(selectionFab._cardIdx)) {
1663
+ openModal(selectionFab._cardIdx, selectionFab._text);
1664
+ }
1665
+ });
1666
+ })();
1667
+ </script>
1668
+ </body>
1669
+ </html>`;
1670
+ }
1671
+
1672
+ module.exports = { renderSliceStageHtml };