@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
@@ -0,0 +1,159 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * The compose card and the uploaded media used to draw their poster from two
5
+ * different places: the card from a quick `<video>` frame grab, the upload
6
+ * from the mediabunny probe. When the probe came back without one, the card
7
+ * showed a thumbnail the published post did not have. The quick frame is now
8
+ * the upload's fallback, so the two cannot disagree.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, it, vi } from "vitest";
12
+
13
+ const processToFile = vi.fn();
14
+ const uploadViaSession = vi.fn();
15
+
16
+ vi.mock("../video-processor.js", () => ({
17
+ VideoProcessor: {
18
+ isSupported: () => true,
19
+ processToFile: (...args: unknown[]) => processToFile(...args),
20
+ },
21
+ }));
22
+
23
+ vi.mock("../upload-session.js", () => ({
24
+ uploadViaSession: (...args: unknown[]) => uploadViaSession(...args),
25
+ }));
26
+
27
+ await import("../compose-bridge.js");
28
+
29
+ const QUICK_POSTER = new Blob(["quick"], { type: "image/webp" });
30
+ const PROBE_POSTER = new Blob(["probe"], { type: "image/webp" });
31
+
32
+ /** Stand in for the `<video>` element `captureQuickPoster` grabs a frame from. */
33
+ function stubFrameCapture(options: { decodes: boolean }) {
34
+ vi.spyOn(document, "createElement").mockImplementation(((tag: string) => {
35
+ if (tag === "canvas") {
36
+ return {
37
+ width: 0,
38
+ height: 0,
39
+ getContext: () => ({ drawImage: () => {} }),
40
+ toBlob: (cb: (blob: Blob | null) => void) => cb(QUICK_POSTER),
41
+ } as unknown as HTMLElement;
42
+ }
43
+ const video = {
44
+ muted: false,
45
+ playsInline: false,
46
+ preload: "",
47
+ videoWidth: 1280,
48
+ videoHeight: 720,
49
+ onloadeddata: null as null | (() => void),
50
+ onerror: null as null | (() => void),
51
+ removeAttribute: () => {},
52
+ load: () => {},
53
+ set src(_value: string) {
54
+ queueMicrotask(() =>
55
+ options.decodes ? this.onloadeddata?.() : this.onerror?.(),
56
+ );
57
+ },
58
+ };
59
+ return video as unknown as HTMLElement;
60
+ }) as typeof document.createElement);
61
+ }
62
+
63
+ function selectVideoFile() {
64
+ document.dispatchEvent(
65
+ new CustomEvent("jant:files-selected", {
66
+ detail: {
67
+ files: [
68
+ {
69
+ file: new File(["source"], "clip.mov", {
70
+ type: "video/quicktime",
71
+ }),
72
+ clientId: "attachment-1",
73
+ },
74
+ ],
75
+ },
76
+ }),
77
+ );
78
+ }
79
+
80
+ async function settle() {
81
+ for (let i = 0; i < 6; i++) {
82
+ await new Promise((resolve) => setTimeout(resolve, 0));
83
+ }
84
+ }
85
+
86
+ function uploadedPoster(): Blob | undefined {
87
+ const metadata = uploadViaSession.mock.calls[0]?.[1] as
88
+ { poster?: Blob } | undefined;
89
+ return metadata?.poster;
90
+ }
91
+
92
+ describe("compose bridge video poster", () => {
93
+ beforeEach(() => {
94
+ document.body.innerHTML = "";
95
+ vi.restoreAllMocks();
96
+ processToFile.mockReset();
97
+ uploadViaSession.mockReset();
98
+ uploadViaSession.mockResolvedValue({
99
+ id: "med_1",
100
+ filename: "clip.mp4",
101
+ url: "/media/clip.mp4",
102
+ mimeType: "video/mp4",
103
+ size: 10,
104
+ });
105
+ vi.stubGlobal("URL", {
106
+ createObjectURL: () => "blob:test",
107
+ revokeObjectURL: () => {},
108
+ });
109
+ });
110
+
111
+ it("uploads the probe's poster when it has one", async () => {
112
+ stubFrameCapture({ decodes: true });
113
+ processToFile.mockResolvedValue({
114
+ file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
115
+ width: 1280,
116
+ height: 720,
117
+ poster: PROBE_POSTER,
118
+ blurhash: "LGK",
119
+ });
120
+
121
+ selectVideoFile();
122
+ await settle();
123
+
124
+ expect(uploadedPoster()).toBe(PROBE_POSTER);
125
+ });
126
+
127
+ it("falls back to the quick frame when the probe found no poster", async () => {
128
+ stubFrameCapture({ decodes: true });
129
+ processToFile.mockResolvedValue({
130
+ file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
131
+ width: 1280,
132
+ height: 720,
133
+ poster: undefined,
134
+ blurhash: undefined,
135
+ });
136
+
137
+ selectVideoFile();
138
+ await settle();
139
+
140
+ expect(uploadViaSession).toHaveBeenCalledTimes(1);
141
+ expect(uploadedPoster()).toBe(QUICK_POSTER);
142
+ });
143
+
144
+ it("uploads without a poster when neither path produced one", async () => {
145
+ stubFrameCapture({ decodes: false });
146
+ processToFile.mockResolvedValue({
147
+ file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
148
+ width: 1280,
149
+ height: 720,
150
+ poster: undefined,
151
+ });
152
+
153
+ selectVideoFile();
154
+ await settle();
155
+
156
+ expect(uploadViaSession).toHaveBeenCalledTimes(1);
157
+ expect(uploadedPoster()).toBeUndefined();
158
+ });
159
+ });
@@ -43,6 +43,46 @@ async function flushBridgeWork(times = 4) {
43
43
  }
