@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -26,6 +26,7 @@ import type {
26
26
  ComposeFormat,
27
27
  ComposeLabels,
28
28
  ComposeAttachment,
29
+ ComposeRowStatus,
29
30
  AttachedTextItem,
30
31
  ComposeEditorSelection,
31
32
  ComposeFullscreenOpenDetail,
@@ -97,43 +98,44 @@ const COMPOSE_TOOLBAR_ICONS = {
97
98
  />
98
99
  <path
99
100
  d="m9 1.95 2.08 4.21 4.65.67-3.36 3.29.8 4.63L9 12.55l-4.17 2.2.8-4.63-3.36-3.29 4.65-.67z"
100
- stroke-width="1.6"
101
101
  />
102
102
  `,
103
+ /* The one filled glyph in the row, so its bars are cut to the weight the
104
+ stroked icons read at — a 2.05 bar beside a 1.42 stroke looks bold. */
103
105
  title: `
104
106
  <rect
105
107
  x="3.35"
106
- y="3.2"
108
+ y="3.37"
107
109
  width="11.3"
108
- height="2.05"
109
- rx="0.68"
110
+ height="1.72"
111
+ rx="0.6"
110
112
  fill="currentColor"
111
113
  stroke="none"
112
114
  />
113
115
  <rect
114
- x="7.8"
116
+ x="7.99"
115
117
  y="4.6"
116
- width="2.4"
118
+ width="2.02"
117
119
  height="9.45"
118
- rx="0.78"
120
+ rx="0.68"
119
121
  fill="currentColor"
120
122
  stroke="none"
121
123
  />
122
124
  <rect
123
125
  x="6.75"
124
- y="13.15"
126
+ y="13.32"
125
127
  width="4.5"
126
- height="1.12"
127
- rx="0.56"
128
+ height="0.95"
129
+ rx="0.48"
128
130
  fill="currentColor"
129
131
  stroke="none"
130
132
  />
131
133
  `,
132
134
  fullscreen: `
133
- <path d="M5.85 3H3v2.85" stroke-width="1.48" />
134
- <path d="M12.15 3H15v2.85" stroke-width="1.48" />
135
- <path d="M3 12.15V15h2.85" stroke-width="1.48" />
136
- <path d="M15 12.15V15h-2.85" stroke-width="1.48" />
135
+ <path d="M5.85 3H3v2.85" />
136
+ <path d="M12.15 3H15v2.85" />
137
+ <path d="M3 12.15V15h2.85" />
138
+ <path d="M15 12.15V15h-2.85" />
137
139
  `,
138
140
  } as const;
139
141
 
@@ -147,7 +149,6 @@ function renderComposeToolbarIcon(
147
149
  viewBox="0 0 18 18"
148
150
  fill="none"
149
151
  stroke="currentColor"
150
- stroke-width="1.55"
151
152
  stroke-linecap="round"
152
153
  stroke-linejoin="round"
153
154
  aria-hidden="true"
@@ -189,6 +190,10 @@ export class JantComposeEditor extends LitElement {
189
190
  threadItem: { type: Boolean, attribute: "thread-item" },
190
191
  removable: { type: Boolean },
191
192
  inlineFormat: { type: Boolean, attribute: "inline-format" },
193
+ titleByDefault: { type: Boolean, attribute: "title-by-default" },
194
+ positionLabel: { type: String, attribute: "position-label" },
195
+ badgeLabel: { type: String, attribute: "badge-label" },
196
+ headerExtra: { attribute: false },
192
197
  slashCommandDiscovered: { type: Boolean },
193
198
  _title: { state: true },
194
199
  _bodyJson: { state: true },
@@ -215,6 +220,29 @@ export class JantComposeEditor extends LitElement {
215
220
  declare threadItem: boolean;
216
221
  declare removable: boolean;
217
222
  declare inlineFormat: boolean;
223
+ /**
224
+ * Whether a note's title field starts visible. A post that opens a thread is
225
+ * usually the one worth naming, so it defaults on there; a reply or a
226
+ * continuation post carries the thread's title already, so it defaults off.
227
+ * On a new single post the owner narrows it further with the author's own
228
+ * remembered answer. Either way the `T` tool overrides it, and a post that
229
+ * arrives with a title always shows the field.
230
+ */
231
+ declare titleByDefault: boolean;
232
+ /**
233
+ * "1/3"-style position marker shown before the format selector. Empty for a
234
+ * single post, where there is no "which one" to answer.
235
+ */
236
+ declare positionLabel: string;
237
+ /** Status marker for the header row, e.g. "Editing". Mutually exclusive with
238
+ * `positionLabel` in practice — a thread post is never an edit. */
239
+ declare badgeLabel: string;
240
+ /**
241
+ * Owner-supplied content for the right side of the format header — the
242
+ * dialog's per-post date/permalink control. Passed in rather than built here
243
+ * because publish metadata belongs to the dialog, not to the editor.
244
+ */
245
+ declare headerExtra: unknown;
218
246
  declare slashCommandDiscovered: boolean;
219
247
  declare _title: string;
220
248
  declare _bodyJson: JSONContent | null;
@@ -243,7 +271,6 @@ export class JantComposeEditor extends LitElement {
243
271
  private _emojiContainer: HTMLElement | null = null;
244
272
  private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
245
273
  private _onDocClickBound = this._onDocumentClick.bind(this);
246
- private _scrollBufferApplied = false;
247
274
  private _filePickerCleanup: (() => void) | null = null;
248
275
  private _suppressAttachedTextOpenUntil = 0;
249
276
  /**
@@ -273,6 +300,10 @@ export class JantComposeEditor extends LitElement {
273
300
  this.threadItem = false;
274
301
  this.removable = false;
275
302
  this.inlineFormat = false;
303
+ this.titleByDefault = true;
304
+ this.positionLabel = "";
305
+ this.badgeLabel = "";
306
+ this.headerExtra = null;
276
307
  this.slashCommandDiscovered = false;
277
308
  this._title = "";
278
309
  this._bodyJson = null;
@@ -280,7 +311,7 @@ export class JantComposeEditor extends LitElement {
280
311
  this._quoteText = "";
281
312
  this._quoteAuthor = "";
282
313
  this._rating = 0;
283
- this._showTitle = false;
314
+ this._showTitle = true;
284
315
  this._showRating = false;
285
316
  this._attachedTexts = [];
286
317
  this._attachments = [];
@@ -778,9 +809,90 @@ export class JantComposeEditor extends LitElement {
778
809
  }
779
810
  }
780
811
 
812
+ /**
813
+ * What this row contributes to the composer's submit state.
814
+ *
815
+ * The editor owns these fields, so it owns the answer: the dialog reduces
816
+ * `getStatus()` over the rows it has rather than reading editors back out of
817
+ * the DOM, which during a render still holds the row it is about to remove.
818
+ *
819
+ * @returns Whether the row holds anything worth keeping, and whether it is
820
+ * complete enough to publish
821
+ * @example
822
+ * editor.getStatus(); // { hasContent: true, publishable: false } — link, no URL yet
823
+ */
824
+ getStatus(): ComposeRowStatus {
825
+ const data = this.getData();
826
+ const hasContent =
827
+ !!data.body ||
828
+ !!data.title.trim() ||
829
+ !!data.url.trim() ||
830
+ !!data.quoteText.trim() ||
831
+ !!data.quoteAuthor.trim() ||
832
+ data.rating > 0 ||
833
+ data.attachments.length > 0 ||
834
+ data.attachedTexts.length > 0;
835
+ const fieldsValid =
836
+ !this.getUrlValidationMessage() && !this.getLinkTitleValidationMessage();
837
+
838
+ let publishable: boolean;
839
+ switch (this.format) {
840
+ // A link is its URL and title, both of which the validators already
841
+ // require — nothing else has to be filled in.
842
+ case "link":
843
+ publishable = fieldsValid;
844
+ break;
845
+ case "quote":
846
+ publishable = fieldsValid && !!data.quoteText.trim();
847
+ break;
848
+ default:
849
+ publishable = fieldsValid && hasContent;
850
+ }
851
+
852
+ return { hasContent, publishable };
853
+ }
854
+
855
+ #lastStatus: ComposeRowStatus | null = null;
856
+
857
+ /** Tell the owner when — and only when — this row's answer changes. */
858
+ #notifyStatus() {
859
+ const status = this.getStatus();
860
+ if (
861
+ this.#lastStatus?.hasContent === status.hasContent &&
862
+ this.#lastStatus?.publishable === status.publishable
863
+ ) {
864
+ return;
865
+ }
866
+ this.#lastStatus = status;
867
+ this.dispatchEvent(
868
+ new CustomEvent<ComposeRowStatus>("jant:compose-status", {
869
+ bubbles: true,
870
+ detail: status,
871
+ }),
872
+ );
873
+ }
874
+
875
+ /**
876
+ * Set the title toggle's default and apply it now.
877
+ *
878
+ * `titleByDefault` alone only takes effect on the first update or on
879
+ * `reset()`, and an owner that resets before Lit re-renders would seed from
880
+ * the previous session's value. An owner that knows the default up front
881
+ * calls this instead.
882
+ *
883
+ * @param show - Whether a note's title field starts visible
884
+ * @example
885
+ * editor.setTitleDefault(false); // next new note opens without a title field
886
+ */
887
+ setTitleDefault(show: boolean) {
888
+ this.titleByDefault = show;
889
+ this._showTitle = show;
890
+ }
891
+
781
892
  reset() {
782
893
  this.#clearPendingInlineImageUploads();
783
894
  this._title = "";
895
+ this._showTitle = this.titleByDefault;
784
896
  this._bodyJson = null;
785
897
  this._editor?.commands.clearContent();
786
898
  this._lastEditorSelection = null;
@@ -788,7 +900,6 @@ export class JantComposeEditor extends LitElement {
788
900
  this._quoteText = "";
789
901
  this._quoteAuthor = "";
790
902
  this._rating = 0;
791
- this._showTitle = false;
792
903
  this._showRating = false;
793
904
  this._attachedTexts = [];
794
905
  // Revoke preview URLs before clearing
@@ -811,9 +922,12 @@ export class JantComposeEditor extends LitElement {
811
922
  status: ComposeAttachment["status"],
812
923
  mediaId: string | null,
813
924
  error: string | null,
925
+ remoteUrl?: string | null,
814
926
  ) {
815
927
  this._attachments = this._attachments.map((a) =>
816
- a.clientId === clientId ? { ...a, status, mediaId, error } : a,
928
+ a.clientId === clientId
929
+ ? { ...a, status, mediaId, error, remoteUrl: remoteUrl ?? a.remoteUrl }
930
+ : a,
817
931
  );
818
932
  }
819
933
 
@@ -978,7 +1092,6 @@ export class JantComposeEditor extends LitElement {
978
1092
  toolbarMode: "compose",
979
1093
  onUpdate: (json) => {
980
1094
  this._bodyJson = json;
981
- this._ensureScrollBuffer();
982
1095
  this.promoteLeadingH1Title();
983
1096
  hideSlashCommandHint(this);
984
1097
  },
@@ -1006,38 +1119,9 @@ export class JantComposeEditor extends LitElement {
1006
1119
  rehost: (src) => this.#rehostInlineImage(src),
1007
1120
  },
1008
1121
  imageNodeLabels: getInlineImageNodeLabels(this.labels),
1122
+ tableControlLabels: this.labels.tableControls,
1009
1123
  });
1010
1124
  this._lastEditorSelection = this._readEditorSelection();
1011
-
1012
- // Lock editor min-height once so new lines fill existing space
1013
- // instead of growing the dialog line-by-line.
1014
- // Skip in page mode where the editor grows freely with the page.
1015
- this._scrollBufferApplied = false;
1016
- const dom = this._editor.view.dom as HTMLElement;
1017
- const isPageMode = !!this.closest(".compose-page-shell");
1018
- if (!isPageMode) {
1019
- const last = dom.lastElementChild as HTMLElement | null;
1020
- const contentH = last ? last.offsetTop + last.offsetHeight : 0;
1021
- const buffer = this.format !== "note" ? 60 : 120;
1022
- dom.style.minHeight = `${contentH + buffer}px`;
1023
- }
1024
- }
1025
-
1026
- /**
1027
- * One-time: adds bottom padding for scroll buffer once the
1028
- * compose-body starts scrolling. Since the dialog is already at
1029
- * max-height by that point, the extra padding doesn't grow it.
1030
- */
1031
- private _ensureScrollBuffer() {
1032
- if (this._scrollBufferApplied) return;
1033
- const dom = this._editor?.view?.dom as HTMLElement | undefined;
1034
- if (!dom) return;
1035
- const body = this.querySelector(".compose-body") as HTMLElement | null;
1036
- if (!body) return;
1037
- if (body.scrollHeight > body.clientHeight + 20) {
1038
- dom.style.paddingBottom = "40px";
1039
- this._scrollBufferApplied = true;
1040
- }
1041
1125
  }
1042
1126
 
1043
1127
  private _destroyEditor() {
@@ -1060,6 +1144,17 @@ export class JantComposeEditor extends LitElement {
1060
1144
  "_attachmentOrder",
1061
1145
  ]);
1062
1146
 
1147
+ protected willUpdate(changed: Map<string, unknown>) {
1148
+ super.willUpdate(changed);
1149
+
1150
+ // `titleByDefault` is an owner-supplied prop, so it is only known once the
1151
+ // first update runs. Seed the toggle from it there and leave it alone after
1152
+ // that — from then on the value belongs to the user (and to `reset()`).
1153
+ if (!this.hasUpdated) {
1154
+ this._showTitle = this.titleByDefault;
1155
+ }
1156
+ }
1157
+
1063
1158
  protected updated(changed: Map<string, unknown>) {
1064
1159
  super.updated(changed);
1065
1160
 
@@ -1097,6 +1192,11 @@ export class JantComposeEditor extends LitElement {
1097
1192
  }
1098
1193
  }
1099
1194
 
1195
+ // Every field that can change this row's answer is reactive, so `updated()`
1196
+ // is where the owner hears about it — including the ones a user edit never
1197
+ // touches, like an upload finishing.
1198
+ this.#notifyStatus();
1199
+
1100
1200
  // Notify parent dialog of content changes for draft auto-save. A format
1101
1201
  // conversion writes content fields too, but it's not a user edit, so skip
1102
1202
  // the notification once when asked.
@@ -1170,6 +1270,8 @@ export class JantComposeEditor extends LitElement {
1170
1270
  showRating?: boolean;
1171
1271
  media?: Array<{
1172
1272
  id: string;
1273
+ /** Preserved so a restored draft's attachmentOrder still maps by clientId */
1274
+ clientId?: string;
1173
1275
  previewUrl: string;
1174
1276
  posterUrl?: string | null;
1175
1277
  alt?: string;
@@ -1195,6 +1297,8 @@ export class JantComposeEditor extends LitElement {
1195
1297
  this._rating = data.rating;
1196
1298
  this._showRating = true;
1197
1299
  }
1300
+ // A post that already has a title shows the field whatever the default is —
1301
+ // otherwise editing a titled reply would silently drop its title.
1198
1302
  if (data.showTitle !== undefined) this._showTitle = data.showTitle;
1199
1303
  else if (data.title && data.format === "note") this._showTitle = true;
1200
1304
  if (data.showRating !== undefined) this._showRating = data.showRating;
@@ -1216,13 +1320,14 @@ export class JantComposeEditor extends LitElement {
1216
1320
  // Convert media attachments to ComposeAttachment[] with status "done"
1217
1321
  if (data.media?.length) {
1218
1322
  const attachments = data.media.map((m) => ({
1219
- clientId: randomUUID(),
1323
+ clientId: m.clientId ?? randomUUID(),
1220
1324
  file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
1221
1325
  previewUrl: m.previewUrl,
1222
1326
  posterUrl: m.posterUrl ?? null,
1223
1327
  status: "done" as const,
1224
1328
  progress: null,
1225
1329
  mediaId: m.id,
1330
+ remoteUrl: m.previewUrl,
1226
1331
  alt: m.alt ?? "",
1227
1332
  error: null,
1228
1333
  summary: m.summary ?? null,
@@ -1280,6 +1385,7 @@ export class JantComposeEditor extends LitElement {
1280
1385
 
1281
1386
  this._showUrlValidation = false;
1282
1387
  this._showLinkTitleValidation = false;
1388
+ this.#notifyStatus();
1283
1389
  }
1284
1390
 
1285
1391
  /** Updates editor content and title from fullscreen close */
@@ -1303,6 +1409,10 @@ export class JantComposeEditor extends LitElement {
1303
1409
  );
1304
1410
  }
1305
1411
  this._lastEditorSelection = selection ?? this._readEditorSelection();
1412
+ // Content handed in from outside has to be visible to the owner in the same
1413
+ // tick: fullscreen's ⌘↵ hands the text back and publishes on the next line,
1414
+ // with no render in between to carry the news.
1415
+ this.#notifyStatus();
1306
1416
  }
1307
1417
 
1308
1418
  private static SUMMARY_LENGTH = 100;
@@ -1527,6 +1637,25 @@ export class JantComposeEditor extends LitElement {
1527
1637
  el.style.height = `${el.scrollHeight}px`;
1528
1638
  }
1529
1639
 
1640
+ /**
1641
+ * Puts the caret in the body when the click landed on blank section instead
1642
+ * of on the editor. The surfaces that reserve height — the `/new` page, the
1643
+ * dialog on a phone — leave a stretch of empty paper under a two-line
1644
+ * editor, and it should behave like paper: click it, write there. Only the
1645
+ * layout wrappers forward; a click that reached a real control belongs to
1646
+ * that control.
1647
+ */
1648
+ private _focusBodyFromBlankSpace = (e: MouseEvent) => {
1649
+ const target = e.target as HTMLElement | null;
1650
+ if (!target) return;
1651
+ const forwards =
1652
+ target.classList.contains("compose-body") ||
1653
+ target.classList.contains("compose-field-enter") ||
1654
+ target.classList.contains("compose-tiptap-wrap");
1655
+ if (!forwards) return;
1656
+ this._editor?.commands.focus("end");
1657
+ };
1658
+
1530
1659
  private _setRating(star: number) {
1531
1660
  this._rating = this._rating === star ? 0 : star;
1532
1661
  }
@@ -1587,6 +1716,7 @@ export class JantComposeEditor extends LitElement {
1587
1716
  status: "pending",
1588
1717
  progress: null,
1589
1718
  mediaId: null,
1719
+ remoteUrl: null,
1590
1720
  alt: "",
1591
1721
  error: null,
1592
1722
  summary: null,
@@ -1709,6 +1839,16 @@ export class JantComposeEditor extends LitElement {
1709
1839
  this._lastFocusedField = target;
1710
1840
  }
1711
1841
 
1842
+ private _handleTitleKeydown(e: globalThis.KeyboardEvent) {
1843
+ if (e.isComposing || e.keyCode === 229) return;
1844
+ if (e.key !== "Enter" || e.metaKey || e.ctrlKey || e.altKey) {
1845
+ return;
1846
+ }
1847
+
1848
+ e.preventDefault();
1849
+ this._editor?.commands.focus("start");
1850
+ }
1851
+
1712
1852
  private _toggleEmojiPicker() {
1713
1853
  if (this._showEmojiPicker) {
1714
1854
  this.closeEmojiPicker();
@@ -1967,34 +2107,15 @@ export class JantComposeEditor extends LitElement {
1967
2107
  return html`
1968
2108
  <div class="compose-field-enter">
1969
2109
  ${this._showTitle
1970
- ? html`
1971
- <div class="compose-note-title-row">
1972
- <input
1973
- type="text"
1974
- .value=${this._title}
1975
- @input=${(e: Event) => this._onInput("_title", e)}
1976
- @focus=${(e: Event) => this._onFieldFocus(e)}
1977
- @keydown=${(e: globalThis.KeyboardEvent) => {
1978
- if (e.isComposing || e.keyCode === 229) return;
1979
- if (e.key === "Enter") {
1980
- e.preventDefault();
1981
- this._editor?.commands.focus("start");
1982
- }
1983
- }}
1984
- class="compose-input compose-note-title"
1985
- placeholder=${this.labels.titlePlaceholder}
1986
- />
1987
- <button
1988
- type="button"
1989
- class="compose-note-title-dismiss"
1990
- @click=${() => {
1991
- this._showTitle = false;
1992
- }}
1993
- >
1994
- ✕
1995
- </button>
1996
- </div>
1997
- `
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
+ />`
1998
2119
  : nothing}
1999
2120
  <div class="compose-tiptap-wrap">
2000
2121
  <div class="compose-tiptap-body"></div>
@@ -2053,6 +2174,7 @@ export class JantComposeEditor extends LitElement {
2053
2174
  .value=${this._title}
2054
2175
  @input=${(e: Event) => this._onInput("_title", e)}
2055
2176
  @focus=${(e: Event) => this._onFieldFocus(e)}
2177
+ @keydown=${this._handleTitleKeydown}
2056
2178
  @blur=${() => {
2057
2179
  this._showLinkTitleValidation = true;
2058
2180
  }}
@@ -2070,9 +2192,7 @@ export class JantComposeEditor extends LitElement {
2070
2192
  : nothing}
2071
2193
  <div class="compose-divider"></div>
2072
2194
  <div class="compose-tiptap-wrap">
2073
- <div
2074
- class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-link"
2075
- ></div>
2195
+ <div class="compose-tiptap-body compose-tiptap-link"></div>
2076
2196
  <span class="compose-slash-discovery-hint" aria-hidden="true">
2077
2197
  ${this.labels.slashHint}
2078
2198
  </span>
@@ -2096,7 +2216,7 @@ export class JantComposeEditor extends LitElement {
2096
2216
  @focus=${(e: Event) => this._onFieldFocus(e)}
2097
2217
  class="compose-input compose-quote-text"
2098
2218
  placeholder=${this.labels.quotePlaceholder}
2099
- rows="5"
2219
+ rows="4"
2100
2220
  ></textarea>
2101
2221
  </div>
2102
2222
  <div class="compose-quote-author-row">
@@ -2141,9 +2261,7 @@ export class JantComposeEditor extends LitElement {
2141
2261
  aria-hidden="true"
2142
2262
  ></div>
2143
2263
  <div class="compose-tiptap-wrap">
2144
- <div
2145
- class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-thoughts-quote"
2146
- ></div>
2264
+ <div class="compose-tiptap-body"></div>
2147
2265
  <span class="compose-slash-discovery-hint" aria-hidden="true">
2148
2266
  ${this.labels.slashHint}
2149
2267
  </span>
@@ -2599,12 +2717,13 @@ export class JantComposeEditor extends LitElement {
2599
2717
  "compose-tool-btn-active": this._showEmojiPicker,
2600
2718
  })}
2601
2719
  title=${this.labels.emoji}
2720
+ aria-pressed=${this._showEmojiPicker ? "true" : "false"}
2602
2721
  @click=${() => this._toggleEmojiPicker()}
2603
2722
  >
2604
2723
  ${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.emoji)}
2605
2724
  </button>
2606
2725
 
2607
- <div class="compose-tool-sep"></div>
2726
+ <div class="compose-tool-gap"></div>
2608
2727
 
2609
2728
  <button
2610
2729
  type="button"
@@ -2613,6 +2732,7 @@ export class JantComposeEditor extends LitElement {
2613
2732
  "compose-tool-btn-active": this._showRating,
2614
2733
  })}
2615
2734
  title=${this.labels.rate}
2735
+ aria-pressed=${this._showRating ? "true" : "false"}
2616
2736
  @click=${() => {
2617
2737
  const willShow = !this._showRating;
2618
2738
  this._showRating = willShow;
@@ -2637,9 +2757,20 @@ export class JantComposeEditor extends LitElement {
2637
2757
  "compose-tool-btn-active": this._showTitle,
2638
2758
  })}
2639
2759
  title=${this.labels.title}
2760
+ aria-pressed=${this._showTitle ? "true" : "false"}
2640
2761
  @click=${() => {
2641
2762
  const willShow = !this._showTitle;
2642
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
+ );
2643
2774
  if (willShow) {
2644
2775
  this.updateComplete.then(() => {
2645
2776
  this.querySelector<HTMLInputElement>(
@@ -2683,14 +2814,10 @@ export class JantComposeEditor extends LitElement {
2683
2814
 
2684
2815
  return html`
2685
2816
  <div class="compose-thread-post-header">
2686
- <div class="compose-segmented compose-thread-segmented">
2687
- <div
2688
- class=${classMap({
2689
- "compose-format-pill": true,
2690
- "compose-format-pill-link": this.format === "link",
2691
- "compose-format-pill-quote": this.format === "quote",
2692
- })}
2693
- ></div>
2817
+ <div class="compose-segmented">
2818
+ <!-- No sliding indicator: the items size to their labels now, so a
2819
+ thirds-based pill would not line up. The active item carries its
2820
+ own background instead. -->
2694
2821
  ${formats.map(
2695
2822
  (f) => html`
2696
2823
  <button
@@ -2715,6 +2842,17 @@ export class JantComposeEditor extends LitElement {
2715
2842
  `,
2716
2843
  )}
2717
2844
  </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}
2855
+ ${this.headerExtra ?? nothing}
2718
2856
  ${this.removable
2719
2857
  ? html`<button
2720
2858
  type="button"
@@ -2756,11 +2894,11 @@ export class JantComposeEditor extends LitElement {
2756
2894
  }
2757
2895
 
2758
2896
  render() {
2897
+ // The format selector lives here in every mode — single post, reply, and
2898
+ // thread item alike — so there is exactly one place it can be.
2759
2899
  return html`
2760
- ${this.threadItem || this.inlineFormat
2761
- ? this._renderFormatHeader()
2762
- : nothing}
2763
- <section class="compose-body">
2900
+ ${this._renderFormatHeader()}
2901
+ <section class="compose-body" @click=${this._focusBodyFromBlankSpace}>
2764
2902
  ${this.format === "note"
2765
2903
  ? this._renderNoteFields()
2766
2904
  : this.format === "link"