@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -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-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -327,7 +327,7 @@ describe("JantPostMenu", () => {
327
327
  }
328
328
 
329
329
  if (
330
- url === "/api/collections/collection-1/posts/post-1" &&
330
+ url === "/api/collections/collection-1/threads/post-1" &&
331
331
  method === "DELETE"
332
332
  ) {
333
333
  selectedIds.splice(0, 1);
@@ -397,7 +397,7 @@ describe("JantPostMenu", () => {
397
397
  keydown(requireElement(options[0] ?? null, "expected first option"), " ");
398
398
  await Promise.resolve();
399
399
  await menu.updateComplete;
400
- expect(menu._postCollectionIds).toEqual([]);
400
+ expect(menu._threadCollectionIds).toEqual([]);
401
401
 
402
402
  options = Array.from(
403
403
  menu.querySelectorAll<HTMLButtonElement>(".post-menu-picker-option"),
@@ -412,6 +412,48 @@ describe("JantPostMenu", () => {
412
412
  expect(document.activeElement).toBe(searchInput);
413
413
  });
414
414
 
415
+ it("derives Child action scope from Thread identity", async () => {
416
+ const { menu, trigger } = await createMenu();
417
+ const article = requireElement(
418
+ trigger.closest<HTMLElement>("article[data-post]"),
419
+ "expected post article",
420
+ );
421
+ article.dataset.postId = "reply-1";
422
+ article.dataset.threadRootId = "post-1";
423
+ document.body.insertAdjacentHTML(
424
+ "afterbegin",
425
+ '<div data-collection-id="collection-1"></div>',
426
+ );
427
+
428
+ click(trigger);
429
+ await menu.updateComplete;
430
+
431
+ expect(menu.textContent).toContain("Edit");
432
+ expect(menu.textContent).toContain("Add to Featured");
433
+ expect(menu.textContent).toContain("Delete");
434
+ expect(menu.querySelector("[data-post-menu-open-collections]")).toBeNull();
435
+ expect(menu.querySelector("[data-post-menu-open-visibility]")).toBeNull();
436
+ expect(menu.textContent).not.toContain("Pin this post");
437
+ expect(menu.textContent).not.toContain("Pin in collection");
438
+ });
439
+
440
+ it("blocks the collection shortcut for a Child without a reply marker", async () => {
441
+ const fetchMock = vi.mocked(globalThis.fetch);
442
+ const { menu, trigger } = await createMenu();
443
+ const article = requireElement(
444
+ trigger.closest<HTMLElement>("article[data-post]"),
445
+ "expected post article",
446
+ );
447
+ article.dataset.postId = "reply-1";
448
+ article.dataset.threadRootId = "post-1";
449
+
450
+ menu.openCollectionsForPost(article);
451
+ await menu.updateComplete;
452
+
453
+ expect(menu.textContent?.trim()).toBe("");
454
+ expect(fetchMock).not.toHaveBeenCalled();
455
+ });
456
+
415
457
  it("closes the collection picker with Enter without toggling the focused option", async () => {
416
458
  const selectedIds = ["collection-1"];
417
459
  const fetchMock = vi.fn(
@@ -466,7 +508,7 @@ describe("JantPostMenu", () => {
466
508
  keydown(firstOption, "Enter");
467
509
  await menu.updateComplete;
468
510
 
469
- expect(menu._postCollectionIds).toEqual(["collection-1"]);
511
+ expect(menu._threadCollectionIds).toEqual(["collection-1"]);
470
512
  expect(menu.textContent?.trim()).toBe("");
471
513
  expect(trigger.getAttribute("aria-expanded")).toBe("false");
472
514
  });
@@ -564,7 +606,7 @@ describe("JantPostMenu", () => {
564
606
  }
565
607
 
566
608
  if (
567
- url === "/api/collections/collection-3/posts" &&
609
+ url === "/api/collections/collection-3/threads" &&
568
610
  method === "POST"
569
611
  ) {
570
612
  selectedIds.push("collection-3");
@@ -572,7 +614,7 @@ describe("JantPostMenu", () => {
572
614
  }
573
615
 
574
616
  if (
575
- url === "/api/collections/collection-2/posts/post-1" &&
617
+ url === "/api/collections/collection-2/threads/post-1" &&
576
618
  method === "DELETE"
577
619
  ) {
578
620
  const index = selectedIds.indexOf("collection-2");
@@ -51,9 +51,9 @@ const labels: SettingsLabels = {
51
51
  contentLanguagePreview: "Readers and search engines see",
52
52
  dashboardLanguage: "Dashboard language",
53
53
  dashboardLanguageHelp: "The language this admin dashboard shows in.",
54
- cjkFont: "CJK Font",
54
+ cjkFont: "CJK fallback",
55
55
  cjkFontHelp:
56
- "Load a serif font optimized for Chinese, Japanese, or Korean content.",
56
+ "Used when the content language has no built-in font profile. Your font theme still controls serif and sans styling.",
57
57
  timeZone: "Time Zone",
58
58
  feeds: "Feeds",
59
59
  mainRssFeed: "Main RSS feed",
@@ -96,9 +96,9 @@ const labels: SettingsLabels = {
96
96
  contentLanguagePreview: "Readers and search engines see",
97
97
  dashboardLanguage: "Dashboard language",
98
98
  dashboardLanguageHelp: "The language this admin dashboard shows in.",
99
- cjkFont: "CJK Font",
99
+ cjkFont: "CJK fallback",
100
100
  cjkFontHelp:
101
- "Load a serif font optimized for Chinese, Japanese, or Korean content.",
101
+ "Used when the content language has no built-in font profile. Your font theme still controls serif and sans styling.",
102
102
  timeZone: "Time Zone",
103
103
  feeds: "Feeds",
104
104
  mainRssFeed: "Main RSS feed",
@@ -137,7 +137,7 @@ const timezones: SettingsTimezone[] = [
137
137
  ];
138
138
 
139
139
  const cjkFonts: SettingsCjkFont[] = [
140
- { value: "off", label: "None" },
140
+ { value: "off", label: "Follow content language" },
141
141
  { value: "zh-Hans", label: "简体中文 (Simplified Chinese)" },
142
142
  ];
143
143
 
@@ -17,6 +17,12 @@ export interface CollectionManagerLabels {
17
17
  dividerLabelPlaceholder: string;
18
18
  newCollection: string;
19
19
  edit: string;
20
+ addToNavigation: string;
21
+ addingToNavigation: string;
22
+ addedToNavigation: string;
23
+ editNavigation: string;
24
+ addToNavigationFailed: string;
25
+ notNow: string;
20
26
  label: string;
21
27
  url: string;
22
28
  linkLabelPlaceholder: string;
@@ -31,8 +37,8 @@ export interface CollectionManagerLabels {
31
37
  deleteLink: string;
32
38
  confirmDeleteLink: string;
33
39
  cancel: string;
34
- entrySingular: string;
35
- entryPlural: string;
40
+ threadSingular: string;
41
+ threadPlural: string;
36
42
  emptyState: string;
37
43
  orderSaved: string;
38
44
  saved: string;
@@ -50,7 +56,7 @@ export interface ManagedCollection {
50
56
  title: string;
51
57
  description: string | null;
52
58
  sortOrder: string;
53
- postCount: number;
59
+ threadCount: number;
54
60
  recentActivityAt: number;
55
61
  }
56
62
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { JSONContent } from "@tiptap/core";
9
9
  import type { CollectionFormLabels } from "./collection-types.js";
10
+ import type { TableControlLabels } from "../tiptap/table-control-labels.js";
10
11
 
11
12
  export type ComposeFormat = "note" | "link" | "quote";
12
13
 
@@ -46,6 +47,7 @@ export interface AttachedTextItem {
46
47
 
47
48
  export interface DraftItem {
48
49
  id: string;
50
+ slug: string;
49
51
  format: ComposeFormat;
50
52
  title: string | null;
51
53
  bodyText: string | null;
@@ -180,6 +182,8 @@ export interface ComposeLabels {
180
182
  discardChangesConfirm: string;
181
183
  drafts: string;
182
184
  draftsEmpty: string;
185
+ previewDraft: string;
186
+ draftActions: string;
183
187
  deleteDraft: string;
184
188
  draftDeleted: string;
185
189
  publishFailedDraft: string;
@@ -229,6 +233,7 @@ export interface ComposeLabels {
229
233
  replyTitle: string;
230
234
  editTitle: string;
231
235
  slashHint: string;
236
+ tableControls: TableControlLabels;
232
237
  collectionFormLabels: CollectionFormLabels;
233
238
  }
234
239
 
@@ -250,6 +255,7 @@ export interface ComposeFullscreenOpenDetail {
250
255
  selection?: ComposeEditorSelection | null;
251
256
  labels?: ComposeLabels;
252
257
  replyContext?: ComposeFullscreenReplyContext | null;
258
+ editorIndex?: number;
253
259
  }
254
260
 
255
261
  export interface ComposeFullscreenCloseDetail {
@@ -258,6 +264,8 @@ export interface ComposeFullscreenCloseDetail {
258
264
  showTitle: boolean;
259
265
  selection?: ComposeEditorSelection | null;
260
266
  replyExpanded: boolean;
267
+ intent?: "publish";
268
+ editorIndex?: number;
261
269
  }
262
270
 
263
271
  export type ComposeVisibility = "public" | "latest_hidden" | "private";
@@ -27,13 +27,16 @@ import {
27
27
  } from "../sortable-list.js";
28
28
  import { showConfirmDialog } from "../confirm.js";
29
29
  import { publicPath } from "../runtime-paths.js";
30
- import { showToast } from "../toast.js";
30
+ import { showToast, showToastWithAction } from "../toast.js";
31
+ import { addCollectionToNavigation } from "../collection-navigation.js";
32
+ import { consumeCollectionCreatedNotice } from "../collection-created-notice.js";
31
33
  import { getDividerCollectionGroup } from "../../lib/collection-groups.js";
32
34
  import {
33
35
  getCollectionEditPath,
34
36
  getCollectionSelectionPath,
35
37
  getCollectionsDirectoryPath,
36
38
  } from "../../lib/collection-paths.js";
39
+ import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
37
40
  import { render as renderMarkdown } from "../../lib/markdown.js";
38
41
  import { formatRelativeAge, toISOString } from "../../lib/time.js";
39
42
  import {
@@ -53,7 +56,7 @@ interface CollectionsResponse {
53
56
  title: string;
54
57
  description: string | null;
55
58
  sortOrder: string;
56
- postCount: number;
59
+ threadCount: number;
57
60
  recentActivityAt: number;
58
61
  }>;
59
62
  directoryItems?: Array<{
@@ -77,6 +80,10 @@ export class JantCollectionsManager extends LitElement {
77
80
  static properties = {
78
81
  items: { type: Array },
79
82
  labels: { type: Object },
83
+ navigationCollectionIds: {
84
+ type: Array,
85
+ attribute: "navigation-collection-ids",
86
+ },
80
87
 
81
88
  _items: { state: true },
82
89
  _reorderMode: { state: true },
@@ -91,12 +98,14 @@ export class JantCollectionsManager extends LitElement {
91
98
  _newLinkUrl: { state: true },
92
99
  _newLinkDescription: { state: true },
93
100
  _addingLink: { state: true },
94
- _hoveringId: { state: true },
95
101
  _showItemMenuId: { state: true },
102
+ _addingToNavigationId: { state: true },
103
+ _createdCollectionId: { state: true },
96
104
  };
97
105
 
98
106
  declare items: CollectionManagerItem[];
99
107
  declare labels: CollectionManagerLabels;
108
+ declare navigationCollectionIds: string[];
100
109
 
101
110
  declare _items: CollectionManagerItem[];
102
111
  declare _reorderMode: boolean;
@@ -111,8 +120,9 @@ export class JantCollectionsManager extends LitElement {
111
120
  declare _newLinkUrl: string;
112
121
  declare _newLinkDescription: string;
113
122
  declare _addingLink: boolean;
114
- declare _hoveringId: string | null;
115
123
  declare _showItemMenuId: string | null;
124
+ declare _addingToNavigationId: string | null;
125
+ declare _createdCollectionId: string | null;
116
126
 
117
127
  #sortable: { destroy(): void } | null = null;
118
128
  #initialized = false;
@@ -189,12 +199,17 @@ export class JantCollectionsManager extends LitElement {
189
199
  connectedCallback() {
190
200
  super.connectedCallback();
191
201
  this.#bindManagerRoot();
202
+ const createdCollectionId = consumeCollectionCreatedNotice();
203
+ if (createdCollectionId) {
204
+ this._createdCollectionId = createdCollectionId;
205
+ }
192
206
  }
193
207
 
194
208
  constructor() {
195
209
  super();
196
210
  this.items = [];
197
211
  this.labels = {} as CollectionManagerLabels;
212
+ this.navigationCollectionIds = [];
198
213
 
199
214
  this._items = [];
200
215
  this._reorderMode = false;
@@ -209,8 +224,9 @@ export class JantCollectionsManager extends LitElement {
209
224
  this._newLinkUrl = "";
210
225
  this._newLinkDescription = "";
211
226
  this._addingLink = false;
212
- this._hoveringId = null;
213
227
  this._showItemMenuId = null;
228
+ this._addingToNavigationId = null;
229
+ this._createdCollectionId = null;
214
230
  }
215
231
 
216
232
  protected update(
@@ -247,7 +263,7 @@ export class JantCollectionsManager extends LitElement {
247
263
 
248
264
  #countLabel(count: number) {
249
265
  return `${count} ${
250
- count === 1 ? this.labels.entrySingular : this.labels.entryPlural
266
+ count === 1 ? this.labels.threadSingular : this.labels.threadPlural
251
267
  }`;
252
268
  }
253
269
 
@@ -323,7 +339,7 @@ export class JantCollectionsManager extends LitElement {
323
339
  title: collection.title,
324
340
  description: collection.description,
325
341
  sortOrder: collection.sortOrder,
326
- postCount: collection.postCount ?? 0,
342
+ threadCount: collection.threadCount ?? 0,
327
343
  recentActivityAt: collection.recentActivityAt,
328
344
  });
329
345
  }
@@ -666,6 +682,32 @@ export class JantCollectionsManager extends LitElement {
666
682
  }
667
683
  }
668
684
 
685
+ async #addCollectionToNavigation(collectionId: string) {
686
+ if (!collectionId || this._addingToNavigationId) return;
687
+
688
+ const usesInlineNotice = this._createdCollectionId === collectionId;
689
+ this._showItemMenuId = null;
690
+ this._addingToNavigationId = collectionId;
691
+ document.removeEventListener("click", this.#closeItemMenu);
692
+
693
+ try {
694
+ await addCollectionToNavigation(collectionId);
695
+ this.navigationCollectionIds = [
696
+ ...new Set([...this.navigationCollectionIds, collectionId]),
697
+ ];
698
+ if (!usesInlineNotice) {
699
+ showToastWithAction(this.labels.addedToNavigation, {
700
+ label: this.labels.editNavigation,
701
+ href: publicPath(NAVIGATION_SETTINGS_PATH),
702
+ });
703
+ }
704
+ } catch {
705
+ showToast(this.labels.addToNavigationFailed, "error");
706
+ } finally {
707
+ this._addingToNavigationId = null;
708
+ }
709
+ }
710
+
669
711
  #toggleLinkEdit(item: CollectionManagerItem) {
670
712
  if (item.type !== "link") return;
671
713
 
@@ -839,13 +881,17 @@ export class JantCollectionsManager extends LitElement {
839
881
  ${collection.description
840
882
  ? html`
841
883
  <div class="collection-directory-description prose">
842
- ${unsafeHTML(renderMarkdown(collection.description))}
884
+ ${unsafeHTML(
885
+ renderMarkdown(collection.description, {
886
+ namespace: collection.id,
887
+ }),
888
+ )}
843
889
  </div>
844
890
  `
845
891
  : nothing}
846
892
  <p class="collection-directory-summary">
847
893
  <span class="collection-directory-meta"
848
- >${this.#countLabel(collection.postCount)}</span
894
+ >${this.#countLabel(collection.threadCount)}</span
849
895
  >
850
896
  <span class="collection-directory-meta-separator" aria-hidden="true"
851
897
  >/</span
@@ -895,24 +941,17 @@ export class JantCollectionsManager extends LitElement {
895
941
  <div
896
942
  class=${classMap({
897
943
  "group relative": true,
944
+ "collection-directory-managed-row": true,
898
945
  "z-50":
899
946
  this._showItemMenuId === item.id || this._editingLinkId === item.id,
900
947
  })}
901
- @mouseenter=${() => {
902
- this._hoveringId = item.id;
903
- }}
904
- @mouseleave=${() => {
905
- if (this._hoveringId === item.id) this._hoveringId = null;
906
- }}
907
948
  >
908
949
  <div
909
950
  class="collection-directory-item collection-directory-item-manageable"
910
951
  >
911
952
  ${body}
912
953
  </div>
913
- ${this._hoveringId === item.id || this._showItemMenuId === item.id
914
- ? this.#renderItemMenu(item)
915
- : nothing}
954
+ ${this.#renderItemMenu(item)}
916
955
  </div>
917
956
  `;
918
957
  }
@@ -1036,7 +1075,9 @@ export class JantCollectionsManager extends LitElement {
1036
1075
  ${item.description
1037
1076
  ? html`
1038
1077
  <div class="collection-directory-description prose">
1039
- ${unsafeHTML(renderMarkdown(item.description))}
1078
+ ${unsafeHTML(
1079
+ renderMarkdown(item.description, { namespace: item.id }),
1080
+ )}
1040
1081
  </div>
1041
1082
  `
1042
1083
  : html`
@@ -1084,25 +1125,17 @@ export class JantCollectionsManager extends LitElement {
1084
1125
  <div
1085
1126
  class=${classMap({
1086
1127
  "group relative": true,
1128
+ "collection-directory-managed-row": true,
1087
1129
  "z-50":
1088
1130
  this._showItemMenuId === item.id || this._editingLinkId === item.id,
1089
1131
  })}
1090
- @mouseenter=${() => {
1091
- this._hoveringId = item.id;
1092
- }}
1093
- @mouseleave=${() => {
1094
- if (this._hoveringId === item.id) this._hoveringId = null;
1095
- }}
1096
1132
  >
1097
1133
  <div
1098
1134
  class="collection-directory-item collection-directory-item-link collection-directory-item-manageable"
1099
1135
  >
1100
1136
  ${body}
1101
1137
  </div>
1102
- ${this._hoveringId === item.id || this._showItemMenuId === item.id
1103
- ? this.#renderItemMenu(item)
1104
- : nothing}
1105
- ${this.#renderLinkEditPanel(item)}
1138
+ ${this.#renderItemMenu(item)} ${this.#renderLinkEditPanel(item)}
1106
1139
  </div>
1107
1140
  `;
1108
1141
  }
@@ -1166,6 +1199,31 @@ export class JantCollectionsManager extends LitElement {
1166
1199
  >
1167
1200
  ${this.labels.edit}
1168
1201
  </a>
1202
+ ${this.navigationCollectionIds.includes(collection.id)
1203
+ ? html`
1204
+ <a
1205
+ href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1206
+ class="collections-page-menu-item"
1207
+ >
1208
+ ${this.labels.editNavigation}
1209
+ </a>
1210
+ `
1211
+ : html`
1212
+ <button
1213
+ type="button"
1214
+ class="collections-page-menu-item"
1215
+ ?disabled=${this._addingToNavigationId ===
1216
+ collection.id}
1217
+ @click=${() =>
1218
+ void this.#addCollectionToNavigation(
1219
+ collection.id,
1220
+ )}
1221
+ >
1222
+ ${this._addingToNavigationId === collection.id
1223
+ ? this.labels.addingToNavigation
1224
+ : this.labels.addToNavigation}
1225
+ </button>
1226
+ `}
1169
1227
  <button
1170
1228
  type="button"
1171
1229
  class="collections-page-menu-item collections-page-menu-item-danger"
@@ -1398,9 +1456,86 @@ export class JantCollectionsManager extends LitElement {
1398
1456
  `;
1399
1457
  }
1400
1458
 
1459
+ #renderCreatedCollectionNotice() {
1460
+ if (!this._createdCollectionId) return nothing;
1461
+
1462
+ const collection = this._items.find(
1463
+ (item) => item.collection?.id === this._createdCollectionId,
1464
+ )?.collection;
1465
+ if (!collection) return nothing;
1466
+
1467
+ const isInNavigation = this.navigationCollectionIds.includes(collection.id);
1468
+
1469
+ return html`
1470
+ <section
1471
+ class="collection-created-notice"
1472
+ role="status"
1473
+ aria-labelledby="collection-created-notice-title"
1474
+ >
1475
+ <p
1476
+ id="collection-created-notice-title"
1477
+ class="collection-created-notice-message"
1478
+ >
1479
+ ${isInNavigation
1480
+ ? this.labels.addedToNavigation
1481
+ : this.labels.formLabels.createdLabel}
1482
+ </p>
1483
+ <div class="collection-created-notice-actions">
1484
+ ${isInNavigation
1485
+ ? html`
1486
+ <a
1487
+ href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1488
+ class="collection-created-notice-action collection-created-notice-action-primary"
1489
+ >
1490
+ ${this.labels.editNavigation}
1491
+ </a>
1492
+ `
1493
+ : html`
1494
+ <button
1495
+ type="button"
1496
+ class="collection-created-notice-action collection-created-notice-action-primary"
1497
+ ?disabled=${this._addingToNavigationId === collection.id}
1498
+ @click=${() =>
1499
+ void this.#addCollectionToNavigation(collection.id)}
1500
+ >
1501
+ ${this._addingToNavigationId === collection.id
1502
+ ? this.labels.addingToNavigation
1503
+ : this.labels.addToNavigation}
1504
+ </button>
1505
+ `}
1506
+ <button
1507
+ type="button"
1508
+ class="collection-created-notice-dismiss"
1509
+ aria-label=${this.labels.notNow}
1510
+ title=${this.labels.notNow}
1511
+ @click=${() => {
1512
+ this._createdCollectionId = null;
1513
+ }}
1514
+ >
1515
+ <svg
1516
+ xmlns="http://www.w3.org/2000/svg"
1517
+ width="14"
1518
+ height="14"
1519
+ viewBox="0 0 24 24"
1520
+ fill="none"
1521
+ stroke="currentColor"
1522
+ stroke-width="2"
1523
+ stroke-linecap="round"
1524
+ stroke-linejoin="round"
1525
+ aria-hidden="true"
1526
+ >
1527
+ <path d="M18 6 6 18" />
1528
+ <path d="m6 6 12 12" />
1529
+ </svg>
1530
+ </button>
1531
+ </div>
1532
+ </section>
1533
+ `;
1534
+ }
1535
+
1401
1536
  render() {
1402
1537
  return html`
1403
- ${this.#renderCreateLinkForm()}
1538
+ ${this.#renderCreatedCollectionNotice()} ${this.#renderCreateLinkForm()}
1404
1539
  ${this.#hasDirectoryContent()
1405
1540
  ? html`
1406
1541
  <div id="collections-manager-list" class="collection-directory">
@@ -11,6 +11,7 @@
11
11
  import { LitElement, html, nothing } from "lit";
12
12
  import { classMap } from "lit/directives/class-map.js";
13
13
  import { unsafeSVG } from "lit/directives/unsafe-svg.js";
14
+ import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
14
15
  import { openNewCompose } from "../compose-launch.js";
15
16
  import { getBestFieldSearchRank, normalizeSearch } from "../search-rank.js";
16
17
 
@@ -122,7 +123,7 @@ const SYSTEM_PAGES: PaletteItem[] = [
122
123
  { title: "Collections", path: "/collections", type: "system" },
123
124
  { title: "Settings", path: "/settings", type: "system" },
124
125
  { title: "General", path: "/settings/general", type: "system" },
125
- { title: "Navigation", path: "/settings/navigation", type: "system" },
126
+ { title: "Navigation", path: NAVIGATION_SETTINGS_PATH, type: "system" },
126
127
  {
127
128
  title: "Appearance",
128
129
  path: "/settings/appearance/color-theme",