@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,5 +1,13 @@
1
- import { describe, expect, it } from "vitest";
2
- import { planImageProcessing } from "../image-processor.js";
1
+ // @vitest-environment happy-dom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ blurhashDimensions,
6
+ planImageProcessing,
7
+ resolveEncodedMimeType,
8
+ type ImageWorkerRequest,
9
+ type ImageWorkerResponse,
10
+ } from "../image-plan.js";
3
11
 
4
12
  const OPTS = { maxShortSide: 1920, maxLongSide: 8192 };
5
13
 
@@ -62,3 +70,317 @@ describe("planImageProcessing", () => {
62
70
  expect(planImageProcessing(1080, 8193, OPTS).passthrough).toBe(true);
63
71
  });
64
72
  });
73
+
74
+ describe("resolveEncodedMimeType", () => {
75
+ it("keeps the requested format when the encoder produced it", () => {
76
+ expect(resolveEncodedMimeType("image/webp", "image/webp")).toBe(
77
+ "image/webp",
78
+ );
79
+ expect(resolveEncodedMimeType("image/png", "image/png")).toBe("image/png");
80
+ });
81
+
82
+ it("switches to JPEG when the encoder silently fell back to PNG", () => {
83
+ expect(resolveEncodedMimeType("image/webp", "image/png")).toBe(
84
+ "image/jpeg",
85
+ );
86
+ });
87
+
88
+ it("switches to JPEG when the encoder produced nothing recognizable", () => {
89
+ expect(resolveEncodedMimeType("image/webp", "")).toBe("image/jpeg");
90
+ });
91
+ });
92
+
93
+ describe("blurhashDimensions", () => {
94
+ it("scales the longest side down to 32px", () => {
95
+ expect(blurhashDimensions(4032, 3024)).toEqual({ width: 32, height: 24 });
96
+ expect(blurhashDimensions(3024, 4032)).toEqual({ width: 24, height: 32 });
97
+ });
98
+
99
+ it("never scales up and never drops below one pixel", () => {
100
+ expect(blurhashDimensions(10, 10)).toEqual({ width: 10, height: 10 });
101
+ expect(blurhashDimensions(100, 6000)).toEqual({ width: 1, height: 32 });
102
+ });
103
+ });
104
+
105
+ // ── The facade: worker first, page canvas as fallback ────────────────────
106
+
107
+ const { FakeWorker, createdWorkers, workerScript, workerSupport } = vi.hoisted(
108
+ () => {
109
+ type WorkerEvent = { data?: unknown; message?: string };
110
+ type Listener = (event: WorkerEvent) => void;
111
+
112
+ class FakeWorker {
113
+ posted: unknown[] = [];
114
+ terminated = false;
115
+ private listeners = new Map<string, Listener[]>();
116
+
117
+ addEventListener(type: string, listener: Listener) {
118
+ const list = this.listeners.get(type) ?? [];
119
+ list.push(listener);
120
+ this.listeners.set(type, list);
121
+ }
122
+
123
+ postMessage(request: unknown) {
124
+ this.posted.push(request);
125
+ const reply = workerScript.reply;
126
+ if (!reply) return;
127
+ queueMicrotask(() => this.emit("message", { data: reply(request) }));
128
+ }
129
+
130
+ terminate() {
131
+ this.terminated = true;
132
+ }
133
+
134
+ emit(type: string, event: WorkerEvent) {
135
+ for (const listener of this.listeners.get(type) ?? []) listener(event);
136
+ }
137
+ }
138
+
139
+ const workerScript: { reply: ((request: unknown) => unknown) | null } = {
140
+ reply: null,
141
+ };
142
+
143
+ return {
144
+ FakeWorker,
145
+ createdWorkers: [] as FakeWorker[],
146
+ workerScript,
147
+ workerSupport: { supported: true },
148
+ };
149
+ },
150
+ );
151
+
152
+ vi.mock("../image-worker-client.js", () => ({
153
+ supportsImageWorker: () => workerSupport.supported,
154
+ createImageWorker: () => {
155
+ const worker = new FakeWorker();
156
+ createdWorkers.push(worker);
157
+ return worker;
158
+ },
159
+ }));
160
+
161
+ const originalCreateObjectURL = URL.createObjectURL;
162
+ const originalRevokeObjectURL = URL.revokeObjectURL;
163
+ const originalImage = globalThis.Image;
164
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
165
+ const originalToBlob = HTMLCanvasElement.prototype.toBlob;
166
+
167
+ const imagesDecodedOnPage = vi.fn();
168
+
169
+ /** Stand in for `<img>` + `<canvas>` — the in-page path, WebP-capable. */
170
+ function installPageCanvasMocks() {
171
+ URL.createObjectURL = vi.fn(() => "blob:image-source");
172
+ URL.revokeObjectURL = vi.fn();
173
+
174
+ class MockImage {
175
+ width = 4032;
176
+ height = 3024;
177
+ onload: null | (() => void) = null;
178
+ onerror: null | (() => void) = null;
179
+ private _src = "";
180
+
181
+ constructor() {
182
+ imagesDecodedOnPage();
183
+ }
184
+
185
+ set src(value: string) {
186
+ this._src = value;
187
+ queueMicrotask(() => this.onload?.());
188
+ }
189
+
190
+ get src() {
191
+ return this._src;
192
+ }
193
+ }
194
+ globalThis.Image = MockImage as unknown as typeof Image;
195
+
196
+ HTMLCanvasElement.prototype.getContext = vi.fn(function (
197
+ this: HTMLCanvasElement,
198
+ ) {
199
+ return {
200
+ drawImage: vi.fn(),
201
+ getImageData: (_x: number, _y: number, w: number, h: number) => ({
202
+ data: new Uint8ClampedArray(w * h * 4),
203
+ }),
204
+ };
205
+ }) as unknown as typeof HTMLCanvasElement.prototype.getContext;
206
+ HTMLCanvasElement.prototype.toBlob = vi.fn(function (
207
+ callback: BlobCallback,
208
+ type?: string,
209
+ ) {
210
+ callback(new Blob(["encoded"], { type: type ?? "image/png" }));
211
+ }) as typeof HTMLCanvasElement.prototype.toBlob;
212
+ }
213
+
214
+ function restorePageCanvasMocks() {
215
+ URL.createObjectURL = originalCreateObjectURL;
216
+ URL.revokeObjectURL = originalRevokeObjectURL;
217
+ globalThis.Image = originalImage;
218
+ HTMLCanvasElement.prototype.getContext = originalGetContext;
219
+ HTMLCanvasElement.prototype.toBlob = originalToBlob;
220
+ }
221
+
222
+ function asRequest(posted: unknown): ImageWorkerRequest {
223
+ return posted as ImageWorkerRequest;
224
+ }
225
+
226
+ function workerResult(request: unknown): ImageWorkerResponse {
227
+ return {
228
+ id: asRequest(request).id,
229
+ ok: true,
230
+ blob: new Blob(["from-worker"], { type: "image/webp" }),
231
+ width: 2560,
232
+ height: 1920,
233
+ processed: true,
234
+ blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
235
+ };
236
+ }
237
+
238
+ async function loadProcessor() {
239
+ const { ImageProcessor } = await import("../image-processor.js");
240
+ return ImageProcessor;
241
+ }
242
+
243
+ describe("ImageProcessor", () => {
244
+ const photo = new File(["raw"], "photo.jpg", { type: "image/jpeg" });
245
+
246
+ beforeEach(() => {
247
+ vi.resetModules();
248
+ createdWorkers.length = 0;
249
+ workerScript.reply = workerResult;
250
+ workerSupport.supported = true;
251
+ imagesDecodedOnPage.mockClear();
252
+ installPageCanvasMocks();
253
+ });
254
+
255
+ afterEach(() => {
256
+ restorePageCanvasMocks();
257
+ });
258
+
259
+ it("processes in the worker and never decodes on the page", async () => {
260
+ const processor = await loadProcessor();
261
+
262
+ const result = await processor.processToFile(photo);
263
+
264
+ expect(createdWorkers).toHaveLength(1);
265
+ const request = asRequest(createdWorkers[0].posted[0]);
266
+ expect(request.file).toBe(photo);
267
+ expect(request.options).toMatchObject({
268
+ maxShortSide: 1920,
269
+ maxLongSide: 8192,
270
+ mimeType: "image/webp",
271
+ });
272
+ expect(result.file.name).toBe("photo.webp");
273
+ expect(result.file.type).toBe("image/webp");
274
+ expect(result).toMatchObject({
275
+ width: 2560,
276
+ height: 1920,
277
+ blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
278
+ });
279
+ expect(imagesDecodedOnPage).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it("keeps one worker warm across images", async () => {
283
+ const processor = await loadProcessor();
284
+
285
+ await processor.processToFile(photo);
286
+ await processor.processToFile(photo);
287
+
288
+ expect(createdWorkers).toHaveLength(1);
289
+ expect(createdWorkers[0].posted).toHaveLength(2);
290
+ });
291
+
292
+ it("hands the original through when the worker says it is too large", async () => {
293
+ workerScript.reply = (request) => ({
294
+ id: asRequest(request).id,
295
+ ok: true,
296
+ blob: photo,
297
+ width: 1080,
298
+ height: 12000,
299
+ processed: false,
300
+ blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
301
+ });
302
+ const processor = await loadProcessor();
303
+
304
+ const result = await processor.processToFile(photo);
305
+
306
+ expect(result.file).toBe(photo);
307
+ expect(result).toMatchObject({ width: 1080, height: 12000 });
308
+ });
309
+
310
+ it("falls back to the page canvas for a file the worker cannot decode", async () => {
311
+ workerScript.reply = (request) => ({
312
+ id: asRequest(request).id,
313
+ ok: false,
314
+ error: "The source image could not be decoded.",
315
+ });
316
+ const processor = await loadProcessor();
317
+
318
+ const result = await processor.processToFile(photo);
319
+
320
+ expect(imagesDecodedOnPage).toHaveBeenCalledTimes(1);
321
+ expect(result.file.name).toBe("photo.webp");
322
+ expect(result.file.type).toBe("image/webp");
323
+ expect(result).toMatchObject({ width: 2560, height: 1920 });
324
+ expect(typeof result.blurhash).toBe("string");
325
+ // The worker is still fine — one bad file does not cost it.
326
+ expect(createdWorkers[0].terminated).toBe(false);
327
+ });
328
+
329
+ it("fails in-flight images over to the page when the worker dies, then starts a new one", async () => {
330
+ workerScript.reply = null;
331
+ const processor = await loadProcessor();
332
+
333
+ const inFlight = processor.processToFile(photo);
334
+ await Promise.resolve();
335
+ createdWorkers[0].emit("error", { message: "worker crashed" });
336
+ const result = await inFlight;
337
+
338
+ expect(result.file.type).toBe("image/webp");
339
+ expect(imagesDecodedOnPage).toHaveBeenCalledTimes(1);
340
+ expect(createdWorkers[0].terminated).toBe(true);
341
+
342
+ workerScript.reply = workerResult;
343
+ await processor.processToFile(photo);
344
+ expect(createdWorkers).toHaveLength(2);
345
+ });
346
+
347
+ it("uses the page canvas where the browser has no OffscreenCanvas", async () => {
348
+ workerSupport.supported = false;
349
+ const processor = await loadProcessor();
350
+
351
+ const result = await processor.processToFile(photo);
352
+
353
+ expect(createdWorkers).toHaveLength(0);
354
+ expect(imagesDecodedOnPage).toHaveBeenCalledTimes(1);
355
+ expect(result.file.type).toBe("image/webp");
356
+ });
357
+
358
+ it("probes the encoder once and encodes each image once on the page", async () => {
359
+ workerSupport.supported = false;
360
+ const processor = await loadProcessor();
361
+
362
+ await processor.processToFile(photo);
363
+ await processor.processToFile(photo);
364
+
365
+ // One 1×1 probe, then one real encode per image — never PNG-then-JPEG.
366
+ expect(HTMLCanvasElement.prototype.toBlob).toHaveBeenCalledTimes(3);
367
+ });
368
+
369
+ it("encodes JPEG on the page when the browser cannot encode WebP", async () => {
370
+ workerSupport.supported = false;
371
+ HTMLCanvasElement.prototype.toBlob = vi.fn(function (
372
+ callback: BlobCallback,
373
+ type?: string,
374
+ ) {
375
+ // Safari: asked for WebP, silently hands back PNG.
376
+ const produced = type === "image/webp" ? "image/png" : type;
377
+ callback(new Blob(["encoded"], { type: produced ?? "image/png" }));
378
+ }) as typeof HTMLCanvasElement.prototype.toBlob;
379
+ const processor = await loadProcessor();
380
+
381
+ const result = await processor.processToFile(photo);
382
+
383
+ expect(result.file.type).toBe("image/jpeg");
384
+ expect(result.file.name).toBe("photo.jpg");
385
+ });
386
+ });
@@ -0,0 +1,92 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const loaded = {
6
+ compose: vi.fn(),
7
+ settings: vi.fn(),
8
+ manage: vi.fn(),
9
+ };
10
+
11
+ /**
12
+ * One turn of the event loop — enough for the observer to see a mutation, and
13
+ * all a negative assertion needs. A load that is expected to happen is waited
14
+ * for with `vi.waitFor` instead: a dynamic import can take more than a tick
15
+ * when the whole suite is running.
16
+ */
17
+ async function settle(): Promise<void> {
18
+ await new Promise((resolve) => setTimeout(resolve, 0));
19
+ }
20
+
21
+ /**
22
+ * A fresh module per test: the loaders remember what they have loaded, and a
23
+ * mock's factory runs once per module registry, so both are re-registered.
24
+ */
25
+ async function freshLazyEntries() {
26
+ vi.resetModules();
27
+ vi.doMock("../../client-compose.js", () => {
28
+ loaded.compose();
29
+ return {};
30
+ });
31
+ vi.doMock("../../client-settings.js", () => {
32
+ loaded.settings();
33
+ return {};
34
+ });
35
+ vi.doMock("../../client-manage.js", () => {
36
+ loaded.manage();
37
+ return {};
38
+ });
39
+ return import("../lazy-entries.js");
40
+ }
41
+
42
+ describe("lazy entries", () => {
43
+ beforeEach(() => {
44
+ document.body.innerHTML = "";
45
+ loaded.compose.mockClear();
46
+ loaded.settings.mockClear();
47
+ loaded.manage.mockClear();
48
+ });
49
+
50
+ it("loads an entry once however often it is asked for", async () => {
51
+ const { ensureCompose } = await freshLazyEntries();
52
+
53
+ await Promise.all([ensureCompose(), ensureCompose()]);
54
+ await ensureCompose();
55
+
56
+ expect(loaded.compose).toHaveBeenCalledTimes(1);
57
+ });
58
+
59
+ it("loads the entries whose markup is on the page, and only those", async () => {
60
+ document.body.innerHTML = `
61
+ <jant-compose-dialog></jant-compose-dialog>
62
+ <jant-settings-general></jant-settings-general>`;
63
+
64
+ const { loadEntriesForPage } = await freshLazyEntries();
65
+ loadEntriesForPage();
66
+
67
+ await vi.waitFor(() => expect(loaded.settings).toHaveBeenCalledTimes(1));
68
+ expect(loaded.manage).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("loads the composer at once on the compose page", async () => {
72
+ document.body.innerHTML = `<section data-page="compose"></section>`;
73
+
74
+ const { loadEntriesForPage } = await freshLazyEntries();
75
+ loadEntriesForPage();
76
+
77
+ await vi.waitFor(() => expect(loaded.compose).toHaveBeenCalledTimes(1));
78
+ });
79
+
80
+ it("keeps watching for markup that arrives after load", async () => {
81
+ const { loadEntriesForPage } = await freshLazyEntries();
82
+ loadEntriesForPage();
83
+ await settle();
84
+ expect(loaded.manage).not.toHaveBeenCalled();
85
+
86
+ const hook = document.createElement("div");
87
+ hook.setAttribute("data-collection-page-actions", "");
88
+ document.body.appendChild(hook);
89
+
90
+ await vi.waitFor(() => expect(loaded.manage).toHaveBeenCalledTimes(1));
91
+ });
92
+ });
@@ -0,0 +1,155 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { __testOnly } from "../media-scroll-hint.js";
5
+
6
+ class TestResizeObserver {
7
+ static instances: TestResizeObserver[] = [];
8
+ readonly observed = new Set<globalThis.Element>();
9
+
10
+ constructor(private readonly callback: () => void) {
11
+ TestResizeObserver.instances.push(this);
12
+ }
13
+
14
+ observe(target: globalThis.Element): void {
15
+ this.observed.add(target);
16
+ }
17
+
18
+ unobserve(target: globalThis.Element): void {
19
+ this.observed.delete(target);
20
+ }
21
+
22
+ disconnect(): void {
23
+ this.observed.clear();
24
+ }
25
+
26
+ trigger(): void {
27
+ this.callback();
28
+ }
29
+ }
30
+
31
+ /** happy-dom reports every layout box as zero, so stub the metrics we read. */
32
+ function setMetrics(
33
+ el: HTMLElement,
34
+ metrics: { clientWidth: number; scrollWidth: number; scrollLeft?: number },
35
+ ): void {
36
+ Object.defineProperty(el, "clientWidth", {
37
+ value: metrics.clientWidth,
38
+ configurable: true,
39
+ });
40
+ Object.defineProperty(el, "scrollWidth", {
41
+ value: metrics.scrollWidth,
42
+ configurable: true,
43
+ });
44
+ Object.defineProperty(el, "scrollLeft", {
45
+ value: metrics.scrollLeft ?? 0,
46
+ configurable: true,
47
+ writable: true,
48
+ });
49
+ }
50
+
51
+ function renderGallery(): { wrap: HTMLElement; scroller: HTMLElement } {
52
+ document.body.innerHTML = `
53
+ <div class="media-gallery-scroll-wrap">
54
+ <div data-post-media tabindex="0">
55
+ <a class="media-visual-frame"></a>
56
+ <a class="media-visual-frame"></a>
57
+ </div>
58
+ <button class="media-gallery-nav media-gallery-nav-prev"></button>
59
+ <button class="media-gallery-nav media-gallery-nav-next"></button>
60
+ </div>
61
+ `;
62
+
63
+ const wrap = document.querySelector<HTMLElement>(
64
+ ".media-gallery-scroll-wrap",
65
+ );
66
+ const scroller = document.querySelector<HTMLElement>("[data-post-media]");
67
+ if (!wrap || !scroller) throw new Error("Expected a gallery in the document");
68
+ return { wrap, scroller };
69
+ }
70
+
71
+ /** Let happy-dom deliver its (microtask-scheduled) MutationObserver records. */
72
+ async function flushMutations(): Promise<void> {
73
+ await Promise.resolve();
74
+ await Promise.resolve();
75
+ }
76
+
77
+ describe("media scroll hint", () => {
78
+ beforeEach(() => {
79
+ TestResizeObserver.instances = [];
80
+ vi.stubGlobal("ResizeObserver", TestResizeObserver);
81
+ vi.stubGlobal(
82
+ "requestAnimationFrame",
83
+ (cb: globalThis.FrameRequestCallback) => {
84
+ cb(0);
85
+ return 0;
86
+ },
87
+ );
88
+ vi.stubGlobal("cancelAnimationFrame", () => {});
89
+ });
90
+
91
+ afterEach(() => {
92
+ vi.unstubAllGlobals();
93
+ document.body.innerHTML = "";
94
+ });
95
+
96
+ it("marks the end edge while the strip overflows", () => {
97
+ const { wrap, scroller } = renderGallery();
98
+ setMetrics(scroller, { clientWidth: 400, scrollWidth: 760 });
99
+
100
+ __testOnly.initWrap(wrap);
101
+
102
+ expect(wrap.classList.contains("can-scroll-end")).toBe(true);
103
+ expect(wrap.classList.contains("can-scroll-start")).toBe(false);
104
+ });
105
+
106
+ it("clears the end edge once the strip grows wide enough to fit", () => {
107
+ const { wrap, scroller } = renderGallery();
108
+ setMetrics(scroller, { clientWidth: 400, scrollWidth: 760 });
109
+
110
+ __testOnly.initWrap(wrap);
111
+ expect(wrap.classList.contains("can-scroll-end")).toBe(true);
112
+
113
+ // A wider window (or a late-settling layout). Nothing overflows any more,
114
+ // so no scroll event can fire — only the resize observer can correct it.
115
+ setMetrics(scroller, { clientWidth: 760, scrollWidth: 760 });
116
+ TestResizeObserver.instances[0]?.trigger();
117
+
118
+ expect(wrap.classList.contains("can-scroll-end")).toBe(false);
119
+ });
120
+
121
+ it("observes the strip and each of its items", () => {
122
+ const { wrap, scroller } = renderGallery();
123
+ setMetrics(scroller, { clientWidth: 760, scrollWidth: 760 });
124
+
125
+ __testOnly.initWrap(wrap);
126
+
127
+ const observer = TestResizeObserver.instances[0];
128
+ expect(observer?.observed.has(scroller)).toBe(true);
129
+ for (const item of scroller.children) {
130
+ expect(observer?.observed.has(item)).toBe(true);
131
+ }
132
+ });
133
+
134
+ it("re-observes items swapped in after a morph", async () => {
135
+ const { wrap, scroller } = renderGallery();
136
+ setMetrics(scroller, { clientWidth: 760, scrollWidth: 760 });
137
+
138
+ __testOnly.initWrap(wrap);
139
+ expect(wrap.classList.contains("can-scroll-end")).toBe(false);
140
+
141
+ scroller.innerHTML = `
142
+ <a class="media-visual-frame"></a>
143
+ <a class="media-visual-frame"></a>
144
+ <a class="media-visual-frame"></a>
145
+ `;
146
+ setMetrics(scroller, { clientWidth: 760, scrollWidth: 1140 });
147
+ await flushMutations();
148
+
149
+ const observer = TestResizeObserver.instances[0];
150
+ for (const item of scroller.children) {
151
+ expect(observer?.observed.has(item)).toBe(true);
152
+ }
153
+ expect(wrap.classList.contains("can-scroll-end")).toBe(true);
154
+ });
155
+ });
@@ -0,0 +1,84 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * `publicPath` vs `viewPath` — the client-side half of the language-view rule.
5
+ *
6
+ * The distinction is the same one the server draws with `toPublicPath` and
7
+ * `toViewPath`: one address site-wide, or one address per language view.
8
+ */
9
+
10
+ import { afterEach, describe, expect, it } from "vitest";
11
+ import { navPath, publicPath, viewPath } from "../runtime-paths.js";
12
+
13
+ afterEach(() => {
14
+ delete document.documentElement.dataset.sitePathPrefix;
15
+ delete document.documentElement.dataset.viewBasePath;
16
+ });
17
+
18
+ describe("viewPath", () => {
19
+ it("carries the language prefix of the view being rendered", () => {
20
+ document.documentElement.dataset.viewBasePath = "/en";
21
+ expect(viewPath("/collections")).toBe("/en/collections");
22
+ expect(viewPath("/recipes")).toBe("/en/recipes");
23
+ });
24
+
25
+ it("leaves publicPath alone, so site-wide addresses stay site-wide", () => {
26
+ document.documentElement.dataset.viewBasePath = "/en";
27
+ expect(publicPath("/collections")).toBe("/collections");
28
+ });
29
+
30
+ it("carries the site path prefix under it", () => {
31
+ document.documentElement.dataset.sitePathPrefix = "/blog";
32
+ document.documentElement.dataset.viewBasePath = "/blog/en";
33
+ expect(viewPath("/collections")).toBe("/blog/en/collections");
34
+ expect(publicPath("/collections")).toBe("/blog/collections");
35
+ });
36
+
37
+ it("falls back to the site path prefix when no view base is published", () => {
38
+ // Pages rendered before the attribute existed, and any surface with no
39
+ // language view: both must keep working as plain public paths.
40
+ document.documentElement.dataset.sitePathPrefix = "/blog";
41
+ expect(viewPath("/collections")).toBe("/blog/collections");
42
+ });
43
+
44
+ it("is a no-op at the root of a single-language site", () => {
45
+ document.documentElement.dataset.viewBasePath = "";
46
+ expect(viewPath("/collections")).toBe("/collections");
47
+ });
48
+
49
+ it("prefixes a root-namespace target the caller vouched for", () => {
50
+ // A collection page lives at /{slug}, alongside post permalinks. Only the
51
+ // call site can tell them apart, so viewPath takes its word for it.
52
+ document.documentElement.dataset.viewBasePath = "/en";
53
+ expect(viewPath("/recipes")).toBe("/en/recipes");
54
+ });
55
+ });
56
+
57
+ describe("navPath", () => {
58
+ afterEach(() => {
59
+ delete document.documentElement.dataset.viewBasePath;
60
+ });
61
+
62
+ it("keeps per-language surfaces in the reader's view", () => {
63
+ document.documentElement.dataset.viewBasePath = "/en";
64
+ expect(navPath("/archive?media=any")).toBe("/en/archive?media=any");
65
+ expect(navPath("/search?q=hello")).toBe("/en/search?q=hello");
66
+ expect(navPath("/collections")).toBe("/en/collections");
67
+ expect(navPath("/collections/a+b")).toBe("/en/collections/a+b");
68
+ });
69
+
70
+ it("leaves site-wide addresses alone, where a prefix would 404", () => {
71
+ document.documentElement.dataset.viewBasePath = "/en";
72
+ expect(navPath("/settings")).toBe("/settings");
73
+ expect(navPath("/settings/language")).toBe("/settings/language");
74
+ expect(navPath("/collections/recipes/edit")).toBe(
75
+ "/collections/recipes/edit",
76
+ );
77
+ });
78
+
79
+ it("leaves the root namespace alone, since a post has one address", () => {
80
+ document.documentElement.dataset.viewBasePath = "/en";
81
+ expect(navPath("/my-post")).toBe("/my-post");
82
+ expect(navPath("/preview/my-post?edit=1")).toBe("/preview/my-post?edit=1");
83
+ });
84
+ });