@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
@@ -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
+ });