@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
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { LitElement, html, nothing } from "lit";
12
+ import type { TemplateResult } from "lit";
12
13
  import { classMap } from "lit/directives/class-map.js";
13
14
  import { ifDefined } from "lit/directives/if-defined.js";
14
15
  import { unsafeSVG } from "lit/directives/unsafe-svg.js";
@@ -36,11 +37,7 @@ import {
36
37
  promoteLeadingH1Title as getLeadingH1TitlePromotion,
37
38
  type LeadingH1TitlePromotion,
38
39
  } from "./compose-title-from-heading.js";
39
- import {
40
- UPLOAD_ACCEPT,
41
- getMediaCategory,
42
- validateUploadFile,
43
- } from "../../lib/upload.js";
40
+ import { getMediaCategory, validateUploadFile } from "../../lib/upload.js";
44
41
  import type { MediaCategory } from "../../lib/upload.js";
45
42
  import {
46
43
  DECORATIVE_QUOTE_MARK_SVG_CONTENT,
@@ -70,6 +67,13 @@ interface ComposeFilePickerCloseDetail {
70
67
  cancelled: boolean;
71
68
  }
72
69
 
70
+ /**
71
+ * Which control a reordered attachment hands focus back to. The strip renders
72
+ * positionally, so without this the focused DOM slot silently retargets a
73
+ * different attachment after every move.
74
+ */
75
+ type AttachmentFocusTarget = "handle" | "move";
76
+
73
77
  const COMPOSE_TOOLBAR_ICONS = {
74
78
  media: `
75
79
  <rect x="2.75" y="3" width="12.5" height="11.25" rx="3" />
@@ -281,12 +285,24 @@ export class JantComposeEditor extends LitElement {
281
285
  * to run this cycle.
282
286
  */
283
287
  private _suppressContentChangedOnce = false;
288
+ /**
289
+ * Set when the title toggle arrives with the post rather than from the
290
+ * owner's default — a handed-over editor keeps the answer it was given
291
+ * instead of having the first update reseed it from `titleByDefault`.
292
+ */
293
+ #showTitleHandedOver = false;
284
294
  #inlineImageUploadGeneration = 0;
285
295
  #inlineImageUploadPromises = new Set<Promise<void>>();
286
296
  #rehostFailureCount = 0;
287
297
  #rehostFailureNoticeTimer: ReturnType<typeof setTimeout> | undefined;
288
298
  #sortable: { destroy(): void } | null = null;
289
299
  #revertNextSibling: globalThis.Node | null = null;
300
+ #focusAfterMove: {
301
+ clientId: string;
302
+ direction: -1 | 1;
303
+ focusTarget: AttachmentFocusTarget;
304
+ } | null = null;
305
+ #coarsePointerQuery: globalThis.MediaQueryList | null | undefined;
290
306
 
291
307
  createRenderRoot() {
292
308
  return this;
@@ -338,6 +354,10 @@ export class JantComposeEditor extends LitElement {
338
354
  this.addEventListener("dragover", this._onDragOver, true);
339
355
  this.addEventListener("dragleave", this._onDragLeave);
340
356
  this.addEventListener("drop", this._onDrop);
357
+ this.#coarsePointer()?.addEventListener(
358
+ "change",
359
+ this._onPointerTypeChange,
360
+ );
341
361
  }
342
362
 
343
363
  disconnectedCallback() {
@@ -362,6 +382,10 @@ export class JantComposeEditor extends LitElement {
362
382
  this._emojiPickerEl = null;
363
383
  this._filePickerCleanup?.();
364
384
  this._filePickerCleanup = null;
385
+ this.#coarsePointer()?.removeEventListener(
386
+ "change",
387
+ this._onPointerTypeChange,
388
+ );
365
389
  }
366
390
 
367
391
  // Tracks dragenter/dragleave nesting so the highlight only clears when the
@@ -1025,20 +1049,6 @@ export class JantComposeEditor extends LitElement {
1025
1049
  return this._title.trim() ? null : this.labels.linkTitleRequired;
1026
1050
  }
1027
1051
 
1028
- private _getInlineUrlValidationMessage(): string | null {
1029
- if (this.format === "link" && !this._url.trim()) {
1030
- return null;
1031
- }
1032
- return this.getUrlValidationMessage();
1033
- }
1034
-
1035
- private _getInlineLinkTitleValidationMessage(): string | null {
1036
- if (this.format !== "link" || !this._title.trim()) {
1037
- return null;
1038
- }
1039
- return this.getLinkTitleValidationMessage();
1040
- }
1041
-
1042
1052
  revealUrlValidation(): string | null {
1043
1053
  this._showUrlValidation = true;
1044
1054
  return this.getUrlValidationMessage();
@@ -1122,6 +1132,12 @@ export class JantComposeEditor extends LitElement {
1122
1132
  tableControlLabels: this.labels.tableControls,
1123
1133
  });
1124
1134
  this._lastEditorSelection = this._readEditorSelection();
1135
+ // An editor built from existing content — after a format switch, or on a
1136
+ // replacement element that took over a post — inherits the uploads still
1137
+ // working on that content. Without this the `blob:` placeholder is left
1138
+ // with nothing watching it: the finished upload has no editor to write
1139
+ // into, and publishing puts the blob URL in the post.
1140
+ this.adoptPendingUploads();
1125
1141
  }
1126
1142
 
1127
1143
  private _destroyEditor() {
@@ -1150,7 +1166,7 @@ export class JantComposeEditor extends LitElement {
1150
1166
  // `titleByDefault` is an owner-supplied prop, so it is only known once the
1151
1167
  // first update runs. Seed the toggle from it there and leave it alone after
1152
1168
  // that — from then on the value belongs to the user (and to `reset()`).
1153
- if (!this.hasUpdated) {
1169
+ if (!this.hasUpdated && !this.#showTitleHandedOver) {
1154
1170
  this._showTitle = this.titleByDefault;
1155
1171
  }
1156
1172
  }
@@ -1190,6 +1206,7 @@ export class JantComposeEditor extends LitElement {
1190
1206
  this.#sortable?.destroy();
1191
1207
  this.#sortable = null;
1192
1208
  }
1209
+ this.#restoreAttachmentFocus();
1193
1210
  }
1194
1211
 
1195
1212
  // Every field that can change this row's answer is reactive, so `updated()`
@@ -1257,6 +1274,63 @@ export class JantComposeEditor extends LitElement {
1257
1274
  this._bodyJson = fields.bodyJson;
1258
1275
  }
1259
1276
 
1277
+ /**
1278
+ * Take over everything another editor holds for its post.
1279
+ *
1280
+ * Entering or leaving thread mode renders a different editor element for the
1281
+ * same post, and the element being replaced has the only copy of what was
1282
+ * written. Every field that belongs to the post moves here, in this one
1283
+ * place: a caller picking fields out by hand drops whichever one it forgets,
1284
+ * which is how a quote's source link used to vanish on "Add to thread".
1285
+ * Fields hidden by the current format move too, so switching back still
1286
+ * finds them.
1287
+ *
1288
+ * Safe before or after this editor's first render, and on a `previous` that
1289
+ * has already left the DOM — its TipTap instance is gone by then, but
1290
+ * `_bodyJson` tracks every edit.
1291
+ *
1292
+ * @param previous - The editor this one replaces
1293
+ * @example
1294
+ * await dialog.updateComplete;
1295
+ * threadRootEditor.takeOver(singlePostEditor);
1296
+ */
1297
+ takeOver(previous: JantComposeEditor) {
1298
+ this._title = previous._title;
1299
+ this._showTitle = previous._showTitle;
1300
+ this.#showTitleHandedOver = true;
1301
+ this._bodyJson = previous._editor?.getJSON() ?? previous._bodyJson;
1302
+ this._url = previous._url;
1303
+ this._quoteText = previous._quoteText;
1304
+ this._quoteAuthor = previous._quoteAuthor;
1305
+ this._rating = previous._rating;
1306
+ this._showRating = previous._showRating;
1307
+ this._attachedTexts = [...previous._attachedTexts];
1308
+ this._attachments = [...previous._attachments];
1309
+ this._attachmentOrder = [...previous._attachmentOrder];
1310
+ this._lastEditorSelection = previous.getEditorSelection();
1311
+ // Before the first render there is no TipTap instance yet; `_initEditor`
1312
+ // builds it from `_bodyJson` and adopts the uploads then.
1313
+ if (this._editor) {
1314
+ this._editor.commands.setContent(
1315
+ this._bodyJson ?? { type: "doc", content: [{ type: "paragraph" }] },
1316
+ );
1317
+ this.adoptPendingUploads();
1318
+ }
1319
+ this.#notifyStatus();
1320
+ }
1321
+
1322
+ /**
1323
+ * Whether this editor holds the attachment an upload is working on.
1324
+ *
1325
+ * @param clientId - The attachment's client-side id
1326
+ * @returns True when the attachment strip includes it
1327
+ * @example
1328
+ * editor.hasAttachment(clientId); // false once the post moved to another editor
1329
+ */
1330
+ hasAttachment(clientId: string): boolean {
1331
+ return this._attachments.some((a) => a.clientId === clientId);
1332
+ }
1333
+
1260
1334
  /** Pre-fill all fields for edit mode or draft restore */
1261
1335
  populate(data: {
1262
1336
  format: string;
@@ -1441,7 +1515,11 @@ export class JantComposeEditor extends LitElement {
1441
1515
  );
1442
1516
  }
1443
1517
 
1444
- private _moveAttachment(clientId: string, direction: -1 | 1) {
1518
+ private _moveAttachment(
1519
+ clientId: string,
1520
+ direction: -1 | 1,
1521
+ focusTarget: AttachmentFocusTarget = "handle",
1522
+ ) {
1445
1523
  const index = this._attachmentOrder.indexOf(clientId);
1446
1524
  const nextIndex = index + direction;
1447
1525
  if (
@@ -1457,9 +1535,50 @@ export class JantComposeEditor extends LitElement {
1457
1535
  if (!item) return;
1458
1536
  nextOrder.splice(nextIndex, 0, item);
1459
1537
  this._attachmentOrder = nextOrder;
1538
+ this.#focusAfterMove = { clientId, direction, focusTarget };
1460
1539
  this.#scrollAttachmentIntoView(clientId);
1461
1540
  }
1462
1541
 
1542
+ /**
1543
+ * Returns focus to the attachment that just moved.
1544
+ *
1545
+ * Lit reuses the strip's DOM slots, so the element that held focus now
1546
+ * renders a different attachment. Called from `updated()`, once the new
1547
+ * order is in the DOM.
1548
+ */
1549
+ #restoreAttachmentFocus() {
1550
+ const pending = this.#focusAfterMove;
1551
+ if (!pending) return;
1552
+ this.#focusAfterMove = null;
1553
+
1554
+ const item = this.querySelector<HTMLElement>(
1555
+ `[data-attachment-id="${pending.clientId}"]`,
1556
+ );
1557
+ if (!item) return;
1558
+
1559
+ if (pending.focusTarget === "handle") {
1560
+ item
1561
+ .querySelector<HTMLElement>("[data-attachment-sortable]")
1562
+ ?.focus({ preventScroll: true });
1563
+ return;
1564
+ }
1565
+
1566
+ // The pressed button turns disabled at either end of the strip, and a
1567
+ // disabled button cannot hold focus — fall back to its twin so the
1568
+ // keyboard path never dead-ends.
1569
+ const target =
1570
+ this.#findMoveButton(item, pending.direction) ??
1571
+ this.#findMoveButton(item, pending.direction === 1 ? -1 : 1);
1572
+ target?.focus({ preventScroll: true });
1573
+ }
1574
+
1575
+ #findMoveButton(item: HTMLElement, direction: -1 | 1) {
1576
+ const button = item.querySelector<HTMLButtonElement>(
1577
+ `[data-attachment-move="${direction === -1 ? "earlier" : "later"}"]`,
1578
+ );
1579
+ return button && !button.disabled ? button : null;
1580
+ }
1581
+
1463
1582
  private _handleAttachmentKeydown(
1464
1583
  clientId: string,
1465
1584
  e: globalThis.KeyboardEvent,
@@ -1483,13 +1602,45 @@ export class JantComposeEditor extends LitElement {
1483
1602
  }
1484
1603
  }
1485
1604
 
1605
+ /**
1606
+ * Matches when the primary pointer is a finger.
1607
+ *
1608
+ * Cached because `matchMedia` allocates a new list each call, and `undefined`
1609
+ * distinguishes "not looked up yet" from "this environment has no
1610
+ * `matchMedia`".
1611
+ */
1612
+ #coarsePointer(): globalThis.MediaQueryList | null {
1613
+ if (this.#coarsePointerQuery !== undefined) return this.#coarsePointerQuery;
1614
+ this.#coarsePointerQuery =
1615
+ typeof window.matchMedia === "function"
1616
+ ? window.matchMedia("(pointer: coarse)")
1617
+ : null;
1618
+ return this.#coarsePointerQuery;
1619
+ }
1620
+
1621
+ private _onPointerTypeChange = () => {
1622
+ this.#sortable?.destroy();
1623
+ this.#sortable = null;
1624
+ this.#initSortable();
1625
+ };
1626
+
1486
1627
  #initSortable() {
1487
1628
  const list = this.querySelector<HTMLElement>("[data-attachment-list]");
1488
1629
  if (!list || this.#sortable || this._attachmentOrder.length <= 1) return;
1489
1630
 
1631
+ // Touch reorders through the move buttons, never by dragging. This strip
1632
+ // scrolls along the axis it would drag, and a near-full-width thumbnail's
1633
+ // drop target sits off-screen — so on a touch device the gesture is worth
1634
+ // more as a plain scroll than as a reorder.
1635
+ if (this.#coarsePointer()?.matches) return;
1636
+
1490
1637
  this.#sortable = Sortable.create(list, {
1491
1638
  ...responsiveSortableOptions,
1492
1639
  chosenClass: "compose-attachment-chosen",
1640
+ // Only reachable on a fine-pointer device that also has a touchscreen,
1641
+ // where the scroll-versus-drag conflict above still applies to touch.
1642
+ // Longer than the shared default so an ambiguous gesture scrolls.
1643
+ delay: 250,
1493
1644
  direction: "horizontal",
1494
1645
  dragClass: "compose-attachment-drag",
1495
1646
  filter:
@@ -1637,6 +1788,24 @@ export class JantComposeEditor extends LitElement {
1637
1788
  el.style.height = `${el.scrollHeight}px`;
1638
1789
  }
1639
1790
 
1791
+ /**
1792
+ * Where the current pointer gesture over the body went down and came up. A
1793
+ * `click` reports the nearest common ancestor of the two, so these are the
1794
+ * only way to tell a press on blank paper from a drag that merely started or
1795
+ * ended there.
1796
+ */
1797
+ #bodyPressTarget: globalThis.EventTarget | null = null;
1798
+ #bodyReleaseTarget: globalThis.EventTarget | null = null;
1799
+
1800
+ private _recordBodyPress = (e: MouseEvent) => {
1801
+ this.#bodyPressTarget = e.target;
1802
+ this.#bodyReleaseTarget = null;
1803
+ };
1804
+
1805
+ private _recordBodyRelease = (e: MouseEvent) => {
1806
+ this.#bodyReleaseTarget = e.target;
1807
+ };
1808
+
1640
1809
  /**
1641
1810
  * Puts the caret in the body when the click landed on blank section instead
1642
1811
  * of on the editor. The surfaces that reserve height — the `/new` page, the
@@ -1644,8 +1813,20 @@ export class JantComposeEditor extends LitElement {
1644
1813
  * editor, and it should behave like paper: click it, write there. Only the
1645
1814
  * layout wrappers forward; a click that reached a real control belongs to
1646
1815
  * that control.
1816
+ *
1817
+ * Selecting a line right-to-left normally lifts the pointer off the text and
1818
+ * over one of those wrappers, and the resulting click is retargeted to the
1819
+ * wrapper — indistinguishable from a click on blank paper by its target
1820
+ * alone. Requiring the press and the release to have landed on that same
1821
+ * blank space keeps a drag-selection from being thrown away.
1647
1822
  */
1648
1823
  private _focusBodyFromBlankSpace = (e: MouseEvent) => {
1824
+ const pressTarget = this.#bodyPressTarget;
1825
+ const releaseTarget = this.#bodyReleaseTarget;
1826
+ this.#bodyPressTarget = null;
1827
+ this.#bodyReleaseTarget = null;
1828
+ if (pressTarget !== e.target || releaseTarget !== e.target) return;
1829
+
1649
1830
  const target = e.target as HTMLElement | null;
1650
1831
  if (!target) return;
1651
1832
  const forwards =
@@ -1660,9 +1841,25 @@ export class JantComposeEditor extends LitElement {
1660
1841
  this._rating = this._rating === star ? 0 : star;
1661
1842
  }
1662
1843
 
1844
+ /**
1845
+ * Where a pasted or dropped image lands: inline in the body, or in the
1846
+ * attachment dock.
1847
+ *
1848
+ * Only a note has an article mode to fall into, and a visible, filled title
1849
+ * is what says the body is prose an image belongs inside. A link's body is
1850
+ * commentary beside a link card and a quote's is commentary beside the
1851
+ * quotation — images there are attachments, always. `_title` couldn't answer
1852
+ * this for them anyway: a link's title is required, so it is never empty
1853
+ * once the post is publishable, and a quote has no title field at all yet
1854
+ * can still be holding one carried over from a format switch.
1855
+ *
1856
+ * Only the default moves — the slash `/image` command inserts inline in
1857
+ * every format.
1858
+ */
1663
1859
  private _shouldPasteInlineImage(file: File): boolean {
1664
1860
  if (!file.type.startsWith("image/")) return false;
1665
- if (this.format === "note" && !this._showTitle) return false;
1861
+ if (this.format !== "note") return false;
1862
+ if (!this._showTitle) return false;
1666
1863
  return this._title.trim().length > 0;
1667
1864
  }
1668
1865
 
@@ -1670,7 +1867,9 @@ export class JantComposeEditor extends LitElement {
1670
1867
  if (!this._fileInput) {
1671
1868
  this._fileInput = document.createElement("input");
1672
1869
  this._fileInput.type = "file";
1673
- this._fileInput.accept = UPLOAD_ACCEPT;
1870
+ // No `accept`: the composer takes any file, and leaving the attribute
1871
+ // off is how HTML says so. `*/*` is not a valid accept token, so every
1872
+ // browser had to work out what it meant before the dialog could open.
1674
1873
  this._fileInput.multiple = true;
1675
1874
  this._fileInput.style.display = "none";
1676
1875
  this._fileInput.addEventListener("change", () =>
@@ -2088,15 +2287,17 @@ export class JantComposeEditor extends LitElement {
2088
2287
  <span class="compose-attachment-preview-fallback-label">
2089
2288
  ${unavailableLabel}
2090
2289
  </span>
2091
- ${category === "video"
2092
- ? html`
2093
- <div class="compose-attachment-play-icon">
2094
- <svg width="24" height="24" viewBox="0 0 24 24" fill="white">
2095
- <path d="M8 5v14l11-7z" />
2096
- </svg>
2097
- </div>
2098
- `
2099
- : nothing}
2290
+ ${
2291
+ category === "video"
2292
+ ? html`
2293
+ <div class="compose-attachment-play-icon">
2294
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="white">
2295
+ <path d="M8 5v14l11-7z" />
2296
+ </svg>
2297
+ </div>
2298
+ `
2299
+ : nothing
2300
+ }
2100
2301
  </div>
2101
2302
  `;
2102
2303
  }
@@ -2106,17 +2307,19 @@ export class JantComposeEditor extends LitElement {
2106
2307
  private _renderNoteFields() {
2107
2308
  return html`
2108
2309
  <div class="compose-field-enter">
2109
- ${this._showTitle
2110
- ? html`<input
2111
- type="text"
2112
- .value=${this._title}
2113
- @input=${(e: Event) => this._onInput("_title", e)}
2114
- @focus=${(e: Event) => this._onFieldFocus(e)}
2115
- @keydown=${this._handleTitleKeydown}
2116
- class="compose-input compose-note-title"
2117
- placeholder=${this.labels.titlePlaceholder}
2118
- />`
2119
- : nothing}
2310
+ ${
2311
+ this._showTitle
2312
+ ? html`<input
2313
+ type="text"
2314
+ .value=${this._title}
2315
+ @input=${(e: Event) => this._onInput("_title", e)}
2316
+ @focus=${(e: Event) => this._onFieldFocus(e)}
2317
+ @keydown=${this._handleTitleKeydown}
2318
+ class="compose-input compose-note-title"
2319
+ placeholder=${this.labels.titlePlaceholder}
2320
+ />`
2321
+ : nothing
2322
+ }
2120
2323
  <div class="compose-tiptap-wrap">
2121
2324
  <div class="compose-tiptap-body"></div>
2122
2325
  <span class="compose-slash-discovery-hint" aria-hidden="true">
@@ -2129,10 +2332,10 @@ export class JantComposeEditor extends LitElement {
2129
2332
 
2130
2333
  private _renderLinkFields() {
2131
2334
  const urlError = this._showUrlValidation
2132
- ? this._getInlineUrlValidationMessage()
2335
+ ? this.getUrlValidationMessage()
2133
2336
  : null;
2134
2337
  const titleError = this._showLinkTitleValidation
2135
- ? this._getInlineLinkTitleValidationMessage()
2338
+ ? this.getLinkTitleValidationMessage()
2136
2339
  : null;
2137
2340
 
2138
2341
  return html`
@@ -2150,7 +2353,10 @@ export class JantComposeEditor extends LitElement {
2150
2353
  @input=${(e: Event) => this._onInput("_url", e)}
2151
2354
  @focus=${(e: Event) => this._onFieldFocus(e)}
2152
2355
  @blur=${() => {
2153
- this._showUrlValidation = true;
2356
+ // Leaving a field empty is not yet a mistake — it becomes one at
2357
+ // submit, where revealUrlValidation() opens this gate anyway.
2358
+ // Blur only speaks up about something already typed and wrong.
2359
+ if (this._url.trim()) this._showUrlValidation = true;
2154
2360
  }}
2155
2361
  class="compose-input compose-url-input"
2156
2362
  placeholder=${this.labels.urlPlaceholder}
@@ -2160,15 +2366,17 @@ export class JantComposeEditor extends LitElement {
2160
2366
  )}
2161
2367
  />
2162
2368
  </div>
2163
- ${urlError
2164
- ? html`<p
2165
- id=${this._urlStatusId}
2166
- class="compose-url-status compose-url-status-error"
2167
- data-compose-url-status="error"
2168
- >
2169
- ${urlError}
2170
- </p>`
2171
- : nothing}
2369
+ ${
2370
+ urlError
2371
+ ? html`<p
2372
+ id=${this._urlStatusId}
2373
+ class="compose-url-status compose-url-status-error"
2374
+ data-compose-url-status="error"
2375
+ >
2376
+ ${urlError}
2377
+ </p>`
2378
+ : nothing
2379
+ }
2172
2380
  <input
2173
2381
  type="text"
2174
2382
  .value=${this._title}
@@ -2176,20 +2384,23 @@ export class JantComposeEditor extends LitElement {
2176
2384
  @focus=${(e: Event) => this._onFieldFocus(e)}
2177
2385
  @keydown=${this._handleTitleKeydown}
2178
2386
  @blur=${() => {
2179
- this._showLinkTitleValidation = true;
2387
+ // Same gate as the URL above: quiet while empty, loud on submit.
2388
+ if (this._title.trim()) this._showLinkTitleValidation = true;
2180
2389
  }}
2181
2390
  class="compose-input compose-link-title"
2182
2391
  placeholder=${this.labels.linkTitlePlaceholder}
2183
2392
  aria-invalid=${titleError ? "true" : "false"}
2184
2393
  />
2185
- ${titleError
2186
- ? html`<p
2187
- class="compose-url-status compose-url-status-error"
2188
- data-compose-link-title-error="error"
2189
- >
2190
- ${titleError}
2191
- </p>`
2192
- : nothing}
2394
+ ${
2395
+ titleError
2396
+ ? html`<p
2397
+ class="compose-url-status compose-url-status-error"
2398
+ data-compose-link-title-error="error"
2399
+ >
2400
+ ${titleError}
2401
+ </p>`
2402
+ : nothing
2403
+ }
2193
2404
  <div class="compose-divider"></div>
2194
2405
  <div class="compose-tiptap-wrap">
2195
2406
  <div class="compose-tiptap-body compose-tiptap-link"></div>
@@ -2246,15 +2457,17 @@ export class JantComposeEditor extends LitElement {
2246
2457
  urlError ? this._urlStatusId : undefined,
2247
2458
  )}
2248
2459
  />
2249
- ${urlError
2250
- ? html`<p
2251
- id=${this._urlStatusId}
2252
- class="compose-url-status compose-url-status-error"
2253
- data-compose-url-status="error"
2254
- >
2255
- ${urlError}
2256
- </p>`
2257
- : nothing}
2460
+ ${
2461
+ urlError
2462
+ ? html`<p
2463
+ id=${this._urlStatusId}
2464
+ class="compose-url-status compose-url-status-error"
2465
+ data-compose-url-status="error"
2466
+ >
2467
+ ${urlError}
2468
+ </p>`
2469
+ : nothing
2470
+ }
2258
2471
  </div>
2259
2472
  <div
2260
2473
  class="compose-divider compose-divider-quote"
@@ -2289,9 +2502,11 @@ export class JantComposeEditor extends LitElement {
2289
2502
  </button>
2290
2503
  `,
2291
2504
  )}
2292
- ${this._rating > 0
2293
- ? html`<span class="compose-star-label">${this._rating}/5</span>`
2294
- : nothing}
2505
+ ${
2506
+ this._rating > 0
2507
+ ? html`<span class="compose-star-label">${this._rating}/5</span>`
2508
+ : nothing
2509
+ }
2295
2510
  </div>
2296
2511
  `;
2297
2512
  }
@@ -2365,16 +2580,20 @@ export class JantComposeEditor extends LitElement {
2365
2580
  ${this._renderFileIcon(a.file.type, 20)}
2366
2581
  </div>
2367
2582
  <span class="compose-attachment-file-name">${a.file.name}</span>
2368
- ${a.summary
2369
- ? html`<span class="compose-attachment-text-summary"
2370
- >${a.summary}</span
2371
- >`
2372
- : nothing}
2373
- ${typeof a.chars === "number" && a.chars > 0
2374
- ? html`<span class="compose-attachment-file-size"
2375
- >${this._formatChars(a.chars)}</span
2376
- >`
2377
- : nothing}
2583
+ ${
2584
+ a.summary
2585
+ ? html`<span class="compose-attachment-text-summary"
2586
+ >${a.summary}</span
2587
+ >`
2588
+ : nothing
2589
+ }
2590
+ ${
2591
+ typeof a.chars === "number" && a.chars > 0
2592
+ ? html`<span class="compose-attachment-file-size"
2593
+ >${this._formatChars(a.chars)}</span
2594
+ >`
2595
+ : nothing
2596
+ }
2378
2597
  </div>
2379
2598
  `;
2380
2599
  }
@@ -2404,46 +2623,52 @@ export class JantComposeEditor extends LitElement {
2404
2623
 
2405
2624
  private _renderAttachmentOverlay(a: ComposeAttachment, index: number) {
2406
2625
  return html`
2407
- ${a.status === "error"
2408
- ? html`
2409
- <button
2410
- type="button"
2411
- class="compose-attachment-overlay compose-attachment-retry"
2412
- @click=${(e: Event) => {
2413
- e.stopPropagation();
2414
- this._retryAllFailed();
2415
- }}
2416
- >
2417
- <span class="compose-retry-content">
2418
- <svg
2419
- width="20"
2420
- height="20"
2421
- viewBox="0 0 24 24"
2422
- fill="none"
2423
- stroke="currentColor"
2424
- stroke-width="2"
2425
- stroke-linecap="round"
2426
- stroke-linejoin="round"
2427
- >
2428
- <path
2429
- d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
2430
- />
2431
- <path d="M3 3v5h5" />
2432
- <path
2433
- d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"
2434
- />
2435
- <path d="M16 16h5v5" />
2436
- </svg>
2437
- <span class="compose-retry-label">${this.labels.retryAll}</span>
2438
- </span>
2439
- ${a.error
2440
- ? html`<span class="compose-attachment-error-msg"
2441
- >${a.error}</span
2442
- >`
2443
- : nothing}
2444
- </button>
2445
- `
2446
- : nothing}
2626
+ ${
2627
+ a.status === "error"
2628
+ ? html`
2629
+ <button
2630
+ type="button"
2631
+ class="compose-attachment-overlay compose-attachment-retry"
2632
+ @click=${(e: Event) => {
2633
+ e.stopPropagation();
2634
+ this._retryAllFailed();
2635
+ }}
2636
+ >
2637
+ <span class="compose-retry-content">
2638
+ <svg
2639
+ width="20"
2640
+ height="20"
2641
+ viewBox="0 0 24 24"
2642
+ fill="none"
2643
+ stroke="currentColor"
2644
+ stroke-width="2"
2645
+ stroke-linecap="round"
2646
+ stroke-linejoin="round"
2647
+ >
2648
+ <path
2649
+ d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
2650
+ />
2651
+ <path d="M3 3v5h5" />
2652
+ <path
2653
+ d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"
2654
+ />
2655
+ <path d="M16 16h5v5" />
2656
+ </svg>
2657
+ <span class="compose-retry-label"
2658
+ >${this.labels.retryAll}</span
2659
+ >
2660
+ </span>
2661
+ ${
2662
+ a.error
2663
+ ? html`<span class="compose-attachment-error-msg"
2664
+ >${a.error}</span
2665
+ >`
2666
+ : nothing
2667
+ }
2668
+ </button>
2669
+ `
2670
+ : nothing
2671
+ }
2447
2672
  ${this._renderAttachmentRemoveButton((e: Event) => {
2448
2673
  e.stopPropagation();
2449
2674
  this._removeAttachment(index);
@@ -2475,6 +2700,87 @@ export class JantComposeEditor extends LitElement {
2475
2700
  `;
2476
2701
  }
2477
2702
 
2703
+ private _renderAttachmentMoveButton(
2704
+ clientId: string,
2705
+ direction: -1 | 1,
2706
+ disabled: boolean,
2707
+ ) {
2708
+ const earlier = direction === -1;
2709
+ const label = earlier
2710
+ ? this.labels.moveAttachmentEarlier
2711
+ : this.labels.moveAttachmentLater;
2712
+
2713
+ return html`
2714
+ <button
2715
+ type="button"
2716
+ class="compose-attachment-move"
2717
+ data-attachment-move=${earlier ? "earlier" : "later"}
2718
+ ?disabled=${disabled}
2719
+ aria-label=${label}
2720
+ title=${label}
2721
+ @click=${(e: Event) => {
2722
+ e.stopPropagation();
2723
+ this._moveAttachment(clientId, direction, "move");
2724
+ }}
2725
+ >
2726
+ <svg
2727
+ width="14"
2728
+ height="14"
2729
+ viewBox="0 0 14 14"
2730
+ fill="none"
2731
+ stroke="currentColor"
2732
+ stroke-width="1.9"
2733
+ stroke-linecap="round"
2734
+ stroke-linejoin="round"
2735
+ >
2736
+ <path
2737
+ d=${
2738
+ earlier ? "M8.75 3.5 5.25 7l3.5 3.5" : "M5.25 3.5 8.75 7l-3.5 3.5"
2739
+ }
2740
+ />
2741
+ </svg>
2742
+ </button>
2743
+ `;
2744
+ }
2745
+
2746
+ /**
2747
+ * Renders the quiet control row under an attachment thumbnail.
2748
+ *
2749
+ * Reorder buttons are the touch-reliable path: dragging a thumbnail competes
2750
+ * with the strip's own horizontal scroll, and a near-full-width thumbnail's
2751
+ * drop target is off-screen anyway. They appear only once there is something
2752
+ * to reorder against.
2753
+ */
2754
+ private _renderAttachmentActions(
2755
+ clientId: string,
2756
+ extra: TemplateResult | typeof nothing = nothing,
2757
+ ) {
2758
+ const total = this._attachmentOrder.length;
2759
+ const index = this._attachmentOrder.indexOf(clientId);
2760
+ const canReorder = total > 1 && index !== -1;
2761
+ if (!canReorder && extra === nothing) return nothing;
2762
+
2763
+ return html`
2764
+ <div class="compose-attachment-actions">
2765
+ ${
2766
+ canReorder
2767
+ ? html`
2768
+ <div class="compose-attachment-move-group">
2769
+ ${this._renderAttachmentMoveButton(clientId, -1, index === 0)}
2770
+ ${this._renderAttachmentMoveButton(
2771
+ clientId,
2772
+ 1,
2773
+ index === total - 1,
2774
+ )}
2775
+ </div>
2776
+ `
2777
+ : nothing
2778
+ }
2779
+ ${extra}
2780
+ </div>
2781
+ `;
2782
+ }
2783
+
2478
2784
  private _renderAttachedTextCard(item: AttachedTextItem, index: number) {
2479
2785
  return html`
2480
2786
  <div class="compose-attachment" data-attachment-id=${item.clientId}>
@@ -2493,19 +2799,22 @@ export class JantComposeEditor extends LitElement {
2493
2799
  ${this._renderFileIcon("text/html; charset=utf-8", 20)}
2494
2800
  </div>
2495
2801
  <span class="compose-attachment-text-summary">${item.summary}</span>
2496
- ${item.bodyJson
2497
- ? html`<span class="compose-attachment-file-size"
2498
- >${this._formatChars(
2499
- this._extractPlainText(item.bodyJson).length,
2500
- )}</span
2501
- >`
2502
- : nothing}
2802
+ ${
2803
+ item.bodyJson
2804
+ ? html`<span class="compose-attachment-file-size"
2805
+ >${this._formatChars(
2806
+ this._extractPlainText(item.bodyJson).length,
2807
+ )}</span
2808
+ >`
2809
+ : nothing
2810
+ }
2503
2811
  </div>
2504
2812
  ${this._renderAttachmentRemoveButton((e: Event) => {
2505
2813
  e.stopPropagation();
2506
2814
  this._removeAttachedText(index);
2507
2815
  })}
2508
2816
  </div>
2817
+ ${this._renderAttachmentActions(item.clientId)}
2509
2818
  </div>
2510
2819
  `;
2511
2820
  }
@@ -2518,115 +2827,122 @@ export class JantComposeEditor extends LitElement {
2518
2827
 
2519
2828
  return html`
2520
2829
  <div class="compose-attachment" data-attachment-id=${a.clientId}>
2521
- ${isFileCard
2522
- ? html`
2523
- <div class="compose-attachment-thumb">
2524
- <div
2525
- class="compose-attachment-sortable"
2526
- data-attachment-sortable
2527
- tabindex="0"
2528
- @keydown=${(e: globalThis.KeyboardEvent) =>
2529
- this._handleAttachmentKeydown(a.clientId, e)}
2530
- >
2531
- ${this._renderAttachmentPreview(a)}
2830
+ ${
2831
+ isFileCard
2832
+ ? html`
2833
+ <div class="compose-attachment-thumb">
2834
+ <div
2835
+ class="compose-attachment-sortable"
2836
+ data-attachment-sortable
2837
+ tabindex="0"
2838
+ @keydown=${(e: globalThis.KeyboardEvent) =>
2839
+ this._handleAttachmentKeydown(a.clientId, e)}
2840
+ >
2841
+ ${this._renderAttachmentPreview(a)}
2842
+ </div>
2843
+ ${this._renderAttachmentOverlay(a, i)}
2532
2844
  </div>
2533
- ${this._renderAttachmentOverlay(a, i)}
2534
- </div>
2535
- `
2536
- : html`
2537
- <div class="compose-attachment-thumb">
2538
- <div
2539
- class="compose-attachment-sortable"
2540
- data-attachment-sortable
2541
- tabindex="0"
2542
- @keydown=${(e: globalThis.KeyboardEvent) =>
2543
- this._handleAttachmentKeydown(a.clientId, e)}
2544
- >
2545
- ${previewFailed
2546
- ? this._renderAttachmentPreviewFallback(visualCategory)
2547
- : category === "video"
2548
- ? html`
2549
- <video
2550
- src=${a.previewUrl}
2551
- poster=${a.posterUrl ?? nothing}
2552
- class="compose-attachment-img"
2553
- preload="metadata"
2554
- .playsInline=${true}
2555
- .muted=${true}
2556
- @loadeddata=${() =>
2557
- this._setAttachmentPreviewFailure(
2558
- a.clientId,
2559
- false,
2560
- )}
2561
- @error=${() =>
2562
- this._setAttachmentPreviewFailure(
2563
- a.clientId,
2564
- true,
2565
- )}
2566
- ></video>
2567
- <div class="compose-attachment-play-icon">
2568
- <svg
2569
- width="24"
2570
- height="24"
2571
- viewBox="0 0 24 24"
2572
- fill="white"
2573
- >
2574
- <path d="M8 5v14l11-7z" />
2575
- </svg>
2576
- </div>
2577
- `
2578
- : a.status === "processing"
2579
- ? html`
2580
- <div class="compose-attachment-processing">
2581
- <svg
2582
- class="animate-spin size-5"
2583
- viewBox="0 0 24 24"
2584
- fill="none"
2585
- stroke="currentColor"
2586
- stroke-width="2"
2587
- >
2588
- <path
2589
- d="M12 2a10 10 0 1 0 10 10"
2590
- stroke-linecap="round"
2845
+ `
2846
+ : html`
2847
+ <div class="compose-attachment-thumb">
2848
+ <div
2849
+ class="compose-attachment-sortable"
2850
+ data-attachment-sortable
2851
+ tabindex="0"
2852
+ @keydown=${(e: globalThis.KeyboardEvent) =>
2853
+ this._handleAttachmentKeydown(a.clientId, e)}
2854
+ >
2855
+ ${
2856
+ previewFailed
2857
+ ? this._renderAttachmentPreviewFallback(visualCategory)
2858
+ : category === "video"
2859
+ ? html`
2860
+ <video
2861
+ src=${a.previewUrl}
2862
+ poster=${a.posterUrl ?? nothing}
2863
+ class="compose-attachment-img"
2864
+ preload="metadata"
2865
+ .playsInline=${true}
2866
+ .muted=${true}
2867
+ @loadeddata=${() =>
2868
+ this._setAttachmentPreviewFailure(
2869
+ a.clientId,
2870
+ false,
2871
+ )}
2872
+ @error=${() =>
2873
+ this._setAttachmentPreviewFailure(
2874
+ a.clientId,
2875
+ true,
2876
+ )}
2877
+ ></video>
2878
+ <div class="compose-attachment-play-icon">
2879
+ <svg
2880
+ width="24"
2881
+ height="24"
2882
+ viewBox="0 0 24 24"
2883
+ fill="white"
2884
+ >
2885
+ <path d="M8 5v14l11-7z" />
2886
+ </svg>
2887
+ </div>
2888
+ `
2889
+ : a.status === "processing"
2890
+ ? html`
2891
+ <div class="compose-attachment-processing">
2892
+ <svg
2893
+ class="animate-spin size-5"
2894
+ viewBox="0 0 24 24"
2895
+ fill="none"
2896
+ stroke="currentColor"
2897
+ stroke-width="2"
2898
+ >
2899
+ <path
2900
+ d="M12 2a10 10 0 1 0 10 10"
2901
+ stroke-linecap="round"
2902
+ />
2903
+ </svg>
2904
+ </div>
2905
+ `
2906
+ : html`
2907
+ <img
2908
+ src=${a.previewUrl}
2909
+ alt=""
2910
+ class="compose-attachment-img"
2911
+ @load=${() =>
2912
+ this._setAttachmentPreviewFailure(
2913
+ a.clientId,
2914
+ false,
2915
+ )}
2916
+ @error=${() =>
2917
+ this._setAttachmentPreviewFailure(
2918
+ a.clientId,
2919
+ true,
2920
+ )}
2591
2921
  />
2592
- </svg>
2593
- </div>
2594
- `
2595
- : html`
2596
- <img
2597
- src=${a.previewUrl}
2598
- alt=""
2599
- class="compose-attachment-img"
2600
- @load=${() =>
2601
- this._setAttachmentPreviewFailure(
2602
- a.clientId,
2603
- false,
2604
- )}
2605
- @error=${() =>
2606
- this._setAttachmentPreviewFailure(
2607
- a.clientId,
2608
- true,
2609
- )}
2610
- />
2611
- `}
2922
+ `
2923
+ }
2924
+ </div>
2925
+ ${this._renderAttachmentOverlay(a, i)}
2612
2926
  </div>
2613
- ${this._renderAttachmentOverlay(a, i)}
2614
- </div>
2615
- `}
2616
- ${category === "image"
2617
- ? html`
2618
- <button
2619
- type="button"
2620
- class=${classMap({
2621
- "compose-attachment-alt": true,
2622
- "compose-attachment-alt-set": a.alt.length > 0,
2623
- })}
2624
- @click=${() => this._openAltPanel(i)}
2625
- >
2626
- ${a.alt.length > 0 ? "ALT" : "+ ALT"}
2627
- </button>
2628
- `
2629
- : nothing}
2927
+ `
2928
+ }
2929
+ ${this._renderAttachmentActions(
2930
+ a.clientId,
2931
+ category === "image"
2932
+ ? html`
2933
+ <button
2934
+ type="button"
2935
+ class=${classMap({
2936
+ "compose-attachment-alt": true,
2937
+ "compose-attachment-alt-set": a.alt.length > 0,
2938
+ })}
2939
+ @click=${() => this._openAltPanel(i)}
2940
+ >
2941
+ ${a.alt.length > 0 ? "ALT" : "+ ALT"}
2942
+ </button>
2943
+ `
2944
+ : nothing,
2945
+ )}
2630
2946
  </div>
2631
2947
  `;
2632
2948
  }
@@ -2685,11 +3001,13 @@ export class JantComposeEditor extends LitElement {
2685
3001
  @click=${() => this._openFilePicker()}
2686
3002
  >
2687
3003
  ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.media)}
2688
- ${this._attachments.length > 0
2689
- ? html`<span class="compose-tool-label"
2690
- >${this.labels.addMore}</span
2691
- >`
2692
- : nothing}
3004
+ ${
3005
+ this._attachments.length > 0
3006
+ ? html`<span class="compose-tool-label"
3007
+ >${this.labels.addMore}</span
3008
+ >`
3009
+ : nothing
3010
+ }
2693
3011
  </button>
2694
3012
 
2695
3013
  <button
@@ -2702,11 +3020,13 @@ export class JantComposeEditor extends LitElement {
2702
3020
  @click=${() => this._openAttachedText()}
2703
3021
  >
2704
3022
  ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.attachedText)}
