@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
@@ -215,6 +215,8 @@ const labels: ComposeLabels = {
215
215
  discardChangesConfirm: "Discard changes?",
216
216
  drafts: "Drafts",
217
217
  draftsEmpty: "No drafts yet. Save a draft to find it here.",
218
+ previewDraft: "Preview",
219
+ draftActions: "Draft actions",
218
220
  deleteDraft: "Delete Draft",
219
221
  draftDeleted: "Draft deleted.",
220
222
  publishFailedDraft: "Couldn't publish. Saved as draft.",
@@ -241,13 +243,13 @@ const labels: ComposeLabels = {
241
243
  publishDateInvalid: "Enter a valid date.",
242
244
  publishDateFutureError:
243
245
  "Choose today or an earlier date, or leave it blank to publish now.",
244
- publishDateSummaryNow: "Publish now",
246
+ publishDateSummaryNow: "Now",
245
247
  publishDateSummaryAction: "Edit publish date",
246
248
  publishSlugLabel: "Custom link",
247
249
  publishSlugPlaceholder: "your-post-link",
248
250
  publishSlugHint: "Leave blank to generate one automatically.",
249
251
  publishSlugAuto: "Generate automatically",
250
- publishSlugSummaryAuto: "Auto link",
252
+ publishSlugSummaryAuto: "Auto",
251
253
  publishSlugSummaryAction: "Edit custom link",
252
254
  publishSlugReset: "Reset link",
253
255
  publishSlugSuggested: "Suggested link",
@@ -263,10 +265,24 @@ const labels: ComposeLabels = {
263
265
  threadLimitReached: "Threads can include up to 20 posts.",
264
266
  showMore: "Show more",
265
267
  showLess: "Show less",
266
- newThread: "New Thread",
267
- replyTitle: "Reply",
268
- editTitle: "Edit",
268
+ closeCompose: "Close compose",
269
+ editing: "Editing",
270
+ composeDialogLabel: "Compose",
269
271
  slashHint: "Type / for commands",
272
+ tableControls: {
273
+ toolbarLabel: "Table controls",
274
+ addRowAbove: "Add row above",
275
+ addRowBelow: "Add row below",
276
+ addColumnBefore: "Add column before",
277
+ addColumnAfter: "Add column after",
278
+ options: "Table options",
279
+ deleteRow: "Delete row",
280
+ deleteColumn: "Delete column",
281
+ toggleHeaderRow: "Toggle header row",
282
+ deleteTable: "Delete table",
283
+ sizePickerLabel: "Choose table size",
284
+ insertTableSize: "Insert %rows% by %cols% table",
285
+ },
270
286
  collectionFormLabels: {
271
287
  titleLabel: "Title",
272
288
  titlePlaceholder: "My Collection",
@@ -292,10 +308,14 @@ const labels: ComposeLabels = {
292
308
 
293
309
  async function createElement(
294
310
  format: string = "note",
311
+ options: { titleByDefault?: boolean } = {},
295
312
  ): Promise<JantComposeEditor> {
296
313
  const el = document.createElement("jant-compose-editor") as JantComposeEditor;
297
314
  el.format = format as "note" | "link" | "quote";
298
315
  el.labels = labels;
316
+ if (options.titleByDefault !== undefined) {
317
+ el.titleByDefault = options.titleByDefault;
318
+ }
299
319
  document.body.appendChild(el);
300
320
  await el.updateComplete;
301
321
  return el;
@@ -779,16 +799,127 @@ describe("JantComposeEditor", () => {
779
799
  expect(el._attachedTexts[0].bodyJson).toBeNull();
780
800
  });
781
801
 
782
- it("shows title toggle only in note mode", async () => {
802
+ it("shows a note's title field by default on a post that starts a thread", async () => {
783
803
  const el = await createElement("note");
804
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
784
805
  expect(el.querySelector('.compose-tool-btn[title="Title"]')).not.toBeNull();
806
+ });
807
+
808
+ it("hides a note's title field by default on a continuation post", async () => {
809
+ const el = await createElement("note", { titleByDefault: false });
810
+ expect(el.querySelector(".compose-note-title")).toBeNull();
811
+ });
812
+
813
+ it("toggles the title field from the toolbar", async () => {
814
+ const el = await createElement("note", { titleByDefault: false });
815
+ const toggle = requireElement(
816
+ el.querySelector<HTMLButtonElement>('.compose-tool-btn[title="Title"]'),
817
+ "expected title toggle",
818
+ );
819
+
820
+ toggle.click();
821
+ await el.updateComplete;
822
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
823
+
824
+ toggle.click();
825
+ await el.updateComplete;
826
+ expect(el.querySelector(".compose-note-title")).toBeNull();
827
+ });
828
+
829
+ it("drops a hidden title from the submitted data", async () => {
830
+ const el = await createElement("note");
831
+ el._title = "Named";
832
+ el._showTitle = false;
833
+ await el.updateComplete;
834
+ expect(el.getData().title).toBe("");
835
+ });
836
+
837
+ it("keeps the title field visible when a post arrives with one", async () => {
838
+ const el = await createElement("note", { titleByDefault: false });
839
+ el.populate({ format: "note", title: "Loaded title" });
840
+ await el.updateComplete;
841
+ expect(el.querySelector(".compose-note-title")).not.toBeNull();
842
+ });
843
+
844
+ it.each([
845
+ ["note", ".compose-note-title"],
846
+ ["link", ".compose-link-title"],
847
+ ] as const)(
848
+ "moves from the %s title to the start of the body on Enter",
849
+ async (format, selector) => {
850
+ const el = await createElement(format);
851
+ if (format === "note") {
852
+ await el.updateComplete;
853
+ }
854
+
855
+ const titleInput = requireElement(
856
+ el.querySelector<HTMLInputElement>(selector),
857
+ `expected ${format} title input`,
858
+ );
859
+ const editor = requireEditor(el);
860
+ titleInput.focus();
861
+
862
+ const event = new globalThis.KeyboardEvent("keydown", {
863
+ key: "Enter",
864
+ bubbles: true,
865
+ cancelable: true,
866
+ });
867
+ titleInput.dispatchEvent(event);
868
+ await new Promise<void>((resolve) => {
869
+ globalThis.requestAnimationFrame(() => resolve());
870
+ });
871
+
872
+ expect(event.defaultPrevented).toBe(true);
873
+ expect(document.activeElement).toBe(editor.view.dom);
874
+ expect(editor.state.selection.from).toBe(1);
875
+ },
876
+ );
877
+
878
+ it("keeps focus in the title while an IME composition is active", async () => {
879
+ const el = await createElement("note");
880
+ await el.updateComplete;
881
+
882
+ const titleInput = requireElement(
883
+ el.querySelector<HTMLInputElement>(".compose-note-title"),
884
+ "expected note title input",
885
+ );
886
+ titleInput.focus();
887
+
888
+ const event = new globalThis.KeyboardEvent("keydown", {
889
+ key: "Enter",
890
+ isComposing: true,
891
+ bubbles: true,
892
+ cancelable: true,
893
+ });
894
+ titleInput.dispatchEvent(event);
895
+
896
+ expect(event.defaultPrevented).toBe(false);
897
+ expect(document.activeElement).toBe(titleInput);
898
+ });
785
899
 
786
- el.format = "link";
900
+ it("leaves Mod-Enter available for the publish shortcut", async () => {
901
+ const el = await createElement("note");
787
902
  await el.updateComplete;
788
- expect(el.querySelector('.compose-tool-btn[title="Title"]')).toBeNull();
903
+
904
+ const titleInput = requireElement(
905
+ el.querySelector<HTMLInputElement>(".compose-note-title"),
906
+ "expected note title input",
907
+ );
908
+ titleInput.focus();
909
+
910
+ const event = new globalThis.KeyboardEvent("keydown", {
911
+ key: "Enter",
912
+ metaKey: true,
913
+ bubbles: true,
914
+ cancelable: true,
915
+ });
916
+ titleInput.dispatchEvent(event);
917
+
918
+ expect(event.defaultPrevented).toBe(false);
919
+ expect(document.activeElement).toBe(titleInput);
789
920
  });
790
921
 
791
- it("keeps title after rate and places fullscreen at the far right of the toolbar", async () => {
922
+ it("places fullscreen at the far right of the toolbar", async () => {
792
923
  const el = await createElement("note");
793
924
  const toolTitles = [
794
925
  ...el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn"),
@@ -827,7 +958,6 @@ describe("JantComposeEditor", () => {
827
958
  it("getData returns current field values", async () => {
828
959
  const el = await createElement("note");
829
960
  el._title = "Test Title";
830
- el._showTitle = true;
831
961
  el._bodyJson = {
832
962
  type: "doc",
833
963
  content: [
@@ -897,31 +1027,11 @@ describe("JantComposeEditor", () => {
897
1027
  expect(data.attachmentOrder).toEqual(["t2"]);
898
1028
  });
899
1029
 
900
- it("getData omits title when showTitle is off in note mode", async () => {
1030
+ it("getData returns the note title as typed", async () => {
901
1031
  const el = await createElement("note");
902
- el._title = "Hidden Title";
903
- el._showTitle = false;
904
-
905
- const data = el.getData();
906
- expect(data.title).toBe("");
907
- });
908
-
909
- it("preserves title in memory when toggling off and restores on toggle on", async () => {
910
- const el = await createElement("note");
911
- el._title = "My Title";
912
- el._showTitle = true;
913
- await el.updateComplete;
914
-
915
- // Toggle off — title stays in memory
916
- el._showTitle = false;
917
- await el.updateComplete;
918
- expect(el._title).toBe("My Title");
919
- expect(el.getData().title).toBe("");
1032
+ el._title = "Kept Title";
920
1033
 
921
- // Toggle back on — title restored
922
- el._showTitle = true;
923
- await el.updateComplete;
924
- expect(el.getData().title).toBe("My Title");
1034
+ expect(el.getData().title).toBe("Kept Title");
925
1035
  });
926
1036
 
927
1037
  it("promotes a leading H1 to the note title", async () => {
@@ -938,7 +1048,6 @@ describe("JantComposeEditor", () => {
938
1048
  const body = JSON.parse(data.body) as JSONContent;
939
1049
 
940
1050
  expect(data.title).toBe("My Markdown Title");
941
- expect(el._showTitle).toBe(true);
942
1051
  expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
943
1052
  });
944
1053
 
@@ -953,7 +1062,6 @@ describe("JantComposeEditor", () => {
953
1062
  const body = JSON.parse(data.body) as JSONContent;
954
1063
 
955
1064
  expect(data.title).toBe("");
956
- expect(el._showTitle).toBe(false);
957
1065
  expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
958
1066
  });
959
1067
 
@@ -961,7 +1069,6 @@ describe("JantComposeEditor", () => {
961
1069
  const el = await createElement("note");
962
1070
  const editor = requireEditor(el);
963
1071
  el._title = "Manual title";
964
- el._showTitle = true;
965
1072
 
966
1073
  editor.commands.setContent(
967
1074
  tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
@@ -1134,6 +1241,7 @@ describe("JantComposeEditor", () => {
1134
1241
  alt: "",
1135
1242
  error: null,
1136
1243
  posterUrl: null,
1244
+ remoteUrl: null,
1137
1245
  summary: null,
1138
1246
  chars: null,
1139
1247
  },
@@ -1167,6 +1275,7 @@ describe("JantComposeEditor", () => {
1167
1275
  alt: "",
1168
1276
  error: null,
1169
1277
  posterUrl: null,
1278
+ remoteUrl: null,
1170
1279
  summary: null,
1171
1280
  chars: null,
1172
1281
  },
@@ -1180,6 +1289,7 @@ describe("JantComposeEditor", () => {
1180
1289
  alt: "",
1181
1290
  error: null,
1182
1291
  posterUrl: null,
1292
+ remoteUrl: null,
1183
1293
  summary: null,
1184
1294
  chars: null,
1185
1295
  },
@@ -1246,7 +1356,7 @@ describe("JantComposeEditor", () => {
1246
1356
  expect(el._attachmentOrder[0]).toBe("t1");
1247
1357
  });
1248
1358
 
1249
- it("pastes clipboard files into attachments when no visible title is set", async () => {
1359
+ it("pastes clipboard files into attachments when no title is set", async () => {
1250
1360
  const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1251
1361
  const el = await createElement("note");
1252
1362
  const events: CustomEvent[] = [];
@@ -1282,7 +1392,6 @@ describe("JantComposeEditor", () => {
1282
1392
  id: "med_test",
1283
1393
  });
1284
1394
  const el = await createElement("note");
1285
- el._showTitle = true;
1286
1395
  el._title = "Essay";
1287
1396
  await el.updateComplete;
1288
1397
 
@@ -1302,30 +1411,9 @@ describe("JantComposeEditor", () => {
1302
1411
  ]);
1303
1412
  });
1304
1413
 
1305
- it("pastes images as attachments when the title row was toggled off after typing", async () => {
1306
- const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1307
- const el = await createElement("note");
1308
- el._showTitle = true;
1309
- el._title = "Essay";
1310
- await el.updateComplete;
1311
- el._showTitle = false;
1312
- await el.updateComplete;
1313
-
1314
- const image = new File(["image"], "clipboard.png", { type: "image/png" });
1315
- const { handled } = triggerEditorPaste(el, [image]);
1316
- await el.updateComplete;
1317
-
1318
- expect(handled).toBe(true);
1319
- expect(uploadWithMetadataMock).not.toHaveBeenCalled();
1320
- expect(el._attachments.map((attachment) => attachment.file.name)).toEqual([
1321
- "clipboard.png",
1322
- ]);
1323
- });
1324
-
1325
1414
  it("pastes images as attachments when only whitespace is in the title", async () => {
1326
1415
  const uploadWithMetadataMock = vi.mocked(uploadWithMetadata);
1327
1416
  const el = await createElement("note");
1328
- el._showTitle = true;
1329
1417
  el._title = " ";
1330
1418
  await el.updateComplete;
1331
1419
 
@@ -14,14 +14,13 @@ const labels = {
14
14
  fullscreen: "Fullscreen",
15
15
  exitFullscreen: "Exit fullscreen",
16
16
  done: "Done",
17
- title: "Title",
18
17
  titlePlaceholder: "Title",
19
18
  bodyPlaceholder: "What's on your mind...",
20
19
  showMore: "Show more",
21
20
  showLess: "Show less",
22
- newThread: "New Thread",
23
- replyTitle: "Reply",
24
- editTitle: "Edit",
21
+ closeCompose: "Close compose",
22
+ editing: "Editing",
23
+ composeDialogLabel: "Compose",
25
24
  } as ComposeLabels;
26
25
 
27
26
  async function flush(el?: JantComposeFullscreen) {
@@ -58,7 +57,7 @@ describe("JantComposeFullscreen", () => {
58
57
  }
59
58
  });
60
59
 
61
- it("opens in body-first mode when the title is hidden", async () => {
60
+ it("shows the title field when the editor arrived with one open", async () => {
62
61
  const el = document.createElement(
63
62
  "jant-compose-fullscreen",
64
63
  ) as JantComposeFullscreen;
@@ -68,29 +67,16 @@ describe("JantComposeFullscreen", () => {
68
67
 
69
68
  document.dispatchEvent(
70
69
  new CustomEvent("jant:fullscreen-open", {
71
- detail: {
72
- json: null,
73
- title: "",
74
- showTitle: false,
75
- labels,
76
- },
70
+ detail: { json: null, title: "", showTitle: true, labels },
77
71
  }),
78
72
  );
79
73
  await flush(el);
80
74
 
81
- expect(el.querySelector(".compose-fullscreen-title")).toBeNull();
82
- expect(
83
- el.querySelector(".compose-fullscreen-title-placeholder"),
84
- ).not.toBeNull();
75
+ expect(el.querySelector(".compose-fullscreen-title")).not.toBeNull();
85
76
  expect(el.textContent).not.toContain("Fullscreen");
86
- expect(
87
- el.querySelector(
88
- ".compose-fullscreen-toolbar .compose-fullscreen-title-placeholder",
89
- ),
90
- ).toBeNull();
91
77
  });
92
78
 
93
- it("reveals the title on demand and returns its visibility on close", async () => {
79
+ it("offers a way back to a title the editor had hidden", async () => {
94
80
  const el = document.createElement(
95
81
  "jant-compose-fullscreen",
96
82
  ) as JantComposeFullscreen;
@@ -100,54 +86,122 @@ describe("JantComposeFullscreen", () => {
100
86
 
101
87
  document.dispatchEvent(
102
88
  new CustomEvent("jant:fullscreen-open", {
103
- detail: {
104
- json: null,
105
- title: "",
106
- showTitle: false,
107
- labels,
108
- },
89
+ detail: { json: null, title: "", showTitle: false, labels },
109
90
  }),
110
91
  );
111
92
  await flush(el);
112
93
 
113
- const toggle = el.querySelector<HTMLButtonElement>(
94
+ expect(el.querySelector(".compose-fullscreen-title")).toBeNull();
95
+ const reveal = el.querySelector<HTMLButtonElement>(
114
96
  ".compose-fullscreen-title-placeholder",
115
97
  );
116
- expect(toggle).not.toBeNull();
117
- toggle?.click();
98
+ expect(reveal).not.toBeNull();
99
+
100
+ reveal?.click();
118
101
  await flush(el);
102
+ expect(el.querySelector(".compose-fullscreen-title")).not.toBeNull();
103
+ });
119
104
 
120
- const input = el.querySelector<HTMLInputElement>(
121
- ".compose-fullscreen-title",
122
- );
123
- expect(input).not.toBeNull();
124
- if (!input) {
125
- throw new Error("expected fullscreen title input");
126
- }
127
- input.value = "Desk Notes";
128
- input.dispatchEvent(new Event("input"));
105
+ it.each(["metaKey", "ctrlKey"] as const)(
106
+ "publishes with %s+Enter and returns the latest editor state",
107
+ async (modifier) => {
108
+ const el = document.createElement(
109
+ "jant-compose-fullscreen",
110
+ ) as JantComposeFullscreen;
111
+ el.labels = labels;
112
+ document.body.appendChild(el);
113
+ await flush(el);
114
+
115
+ document.dispatchEvent(
116
+ new CustomEvent("jant:fullscreen-open", {
117
+ detail: {
118
+ json: null,
119
+ title: "Keyboard post",
120
+ labels,
121
+ editorIndex: 2,
122
+ },
123
+ }),
124
+ );
125
+ await flush(el);
129
126
 
130
- let detail: ComposeFullscreenCloseDetail | null = null;
131
- document.addEventListener(
132
- "jant:fullscreen-close",
133
- (event) => {
134
- detail = (event as CustomEvent<ComposeFullscreenCloseDetail>).detail;
135
- },
136
- { once: true },
127
+ requireEditor(el).commands.setContent({
128
+ type: "doc",
129
+ content: [
130
+ {
131
+ type: "paragraph",
132
+ content: [{ type: "text", text: "Publish from fullscreen" }],
133
+ },
134
+ ],
135
+ });
136
+
137
+ let detail: ComposeFullscreenCloseDetail | null = null;
138
+ document.addEventListener(
139
+ "jant:fullscreen-close",
140
+ (event) => {
141
+ detail = (event as CustomEvent<ComposeFullscreenCloseDetail>).detail;
142
+ },
143
+ { once: true },
144
+ );
145
+
146
+ el.querySelector(".compose-fullscreen-dialog")?.dispatchEvent(
147
+ new globalThis.KeyboardEvent("keydown", {
148
+ key: "Enter",
149
+ [modifier]: true,
150
+ bubbles: true,
151
+ cancelable: true,
152
+ }),
153
+ );
154
+ await flush(el);
155
+
156
+ expect(detail).toMatchObject({
157
+ title: "Keyboard post",
158
+ intent: "publish",
159
+ editorIndex: 2,
160
+ json: {
161
+ type: "doc",
162
+ content: [
163
+ {
164
+ type: "paragraph",
165
+ content: [{ type: "text", text: "Publish from fullscreen" }],
166
+ },
167
+ ],
168
+ },
169
+ });
170
+ expect(el.querySelector(".compose-fullscreen-dialog")).toBeNull();
171
+ },
172
+ );
173
+
174
+ it("does not publish while an IME composition is active", async () => {
175
+ const el = document.createElement(
176
+ "jant-compose-fullscreen",
177
+ ) as JantComposeFullscreen;
178
+ el.labels = labels;
179
+ document.body.appendChild(el);
180
+ await flush(el);
181
+
182
+ document.dispatchEvent(
183
+ new CustomEvent("jant:fullscreen-open", {
184
+ detail: {
185
+ json: null,
186
+ title: "",
187
+ labels,
188
+ },
189
+ }),
137
190
  );
191
+ await flush(el);
138
192
 
139
- el.querySelector<HTMLButtonElement>(".compose-fullscreen-done")?.click();
193
+ el.querySelector(".compose-fullscreen-dialog")?.dispatchEvent(
194
+ new globalThis.KeyboardEvent("keydown", {
195
+ key: "Enter",
196
+ metaKey: true,
197
+ isComposing: true,
198
+ bubbles: true,
199
+ cancelable: true,
200
+ }),
201
+ );
202
+ await flush(el);
140
203
 
141
- expect(detail).toMatchObject({
142
- json: {
143
- type: "doc",
144
- content: [{ type: "paragraph" }],
145
- },
146
- title: "Desk Notes",
147
- showTitle: true,
148
- replyExpanded: false,
149
- selection: { from: 1, to: 1 },
150
- });
204
+ expect(el.querySelector(".compose-fullscreen-dialog")).not.toBeNull();
151
205
  });
152
206
 
153
207
  it("restores and returns the current editor selection", async () => {
@@ -171,7 +225,6 @@ describe("JantComposeFullscreen", () => {
171
225
  ],
172
226
  },
173
227
  title: "",
174
- showTitle: false,
175
228
  selection: { from: 4, to: 4 },
176
229
  labels,
177
230
  },
@@ -214,7 +267,7 @@ describe("JantComposeFullscreen", () => {
214
267
  detail: {
215
268
  json: null,
216
269
  title: "",
217
- showTitle: false,
270
+ showTitle: true,
218
271
  labels,
219
272
  replyContext: {
220
273
  contentHtml: "<p>A quiet sentence worth answering.</p>",
@@ -229,9 +282,10 @@ describe("JantComposeFullscreen", () => {
229
282
  expect(el.textContent).toContain("A quiet sentence worth answering.");
230
283
  expect(el.textContent).toContain("Mar 10, 2026");
231
284
  expect(el.textContent).toContain("Show more");
285
+ // The reply's title field lives inside its own row, not the toolbar.
232
286
  expect(
233
287
  el.querySelector(
234
- ".compose-fullscreen-editor-row .compose-fullscreen-title-placeholder-reply",
288
+ ".compose-fullscreen-editor-row .compose-fullscreen-title-reply",
235
289
  ),
236
290
  ).not.toBeNull();
237
291
  });
@@ -249,7 +303,6 @@ describe("JantComposeFullscreen", () => {
249
303
  detail: {
250
304
  json: null,
251
305
  title: "Follow-up",
252
- showTitle: true,
253
306
  labels,
254
307
  replyContext: {
255
308
  contentHtml: "<p>Thread seed.</p>",
@@ -286,7 +339,6 @@ describe("JantComposeFullscreen", () => {
286
339
  detail: {
287
340
  json: null,
288
341
  title: "",
289
- showTitle: false,
290
342
  labels,
291
343
  replyContext: {
292
344
  contentHtml: "<p>One more thread preview.</p>",
@@ -332,7 +384,6 @@ describe("JantComposeFullscreen", () => {
332
384
  detail: {
333
385
  json: null,
334
386
  title: "",
335
- showTitle: false,
336
387
  labels,
337
388
  },
338
389
  }),
@@ -387,7 +438,6 @@ describe("JantComposeFullscreen", () => {
387
438
  ],
388
439
  },
389
440
  title: "",
390
- showTitle: false,
391
441
  labels,
392
442
  },
393
443
  }),