44
44
  }
45
45
 
46
+ /** A minimal published-post detail, as the composer dispatches it. */
47
+ function singlePostDetail(overrides: Record<string, unknown> = {}) {
48
+ return {
49
+ format: "quote",
50
+ title: "",
51
+ body: '{"type":"doc","content":[]}',
52
+ url: "",
53
+ quoteText: "A borrowed sentence.",
54
+ quoteAuthor: "",
55
+ slug: "",
56
+ status: "published",
57
+ visibility: "public",
58
+ rating: 0,
59
+ collectionIds: [],
60
+ attachments: [],
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ /** The same, as a two-quote thread. */
66
+ function threadSubmitDetail(overrides: Record<string, unknown> = {}) {
67
+ const first = singlePostDetail();
68
+ return {
69
+ ...first,
70
+ pendingAttachments: [],
71
+ threadPosts: [first, singlePostDetail({ quoteText: "And its answer." })],
72
+ ...overrides,
73
+ };
74
+ }
75
+
76
+ /** Text of the compose progress toast, which the result message takes over. */
77
+ function progressToastText(): string {
78
+ return (
79
+ document
80
+ .getElementById("toast-compose-deferred")
81
+ ?.querySelector("span")
82
+ ?.textContent?.trim() ?? ""
83
+ );
84
+ }
85
+
46
86
  function renderPostView(postId: string) {
47
87
  const postView = document.createElement("div");
48
88
  postView.dataset.postView = "";
@@ -968,4 +1008,236 @@ describe("compose bridge", () => {
968
1008
 
969
1009
  expect(assignSpy).toHaveBeenCalledWith("/blog/published-post");
970
1010
  });
1011
+
1012
+ it("saves a thread as a draft when the connection drops, not just when the server refuses", async () => {
1013
+ // A publish that never reaches the server and one the server rejects are
1014
+ // the same event to the author: the post did not go up. Both earn the
1015
+ // save-as-draft attempt.
1016
+ const composeEl = document.createElement(
1017
+ "jant-compose-dialog",
1018
+ ) as ComposeHarness;
1019
+ composeEl.refreshCollections = vi.fn(async () => true);
1020
+ composeEl.pageMode = false;
1021
+ composeEl.openNew = vi.fn(async () => {});
1022
+ composeEl.clearLocalDraftFromStorage = vi.fn();
1023
+ composeEl.labels = {
1024
+ publishFailedDraft: "Couldn't publish. Saved as draft.",
1025
+ };
1026
+ document.body.appendChild(composeEl);
1027
+
1028
+ const bodies: string[] = [];
1029
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
1030
+ const raw =
1031
+ typeof input === "string"
1032
+ ? input
1033
+ : input instanceof URL
1034
+ ? input.toString()
1035
+ : input.url;
1036
+ if (new URL(raw, "http://localhost").pathname !== "/compose/thread") {
1037
+ throw new Error(`Unexpected fetch: ${raw}`);
1038
+ }
1039
+ const body = String(init?.body ?? "");
1040
+ bodies.push(body);
1041
+ // The publish attempt never completes; the draft attempt gets through.
1042
+ if (bodies.length === 1) throw new TypeError("Failed to fetch");
1043
+ return new Response(JSON.stringify({ status: "draft" }), {
1044
+ status: 200,
1045
+ headers: { "Content-Type": "application/json" },
1046
+ });
1047
+ });
1048
+
1049
+ composeEl.dispatchEvent(
1050
+ new CustomEvent("jant:compose-submit-deferred", {
1051
+ bubbles: true,
1052
+ detail: threadSubmitDetail(),
1053
+ }),
1054
+ );
1055
+ await flushBridgeWork();
1056
+
1057
+ expect(bodies).toHaveLength(2);
1058
+ expect(JSON.parse(bodies[1]).posts[0].status).toBe("draft");
1059
+ // The work reached the server, so the local copy is no longer the only one.
1060
+ expect(composeEl.clearLocalDraftFromStorage).toHaveBeenCalled();
1061
+ expect(composeEl.openNew).not.toHaveBeenCalled();
1062
+ });
1063
+
1064
+ it("reopens the composer with its local draft when the connection stays down", async () => {
1065
+ const composeEl = document.createElement(
1066
+ "jant-compose-dialog",
1067
+ ) as ComposeHarness;
1068
+ composeEl.refreshCollections = vi.fn(async () => true);
1069
+ composeEl.pageMode = false;
1070
+ composeEl.openNew = vi.fn(async () => {});
1071
+ composeEl.clearLocalDraftFromStorage = vi.fn();
1072
+ document.body.appendChild(composeEl);
1073
+
1074
+ let calls = 0;
1075
+ vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
1076
+ calls += 1;
1077
+ throw new TypeError("Failed to fetch");
1078
+ });
1079
+
1080
+ composeEl.dispatchEvent(
1081
+ new CustomEvent("jant:compose-submit-deferred", {
1082
+ bubbles: true,
1083
+ detail: threadSubmitDetail(),
1084
+ }),
1085
+ );
1086
+ await flushBridgeWork();
1087
+
1088
+ // Publish, then the draft attempt — both dropped.
1089
+ expect(calls).toBe(2);
1090
+ expect(composeEl.openNew).toHaveBeenCalledWith({
1091
+ restoreDraft: true,
1092
+ restoreToast: false,
1093
+ restoreMedia: [],
1094
+ });
1095
+ // Nothing reached the server, so the local copy must stay.
1096
+ expect(composeEl.clearLocalDraftFromStorage).not.toHaveBeenCalled();
1097
+ });
1098
+
1099
+ it("saves a single post as a draft when the connection drops", async () => {
1100
+ const composeEl = document.createElement(
1101
+ "jant-compose-dialog",
1102
+ ) as ComposeHarness;
1103
+ composeEl.refreshCollections = vi.fn(async () => true);
1104
+ composeEl.pageMode = false;
1105
+ composeEl.openNew = vi.fn(async () => {});
1106
+ composeEl.clearLocalDraftFromStorage = vi.fn();
1107
+ document.body.appendChild(composeEl);
1108
+
1109
+ const bodies: string[] = [];
1110
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
1111
+ const raw =
1112
+ typeof input === "string"
1113
+ ? input
1114
+ : input instanceof URL
1115
+ ? input.toString()
1116
+ : input.url;
1117
+ if (new URL(raw, "http://localhost").pathname !== "/compose") {
1118
+ throw new Error(`Unexpected fetch: ${raw}`);
1119
+ }
1120
+ bodies.push(String(init?.body ?? ""));
1121
+ if (bodies.length === 1) throw new TypeError("Failed to fetch");
1122
+ return new Response(JSON.stringify({ status: "draft" }), {
1123
+ status: 200,
1124
+ headers: { "Content-Type": "application/json" },
1125
+ });
1126
+ });
1127
+
1128
+ composeEl.dispatchEvent(
1129
+ new CustomEvent("jant:compose-submit-deferred", {
1130
+ bubbles: true,
1131
+ detail: { ...singlePostDetail(), pendingAttachments: [] },
1132
+ }),
1133
+ );
1134
+ await flushBridgeWork();
1135
+
1136
+ expect(bodies).toHaveLength(2);
1137
+ expect(JSON.parse(bodies[1]).status).toBe("draft");
1138
+ });
1139
+
1140
+ it("names the publish in a toast while the request is still running", async () => {
1141
+ const composeEl = document.createElement(
1142
+ "jant-compose-dialog",
1143
+ ) as ComposeHarness;
1144
+ composeEl.refreshCollections = vi.fn(async () => true);
1145
+ composeEl.pageMode = true;
1146
+ composeEl.reset = vi.fn();
1147
+ composeEl.updateComplete = Promise.resolve();
1148
+ document.body.appendChild(composeEl);
1149
+
1150
+ let release: (() => void) | undefined;
1151
+ const inFlight = new Promise<void>((resolve) => {
1152
+ release = resolve;
1153
+ });
1154
+
1155
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
1156
+ const raw =
1157
+ typeof input === "string"
1158
+ ? input
1159
+ : input instanceof URL
1160
+ ? input.toString()
1161
+ : input.url;
1162
+ if (new URL(raw, "http://localhost").pathname !== "/compose") {
1163
+ throw new Error(`Unexpected fetch: ${raw}`);
1164
+ }
1165
+ // A Link post waits on the server while its preview thumbnail is
1166
+ // fetched and stored — the toast has to survive that whole wait.
1167
+ await inFlight;
1168
+ return new Response(
1169
+ JSON.stringify({ status: "published", permalink: "/a-link" }),
1170
+ { status: 200, headers: { "Content-Type": "application/json" } },
1171
+ );
1172
+ });
1173
+
1174
+ composeEl.dispatchEvent(
1175
+ new CustomEvent("jant:compose-submit-deferred", {
1176
+ bubbles: true,
1177
+ detail: {
1178
+ ...singlePostDetail({
1179
+ format: "link",
1180
+ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
1181
+ quoteText: "",
1182
+ }),
1183
+ pendingAttachments: [],
1184
+ },
1185
+ }),
1186
+ );
1187
+
1188
+ await flushBridgeWork();
1189
+ expect(progressToastText()).toBe("Publishing...");
1190
+
1191
+ release?.();
1192
+ await flushBridgeWork();
1193
+ expect(progressToastText()).toBe("Published!");
1194
+ });
1195
+
1196
+ it("names a draft save while the request is still running", async () => {
1197
+ const composeEl = document.createElement(
1198
+ "jant-compose-dialog",
1199
+ ) as ComposeHarness;
1200
+ composeEl.refreshCollections = vi.fn(async () => true);
1201
+ composeEl.pageMode = false;
1202
+ document.body.appendChild(composeEl);
1203
+
1204
+ let release: (() => void) | undefined;
1205
+ const inFlight = new Promise<void>((resolve) => {
1206
+ release = resolve;
1207
+ });
1208
+
1209
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
1210
+ const raw =
1211
+ typeof input === "string"
1212
+ ? input
1213
+ : input instanceof URL
1214
+ ? input.toString()
1215
+ : input.url;
1216
+ if (new URL(raw, "http://localhost").pathname !== "/compose") {
1217
+ throw new Error(`Unexpected fetch: ${raw}`);
1218
+ }
1219
+ await inFlight;
1220
+ return new Response(
1221
+ JSON.stringify({ status: "draft", toast: "Draft saved." }),
1222
+ { status: 200, headers: { "Content-Type": "application/json" } },
1223
+ );
1224
+ });
1225
+
1226
+ composeEl.dispatchEvent(
1227
+ new CustomEvent("jant:compose-submit-deferred", {
1228
+ bubbles: true,
1229
+ detail: {
1230
+ ...singlePostDetail({ status: "draft" }),
1231
+ pendingAttachments: [],
1232
+ },
1233
+ }),
1234
+ );
1235
+
1236
+ await flushBridgeWork();
1237
+ expect(progressToastText()).toBe("Saving...");
1238
+
1239
+ release?.();
1240
+ await flushBridgeWork();
1241
+ expect(progressToastText()).toBe("Draft saved.");
1242
+ });
971
1243
  });
