@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -7,11 +7,12 @@ vi.mock("../../upload-with-metadata.js", () => ({
7
7
  import type { Editor, JSONContent } from "@tiptap/core";
8
8
  import type { Slice } from "@tiptap/pm/model";
9
9
  import * as ProseMirrorView from "@tiptap/pm/view";
10
- import type { ComposeLabels } from "../compose-types.js";
10
+ import type { ComposeAttachment, ComposeLabels } from "../compose-types.js";
11
11
  import { renderTiptapJson } from "../../../lib/tiptap-render.js";
12
12
  import "../jant-compose-editor.js";
13
13
  import type { JantComposeEditor } from "../jant-compose-editor.js";
14
14
  import { uploadWithMetadata } from "../../upload-with-metadata.js";
15
+ import Sortable from "sortablejs";
15
16
 
16
17
  function requireElement<T extends globalThis.Element>(
17
18
  element: T | null,
@@ -108,6 +109,57 @@ function triggerEditorPaste(el: JantComposeEditor, files: File[]) {
108
109
  return { handled, event };
109
110
  }
110
111
 
112
+ /**
113
+ * Drops files on the element itself, the way a drag onto the composer chrome
114
+ * (rather than into the ProseMirror body) arrives.
115
+ */
116
+ function triggerEditorDrop(el: JantComposeEditor, files: File[]) {
117
+ const event = new Event("drop", { bubbles: true, cancelable: true });
118
+ Object.defineProperty(event, "dataTransfer", {
119
+ value: {
120
+ items: files.map((file) => ({
121
+ kind: "file",
122
+ type: file.type,
123
+ getAsFile: () => file,
124
+ })),
125
+ files,
126
+ },
127
+ });
128
+ el.dispatchEvent(event);
129
+ return event;
130
+ }
131
+
132
+ /** Every image src currently in the editor's document, in document order. */
133
+ function imageSrcs(el: JantComposeEditor): string[] {
134
+ const srcs: string[] = [];
135
+ requireEditor(el).state.doc.descendants((node) => {
136
+ if (node.type.name === "image") srcs.push(node.attrs.src as string);
137
+ });
138
+ return srcs;
139
+ }
140
+
141
+ /** The same, read off the JSON the composer would submit. */
142
+ function bodyImageSrcs(json: unknown): string[] {
143
+ const srcs: string[] = [];
144
+ const walk = (node: unknown) => {
145
+ if (!node || typeof node !== "object") return;
146
+ const candidate = node as {
147
+ type?: string;
148
+ attrs?: { src?: unknown };
149
+ content?: unknown[];
150
+ };
151
+ if (
152
+ candidate.type === "image" &&
153
+ typeof candidate.attrs?.src === "string"
154
+ ) {
155
+ srcs.push(candidate.attrs.src);
156
+ }
157
+ for (const child of candidate.content ?? []) walk(child);
158
+ };
159
+ walk(json);
160
+ return srcs;
161
+ }
162
+
111
163
  function parsePastedText(el: JantComposeEditor, text: string): Slice | null {
112
164
  const editor = requireEditor(el);
113
165
  let slice: Slice | null = null;
@@ -152,6 +204,7 @@ const parseFromClipboard = (
152
204
 
153
205
  const labels: ComposeLabels = {
154
206
  cancel: "Cancel",
207
+ close: "Close",
155
208
  note: "Note",
156
209
  link: "Link",
157
210
  quote: "Quote",
@@ -190,7 +243,11 @@ const labels: ComposeLabels = {
190
243
  altHint: "Alt text improves accessibility",
191
244
  addMore: "Add",
192
245
  removeAttachment: "Remove attachment",
246
+ moveAttachmentEarlier: "Move attachment earlier",
247
+ moveAttachmentLater: "Move attachment later",
193
248
  uploading: "Uploading...",
249
+ publishing: "Publishing...",
250
+ saving: "Saving...",
194
251
  loadingPost: "Loading post...",
195
252
  loadPostFailed: "Couldn't load this post. Try again.",
196
253
  published: "Published!",
@@ -201,7 +258,7 @@ const labels: ComposeLabels = {
201
258
  confirmCloseTitle: "Save to drafts?",
202
259
  confirmCloseSubtitle: "Save to drafts to edit and post at a later time.",
203
260
  confirmCloseSave: "Save",
204
- confirmCloseCancel: "Cancel",
261
+ confirmCloseCancel: "Keep editing",
205
262
  confirmCloseDiscard: "Don't save",
206
263
  confirmAttachedTitle: "Save text attachment?",
207
264
  confirmAttachedSubtitle:
@@ -220,14 +277,32 @@ const labels: ComposeLabels = {
220
277
  deleteDraft: "Delete Draft",
221
278
  draftDeleted: "Draft deleted.",
222
279
  publishFailedDraft: "Couldn't publish. Saved as draft.",
280
+ publishFailed: "Couldn't publish. Try again.",
281
+ publishFailedOffline: "Couldn't reach the server. Try again.",
223
282
  uploadFailedDraft: "Some uploads failed. Saved as draft.",
224
283
  addCollection: "Add Collection",
225
284
  collectionCountLabel: "%name% + %count% more",
226
285
  draftRestored: "Draft restored.",
286
+ draftStoreFailed: "This browser can't keep a local copy. Save as a draft.",
227
287
  reply: "Reply",
228
288
  publishHideFromLatest: "Hide from Latest",
229
289
  publishPrivate: "Post as Private",
230
290
  publishSettings: "Publish settings",
291
+ languageLabel: "Language",
292
+ languageAuto: "Detect",
293
+ languageAutoHint: "Read from what you write",
294
+ languageAutoDetected: "Read from what you write — looks like {language}",
295
+ languageAutoPending: "Not enough text to tell yet — publishes in {language}",
296
+ languageTriggerLabel: "Language: {language}",
297
+ translationOf: "Translation of “{title}”",
298
+ translationContext: "Translating",
299
+ translationContextInLanguage: "Translating into {language}",
300
+ translationContextOpen: "Open the original in a new tab",
301
+ translationContextOriginal: "The original",
302
+ translationContextHide: "Hide",
303
+ translationContextHideLong: "Hide the original",
304
+ translationContextShow: "Show",
305
+ translationContextShowLong: "Show the original",
231
306
  publishVisibilityLabel: "Visibility",
232
307
  publishVisibilityPublic: "Public",
233
308
  publishVisibilityPublicHint: "Appears in Latest.",
@@ -243,13 +318,13 @@ const labels: ComposeLabels = {
243
318
  publishDateInvalid: "Enter a valid date.",
244
319
  publishDateFutureError:
245
320
  "Choose today or an earlier date, or leave it blank to publish now.",
246
- publishDateSummaryNow: "Publish now",
321
+ publishDateSummaryNow: "Now",
247
322
  publishDateSummaryAction: "Edit publish date",
248
323
  publishSlugLabel: "Custom link",
249
324
  publishSlugPlaceholder: "your-post-link",
250
325
  publishSlugHint: "Leave blank to generate one automatically.",
251
326
  publishSlugAuto: "Generate automatically",
252
- publishSlugSummaryAuto: "Auto link",
327
+ publishSlugSummaryAuto: "Auto",
253
328
  publishSlugSummaryAction: "Edit custom link",
254
329
  publishSlugReset: "Reset link",
255
330
  publishSlugSuggested: "Suggested link",
@@ -265,9 +340,8 @@ const labels: ComposeLabels = {
265
340
  threadLimitReached: "Threads can include up to 20 posts.",
266
341
  showMore: "Show more",
267
342
  showLess: "Show less",
268
- newThread: "New Thread",
269
- replyTitle: "Reply",
270
- editTitle: "Edit",
343
+ editing: "Editing",
344
+ composeDialogLabel: "Compose",
271
345
  slashHint: "Type / for commands",
272
346
  tableControls: {
273
347
  toolbarLabel: "Table controls",
@@ -295,28 +369,77 @@ const labels: ComposeLabels = {
295
369
  quickHint: "More options are available after you create it.",
296
370
  quickSubmitLabel: "Done",
297
371
  createdLabel: "Collection created.",
298
- descriptionLabel: "Description (optional)",
299
- descriptionPlaceholder: "What's this collection about?",
300
- sortOrderLabel: "Sort Order",
301
- sortNewest: "Newest first",
302
- sortOldest: "Oldest first",
303
- sortRatingDesc: "Highest rated",
304
- submitLabel: "Save",
305
372
  cancelLabel: "Cancel",
306
373
  },
307
374
  };
308
375
 
309
376
  async function createElement(
310
377
  format: string = "note",
378
+ options: { titleByDefault?: boolean } = {},
311
379
  ): Promise<JantComposeEditor> {
312
380
  const el = document.createElement("jant-compose-editor") as JantComposeEditor;
313
381
  el.format = format as "note" | "link" | "quote";
314
382
  el.labels = labels;
383
+ if (options.titleByDefault !== undefined) {
384
+ el.titleByDefault = options.titleByDefault;
385
+ }
315
386
  document.body.appendChild(el);
316
387
  await el.updateComplete;
317
388
  return el;
318
389
  }
319
390
 
391
+ function mediaAttachment(clientId: string, mediaId: string): ComposeAttachment {
392
+ const blob = new Blob(["fake"], { type: "image/png" });
393
+ return {
394
+ clientId,
395
+ file: new File([blob], `${clientId}.png`, { type: "image/png" }),
396
+ previewUrl: URL.createObjectURL(blob),
397
+ status: "done",
398
+ progress: null,
399
+ mediaId,
400
+ alt: "",
401
+ error: null,
402
+ posterUrl: null,
403
+ remoteUrl: null,
404
+ summary: null,
405
+ chars: null,
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Forces the primary-pointer media query, and returns a restore function.
411
+ * Every call site restores in a `finally` so one test cannot leak into another.
412
+ */
413
+ function stubPrimaryPointer(kind: "coarse" | "fine"): () => void {
414
+ const original = window.matchMedia;
415
+ // Overrides `matches` on a real MediaQueryList, so the component still gets
416
+ // a live object to subscribe to.
417
+ window.matchMedia = (query: string) => {
418
+ const list = original.call(window, query);
419
+ Object.defineProperty(list, "matches", {
420
+ configurable: true,
421
+ value: query.includes(`pointer: ${kind}`),
422
+ });
423
+ return list;
424
+ };
425
+ return () => {
426
+ window.matchMedia = original;
427
+ };
428
+ }
429
+
430
+ function moveButton(
431
+ el: JantComposeEditor,
432
+ clientId: string,
433
+ direction: "earlier" | "later",
434
+ ): HTMLButtonElement {
435
+ return requireElement(
436
+ el.querySelector<HTMLButtonElement>(
437
+ `[data-attachment-id="${clientId}"] [data-attachment-move="${direction}"]`,
438
+ ),
439
+ `expected ${direction} move button for ${clientId}`,
440
+ );
441
+ }
442
+
320
443
  function tiptapDoc(...content: JSONContent[]): JSONContent {
321
444
  return { type: "doc", content };
322
445
  }
@@ -336,6 +459,22 @@ function tiptapParagraph(text: string): JSONContent {
336
459
  };
337
460
  }
338
461
 
462
+ /**
463
+ * Replays a pointer gesture. The browser retargets a click to the nearest
464
+ * common ancestor of the press and the release, so `clickTarget` is separate
465
+ * from the two — a drag that starts on the text and ends over a layout wrapper
466
+ * reports the wrapper.
467
+ */
468
+ function pressAndClick(
469
+ pressTarget: HTMLElement,
470
+ releaseTarget: HTMLElement,
471
+ clickTarget: HTMLElement = releaseTarget,
472
+ ) {
473
+ pressTarget.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
474
+ releaseTarget.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
475
+ clickTarget.dispatchEvent(new MouseEvent("click", { bubbles: true }));
476
+ }
477
+
339
478
  async function toggleEmojiPicker(el: JantComposeEditor) {
340
479
  (
341
480
  el as unknown as {
@@ -795,13 +934,46 @@ describe("JantComposeEditor", () => {
795
934
  expect(el._attachedTexts[0].bodyJson).toBeNull();
796
935
  });
797
936
 
798
- it("shows title toggle only in note mode", async () => {
937
+ it("shows a note's title field by default on a post that starts a thread", async () => {
799
938
  const el = await createElement("note");
939
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
800
940
  expect(el.querySelector('.compose-tool-btn[title="Title"]')).not.toBeNull();
941
+ });
942
+
943
+ it("hides a note's title field by default on a continuation post", async () => {
944
+ const el = await createElement("note", { titleByDefault: false });
945
+ expect(el.querySelector(".compose-note-title")).toBeNull();
946
+ });
947
+
948
+ it("toggles the title field from the toolbar", async () => {
949
+ const el = await createElement("note", { titleByDefault: false });
950
+ const toggle = requireElement(
951
+ el.querySelector<HTMLButtonElement>('.compose-tool-btn[title="Title"]'),
952
+ "expected title toggle",
953
+ );
954
+
955
+ toggle.click();
956
+ await el.updateComplete;
957
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
958
+
959
+ toggle.click();
960
+ await el.updateComplete;
961
+ expect(el.querySelector(".compose-note-title")).toBeNull();
962
+ });
801
963
 
802
- el.format = "link";
964
+ it("drops a hidden title from the submitted data", async () => {
965
+ const el = await createElement("note");
966
+ el._title = "Named";
967
+ el._showTitle = false;
803
968
  await el.updateComplete;
804
- expect(el.querySelector('.compose-tool-btn[title="Title"]')).toBeNull();
969
+ expect(el.getData().title).toBe("");
970
+ });
971
+
972
+ it("keeps the title field visible when a post arrives with one", async () => {
973
+ const el = await createElement("note", { titleByDefault: false });
974
+ el.populate({ format: "note", title: "Loaded title" });
975
+ await el.updateComplete;
976
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
805
977
  });
806
978
 
807
979
  it.each([
@@ -812,7 +984,6 @@ describe("JantComposeEditor", () => {
812
984
  async (format, selector) => {
813
985
  const el = await createElement(format);
814
986
  if (format === "note") {
815
- el._showTitle = true;
816
987
  await el.updateComplete;
817
988
  }
818
989
 
@@ -841,7 +1012,6 @@ describe("JantComposeEditor", () => {
841
1012
 
842
1013
  it("keeps focus in the title while an IME composition is active", async () => {
843
1014
  const el = await createElement("note");
844
- el._showTitle = true;
845
1015
  await el.updateComplete;
846
1016
 
847
1017
  const titleInput = requireElement(
@@ -864,7 +1034,6 @@ describe("JantComposeEditor", () => {
864
1034
 
865
1035
  it("leaves Mod-Enter available for the publish shortcut", async () => {
866
1036
  const el = await createElement("note");
867
- el._showTitle = true;
868
1037
  await el.updateComplete;
869
1038
 
870
1039
  const titleInput = requireElement(
@@ -885,7 +1054,7 @@ describe("JantComposeEditor", () => {
885
1054
  expect(document.activeElement).toBe(titleInput);
886
1055
  });
887
1056
 
888
- it("keeps title after rate and places fullscreen at the far right of the toolbar", async () => {
1057
+ it("places fullscreen at the far right of the toolbar", async () => {
889
1058
  const el = await createElement("note");
890
1059
  const toolTitles = [
891
1060
  ...el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn"),
@@ -924,7 +1093,6 @@ describe("JantComposeEditor", () => {
924
1093
  it("getData returns current field values", async () => {
925
1094
  const el = await createElement("note");
926
1095
  el._title = "Test Title";
927
- el._showTitle = true;
928
1096
  el._bodyJson = {
929
1097
  type: "doc",
930
1098
  content: [
@@ -994,31 +1162,11 @@ describe("JantComposeEditor", () => {
994
1162
  expect(data.attachmentOrder).toEqual(["t2"]);
995
1163
  });
996
1164
 
997
- it("getData omits title when showTitle is off in note mode", async () => {
1165
+ it("getData returns the note title as typed", async () => {
998
1166
  const el = await createElement("note");
999
- el._title = "Hidden Title";
1000
- el._showTitle = false;
1001
-
1002
- const data = el.getData();
1003
- expect(data.title).toBe("");
1004
- });
1167
+ el._title = "Kept Title";
1005
1168
 
1006
- it("preserves title in memory when toggling off and restores on toggle on", async () => {
1007
- const el = await createElement("note");
1008
- el._title = "My Title";
1009
- el._showTitle = true;
1010
- await el.updateComplete;
1011
-
1012
- // Toggle off — title stays in memory
1013
- el._showTitle = false;
1014
- await el.updateComplete;
1015
- expect(el._title).toBe("My Title");
1016
- expect(el.getData().title).toBe("");
1017
-
1018
- // Toggle back on — title restored
1019
- el._showTitle = true;
1020
- await el.updateComplete;
1021
- expect(el.getData().title).toBe("My Title");
1169
+ expect(el.getData().title).toBe("Kept Title");
1022
1170
  });
1023
1171
 
1024
1172
  it("promotes a leading H1 to the note title", async () => {
@@ -1035,7 +1183,6 @@ describe("JantComposeEditor", () => {
1035
1183
  const body = JSON.parse(data.body) as JSONContent;
1036
1184
 
1037
1185
  expect(data.title).toBe("My Markdown Title");
1038
- expect(el._showTitle).toBe(true);
1039
1186
  expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
1040
1187
  });
1041
1188
 
@@ -1050,7 +1197,6 @@ describe("JantComposeEditor", () => {
1050
1197
  const body = JSON.parse(data.body) as JSONContent;
1051
1198
 
1052
1199
  expect(data.title).toBe("");
1053
- expect(el._showTitle).toBe(false);
1054
1200
  expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
1055
1201
  });
1056
1202
 
@@ -1058,7 +1204,6 @@ describe("JantComposeEditor", () => {
1058
1204
  const el = await createElement("note");
1059
1205
  const editor = requireEditor(el);
1060
1206
  el._title = "Manual title";
1061
- el._showTitle = true;
1062
1207
 
1063
1208
  editor.commands.setContent(
1064
1209
  tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
@@ -1231,6 +1376,7 @@ describe("JantComposeEditor", () => {
1231
1376
  alt: "",
1232
1377
  error: null,
1233
1378
  posterUrl: null,
1379
+ remoteUrl: null,
1234
1380
  summary: null,
1235
1381
  chars: null,
1236
1382
  },
@@ -1251,35 +1397,9 @@ describe("JantComposeEditor", () => {
1251
1397
 
1252
1398
  it("moves attachments later with keyboard controls", async () => {
1253
1399
  const el = await createElement("note");
1254
- const blob = new Blob(["fake"], { type: "image/png" });
1255
- const file = new File([blob], "test.png", { type: "image/png" });
1256
1400
  el._attachments = [
1257
- {
1258
- clientId: "a1",
1259
- file,
1260
- previewUrl: URL.createObjectURL(blob),
1261
- status: "done",
1262
- progress: null,
1263
- mediaId: "m1",
1264
- alt: "",
1265
- error: null,
1266
- posterUrl: null,
1267
- summary: null,
1268
- chars: null,
1269
- },
1270
- {
1271
- clientId: "a2",
1272
- file,
1273
- previewUrl: URL.createObjectURL(blob),
1274
- status: "done",
1275
- progress: null,
1276
- mediaId: "m2",
1277
- alt: "",
1278
- error: null,
1279
- posterUrl: null,
1280
- summary: null,
1281
- chars: null,
1282
- },
1401
+ mediaAttachment("a1", "m1"),
1402
+ mediaAttachment("a2", "m2"),
1283
1403
  ];
1284
1404
  el._attachmentOrder = ["a1", "a2"];
1285
1405
  await el.updateComplete;
@@ -1301,6 +1421,127 @@ describe("JantComposeEditor", () => {
1301
1421
  expect(el._attachmentOrder).toEqual(["a2", "a1"]);
1302
1422
  });
1303
1423
 
1424
+ it("reorders attachments with the move buttons", async () => {
1425
+ const el = await createElement("note");
1426
+ el._attachments = [
1427
+ mediaAttachment("a1", "m1"),
1428
+ mediaAttachment("a2", "m2"),
1429
+ mediaAttachment("a3", "m3"),
1430
+ ];
1431
+ el._attachmentOrder = ["a1", "a2", "a3"];
1432
+ await el.updateComplete;
1433
+
1434
+ moveButton(el, "a1", "later").click();
1435
+ await el.updateComplete;
1436
+ expect(el._attachmentOrder).toEqual(["a2", "a1", "a3"]);
1437
+
1438
+ moveButton(el, "a1", "earlier").click();
1439
+ await el.updateComplete;
1440
+ expect(el._attachmentOrder).toEqual(["a1", "a2", "a3"]);
1441
+ });
1442
+
1443
+ it("disables the move button at each end of the strip", async () => {
1444
+ const el = await createElement("note");
1445
+ el._attachments = [
1446
+ mediaAttachment("a1", "m1"),
1447
+ mediaAttachment("a2", "m2"),
1448
+ ];
1449
+ el._attachmentOrder = ["a1", "a2"];
1450
+ await el.updateComplete;
1451
+
1452
+ expect(moveButton(el, "a1", "earlier").disabled).toBe(true);
1453
+ expect(moveButton(el, "a1", "later").disabled).toBe(false);
1454
+ expect(moveButton(el, "a2", "earlier").disabled).toBe(false);
1455
+ expect(moveButton(el, "a2", "later").disabled).toBe(true);
1456
+ });
1457
+
1458
+ it("keeps focus on the attachment that moved", async () => {
1459
+ const el = await createElement("note");
1460
+ el._attachments = [
1461
+ mediaAttachment("a1", "m1"),
1462
+ mediaAttachment("a2", "m2"),
1463
+ mediaAttachment("a3", "m3"),
1464
+ ];
1465
+ el._attachmentOrder = ["a1", "a2", "a3"];
1466
+ await el.updateComplete;
1467
+
1468
+ const later = moveButton(el, "a1", "later");
1469
+ later.focus();
1470
+ later.click();
1471
+ await el.updateComplete;
1472
+
1473
+ expect(document.activeElement).toBe(moveButton(el, "a1", "later"));
1474
+ });
1475
+
1476
+ it("falls back to the opposite button when a move reaches the end", async () => {
1477
+ const el = await createElement("note");
1478
+ el._attachments = [
1479
+ mediaAttachment("a1", "m1"),
1480
+ mediaAttachment("a2", "m2"),
1481
+ ];
1482
+ el._attachmentOrder = ["a1", "a2"];
1483
+ await el.updateComplete;
1484
+
1485
+ const later = moveButton(el, "a1", "later");
1486
+ later.focus();
1487
+ later.click();
1488
+ await el.updateComplete;
1489
+
1490
+ expect(el._attachmentOrder).toEqual(["a2", "a1"]);
1491
+ expect(moveButton(el, "a1", "later").disabled).toBe(true);
1492
+ expect(document.activeElement).toBe(moveButton(el, "a1", "earlier"));
1493
+ });
1494
+
1495
+ it("omits move buttons when there is nothing to reorder", async () => {
1496
+ const el = await createElement("note");
1497
+ el._attachments = [mediaAttachment("a1", "m1")];
1498
+ el._attachmentOrder = ["a1"];
1499
+ await el.updateComplete;
1500
+
1501
+ expect(el.querySelector("[data-attachment-move]")).toBeNull();
1502
+ expect(el.querySelector(".compose-attachment-alt")).not.toBeNull();
1503
+ });
1504
+
1505
+ it("leaves drag reordering off on a touch device", async () => {
1506
+ const create = vi.spyOn(Sortable, "create");
1507
+ const restorePointer = stubPrimaryPointer("coarse");
1508
+ try {
1509
+ const el = await createElement("note");
1510
+ el._attachments = [
1511
+ mediaAttachment("a1", "m1"),
1512
+ mediaAttachment("a2", "m2"),
1513
+ ];
1514
+ el._attachmentOrder = ["a1", "a2"];
1515
+ await el.updateComplete;
1516
+
1517
+ expect(create).not.toHaveBeenCalled();
1518
+ // The buttons are the whole reordering path there, so they must stay.
1519
+ expect(moveButton(el, "a1", "later").disabled).toBe(false);
1520
+ } finally {
1521
+ restorePointer();
1522
+ create.mockRestore();
1523
+ }
1524
+ });
1525
+
1526
+ it("keeps drag reordering on a pointer device", async () => {
1527
+ const create = vi.spyOn(Sortable, "create");
1528
+ const restorePointer = stubPrimaryPointer("fine");
1529
+ try {
1530
+ const el = await createElement("note");
1531
+ el._attachments = [
1532
+ mediaAttachment("a1", "m1"),
1533
+ mediaAttachment("a2", "m2"),
1534
+ ];
1535
+ el._attachmentOrder = ["a1", "a2"];
1536
+ await el.updateComplete;
1537
+
1538
+ expect(create).toHaveBeenCalledTimes(1);
1539
+ } finally {
1540
+ restorePointer();
1541
+ create.mockRestore();
1542
+ }
1543
+ });
1544
+
1304
1545
  it("preserves mixed attachment order when populate provides one", async () => {
1305
1546
  const el = await createElement("note");
1306
1547
 
@@ -1343,7 +1584,7 @@ describe("JantComposeEditor", () => {
1343
1584
  expect(el._attachmentOrder[0]).toBe("t1");
1344
1585
  });
1345
1586
 
1346
- it("pastes clipboard files into attachments when no visible title is set", async () => {
1587
+ it("pastes clipboard files into attachments when no title is set", async () => {
1347
1588
  const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1348
1589
  const el = await createElement("note");
1349
1590
  const events: CustomEvent[] = [];
@@ -1379,7 +1620,6 @@ describe("JantComposeEditor", () => {
1379
1620
  id: "med_test",
1380
1621
  });
1381
1622
  const el = await createElement("note");
1382
- el._showTitle = true;
1383
1623
  el._title = "Essay";
1384
1624
  await el.updateComplete;
1385
1625
 
@@ -1399,13 +1639,29 @@ describe("JantComposeEditor", () => {
1399
1639
  ]);
1400
1640
  });
1401
1641
 
1402
- it("pastes images as attachments when the title row was toggled off after typing", async () => {
1642
+ it("pastes images as attachments when only whitespace is in the title", async () => {
1403
1643
  const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1404
1644
  const el = await createElement("note");
1405
- el._showTitle = true;
1406
- el._title = "Essay";
1645
+ el._title = " ";
1407
1646
  await el.updateComplete;
1408
- el._showTitle = false;
1647
+
1648
+ const image = new File(["image"], "clipboard.png", { type: "image/png" });
1649
+ const { handled } = triggerEditorPaste(el, [image]);
1650
+ await el.updateComplete;
1651
+
1652
+ expect(handled).toBe(true);
1653
+ expect(uploadWithMetadataMock).not.toHaveBeenCalled();
1654
+ expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
1655
+ "clipboard.png",
1656
+ ]);
1657
+ });
1658
+
1659
+ it("pastes images as attachments in a link, title or no title", async () => {
1660
+ const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1661
+ const el = await createElement("link");
1662
+ // A link's title is required to publish, so it is filled in every link
1663
+ // worth pasting into — it can't be the signal that picks inline.
1664
+ el._title = "An article worth linking";
1409
1665
  await el.updateComplete;
1410
1666
 
1411
1667
  const image = new File(["image"], "clipboard.png", { type: "image/png" });
@@ -1419,11 +1675,12 @@ describe("JantComposeEditor", () => {
1419
1675
  ]);
1420
1676
  });
1421
1677
 
1422
- it("pastes images as attachments when only whitespace is in the title", async () => {
1678
+ it("pastes images as attachments in a quote carrying a title from a format switch", async () => {
1423
1679
  const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1424
- const el = await createElement("note");
1425
- el._showTitle = true;
1426
- el._title = " ";
1680
+ const el = await createElement("quote");
1681
+ // A quote has no title field, but switching over from a note leaves one
1682
+ // behind. Nothing the author can see may decide where the image lands.
1683
+ el._title = "Essay";
1427
1684
  await el.updateComplete;
1428
1685
 
1429
1686
  const image = new File(["image"], "clipboard.png", { type: "image/png" });
@@ -1436,4 +1693,242 @@ describe("JantComposeEditor", () => {
1436
1693
  "clipboard.png",
1437
1694
  ]);
1438
1695
  });
1696
+
1697
+ it("carries an in-flight inline upload across a format switch", async () => {
1698
+ const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1699
+ let finishUpload: (value: { url: string; id: string }) => void = () => {};
1700
+ uploadWithMetadataMock.mockReturnValue(
1701
+ new Promise((resolve) => {
1702
+ finishUpload = resolve;
1703
+ }),
1704
+ );
1705
+
1706
+ const el = await createElement("note");
1707
+ el._title = "Essay";
1708
+ await el.updateComplete;
1709
+
1710
+ const image = new File(["image"], "clipboard.png", { type: "image/png" });
1711
+ triggerEditorPaste(el, [image]);
1712
+ await el.updateComplete;
1713
+ expect(imageSrcs(el).every((src) => src.startsWith("blob:"))).toBe(true);
1714
+ expect(imageSrcs(el)).toHaveLength(1);
1715
+
1716
+ // The body survives the switch, so the upload watching it has to survive
1717
+ // too — otherwise the blob: placeholder is what gets published.
1718
+ el.format = "quote";
1719
+ await el.updateComplete;
1720
+ await el.updateComplete;
1721
+
1722
+ finishUpload({
1723
+ url: "https://example.test/clipboard.webp",
1724
+ id: "med_test",
1725
+ });
1726
+ await vi.waitFor(() => {
1727
+ expect(imageSrcs(el)).toEqual(["https://example.test/clipboard.webp"]);
1728
+ });
1729
+ expect(bodyImageSrcs(el._bodyJson)).toEqual([
1730
+ "https://example.test/clipboard.webp",
1731
+ ]);
1732
+ });
1733
+
1734
+ it("drops images into a link as attachments, same as pasting them", async () => {
1735
+ const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1736
+ const el = await createElement("link");
1737
+ el._title = "An article worth linking";
1738
+ await el.updateComplete;
1739
+
1740
+ const image = new File(["image"], "dropped.png", { type: "image/png" });
1741
+ triggerEditorDrop(el, [image]);
1742
+ await el.updateComplete;
1743
+
1744
+ expect(uploadWithMetadataMock).not.toHaveBeenCalled();
1745
+ expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
1746
+ "dropped.png",
1747
+ ]);
1748
+ });
1749
+
1750
+ it("focuses the body end when blank space around the editor is clicked", async () => {
1751
+ const el = await createElement("note");
1752
+ const editor = requireEditor(el);
1753
+ editor.commands.setContent(
1754
+ tiptapDoc(tiptapParagraph("One two three"), tiptapParagraph("Tail")),
1755
+ );
1756
+ editor.commands.setTextSelection(1);
1757
+
1758
+ const blank = requireElement(
1759
+ el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
1760
+ "expected compose tiptap wrapper",
1761
+ );
1762
+ pressAndClick(blank, blank);
1763
+
1764
+ expect(editor.state.selection.empty).toBe(true);
1765
+ expect(editor.state.selection.from).toBe(editor.state.doc.content.size - 1);
1766
+ });
1767
+
1768
+ it("keeps a drag-selection that lifts off the text onto blank space", async () => {
1769
+ const el = await createElement("note");
1770
+ const editor = requireEditor(el);
1771
+ editor.commands.setContent(
1772
+ tiptapDoc(
1773
+ {
1774
+ type: "blockquote",
1775
+ content: [tiptapParagraph("One two three four five six")],
1776
+ },
1777
+ tiptapParagraph("Tail after the quote"),
1778
+ ),
1779
+ );
1780
+ // Right-to-left drag across the quote: anchor after "six", head before "One".
1781
+ editor.commands.setTextSelection({ from: 2, to: 29 });
1782
+
1783
+ const quote = requireElement(
1784
+ el.querySelector<HTMLElement>(".compose-tiptap-body blockquote p"),
1785
+ "expected blockquote paragraph",
1786
+ );
1787
+ const blank = requireElement(
1788
+ el.querySelector<HTMLElement>(".compose-tiptap-wrap"),
1789
+ "expected compose tiptap wrapper",
1790
+ );
1791
+ // The pointer went down on the text and came up over the wrapper, so the
1792
+ // browser retargets the click to their common ancestor.
1793
+ pressAndClick(quote, blank, blank);
1794
+
1795
+ expect(editor.state.selection.from).toBe(2);
1796
+ expect(editor.state.selection.to).toBe(29);
1797
+ });
1798
+ });
1799
+
1800
+ describe("JantComposeEditor.takeOver", () => {
1801
+ beforeEach(() => {
1802
+ document.body.innerHTML = "";
1803
+ vi.clearAllMocks();
1804
+ });
1805
+
1806
+ /** An editor the dialog is about to render in place of another one. */
1807
+ function unrenderedEditor(format: "note" | "link" | "quote") {
1808
+ const el = document.createElement(
1809
+ "jant-compose-editor",
1810
+ ) as JantComposeEditor;
1811
+ el.format = format;
1812
+ el.labels = labels;
1813
+ document.body.appendChild(el);
1814
+ return el;
1815
+ }
1816
+
1817
+ it("hands every post field to the editor that replaces it", async () => {
1818
+ // One sample per field the editor holds for its post. The first assertion
1819
+ // keeps this table complete: a field the editor gains without a way across
1820
+ // is how a quote's source link used to vanish on "Add to thread".
1821
+ const samples: Record<string, unknown> = {
1822
+ _title: "Left behind by a format switch",
1823
+ _showTitle: false,
1824
+ _bodyJson: tiptapDoc(tiptapParagraph("My thoughts on it")),
1825
+ _url: "https://example.com/source",
1826
+ _quoteText: "A borrowed sentence.",
1827
+ _quoteAuthor: "Someone Else",
1828
+ _rating: 4,
1829
+ _showRating: true,
1830
+ _attachedTexts: [
1831
+ {
1832
+ clientId: "text-1",
1833
+ bodyJson: tiptapDoc(tiptapParagraph("Attached")),
1834
+ bodyHtml: "",
1835
+ summary: "Attached",
1836
+ },
1837
+ ],
1838
+ _attachments: [mediaAttachment("media-1", "med_1")],
1839
+ _attachmentOrder: ["text-1", "media-1"],
1840
+ };
1841
+ const contentProps = (
1842
+ customElements.get("jant-compose-editor") as unknown as {
1843
+ _CONTENT_PROPS: Set<string>;
1844
+ }
1845
+ )._CONTENT_PROPS;
1846
+ expect(Object.keys(samples).sort()).toEqual(
1847
+ [...contentProps, "_attachments"].sort(),
1848
+ );
1849
+
1850
+ const previous = await createElement("quote");
1851
+ for (const [key, value] of Object.entries(samples)) {
1852
+ if (key === "_bodyJson") continue;
1853
+ (previous as unknown as Record<string, unknown>)[key] = value;
1854
+ }
1855
+ requireEditor(previous).commands.setContent(
1856
+ samples._bodyJson as JSONContent,
1857
+ );
1858
+ await previous.updateComplete;
1859
+ // The dialog swaps elements before handing over, so the old one has
1860
+ // already left the DOM — and taken its TipTap instance with it.
1861
+ previous.remove();
1862
+
1863
+ const next = await createElement("quote");
1864
+ next.takeOver(previous);
1865
+ await next.updateComplete;
1866
+
1867
+ for (const [key, value] of Object.entries(samples)) {
1868
+ if (key === "_bodyJson") continue;
1869
+ expect((next as unknown as Record<string, unknown>)[key], key).toEqual(
1870
+ value,
1871
+ );
1872
+ }
1873
+ expect(next.getNormalizedBodyJson()).toEqual(
1874
+ previous.getNormalizedBodyJson(),
1875
+ );
1876
+ expect(requireEditor(next).getText()).toBe("My thoughts on it");
1877
+ expect(next.getData().url).toBe("https://example.com/source");
1878
+ });
1879
+
1880
+ it("keeps a handed-over title toggle through its first render", async () => {
1881
+ const previous = await createElement("note", { titleByDefault: true });
1882
+ previous._showTitle = false;
1883
+ previous.remove();
1884
+
1885
+ const next = unrenderedEditor("note");
1886
+ next.titleByDefault = true;
1887
+ next.takeOver(previous);
1888
+ await next.updateComplete;
1889
+
1890
+ expect(next._showTitle).toBe(false);
1891
+ });
1892
+
1893
+ it.each([
1894
+ ["after", true],
1895
+ ["before", false],
1896
+ ])(
1897
+ "carries an in-flight inline upload to an editor taking over %s its first render",
1898
+ async (_when, renderFirst) => {
1899
+ const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1900
+ let finishUpload: (value: { url: string; id: string }) => void = () => {};
1901
+ uploadWithMetadataMock.mockReturnValue(
1902
+ new Promise((resolve) => {
1903
+ finishUpload = resolve;
1904
+ }),
1905
+ );
1906
+
1907
+ const previous = await createElement("note");
1908
+ previous._title = "Essay";
1909
+ await previous.updateComplete;
1910
+ const image = new File(["image"], "clipboard.png", { type: "image/png" });
1911
+ triggerEditorPaste(previous, [image]);
1912
+ await previous.updateComplete;
1913
+ expect(imageSrcs(previous)).toHaveLength(1);
1914
+ expect(imageSrcs(previous)[0]).toMatch(/^blob:/);
1915
+ previous.remove();
1916
+
1917
+ const next = unrenderedEditor("note");
1918
+ if (renderFirst) await next.updateComplete;
1919
+ next.takeOver(previous);
1920
+ await next.updateComplete;
1921
+ expect(imageSrcs(next)[0]).toMatch(/^blob:/);
1922
+
1923
+ finishUpload({ url: "https://example.test/clipboard.webp", id: "med_1" });
1924
+ await vi.waitFor(() => {
1925
+ expect(imageSrcs(next)).toEqual([
1926
+ "https://example.test/clipboard.webp",
1927
+ ]);
1928
+ });
1929
+ expect(bodyImageSrcs(next._bodyJson)).toEqual([
1930
+ "https://example.test/clipboard.webp",
1931
+ ]);
1932
+ },
1933
+ );
1439
1934
  });