2705
- ${hasAttached
2706
- ? html`<span class="compose-tool-label"
2707
- >${this.labels.addMore}</span
2708
- >`
2709
- : nothing}
3023
+ ${
3024
+ hasAttached
3025
+ ? html`<span class="compose-tool-label"
3026
+ >${this.labels.addMore}</span
3027
+ >`
3028
+ : nothing
3029
+ }
2710
3030
  </button>
2711
3031
 
2712
3032
  <button
@@ -2748,57 +3068,61 @@ export class JantComposeEditor extends LitElement {
2748
3068
  ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.rate)}
2749
3069
  </button>
2750
3070
 
2751
- ${this.format === "note"
2752
- ? html`
2753
- <button
2754
- type="button"
2755
- class=${classMap({
2756
- "compose-tool-btn": true,
2757
- "compose-tool-btn-active": this._showTitle,
2758
- })}
2759
- title=${this.labels.title}
2760
- aria-pressed=${this._showTitle ? "true" : "false"}
2761
- @click=${() => {
2762
- const willShow = !this._showTitle;
2763
- this._showTitle = willShow;
2764
- // Only this deliberate click is worth remembering. The other
2765
- // writers of `_showTitle` — H1 promotion, format conversion,
2766
- // fullscreen reveal — are the program acting on one post, not
2767
- // the author stating a habit, so they stay silent.
2768
- this.dispatchEvent(
2769
- new CustomEvent("jant:title-toggle", {
2770
- bubbles: true,
2771
- detail: { showTitle: willShow },
2772
- }),
2773
- );
2774
- if (willShow) {
2775
- this.updateComplete.then(() => {
2776
- this.querySelector<HTMLInputElement>(
2777
- ".compose-note-title",
2778
- )?.focus();
2779
- });
2780
- }
2781
- }}
2782
- >
2783
- ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.title)}
2784
- </button>
2785
- `
2786
- : nothing}
2787
- ${this.format === "note"
2788
- ? html`
2789
- <div class="compose-tool-view-group">
3071
+ ${
3072
+ this.format === "note"
3073
+ ? html`
2790
3074
  <button
2791
3075
  type="button"
2792
- class="compose-tool-btn compose-tool-btn-view"
2793
- title=${this.labels.fullscreen}
2794
- aria-label=${this.labels.fullscreen}
2795
- @click=${() => this.openFullscreen()}
3076
+ class=${classMap({
3077
+ "compose-tool-btn": true,
3078
+ "compose-tool-btn-active": this._showTitle,
3079
+ })}
3080
+ title=${this.labels.title}
3081
+ aria-pressed=${this._showTitle ? "true" : "false"}
3082
+ @click=${() => {
3083
+ const willShow = !this._showTitle;
3084
+ this._showTitle = willShow;
3085
+ // Only this deliberate click is worth remembering. The other
3086
+ // writers of `_showTitle` — H1 promotion, format conversion,
3087
+ // fullscreen reveal — are the program acting on one post, not
3088
+ // the author stating a habit, so they stay silent.
3089
+ this.dispatchEvent(
3090
+ new CustomEvent("jant:title-toggle", {
3091
+ bubbles: true,
3092
+ detail: { showTitle: willShow },
3093
+ }),
3094
+ );
3095
+ if (willShow) {
3096
+ this.updateComplete.then(() => {
3097
+ this.querySelector<HTMLInputElement>(
3098
+ ".compose-note-title",
3099
+ )?.focus();
3100
+ });
3101
+ }
3102
+ }}
2796
3103
  >
2797
- ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.fullscreen)}
3104
+ ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.title)}
2798
3105
  </button>
