@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,158 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ vi.mock("../toast.js", () => ({
6
+ showToast: vi.fn(),
7
+ showToastWithAction: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("../confirm.js", () => ({
11
+ showConfirmDialog: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("../smart-collection-dialog-host.js", () => ({
15
+ openSmartCollectionDialog: vi.fn(),
16
+ }));
17
+
18
+ function createMarkup() {
19
+ document.body.innerHTML = `
20
+ <div id="toast-container"></div>
21
+ <div
22
+ data-smart-collection-page-actions
23
+ data-smart-collection-id="smc_1"
24
+ data-collection-page-labels='{"addToNavigation":"Add to Navigation","addingToNavigation":"Adding…","addedToNavigation":"Collection added to navigation.","editNavigation":"Edit Navigation","addToNavigationFailed":"Couldn\\u0027t add this collection to navigation. Try again.","confirmDelete":"Delete this smart collection?","deleteCollection":"Delete","cancel":"Cancel","saveFailed":"Couldn\\u0027t save. Try again in a moment.","deleted":"Smart collection deleted."}'
25
+ data-collection-in-navigation="false"
26
+ data-collection-page-redirect-url="/collections"
27
+ >
28
+ <button
29
+ type="button"
30
+ data-collection-page-action="toggle-menu"
31
+ aria-expanded="false"
32
+ >
33
+ More actions
34
+ </button>
35
+ <div data-collection-page-menu hidden>
36
+ <button type="button" role="menuitem" data-collection-page-action="edit">
37
+ Edit
38
+ </button>
39
+ <button
40
+ type="button"
41
+ role="menuitem"
42
+ data-collection-page-action="add-to-navigation"
43
+ >
44
+ <span class="collections-page-menu-item-label">Add to Navigation</span>
45
+ </button>
46
+ <a
47
+ href="/settings/navigation"
48
+ role="menuitem"
49
+ data-collection-page-edit-navigation
50
+ hidden
51
+ >
52
+ Edit Navigation
53
+ </a>
54
+ <button
55
+ type="button"
56
+ role="menuitem"
57
+ data-collection-page-action="delete"
58
+ >
59
+ Delete
60
+ </button>
61
+ </div>
62
+ </div>
63
+ `;
64
+ }
65
+
66
+ async function flushAsyncWork() {
67
+ await new Promise((resolve) => setTimeout(resolve, 0));
68
+ await new Promise((resolve) => setTimeout(resolve, 0));
69
+ }
70
+
71
+ describe("smart collection page actions", () => {
72
+ beforeEach(() => {
73
+ vi.resetModules();
74
+ vi.clearAllMocks();
75
+ window.location.href = "http://localhost/reading";
76
+ });
77
+
78
+ // The same request a collection page sends, down to the placement and the
79
+ // header-refresh hint — the two kinds are one thing to the reader, so the
80
+ // action cannot quietly put them in different parts of the nav.
81
+ it("adds the smart collection to navigation and reveals the settings action", async () => {
82
+ createMarkup();
83
+ const fetchMock = vi.fn().mockResolvedValue(
84
+ new Response(JSON.stringify({ id: "nav-1" }), {
85
+ status: 201,
86
+ headers: { "Content-Type": "application/json" },
87
+ }),
88
+ );
89
+ vi.stubGlobal("fetch", fetchMock);
90
+
91
+ const { showToastWithAction } = await import("../toast.js");
92
+ await import("../smart-collection-page-actions.js");
93
+
94
+ const addButton = document.querySelector<HTMLButtonElement>(
95
+ "[data-collection-page-action='add-to-navigation']",
96
+ );
97
+ const editNavigationLink = document.querySelector<HTMLAnchorElement>(
98
+ "[data-collection-page-edit-navigation]",
99
+ );
100
+
101
+ addButton?.click();
102
+ await flushAsyncWork();
103
+
104
+ expect(fetchMock).toHaveBeenCalledWith("/api/nav-items", {
105
+ method: "POST",
106
+ headers: {
107
+ "Content-Type": "application/json",
108
+ Accept: "application/json",
109
+ "X-Jant-Site-Header": "include",
110
+ },
111
+ body: JSON.stringify({
112
+ type: "smart_collection",
113
+ smartCollectionId: "smc_1",
114
+ placement: "header",
115
+ }),
116
+ });
117
+ expect(addButton?.hidden).toBe(true);
118
+ expect(editNavigationLink?.hidden).toBe(false);
119
+ expect(
120
+ document.querySelector<HTMLElement>(
121
+ "[data-smart-collection-page-actions]",
122
+ )?.dataset.collectionInNavigation,
123
+ ).toBe("true");
124
+ expect(showToastWithAction).toHaveBeenCalledWith(
125
+ "Collection added to navigation.",
126
+ {
127
+ label: "Edit Navigation",
128
+ href: "/settings/navigation",
129
+ },
130
+ );
131
+ });
132
+
133
+ it("keeps the action available when the request fails", async () => {
134
+ createMarkup();
135
+ const fetchMock = vi
136
+ .fn()
137
+ .mockResolvedValue(new Response(null, { status: 500 }));
138
+ vi.stubGlobal("fetch", fetchMock);
139
+
140
+ const { showToast } = await import("../toast.js");
141
+ await import("../smart-collection-page-actions.js");
142
+
143
+ const addButton = document.querySelector<HTMLButtonElement>(
144
+ "[data-collection-page-action='add-to-navigation']",
145
+ );
146
+
147
+ addButton?.click();
148
+ await flushAsyncWork();
149
+
150
+ expect(showToast).toHaveBeenCalledWith(
151
+ "Couldn't add this collection to navigation. Try again.",
152
+ "error",
153
+ );
154
+ expect(addButton?.hidden).toBe(false);
155
+ expect(addButton?.disabled).toBe(false);
156
+ expect(addButton?.textContent).toContain("Add to Navigation");
157
+ });
158
+ });
@@ -5,6 +5,8 @@ import {
5
5
  QUEUED_TOAST_STORAGE_KEY,
6
6
  consumeQueuedToast,
7
7
  queueToastForNextPage,
8
+ replaceWithAutoClose,
9
+ showPersistentToast,
8
10
  showToast,
9
11
  showToastWithAction,
10
12
  } from "../toast.js";
@@ -110,6 +112,27 @@ describe("toast", () => {
110
112
  expect(container?.parentElement).toBe(document.body);
111
113
  });
112
114
 
115
+ it("follows a dialog that opened after the progress toast did", async () => {
116
+ showPersistentToast("compose-deferred", "Publishing...");
117
+
118
+ // A failed publish reopens the composer, which makes everything outside
119
+ // it inert — the result toast has to move in with it.
120
+ const dialog = document.createElement("dialog");
121
+ document.body.appendChild(dialog);
122
+ dialog.showModal();
123
+
124
+ replaceWithAutoClose("compose-deferred", "Couldn't publish.", "error");
125
+
126
+ const container = document.getElementById("toast-container");
127
+ expect(container?.parentElement).toBe(dialog);
128
+ expect(container?.querySelector(".toast span")?.textContent).toBe(
129
+ "Couldn't publish.",
130
+ );
131
+
132
+ dialog.close();
133
+ await new Promise((resolve) => setTimeout(resolve, 0));
134
+ });
135
+
113
136
  it("keeps action toasts available longer than passive notifications", () => {
114
137
  vi.useFakeTimers();
115
138
  showToastWithAction("Collection created.", {
@@ -0,0 +1,175 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * The source probe feeds four independent things: the codec that decides
5
+ * whether a re-encode is needed, the bitrate that sizes it, the duration, and
6
+ * the poster/blurhash pair. They used to share one try/catch, so a single
7
+ * failing step returned nothing at all — a mediabunny version without
8
+ * `getCodec()` cost every upload its thumbnail while the video itself went
9
+ * through fine. Each step is now degraded on its own.
10
+ */
11
+
12
+ import { beforeEach, describe, expect, it, vi } from "vitest";
13
+
14
+ const trackStub = {
15
+ displayWidth: 1280,
16
+ displayHeight: 720,
17
+ rotation: 0,
18
+ getCodec: vi.fn(async () => "avc"),
19
+ computePacketStats: vi.fn(async () => ({ averageBitrate: 2_000_000 })),
20
+ };
21
+
22
+ const conversionOptions: { video?: Record<string, unknown> }[] = [];
23
+
24
+ const canvasSinkStub = {
25
+ getCanvas: vi.fn(async () => ({ canvas: makeCanvas(1280, 720) })),
26
+ };
27
+
28
+ vi.mock("mediabunny", () => {
29
+ class Input {
30
+ getPrimaryVideoTrack = async () => trackStub;
31
+ computeDuration = async () => 20;
32
+ dispose = () => {};
33
+ }
34
+ class CanvasSink {
35
+ getCanvas = () => canvasSinkStub.getCanvas();
36
+ }
37
+ class BufferTarget {
38
+ buffer = new ArrayBuffer(64);
39
+ }
40
+ return {
41
+ Input,
42
+ CanvasSink,
43
+ BufferTarget,
44
+ BlobSource: class {},
45
+ Output: class {},
46
+ Mp4OutputFormat: class {},
47
+ Quality: class {
48
+ constructor(readonly options: unknown) {}
49
+ },
50
+ ALL_FORMATS: [],
51
+ Conversion: {
52
+ init: async (options: { video?: Record<string, unknown> }) => {
53
+ conversionOptions.push(options);
54
+ return { onProgress: undefined, execute: async () => {} };
55
+ },
56
+ },
57
+ };
58
+ });
59
+
60
+ vi.mock("../../lib/mp4-track-flags.js", () => ({
61
+ zeroTrackAlternateGroups: () => {},
62
+ }));
63
+
64
+ /** A canvas whose 2D context and WebP encoding both succeed. */
65
+ function makeCanvas(width: number, height: number): HTMLCanvasElement {
66
+ return {
67
+ width,
68
+ height,
69
+ getContext: () => ({
70
+ drawImage: () => {},
71
+ getImageData: (_x: number, _y: number, w: number, h: number) => ({
72
+ data: new Uint8ClampedArray(w * h * 4).fill(120),
73
+ }),
74
+ }),
75
+ toBlob: (cb: (blob: Blob | null) => void) =>
76
+ cb(new Blob(["poster"], { type: "image/webp" })),
77
+ } as unknown as HTMLCanvasElement;
78
+ }
79
+
80
+ const { VideoProcessor } = await import("../video-processor.js");
81
+
82
+ beforeEach(() => {
83
+ conversionOptions.length = 0;
84
+ trackStub.getCodec.mockImplementation(async () => "avc");
85
+ trackStub.computePacketStats.mockImplementation(async () => ({
86
+ averageBitrate: 2_000_000,
87
+ }));
88
+ canvasSinkStub.getCanvas.mockImplementation(async () => ({
89
+ canvas: makeCanvas(1280, 720),
90
+ }));
91
+
92
+ vi.spyOn(document, "createElement").mockImplementation(((tag: string) => {
93
+ if (tag === "canvas") return makeCanvas(1280, 720);
94
+ // `probeVideoDimensions` loads the transcoded output in a <video> element.
95
+ const video = {
96
+ preload: "",
97
+ onloadedmetadata: null as null | (() => void),
98
+ onerror: null,
99
+ videoWidth: 1280,
100
+ videoHeight: 720,
101
+ set src(_value: string) {
102
+ queueMicrotask(() => this.onloadedmetadata?.());
103
+ },
104
+ };
105
+ return video as unknown as HTMLElement;
106
+ }) as typeof document.createElement);
107
+
108
+ vi.stubGlobal("URL", {
109
+ createObjectURL: () => "blob:test",
110
+ revokeObjectURL: () => {},
111
+ });
112
+ vi.stubGlobal("VideoEncoder", class {});
113
+ });
114
+
115
+ function sourceFile() {
116
+ return new File(["source"], "clip.mov", { type: "video/quicktime" });
117
+ }
118
+
119
+ describe("VideoProcessor.processToFile source probing", () => {
120
+ it("still captures a poster when the codec cannot be read", async () => {
121
+ // The exact shape of a stale install: the installed mediabunny has no
122
+ // `getCodec()`, so the call throws before the frame is ever captured.
123
+ trackStub.getCodec.mockImplementation(() => {
124
+ throw new TypeError("videoTrack.getCodec is not a function");
125
+ });
126
+ vi.spyOn(console, "warn").mockImplementation(() => {});
127
+
128
+ const result = await VideoProcessor.processToFile(sourceFile());
129
+
130
+ expect(result.poster).toBeInstanceOf(Blob);
131
+ expect(result.blurhash).toBeTruthy();
132
+ expect(result.durationSeconds).toBe(20);
133
+ // An unreadable codec cannot be vouched for, so the file is re-encoded.
134
+ expect(conversionOptions[0]?.video).toHaveProperty("quality");
135
+ });
136
+
137
+ it("still captures a poster when the bitrate cannot be computed", async () => {
138
+ trackStub.computePacketStats.mockRejectedValue(
139
+ new Error("no sample table"),
140
+ );
141
+ vi.spyOn(console, "warn").mockImplementation(() => {});
142
+
143
+ const result = await VideoProcessor.processToFile(sourceFile());
144
+
145
+ expect(result.poster).toBeInstanceOf(Blob);
146
+ expect(result.blurhash).toBeTruthy();
147
+ });
148
+
149
+ it("still reports the duration when the frame cannot be decoded", async () => {
150
+ canvasSinkStub.getCanvas.mockRejectedValue(
151
+ new Error("unsupported codec configuration"),
152
+ );
153
+ vi.spyOn(console, "warn").mockImplementation(() => {});
154
+
155
+ const result = await VideoProcessor.processToFile(sourceFile());
156
+
157
+ expect(result.poster).toBeUndefined();
158
+ expect(result.blurhash).toBeUndefined();
159
+ // The half that does not need a decoder survives.
160
+ expect(result.durationSeconds).toBe(20);
161
+ expect(conversionOptions[0]?.video).not.toHaveProperty("quality");
162
+ });
163
+
164
+ it("names the failing step on the console rather than failing silently", async () => {
165
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
166
+ trackStub.getCodec.mockRejectedValue(new Error("boom"));
167
+
168
+ await VideoProcessor.processToFile(sourceFile());
169
+
170
+ expect(warn).toHaveBeenCalledWith(
171
+ expect.stringContaining("read codec"),
172
+ expect.any(Error),
173
+ );
174
+ });
175
+ });
@@ -0,0 +1,181 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { planVideoProcessing } from "../video-processor.js";
3
+
4
+ const OPTS = { maxLongEdge: 1920, maxShortEdge: 1080 };
5
+
6
+ const mbps = (n: number) => n * 1_000_000;
7
+
8
+ describe("planVideoProcessing", () => {
9
+ describe("copying", () => {
10
+ it("copies an H.264 video that already fits", () => {
11
+ expect(
12
+ planVideoProcessing(
13
+ {
14
+ width: 1280,
15
+ height: 720,
16
+ videoCodec: "avc",
17
+ videoBitrate: mbps(5),
18
+ },
19
+ OPTS,
20
+ ),
21
+ ).toEqual({
22
+ needsResize: false,
23
+ width: 1280,
24
+ height: 720,
25
+ videoNeedsReencode: false,
26
+ videoBitrate: undefined,
27
+ });
28
+ });
29
+
30
+ it("copies a video sitting exactly on the caps", () => {
31
+ const plan = planVideoProcessing(
32
+ { width: 1920, height: 1080, videoCodec: "avc" },
33
+ OPTS,
34
+ );
35
+ expect(plan.needsResize).toBe(false);
36
+ expect(plan.videoNeedsReencode).toBe(false);
37
+ });
38
+
39
+ it("re-encodes an HEVC video even when it fits", () => {
40
+ const plan = planVideoProcessing(
41
+ { width: 1280, height: 720, videoCodec: "hevc", videoBitrate: mbps(5) },
42
+ OPTS,
43
+ );
44
+ expect(plan.needsResize).toBe(false);
45
+ expect(plan.videoNeedsReencode).toBe(true);
46
+ });
47
+
48
+ it("re-encodes an unknown video codec", () => {
49
+ const plan = planVideoProcessing(
50
+ { width: 1280, height: 720, videoCodec: null },
51
+ OPTS,
52
+ );
53
+ expect(plan.videoNeedsReencode).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe("resizing", () => {
58
+ it("scales 4K down to the long-edge cap", () => {
59
+ const plan = planVideoProcessing(
60
+ { width: 3840, height: 2160, videoCodec: "avc" },
61
+ OPTS,
62
+ );
63
+ expect(plan.needsResize).toBe(true);
64
+ expect(plan.width).toBe(1920);
65
+ expect(plan.height).toBe(1080);
66
+ expect(plan.videoNeedsReencode).toBe(true);
67
+ });
68
+
69
+ it("caps portrait video by its short edge, not its long edge", () => {
70
+ const plan = planVideoProcessing(
71
+ { width: 2160, height: 3840, videoCodec: "avc" },
72
+ OPTS,
73
+ );
74
+ expect(plan.width).toBe(1080);
75
+ expect(plan.height).toBe(1920);
76
+ });
77
+
78
+ it("caps an ultra-wide video by its long edge", () => {
79
+ const plan = planVideoProcessing(
80
+ { width: 3840, height: 1080, videoCodec: "avc" },
81
+ OPTS,
82
+ );
83
+ expect(plan.width).toBe(1920);
84
+ expect(plan.height).toBe(540);
85
+ });
86
+
87
+ it("rounds scaled dimensions up to even numbers for H.264", () => {
88
+ const plan = planVideoProcessing(
89
+ { width: 3840, height: 2158, videoCodec: "avc" },
90
+ OPTS,
91
+ );
92
+ expect(plan.width % 2).toBe(0);
93
+ expect(plan.height % 2).toBe(0);
94
+ });
95
+
96
+ it("falls back to the caps when dimensions are unknown", () => {
97
+ const plan = planVideoProcessing({ videoCodec: "avc" }, OPTS);
98
+ expect(plan.needsResize).toBe(false);
99
+ expect(plan.width).toBe(1920);
100
+ expect(plan.height).toBe(1080);
101
+ });
102
+ });
103
+
104
+ describe("bitrate", () => {
105
+ it("targets roughly 8 Mbps for a 1080p re-encode", () => {
106
+ const plan = planVideoProcessing(
107
+ {
108
+ width: 1920,
109
+ height: 1080,
110
+ videoCodec: "hevc",
111
+ videoBitrate: mbps(50),
112
+ },
113
+ OPTS,
114
+ );
115
+ expect(plan.videoBitrate).toBeGreaterThan(mbps(7));
116
+ expect(plan.videoBitrate).toBeLessThan(mbps(9));
117
+ });
118
+
119
+ it("never exceeds what the source content carries", () => {
120
+ // A 960×448 HEVC clip at 0.72 Mbps: re-encoding at the size-appropriate
121
+ // 1.8 Mbps would spend most of those bits on the source's own artifacts.
122
+ const plan = planVideoProcessing(
123
+ {
124
+ width: 960,
125
+ height: 448,
126
+ videoCodec: "hevc",
127
+ videoBitrate: 717_000,
128
+ },
129
+ OPTS,
130
+ );
131
+ // 0.72 Mbps HEVC ≈ 1.2 Mbps of H.264, well under the ~1.8 Mbps target.
132
+ expect(plan.videoBitrate).toBeGreaterThan(mbps(1));
133
+ expect(plan.videoBitrate).toBeLessThan(mbps(1.4));
134
+ });
135
+
136
+ it("allows H.264 more bits than the HEVC source it replaces", () => {
137
+ const source = { width: 1280, height: 720, videoBitrate: mbps(1) };
138
+ const fromHevc = planVideoProcessing(
139
+ { ...source, videoCodec: "hevc" },
140
+ OPTS,
141
+ );
142
+ const fromAv1 = planVideoProcessing(
143
+ { ...source, videoCodec: "av1" },
144
+ OPTS,
145
+ );
146
+ expect(fromHevc.videoBitrate).toBeGreaterThan(mbps(1));
147
+ expect(fromAv1.videoBitrate).toBeGreaterThan(fromHevc.videoBitrate!);
148
+ });
149
+
150
+ it("scales the source cap down along with a downscale", () => {
151
+ // 4K at 40 Mbps → 1080p. The source cap must account for the quarter
152
+ // pixel count, not wave 40 Mbps of 4K bitrate through unchanged.
153
+ const plan = planVideoProcessing(
154
+ {
155
+ width: 3840,
156
+ height: 2160,
157
+ videoCodec: "hevc",
158
+ videoBitrate: mbps(40),
159
+ },
160
+ OPTS,
161
+ );
162
+ expect(plan.videoBitrate).toBeLessThan(mbps(9));
163
+ });
164
+
165
+ it("falls back to the size target when the source bitrate is unknown", () => {
166
+ const plan = planVideoProcessing(
167
+ { width: 1920, height: 1080, videoCodec: "hevc" },
168
+ OPTS,
169
+ );
170
+ expect(plan.videoBitrate).toBeGreaterThan(mbps(7));
171
+ });
172
+
173
+ it("leaves the bitrate unset when the track is copied", () => {
174
+ const plan = planVideoProcessing(
175
+ { width: 1280, height: 720, videoCodec: "avc", videoBitrate: mbps(3) },
176
+ OPTS,
177
+ );
178
+ expect(plan.videoBitrate).toBeUndefined();
179
+ });
180
+ });
181
+ });
@@ -1,23 +1,17 @@
1
1
  /**
2
2
  * Client-side Audio Processor
3
3
  *
4
- * Transcodes audio files to AAC in an M4A container (MP4 audio-only)
5
- * using mediabunny. Mirrors the video-processor pattern but discards
6
- * any video track and skips poster/blurhash extraction.
4
+ * Remuxes audio files into an M4A container (MP4 audio-only) using
5
+ * mediabunny, transcoding to AAC only when the source is not already AAC.
6
+ * A source that needs nothing has its encoded packets copied across, so it
7
+ * arrives without a generation of re-encoding loss. Mirrors the
8
+ * video-processor pattern but discards any video track and skips
9
+ * poster/blurhash extraction.
7
10
  *
8
11
  * Requires WebCodecs API support — check `isSupported()` before use.
9
12
  */
10
13
 
11
- import {
12
- Input,
13
- Output,
14
- Mp4OutputFormat,
15
- BufferTarget,
16
- BlobSource,
17
- Conversion,
18
- QUALITY_HIGH,
19
- ALL_FORMATS,
20
- } from "mediabunny";
14
+ import { loadMediabunny } from "./mediabunny.js";
21
15
 
22
16
  export interface AudioProcessResult {
23
17
  file: File;
@@ -33,7 +27,8 @@ function isSupported(): boolean {
33
27
  }
34
28
 
35
29
  /**
36
- * Process an audio file: transcode to AAC in an M4A (MP4) container.
30
+ * Process an audio file: remux into an M4A (MP4) container, transcoding to
31
+ * AAC only when the source is not already AAC.
37
32
  *
38
33
  * @param file - Source audio file
39
34
  * @param onProgress - Optional callback receiving progress from 0 to 1
@@ -43,6 +38,15 @@ async function processToFile(
43
38
  file: File,
44
39
  onProgress?: (progress: number) => void,
45
40
  ): Promise<AudioProcessResult> {
41
+ const {
42
+ Input,
43
+ Output,
44
+ Mp4OutputFormat,
45
+ BufferTarget,
46
+ BlobSource,
47
+ Conversion,
48
+ ALL_FORMATS,
49
+ } = await loadMediabunny();
46
50
  const input = new Input({
47
51
  source: new BlobSource(file),
48
52
  formats: ALL_FORMATS,
@@ -54,11 +58,16 @@ async function processToFile(
54
58
  });
55
59
 
56
60
  try {
61
+ // Setting a quality on the track is what disables mediabunny's copy fast
62
+ // path, and a needless decode/encode round trip costs a generation of
63
+ // quality. Leaving it unset lets an AAC source be copied outright;
64
+ // anything else falls back to mediabunny's default, which for AAC lands
65
+ // on 192 kbps — the highest rate it will pick at any quality level.
57
66
  const conversion = await Conversion.init({
58
67
  input,
59
68
  output,
60
69
  video: { discard: true },
61
- audio: { codec: "aac", bitrate: QUALITY_HIGH },
70
+ audio: { codec: "aac" },
62
71
  });
63
72
 
64
73
  if (onProgress) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Client-side Audio Waveform Extraction
3
+ *
4
+ * Decodes an audio file with the Web Audio API and samples the peaks the
5
+ * player draws. Image dimensions and blurhash come from `ImageProcessor`;
6
+ * video dimensions, poster, and blurhash come from `VideoProcessor`.
7
+ */
8
+
9
+ /**
10
+ * Extract waveform peak amplitudes from an audio file.
11
+ * Decodes via Web Audio API and returns a JSON string of ~100 normalized peak values (0–1).
12
+ *
13
+ * @param file - Audio file to extract peaks from
14
+ * @returns JSON string of peak values, e.g. "[0.2,0.8,0.5,...]"
15
+ */
16
+ export async function extractAudioWaveform(file: File): Promise<string> {
17
+ const buffer = await file.arrayBuffer();
18
+ const audioCtx = new AudioContext();
19
+
20
+ try {
21
+ const decoded = await audioCtx.decodeAudioData(buffer);
22
+ const raw = decoded.getChannelData(0);
23
+ const count = 100;
24
+ const step = Math.max(1, Math.floor(raw.length / count));
25
+ const peaks: number[] = new Array(count);
26
+
27
+ for (let i = 0; i < count; i++) {
28
+ let max = 0;
29
+ const start = i * step;
30
+ const end = Math.min(start + step, raw.length);
31
+ for (let j = start; j < end; j++) {
32
+ const v = Math.abs(raw[j]);
33
+ if (v > max) max = v;
34
+ }
35
+ peaks[i] = max;
36
+ }
37
+
38
+ let maxPeak = 0;
39
+ for (const p of peaks) if (p > maxPeak) maxPeak = p;
40
+ if (maxPeak > 0) {
41
+ for (let i = 0; i < count; i++)
42
+ peaks[i] = Math.round((peaks[i] / maxPeak) * 100) / 100;
43
+ }
44
+
45
+ return JSON.stringify(peaks);
46
+ } finally {
47
+ await audioCtx.close();
48
+ }
49
+ }