@@ -1,9 +1,18 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { afterEach, describe, expect, it } from "vitest";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const lazyEntries = vi.hoisted(() => ({
6
+ ensureCompose: vi.fn(async () => {}),
7
+ }));
8
+
9
+ vi.mock("../lazy-entries.js", () => lazyEntries);
10
+
4
11
  import {
12
+ ensureComposeDialog,
5
13
  getReplyRefreshTarget,
6
14
  getReplyTargetArticle,
15
+ readComposeDialogLabels,
7
16
  } from "../compose-launch.js";
8
17
 
9
18
  function getArticle(): HTMLElement {
@@ -167,3 +176,55 @@ describe("getReplyTargetArticle", () => {
167
176
  );
168
177
  });
169
178
  });
179
+
180
+ describe("ensureComposeDialog", () => {
181
+ afterEach(() => {
182
+ document.body.innerHTML = "";
183
+ lazyEntries.ensureCompose.mockClear();
184
+ });
185
+
186
+ it("loads the composer's bundle before handing the element back", async () => {
187
+ document.body.innerHTML = `<jant-compose-dialog></jant-compose-dialog>`;
188
+
189
+ const dialog = await ensureComposeDialog();
190
+
191
+ expect(lazyEntries.ensureCompose).toHaveBeenCalledTimes(1);
192
+ expect(dialog).toBe(document.querySelector("jant-compose-dialog"));
193
+ });
194
+
195
+ it("loads nothing on a page without a composer", async () => {
196
+ expect(await ensureComposeDialog()).toBeNull();
197
+ expect(lazyEntries.ensureCompose).not.toHaveBeenCalled();
198
+ });
199
+ });
200
+
201
+ describe("readComposeDialogLabels", () => {
202
+ afterEach(() => {
203
+ document.body.innerHTML = "";
204
+ });
205
+
206
+ it("reads the server-rendered attribute before the element upgrades", () => {
207
+ document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"Add Collection"}'></jant-compose-dialog>`;
208
+
209
+ expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
210
+ });
211
+
212
+ it("prefers the upgraded element's property", () => {
213
+ document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"stale"}'></jant-compose-dialog>`;
214
+ const dialog = document.querySelector(
215
+ "jant-compose-dialog",
216
+ ) as HTMLElement & {
217
+ labels?: unknown;
218
+ };
219
+ dialog.labels = { addCollection: "Add Collection" };
220
+
221
+ expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
222
+ });
223
+
224
+ it("returns null without a composer or with unreadable labels", () => {
225
+ expect(readComposeDialogLabels()).toBeNull();
226
+
227
+ document.body.innerHTML = `<jant-compose-dialog labels="{not json"></jant-compose-dialog>`;
228
+ expect(readComposeDialogLabels()).toBeNull();
229
+ });
230
+ });
@@ -1,6 +1,16 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ // The composer's bundle loads on first use; the harness element below stands
6
+ // in for the upgraded dialog, so loading is a no-op here.
7
+ vi.mock("../lazy-entries.js", () => ({
8
+ ensureCompose: async () => {},
9
+ ensureSettings: async () => {},
10
+ ensureManage: async () => {},
11
+ loadEntriesForPage: () => {},
12
+ }));
13
+
4
14
  import { __testOnly as composeDiscoveryTestOnly } from "../compose-discovery.js";