2799
- </div>
2800
- `
2801
- : nothing}
3106
+ `
3107
+ : nothing
3108
+ }
3109
+ ${
3110
+ this.format === "note"
3111
+ ? html`
3112
+ <div class="compose-tool-view-group">
3113
+ <button
3114
+ type="button"
3115
+ class="compose-tool-btn compose-tool-btn-view"
3116
+ title=${this.labels.fullscreen}
3117
+ aria-label=${this.labels.fullscreen}
3118
+ @click=${() => this.openFullscreen()}
3119
+ >
3120
+ ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.fullscreen)}
3121
+ </button>
3122
+ </div>
3123
+ `
3124
+ : nothing
3125
+ }
2802
3126
  </div>
2803
3127
  `;
2804
3128
  }
@@ -2842,42 +3166,48 @@ export class JantComposeEditor extends LitElement {
2842
3166
  `,
2843
3167
  )}
2844
3168
  </div>
2845
- ${this.positionLabel
2846
- ? html`<span class="compose-post-position"
2847
- >${this.positionLabel}</span
2848
- >`
2849
- : nothing}
2850
- ${this.badgeLabel
2851
- ? html`<span class="compose-post-position compose-post-badge"
2852
- >${this.badgeLabel}</span
2853
- >`
2854
- : nothing}
3169
+ ${
3170
+ this.positionLabel
3171
+ ? html`<span class="compose-post-position"
3172
+ >${this.positionLabel}</span
3173
+ >`
3174
+ : nothing
3175
+ }
3176
+ ${
3177
+ this.badgeLabel
3178
+ ? html`<span class="compose-post-position compose-post-badge"
3179
+ >${this.badgeLabel}</span
3180
+ >`
3181
+ : nothing
3182
+ }
2855
3183
  ${this.headerExtra ?? nothing}
