@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,68 @@
1
+ /**
2
+ * Type definitions for the smart collection dialog.
3
+ */
4
+
5
+ import type { FILTER_DIMENSION_KEYS } from "../../lib/filter-dimensions.js";
6
+
7
+ /** One condition row: which dimension, and the value it holds. */
8
+ export interface SmartConditionRow {
9
+ key: (typeof FILTER_DIMENSION_KEYS)[number];
10
+ /**
11
+ * The value in its URL spelling.
12
+ *
13
+ * The same string the query string carries, because the registry's parser is
14
+ * the only thing that turns a control's output into a stored value. A control
15
+ * that invented its own encoding would be a second vocabulary.
16
+ */
17
+ value: string;
18
+ }
19
+
20
+ /** Everything the dialog can be opened with. */
21
+ export interface SmartCollectionDialogState {
22
+ title?: string;
23
+ description?: string | null;
24
+ selection?: Record<string, unknown>;
25
+ sort?: string;
26
+ layout?: string | null;
27
+ }
28
+
29
+ export interface SmartCollectionDialogLabels {
30
+ createHeading: string;
31
+ editHeading: string;
32
+ /** What a smart collection is, shown once, when creating one. */
33
+ whatItIs: string;
34
+ title: string;
35
+ /** The collection link, in the collection form's own words. */
36
+ link: string;
37
+ linkHelp: string;
38
+ editLink: string;
39
+ resetLink: string;
40
+ linkTaken: string;
41
+ linkInvalid: string;
42
+ linkReserved: string;
43
+ linkTooLong: string;
44
+ linkMovesWarning: string;
45
+ description: string;
46
+ conditionsHeading: string;
47
+ matchAllHint: string;
48
+ noConditions: string;
49
+ addCondition: string;
50
+ removeCondition: string;
51
+ countSummary: string;
52
+ counting: string;
53
+ displayHeading: string;
54
+ orderBy: string;
55
+ layout: string;
56
+ cancel: string;
57
+ save: string;
58
+ saved: string;
59
+ saveFailed: string;
60
+ loadFailed: string;
61
+ titleAndLinkRequired: string;
62
+ /** Dimension names, keyed by dimension key. */
63
+ dimensions: Record<string, string>;
64
+ /** Value labels, keyed by `<dimension>.<value>`. */
65
+ values: Record<string, string>;
66
+ sortOptions: Record<string, string>;
67
+ layoutOptions: Record<string, string>;
68
+ }
@@ -15,10 +15,7 @@ import type { PostAttachmentInput } from "../types.js";
15
15
  import { AudioProcessor } from "./audio-processor.js";
16
16
  import { ImageProcessor } from "./image-processor.js";
17
17
  import { VideoProcessor } from "./video-processor.js";
