@jant/core 0.6.16 → 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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -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-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  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-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  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__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. 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
+ });