5
15
  import { __testOnly as composeShortcutsTestOnly } from "../compose-shortcuts.js";
6
16
 
@@ -24,6 +34,11 @@ function dispatchShortcut(
24
34
  return event;
25
35
  }
26
36
 
37
+ /** Let a shortcut's trip through the lazy loader settle. */
38
+ async function flush(): Promise<void> {
39
+ await new Promise((resolve) => setTimeout(resolve, 0));
40
+ }
41
+
27
42
  function createComposeHarness(): ComposeHarness {
28
43
  const composeEl = document.createElement(
29
44
  "jant-compose-dialog",
@@ -108,7 +123,7 @@ describe("compose shortcuts", () => {
108
123
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
109
124
  });
110
125
 
111
- it("opens a collection-scoped composer on collection pages with n", () => {
126
+ it("opens a collection-scoped composer on collection pages with n", async () => {
112
127
  const composeEl = createComposeHarness();
113
128
 
114
129
  const collectionPage = document.createElement("div");
@@ -118,6 +133,8 @@ describe("compose shortcuts", () => {
118
133
 
119
134
  const event = dispatchShortcut(document, "n");
120
135
 
136
+ await flush();
137
+
121
138
  expect(event.defaultPrevented).toBe(true);
122
139
  expect(composeEl.openNew).toHaveBeenCalledWith({ collectionId: "col-2" });
123
140
  });
@@ -126,7 +143,8 @@ describe("compose shortcuts", () => {
126
143
  createComposeHarness();
127
144
 
128
145
  dispatchShortcut(document, "n");
129
- await Promise.resolve();
146
+
147
+ await flush();
130
148
 
131
149
  expect(globalThis.fetch).toHaveBeenCalledWith(
132
150
  composeDiscoveryTestOnly.COMPOSE_OPEN_SHORTCUT_DISCOVERY_API_PATH,
@@ -139,7 +157,7 @@ describe("compose shortcuts", () => {
139
157
  });
140
158
  });
141
159
 
142
- it("ignores n while focus is inside an input", () => {
160
+ it("ignores n while focus is inside an input", async () => {
143
161
  const composeEl = createComposeHarness();
144
162
 
145
163
  const input = document.createElement("textarea");
@@ -147,10 +165,12 @@ describe("compose shortcuts", () => {
147
165
 
148
166
  dispatchShortcut(input, "n");
149
167
 
168
+ await flush();
169
+
150
170
  expect(composeEl.openNew).not.toHaveBeenCalled();
151
171
  });
152
172
 
153
- it("opens a reply composer for the latest thread post on detail pages with r", () => {
173
+ it("opens a reply composer for the latest thread post on detail pages with r", async () => {
154
174
  const composeEl = createComposeHarness();
155
175
 
156
176
  const postView = document.createElement("div");
@@ -201,6 +221,8 @@ describe("compose shortcuts", () => {
201
221
 
202
222
  const event = dispatchShortcut(document, "r");
203
223
 
224
+ await flush();
225
+
204
226
  expect(event.defaultPrevented).toBe(true);
205
227
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
206
228
 
@@ -224,7 +246,7 @@ describe("compose shortcuts", () => {
224
246
  );
225
247
  });
226
248
 
227
- it("targets the latest thread post for reply shortcuts even when an older post is hovered", () => {
249
+ it("targets the latest thread post for reply shortcuts even when an older post is hovered", async () => {
228
250
  const composeEl = createComposeHarness();
229
251
  const { hoveredArticle } = renderThreadDetailPage();
230
252
  const originalQuerySelector = document.querySelector.bind(document);
@@ -240,6 +262,8 @@ describe("compose shortcuts", () => {
240
262
 
241
263
  const event = dispatchShortcut(document, "r");
242
264
 
265
+ await flush();
266
+
243
267
  expect(event.defaultPrevented).toBe(true);
244
268
  expect(composeEl.openReply).toHaveBeenCalledWith(
245
269
  "post-latest",
@@ -265,7 +289,8 @@ describe("compose shortcuts", () => {
265
289
  );
266
290
 
267
291
  const event = dispatchShortcut(document, "e");
268
- await Promise.resolve();
292
+
293
+ await flush();
269
294
 
270
295
  expect(event.defaultPrevented).toBe(true);
271
296
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
@@ -279,7 +304,8 @@ describe("compose shortcuts", () => {
279
304
  globalThis.history.pushState({}, "", "/about?edit=1");
280
305
 
281
306
  composeShortcutsTestOnly.openEditFromQueryParam();
282
- await Promise.resolve();
307
+
308
+ await flush();
283
309
 
284
310
  expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
285
311
  expect(window.location.pathname).toBe("/about");
@@ -296,7 +322,8 @@ describe("compose shortcuts", () => {
296
322
  globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
297
323
 
298
324
  composeShortcutsTestOnly.openEditFromQueryParam();
299
- await Promise.resolve();
325
+
326
+ await flush();
300
327
 
301
328
  expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
302
329
  expect(composeEl.openEdit).not.toHaveBeenCalled();
@@ -304,7 +331,7 @@ describe("compose shortcuts", () => {
304
331
  expect(window.location.search).toBe("");
305
332
  });
306
333
 
307
- it("keeps using the hovered post for reply shortcuts in the timeline", () => {
334
+ it("keeps using the hovered post for reply shortcuts in the timeline", async () => {
308
335
  const composeEl = createComposeHarness();
309
336
  const article = document.createElement("article");
310
337
  article.dataset.post = "";
@@ -332,6 +359,8 @@ describe("compose shortcuts", () => {
332
359
 
333
360
  const event = dispatchShortcut(document, "r");
334
361
 
362
+ await flush();
363
+
335
364
  expect(event.defaultPrevented).toBe(true);
336
365
  expect(composeEl.openReply).toHaveBeenCalledTimes(1);
337
366