18
- import {
19
- extractMediaMetadata,
20
- extractAudioWaveform,
21
- } from "./media-metadata.js";
18
+ import { extractAudioWaveform } from "./audio-waveform.js";
22
19
  import {
23
20
  showToast,
24
21
  showPersistentToast,
@@ -26,7 +23,6 @@ import {
26
23
  replaceWithAutoClose,
27
24
  queueToastForNextPage,
28
25
  } from "./toast.js";
29
- import { getComposeDialog, openReplyForArticle } from "./compose-launch.js";
30
26
  import {
31
27
  refreshPostCardView,
32
28
  refreshPostPageView,
@@ -37,12 +33,79 @@ import { uploadViaSession } from "./upload-session.js";
37
33
  import type { UploadSessionResult } from "./upload-session.js";
38
34
  import { publicPath } from "./runtime-paths.js";
39
35
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
40
- import { getMediaCategory } from "../lib/upload.js";
36
+ import { getMediaCategory, sniffImageMimeType } from "../lib/upload.js";
41
37
  import {
42
38
  resolveInlineImageUrls,
43
39
  hasPendingInlineImagePlaceholders,
44
40
  } from "./tiptap/inline-image-upload.js";
45
41
 
42
+ /**
43
+ * POST a compose payload, treating a dropped connection as a failed response.
44
+ *
45
+ * A publish that never reaches the server and a publish the server rejects are
46
+ * the same event to the author: the post did not go up. Letting `fetch` reject
47
+ * here would send only one of them down the recovery path, so the request that
48
+ * failed hardest — no connection at all — would be the one that skipped saving
49
+ * a draft.
50
+ *
51
+ * @param url - Compose endpoint to post to
52
+ * @param body - Request body, already serialized
53
+ * @param method - HTTP method, defaulting to POST
54
+ * @returns The response, or null when the request never completed
55
+ * @example
56
+ * const res = await postCompose("/compose/thread", JSON.stringify(threadBody));
57
+ * if (!res?.ok) await saveAsDraftInstead();
58
+ */
59
+ async function postCompose(
60
+ url: string,
61
+ body: string,
62
+ method = "POST",
63
+ ): Promise<globalThis.Response | null> {
64
+ try {
65
+ return await fetch(url, {
66
+ method,
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json",
70
+ },
71
+ body,
72
+ });
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * What to tell the author when a compose request did not land.
80
+ *
81
+ * Prefers the server's own words, because it knows what was wrong with the
82
+ * post. A request that never completed has no words of its own, so it says so
83
+ * — "no connection" and "the server refused this" call for different next
84
+ * steps from the author.
85
+ *
86
+ * @param res - The response, or null when the request never completed
87
+ * @param labels - Composer labels, absent before the element has upgraded
88
+ * @returns A message ready to show in a toast
89
+ * @example
90
+ * await handleSubmitError(await submitErrorMessage(res, labels));
91
+ */
92
+ async function submitErrorMessage(
93
+ res: globalThis.Response | null,
94
+ labels: JantComposeDialog["labels"] | undefined,
95
+ ): Promise<string> {
96
+ if (!res) {
97
+ return (
98
+ labels?.publishFailedOffline ?? "Couldn't reach the server. Try again."
99
+ );
100
+ }
101
+ const data = await readJsonObject(res);
102
+ return (
103
+ getJsonString(data, "error") ??
104
+ labels?.publishFailed ??
105
+ "Couldn't publish. Try again."
106
+ );
107
+ }
108
+
46
109
  /**
47
110
  * Whether a serialized post body still references inline image placeholders
48
111
  * pending upload or paste-rehost. Drives the "uploading" toast and whether to
@@ -143,19 +206,46 @@ function refreshUploadToast() {
143
206
  /**
144
207
  * Track completed upload results by clientId.
145
208
  *
146
- * When an attachment is migrated between editor instances (e.g. entering thread
147
- * mode while an upload is in-flight), `updateAttachmentStatus` fires on the old
148
- * detached editor — not the new thread editor. By the time the user submits,
149
- * the upload promise has been deleted from `uploadPromises`, so the deferred
150
- * handler can't look it up. This map provides a fallback: we record the result
151
- * here as soon as any upload succeeds, and `buildRequestAttachments` reads it
152
- * when `attachment.mediaId` is null and the clientId isn't in `uploadPromises`.
209
+ * By the time the user submits, a finished upload's promise has been deleted
210
+ * from `uploadPromises`, so the deferred handler can't look it up. This map
211
+ * provides a fallback: we record the result here as soon as any upload
212
+ * succeeds, and `buildRequestAttachments` reads it when `attachment.mediaId`
213
+ * is null and the clientId isn't in `uploadPromises`.
153
214
  *
154
215
  * The full result (not just the id) is kept so a failed publish can restore
155
216
  * uploaded attachments into the reopened compose dialog.
156
217
  */
157
218
  const completedUploads = new Map<string, UploadSessionResult>();
158
219
 
220
+ /**
221
+ * The editor holding an attachment now.
222
+ *
223
+ * Entering or leaving thread mode renders the post in a new editor element,
224
+ * which takes the attachments over from the one it replaced. An upload that
225
+ * outlives the swap has to follow the attachment: reporting to the editor it
226
+ * started in leaves the new one showing "uploading" forever, never showing an
227
+ * error, and keeping the finished file out of the local draft. Until a
228
+ * connected editor holds the attachment — the moment between the swap and the
229
+ * handover — the starting editor is still the one with the latest copy.
230
+ *
231
+ * @param started - The editor the upload was started from
232
+ * @param clientId - The attachment's client-side id
233
+ * @returns The editor to report progress and status to
234
+ */
235
+ function attachmentOwner(
236
+ started: JantComposeEditor | null,
237
+ clientId: string,
238
+ ): JantComposeEditor | null {
239
+ if (!started || started.isConnected) return started;
240
+ const editors = document.querySelectorAll<JantComposeEditor>(
241
+ "jant-compose-editor",
242
+ );
243
+ for (const editor of editors) {
244
+ if (editor.hasAttachment(clientId)) return editor;
245
+ }
246
+ return started;
247
+ }
248
+
159
249
  /**
160
250
  * Quickly grab the very first decoded frame of a video as a small poster.
161
251
  * Uses a 3s timeout — returns null on any failure so callers don't stall.
@@ -225,6 +315,19 @@ function captureQuickPoster(file: File): Promise<Blob | null> {
225
315
  });
226
316
  }
227
317
 
318
+ /** Bytes that settle whether a file is HEIC: the `ftyp` box and its brand. */
319
+ const HEIC_SNIFF_BYTES = 12;
320
+
321
+ /**
322
+ * Whether the file's bytes say HEIC/HEIF, whatever its name or type claims.
323
+ * Reads the header only — the decoder's own check pulled the whole file into
324
+ * memory to look at four bytes of it.
325
+ */
326
+ async function isHeicFile(file: File): Promise<boolean> {
327
+ const head = await file.slice(0, HEIC_SNIFF_BYTES).arrayBuffer();
328
+ return sniffImageMimeType(new Uint8Array(head)) === "image/heic";
329
+ }
330
+
228
331
  /**
229
332
  * Upload a single file: process locally, then send it through the upload
230
333
  * session API so the backend can choose relay vs direct transport.
@@ -235,6 +338,7 @@ async function uploadFile(
235
338
  clientId: string,
236
339
  editor: JantComposeEditor | null,
237
340
  ): Promise<string | null> {
341
+ const owner = () => attachmentOwner(editor, clientId);
238
342
  // Capture cheap metadata up-front so we can release `file` (the original
239
343
  // potentially-huge blob) as soon as transcoding finishes. On iOS Safari
240
344
  // holding a 300MB+ source blob alongside the transcoded output, upload
@@ -254,7 +358,7 @@ async function uploadFile(
254
358
  if (fileType.startsWith("video/")) {
255
359
  // Video: transcode with mediabunny (requires WebCodecs)
256
360
  if (!VideoProcessor.isSupported()) {
257
- editor?.updateAttachmentStatus(
361
+ owner()?.updateAttachmentStatus(
258
362
  clientId,
259
363
  "error",
260
364
  null,
@@ -265,14 +369,19 @@ async function uploadFile(
265
369
 
266
370
  // Capture the first frame quickly so Safari shows a preview while
267
371
  // the heavy transcoding runs. Chrome shows it natively via
268
- // <video preload="metadata">, Safari does not.
269
- captureQuickPoster(file).then((blob) => {
270
- if (blob) editor?.updateAttachmentPoster(clientId, blob);
271
- });
272
-
273
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
372
+ // <video preload="metadata">, Safari does not. Kept, rather than shown
373
+ // and dropped: it is also the fallback for the upload below, so the
374
+ // card and the stored media can never disagree about having a poster.
375
+ const quickPoster = captureQuickPoster(file)
376
+ .then((blob) => {
377
+ if (blob) owner()?.updateAttachmentPoster(clientId, blob);
378
+ return blob;
379
+ })
380
+ .catch(() => null);
381
+
382
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
274
383
  const result = await VideoProcessor.processToFile(file, (progress) => {
275
- editor?.updateAttachmentProgress(clientId, progress);
384
+ owner()?.updateAttachmentProgress(clientId, progress);
276
385
  });
277
386
  toUpload = result.file;
278
387
  // Drop the original blob ref now that we have the transcoded output.
@@ -281,14 +390,18 @@ async function uploadFile(
281
390
  height = result.height;
282
391
  durationSeconds = result.durationSeconds;
283
392
  blurhash = result.blurhash;
284
- poster = result.poster;
393
+ // The probe's poster is the better one — a frame from a tenth of the way
394
+ // in, at full decode quality. Falling back to the quick one costs a
395
+ // slightly worse still; not falling back costs a thumbnail entirely, on
396
+ // exactly the files whose frames the probe could not read.
397
+ poster = result.poster ?? (await quickPoster) ?? undefined;
285
398
  if (poster) {
286
- editor?.updateAttachmentPoster(clientId, poster);
399
+ owner()?.updateAttachmentPoster(clientId, poster);
287
400
  }
288
401
  } else if (fileType.startsWith("audio/")) {
289
402
  // Audio: transcode to AAC (.m4a) (requires WebCodecs)
290
403
  if (!AudioProcessor.isSupported()) {
291
- editor?.updateAttachmentStatus(
404
+ owner()?.updateAttachmentStatus(
292
405
  clientId,
293
406
  "error",
294
407
  null,
@@ -304,9 +417,9 @@ async function uploadFile(
304
417
  // Waveform extraction is best-effort
305
418
  }
306
419
 
307
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
420
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
308
421
  const result = await AudioProcessor.processToFile(file, (progress) => {
309
- editor?.updateAttachmentProgress(clientId, progress);
422
+ owner()?.updateAttachmentProgress(clientId, progress);
310
423
  });
311
424
  toUpload = result.file;
312
425
  file = null as unknown as File;
@@ -318,9 +431,11 @@ async function uploadFile(
318
431
  // Image: convert HEIC/HEIF if needed, then resize + convert to WebP
319
432
  let imageFile = file;
320
433
  try {
321
- const { isHeic, heicTo } = await import("heic-to");
322
- if (await isHeic(imageFile)) {
323
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
434
+ if (await isHeicFile(imageFile)) {
435
+ // heic-to carries libheif — a 3MB chunk — so it is fetched for a
436
+ // file whose bytes say HEIC, not for every photo on the off chance.
437
+ const { heicTo } = await import("heic-to");
438
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
324
439
  const blob = await heicTo({
325
440
  blob: imageFile,
326
441
  type: "image/jpeg",
@@ -329,16 +444,17 @@ async function uploadFile(
329
444
  imageFile = new File([blob], fileName.replace(/\.heic$/i, ".jpg"), {
330
445
  type: "image/jpeg",
331
446
  });
332
- editor?.updateAttachmentPreview(clientId, imageFile);
447
+ owner()?.updateAttachmentPreview(clientId, imageFile);
333
448
  }
334
449
  const result = await ImageProcessor.processToFile(imageFile);
335
450
  toUpload = result.file;
336
451
  width = result.width;
337
452
  height = result.height;
453
+ blurhash = result.blurhash;
338
454
  file = null as unknown as File;
339
455
  imageFile = null as unknown as File;
340
456
  } catch {
341
- editor?.removeAttachment(clientId);
457
+ owner()?.removeAttachment(clientId);
342
458
  showToast("Image format not supported.", "error");
343
459
  return null;
344
460
  }
@@ -347,21 +463,7 @@ async function uploadFile(
347
463
  }
348
464
 
349
465
  // Update status to uploading
350
- editor?.updateAttachmentStatus(clientId, "uploading", null, null);
351
-
352
- // Extract metadata for non-video files (video metadata comes from VideoProcessor)
353
- // Audio waveform is already extracted above (before AudioProcessor runs).
354
- if (!fileType.startsWith("video/")) {
355
- const meta = await extractMediaMetadata(toUpload);
356
- width ??= meta.width;
357
- height ??= meta.height;
358
- blurhash ??= meta.blurhash;
359
- waveform ??= meta.waveform;
360
- if (!poster && meta.poster) {
361
- poster = meta.poster;
362
- editor?.updateAttachmentPoster(clientId, poster);
363
- }
364
- }
466
+ owner()?.updateAttachmentStatus(clientId, "uploading", null, null);
365
467
 
366
468
  // Text attachments keep summary/chars in the media record. This covers
367
469
  // plain text-file uploads (.md, .txt, .csv). Jant-composed rich text
@@ -398,7 +500,7 @@ async function uploadFile(
398
500
  chars,
399
501
  },
400
502
  (progress) => {
401
- editor?.updateAttachmentProgress(clientId, progress);
503
+ owner()?.updateAttachmentProgress(clientId, progress);
402
504
  uploadProgress.set(clientId, progress);
403
505
  refreshUploadToast();
404
506
  },
@@ -406,7 +508,7 @@ async function uploadFile(
406
508
 
407
509
  uploadProgress.set(clientId, 1);
408
510
  refreshUploadToast();
409
- editor?.updateAttachmentStatus(
511
+ owner()?.updateAttachmentStatus(
410
512
  clientId,
411
513
  "done",
412
514
  result.id,
@@ -419,7 +521,7 @@ async function uploadFile(
419
521
  uploadProgress.delete(clientId);
420
522
  refreshUploadToast();
421
523
  const message = error instanceof Error ? error.message : "Upload failed";
422
- editor?.updateAttachmentStatus(clientId, "error", null, message);
524
+ owner()?.updateAttachmentStatus(clientId, "error", null, message);
423
525
  // Error is shown on the attachment thumbnail; only toast when there's no editor context.
424
526
  if (!editor) showToast(message, "error");
425
527
  return null;
@@ -469,38 +571,6 @@ document.addEventListener("jant:files-selected", (e: Event) => {
469
571
  }
470
572
  });
471
573
 
472
- // ── Reply trigger handler ───────────────────────────────────────────
473
-
474
- document.addEventListener("click", (e: MouseEvent) => {
475
- const trigger = (e.target as HTMLElement).closest<HTMLButtonElement>(
476
- "[data-reply-trigger]",
477
- );
478
- if (!trigger) return;
479
-
480
- const article = trigger.closest<HTMLElement>("article[data-post]");
481
- if (!article) return;
482
- void openReplyForArticle(article);
483
- });
484
-
485
- // ── Draft badge → editor ────────────────────────────────────────────
486
- //
487
- // The badge on an inline draft doubles as its most likely next action. Publish
488
- // and Delete live in the post menu with every other post action.
489
-
490
- document.addEventListener("click", (e: MouseEvent) => {
491
- const trigger = (e.target as HTMLElement).closest<HTMLElement>(
492
- "[data-draft-continue]",
493
- );
494
- if (!trigger) return;
495
-
496
- const postId =
497
- trigger.dataset.postId ??
498
- trigger.closest<HTMLElement>("article[data-post]")?.dataset.postId;
499
- if (!postId) return;
500
-
501
- void getComposeDialog()?.openEdit(postId);
502
- });
503
-
504
574
  // ── Submit handler ──────────────────────────────────────────────────
505
575
 
506
576
  /** Build the JSON body for both create and update requests */
@@ -564,6 +634,12 @@ function buildPostBody(
564
634
  attachments: attachments.length > 0 ? attachments : undefined,
565
635
  replyToId: detail.replyToId || undefined,
566
636
  quietReply: detail.quietReply || undefined,
637
+ // Absent means the author left the language to detection; the server runs
638
+ // the same detector against the final text.
639
+ language: detail.language || undefined,
640
+ // A translation group is only ever minted on create — an edit that carried
641
+ // this would silently regroup an existing post.
642
+ translationOfId: isEdit ? undefined : detail.translationOfId || undefined,
567
643
  };
568
644
  }
569
645
 
@@ -635,32 +711,49 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
635
711
  // Get labels for toast messages
636
712
  const labels = composeEl?.labels;
637
713
  const uploadingMsg = labels?.uploading ?? "Uploading...";
714
+ const publishingMsg = labels?.publishing ?? "Publishing...";
715
+ const savingMsg = labels?.saving ?? "Saving...";
638
716
  const hasInlinePending = detail.threadPosts
639
717
  ? detail.threadPosts.some((p) => bodyHasPendingInline(p.body))
640
718
  : bodyHasPendingInline(detail.body);
641
719
  const hasPending = detail.pendingAttachments.length > 0 || hasInlinePending;
642
720
  const publishedMsg = labels?.published ?? "Published!";
643
721
  const viewLabel = labels?.view ?? "View";
644
-
645
- // Show persistent toast only when uploads are still in flight
646
- if (hasPending) {
647
- showPersistentToast("compose-deferred", uploadingMsg);
648
- if (detail.pendingAttachments.length > 0) {
649
- activeUploadToast = {
650
- clientIds: detail.pendingAttachments.map((a) => a.clientId),
651
- baseMsg: uploadingMsg,
652
- };
653
- refreshUploadToast();
654
- }
722
+ const isEdit = !!detail.editPostId;
723
+ const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
724
+ // Publishing a draft goes through the edit endpoint but still ends published.
725
+ const willPublish =
726
+ detail.status === "published" && (!isEdit || !!detail.draftSourceId);
727
+
728
+ // The composer closes the moment it dispatches, so this toast is the only
729
+ // sign that work is still running. Uploads name themselves and grow a
730
+ // percentage; everything else says what it is doing — a Link post can sit on
731
+ // the server for seconds while its preview thumbnail is fetched and stored.
732
+ let progressToastOpen = true;
733
+ showPersistentToast(
734
+ "compose-deferred",
735
+ hasPending ? uploadingMsg : willPublish ? publishingMsg : savingMsg,
736
+ );
737
+ if (detail.pendingAttachments.length > 0) {
738
+ activeUploadToast = {
739
+ clientIds: detail.pendingAttachments.map((a) => a.clientId),
740
+ baseMsg: uploadingMsg,
741
+ };
742
+ refreshUploadToast();
655
743
  }
656
744
 
657
- /** Show result toast — replaces persistent toast if one exists, otherwise shows a new one */
745
+ /**
746
+ * Show a result toast. The first one takes over the progress toast in place;
747
+ * a later message (a retry result, say) gets its own toast instead of
748
+ * overwriting one that has not been read yet.
749
+ */
658
750
  const toastMsg = (msg: string, type: "success" | "error" = "success") => {
659
751
  if (activeUploadToast) {
660
752
  for (const id of activeUploadToast.clientIds) uploadProgress.delete(id);
661
753
  activeUploadToast = null;
662
754
  }
663
- if (hasPending) {
755
+ if (progressToastOpen) {
756
+ progressToastOpen = false;
664
757
  replaceWithAutoClose("compose-deferred", msg, type);
665
758
  } else {
666
759
  showToast(msg, type);
@@ -783,8 +876,6 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
783
876
  globalThis.location.assign(composeEl.closeHref || publicPath("/"));
784
877
  return true;
785
878
  };
786
- const isEdit = !!detail.editPostId;
787
- const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
788
879
  let draftFallback: "upload" | "server" | null = null;
789
880
 
790
881
  try {
@@ -854,30 +945,23 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
854
945
  threadBody.replaceThreadId = detail.editPostId;
855
946
  }
856
947
 
857
- const res = await fetch("/compose/thread", {
858
- method: "POST",
859
- headers: {
860
- "Content-Type": "application/json",
861
- Accept: "application/json",
862
- },
863
- body: JSON.stringify(threadBody),
864
- });
948
+ const res = await postCompose(
949
+ "/compose/thread",
950
+ JSON.stringify(threadBody),
951
+ );
865
952
 
866
- if (!res.ok) {
867
- // Server error on publish: retry as draft
953
+ if (!res?.ok) {
954
+ // The publish did not land — whether the server refused it or the
955
+ // request never arrived. Try to keep the work as a draft either way.
868
956
  if (detail.status === "published" && !draftFallback) {
869
957
  const retryPayload = {
870
958
  posts: postsPayload.map((p) => ({ ...p, status: "draft" })),
871
959
  };
872
- const retryRes = await fetch("/compose/thread", {
873
- method: "POST",
874
- headers: {
875
- "Content-Type": "application/json",
876
- Accept: "application/json",
877
- },
878
- body: JSON.stringify(retryPayload),
879
- });
880
- if (retryRes.ok) {
960
+ const retryRes = await postCompose(
961
+ "/compose/thread",
962
+ JSON.stringify(retryPayload),
963
+ );
964
+ if (retryRes?.ok) {
881
965
  draftFallback = "server";
882
966
  clearRecoveredLocalDraft();
883
967
  const fallbackMsg =
@@ -888,10 +972,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
888
972
  return;
889
973
  }
890
974
  }
891
- const data = await readJsonObject(res);
892
- await handleSubmitError(
893
- getJsonString(data, "error") ?? "Something went wrong",
894
- );
975
+ await handleSubmitError(await submitErrorMessage(res, labels));
895
976
  return;
896
977
  }
897
978
 
@@ -974,29 +1055,24 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
974
1055
  requestAttachments,
975
1056
  );
976
1057
 
977
- const res = await fetch(endpoint, {
1058
+ const res = await postCompose(
1059
+ endpoint,
1060
+ JSON.stringify(bodyPayload),
978
1061
  method,
979
- headers: {
980
- "Content-Type": "application/json",
981
- Accept: "application/json",
982
- },
983
- body: JSON.stringify(bodyPayload),
984
- });
1062
+ );
985
1063
 
986
- if (!res.ok) {
987
- // Server error on a new publish: retry as draft
1064
+ if (!res?.ok) {
1065
+ // As in thread mode: a refused request and a request that never arrived
1066
+ // both mean the post is not up, so both get the save-as-draft attempt.
988
1067
  if (detail.status === "published" && !isEdit && !draftFallback) {
989
1068
  const retryPayload = { ...bodyPayload, status: "draft" };
990
- const retryRes = await fetch(endpoint, {
1069
+ const retryRes = await postCompose(
1070
+ endpoint,
1071
+ JSON.stringify(retryPayload),
991
1072
  method,
992
- headers: {
993
- "Content-Type": "application/json",
994
- Accept: "application/json",
995
- },
996
- body: JSON.stringify(retryPayload),
997
- });
1073
+ );
998
1074
 
999
- if (retryRes.ok) {
1075
+ if (retryRes?.ok) {
1000
1076
  draftFallback = "server";
1001
1077
  clearRecoveredLocalDraft();
1002
1078
  const retryData = await readJsonObject(retryRes);
@@ -1013,10 +1089,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1013
1089
  }
1014
1090
  }
1015
1091
 
1016
- const data = await readJsonObject(res);
1017
- await handleSubmitError(
1018
- getJsonString(data, "error") ?? "Something went wrong",
1019
- );
1092
+ await handleSubmitError(await submitErrorMessage(res, labels));
1020
1093
  return;
1021
1094
  }
1022
1095
 
@@ -1168,7 +1241,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1168
1241
  dispatchSubmitComplete(composeEl, "draft");
1169
1242
  }
1170
1243
  } catch {
1171
- await handleSubmitError("Something went wrong");
1244
+ await handleSubmitError(await submitErrorMessage(null, labels));
1172
1245
  }
1173
1246
  });
1174
1247