2856
- ${this.removable
2857
- ? html`<button
2858
- type="button"
2859
- class="compose-thread-post-remove"
2860
- title="Remove post"
2861
- @click=${() => {
2862
- this.dispatchEvent(
2863
- new CustomEvent("jant:thread-remove", { bubbles: true }),
2864
- );
2865
- }}
2866
- >
2867
- <svg
2868
- width="14"
2869
- height="14"
2870
- viewBox="0 0 16 16"
2871
- fill="none"
2872
- stroke="currentColor"
2873
- stroke-width="1.8"
2874
- stroke-linecap="round"
2875
- aria-hidden="true"
3184
+ ${
3185
+ this.removable
3186
+ ? html`<button
3187
+ type="button"
3188
+ class="compose-thread-post-remove"
3189
+ title="Remove post"
3190
+ @click=${() => {
3191
+ this.dispatchEvent(
3192
+ new CustomEvent("jant:thread-remove", { bubbles: true }),
3193
+ );
3194
+ }}
2876
3195
  >
2877
- ${unsafeSVG(CLOSE_ICON)}
2878
- </svg>
2879
- </button>`
2880
- : nothing}
3196
+ <svg
3197
+ width="14"
3198
+ height="14"
3199
+ viewBox="0 0 16 16"
3200
+ fill="none"
3201
+ stroke="currentColor"
3202
+ stroke-width="1.8"
3203
+ stroke-linecap="round"
3204
+ aria-hidden="true"
3205
+ >
3206
+ ${unsafeSVG(CLOSE_ICON)}
3207
+ </svg>
3208
+ </button>`
3209
+ : nothing
3210
+ }
2881
3211
  </div>
2882
3212
  `;
2883
3213
  }
@@ -2898,12 +3228,19 @@ export class JantComposeEditor extends LitElement {
2898
3228
  // thread item alike — so there is exactly one place it can be.
2899
3229
  return html`
2900
3230
  ${this._renderFormatHeader()}
2901
- <section class="compose-body" @click=${this._focusBodyFromBlankSpace}>
2902
- ${this.format === "note"
2903
- ? this._renderNoteFields()
2904
- : this.format === "link"
2905
- ? this._renderLinkFields()
2906
- : this._renderQuoteFields()}
3231
+ <section
3232
+ class="compose-body"
3233
+ @mousedown=${this._recordBodyPress}
3234
+ @mouseup=${this._recordBodyRelease}
3235
+ @click=${this._focusBodyFromBlankSpace}
3236
+ >
3237
+ ${
3238
+ this.format === "note"
3239
+ ? this._renderNoteFields()
3240
+ : this.format === "link"
3241
+ ? this._renderLinkFields()
3242
+ : this._renderQuoteFields()
3243
+ }
2907
3244
  </section>
2908
3245
  ${this._renderAttachmentDock()} ${this._renderStarRating()}
2909
3246
  ${this._renderToolsRow()}