@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
@@ -19,6 +19,7 @@ import { toPublicPath } from "../../lib/url.js";
19
19
  import { TimelineFeed } from "../feed/TimelineFeed.js";
20
20
  import { getCollectionMutationLabels } from "../shared/collection-management-labels.js";
21
21
  import { getIconSvg } from "../../lib/icons.js";
22
+ import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
22
23
 
23
24
  const escapeJson = (data: unknown) =>
24
25
  JSON.stringify(data).replace(/</g, "\\u003c");
@@ -35,7 +36,9 @@ export const CollectionPage: FC<CollectionPageProps> = ({
35
36
  defaultSort,
36
37
  showRatingSort,
37
38
  isAuthenticated,
39
+ isInNavigation = false,
38
40
  sitePathPrefix = "",
41
+ feedHref,
39
42
  }) => {
40
43
  const primaryCollection = collections[0];
41
44
  if (!primaryCollection) return null;
@@ -58,6 +61,10 @@ export const CollectionPage: FC<CollectionPageProps> = ({
58
61
  )}`,
59
62
  sitePathPrefix,
60
63
  );
64
+ const navigationSettingsUrl = toPublicPath(
65
+ NAVIGATION_SETTINGS_PATH,
66
+ sitePathPrefix,
67
+ );
61
68
  const sortUiId = isAggregate
62
69
  ? collections.map((collection) => collection.slug).join("-")
63
70
  : primaryCollection.id;
@@ -226,7 +233,9 @@ export const CollectionPage: FC<CollectionPageProps> = ({
226
233
  <div
227
234
  class="collection-page-description prose"
228
235
  dangerouslySetInnerHTML={{
229
- __html: renderMarkdown(primaryCollection.description),
236
+ __html: renderMarkdown(primaryCollection.description, {
237
+ namespace: primaryCollection.id,
238
+ }),
230
239
  }}
231
240
  />
232
241
  ) : null}
@@ -298,25 +307,30 @@ export const CollectionPage: FC<CollectionPageProps> = ({
298
307
  "@context: Plural thread count label on collection detail page",
299
308
  }),
300
309
  )}
301
- {pageLabel ? <span> / {pageLabel}</span> : null}{" "}
302
- <a
303
- href={toPublicPath(`${pagePath}/feed`, sitePathPrefix)}
304
- class="feed-link"
305
- title={i18n._(
306
- msg({
307
- message: "RSS feed",
308
- comment:
309
- "@context: Tooltip for the RSS feed link on a collection page",
310
- }),
311
- )}
312
- rel="noopener noreferrer"
313
- >
314
- <span
315
- dangerouslySetInnerHTML={{
316
- __html: getIconSvg("rss") ?? "",
317
- }}
318
- />
319
- </a>
310
+ {pageLabel ? <span> / {pageLabel}</span> : null}
311
+ {feedHref ? (
312
+ <>
313
+ {" "}
314
+ <a
315
+ href={toPublicPath(feedHref, sitePathPrefix)}
316
+ class="feed-link"
317
+ title={i18n._(
318
+ msg({
319
+ message: "RSS feed",
320
+ comment:
321
+ "@context: Tooltip for the RSS feed link on a collection page",
322
+ }),
323
+ )}
324
+ rel="noopener noreferrer"
325
+ >
326
+ <span
327
+ dangerouslySetInnerHTML={{
328
+ __html: getIconSvg("rss") ?? "",
329
+ }}
330
+ />
331
+ </a>
332
+ </>
333
+ ) : null}
320
334
  </p>
321
335
  <span class="collection-page-meta-divider" aria-hidden="true">
322
336
  &middot;
@@ -450,6 +464,7 @@ export const CollectionPage: FC<CollectionPageProps> = ({
450
464
  data-collection-page-actions
451
465
  data-collection-id={primaryCollection.id}
452
466
  data-collection-page-labels={escapeJson(mutationLabels)}
467
+ data-collection-in-navigation={String(isInNavigation)}
453
468
  data-collection-page-redirect-url={toPublicPath(
454
469
  getCollectionsDirectoryPath(),
455
470
  sitePathPrefix,
@@ -510,6 +525,70 @@ export const CollectionPage: FC<CollectionPageProps> = ({
510
525
  {mutationLabels.edit}
511
526
  </span>
512
527
  </a>
528
+ <button
529
+ type="button"
530
+ class="collections-page-menu-item"
531
+ role="menuitem"
532
+ data-collection-page-action="add-to-navigation"
533
+ hidden={isInNavigation}
534
+ >
535
+ <span
536
+ class="collections-page-menu-item-icon"
537
+ aria-hidden="true"
538
+ >
539
+ <svg
540
+ xmlns="http://www.w3.org/2000/svg"
541
+ width="16"
542
+ height="16"
543
+ viewBox="0 0 24 24"
544
+ fill="none"
545
+ stroke="currentColor"
546
+ stroke-width="2"
547
+ stroke-linecap="round"
548
+ stroke-linejoin="round"
549
+ >
550
+ <path d="M12 5v14" />
551
+ <path d="M5 12h14" />
552
+ </svg>
553
+ </span>
554
+ <span class="collections-page-menu-item-label">
555
+ {mutationLabels.addToNavigation}
556
+ </span>
557
+ </button>
558
+ <a
559
+ href={navigationSettingsUrl}
560
+ class="collections-page-menu-item"
561
+ role="menuitem"
562
+ data-collection-page-edit-navigation
563
+ hidden={!isInNavigation}
564
+ >
565
+ <span
566
+ class="collections-page-menu-item-icon"
567
+ aria-hidden="true"
568
+ >
569
+ <svg
570
+ xmlns="http://www.w3.org/2000/svg"
571
+ width="16"
572
+ height="16"
573
+ viewBox="0 0 24 24"
574
+ fill="none"
575
+ stroke="currentColor"
576
+ stroke-width="2"
577
+ stroke-linecap="round"
578
+ stroke-linejoin="round"
579
+ >
580
+ <path d="M8 6h13" />
581
+ <path d="M8 12h13" />
582
+ <path d="M8 18h13" />
583
+ <path d="M3 6h.01" />
584
+ <path d="M3 12h.01" />
585
+ <path d="M3 18h.01" />
586
+ </svg>
587
+ </span>
588
+ <span class="collections-page-menu-item-label">
589
+ {mutationLabels.editNavigation}
590
+ </span>
591
+ </a>
513
592
  <button
514
593
  type="button"
515
594
  class="collections-page-menu-item collections-page-menu-item-danger"
@@ -556,7 +635,7 @@ export const CollectionPage: FC<CollectionPageProps> = ({
556
635
  ? i18n._(
557
636
  msg({
558
637
  message:
559
- "Nothing here yet. Add posts to one of these collections to fill this view.",
638
+ "Nothing here yet. Add threads to one of these collections to fill this view.",
560
639
  comment:
561
640
  "@context: Empty state message on an aggregate collection page",
562
641
  }),
@@ -564,7 +643,7 @@ export const CollectionPage: FC<CollectionPageProps> = ({
564
643
  : i18n._(
565
644
  msg({
566
645
  message:
567
- "This collection is empty. Add posts from the editor.",
646
+ "This collection is empty. Add threads from the editor.",
568
647
  comment: "@context: Empty state message",
569
648
  }),
570
649
  )}
@@ -14,13 +14,14 @@ import { CollectionsManager } from "../shared/CollectionsManager.js";
14
14
  export const CollectionsPage: FC<CollectionsPageProps> = ({
15
15
  items,
16
16
  isAuthenticated,
17
+ navigationCollectionIds = [],
17
18
  sitePathPrefix = "",
18
19
  siteOrigin = "",
19
20
  }) => {
20
21
  const { i18n } = useLingui();
21
22
  const emptyMessage = i18n._(
22
23
  msg({
23
- message: "No collections yet. Start one to organize posts by topic.",
24
+ message: "No collections yet. Start one to organize threads by topic.",
24
25
  comment: "@context: Empty state message on collections page",
25
26
  }),
26
27
  );
@@ -30,6 +31,7 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
30
31
  <div class="py-6" data-page="collections">
31
32
  <CollectionsManager
32
33
  items={items}
34
+ navigationCollectionIds={navigationCollectionIds}
33
35
  sitePathPrefix={sitePathPrefix}
34
36
  siteOrigin={siteOrigin}
35
37
  />
@@ -12,10 +12,22 @@
12
12
  import { msg } from "@lingui/core/macro";
13
13
  import type { FC } from "hono/jsx";
14
14
  import { useLingui } from "../../i18n/context.js";
15
- import type { PostPageProps, PostView } from "../../types.js";
15
+ import type {
16
+ PostPageProps,
17
+ PostView,
18
+ TimelineCardDisplayOptions,
19
+ } from "../../types.js";
16
20
  import { TimelineItemFromPost } from "../feed/TimelineItem.js";
17
21
 
18
- const renderThreadItem = (tp: PostView, currentId: string) => {
22
+ const PREVIEW_DISPLAY: TimelineCardDisplayOptions = {
23
+ footer: { hideActions: true, hideReply: true, hideTimestamp: false },
24
+ };
25
+
26
+ const renderThreadItem = (
27
+ tp: PostView,
28
+ currentId: string,
29
+ isPreview: boolean,
30
+ ) => {
19
31
  const isCurrent = tp.id === currentId;
20
32
  return (
21
33
  <div
@@ -27,16 +39,19 @@ const renderThreadItem = (tp: PostView, currentId: string) => {
27
39
  <TimelineItemFromPost
28
40
  post={tp}
29
41
  mode="detail"
30
- display={{ footer: { hideTimestamp: false } }}
42
+ display={
43
+ isPreview ? PREVIEW_DISPLAY : { footer: { hideTimestamp: false } }
44
+ }
31
45
  />
32
46
  </div>
33
47
  );
34
48
  };
35
49
 
36
- const ThreadDetail: FC<{ post: PostView; threadPosts: PostView[] }> = ({
37
- post,
38
- threadPosts,
39
- }) => {
50
+ const ThreadDetail: FC<{
51
+ post: PostView;
52
+ threadPosts: PostView[];
53
+ isPreview: boolean;
54
+ }> = ({ post, threadPosts, isPreview }) => {
40
55
  const { i18n } = useLingui();
41
56
  const showMoreLabel = i18n._(
42
57
  msg({
@@ -66,7 +81,7 @@ const ThreadDetail: FC<{ post: PostView; threadPosts: PostView[] }> = ({
66
81
  data-thread-context
67
82
  data-collapsed=""
68
83
  >
69
- {ancestors.map((tp) => renderThreadItem(tp, post.id))}
84
+ {ancestors.map((tp) => renderThreadItem(tp, post.id, isPreview))}
70
85
  </div>
71
86
  <button
72
87
  type="button"
@@ -94,12 +109,16 @@ const ThreadDetail: FC<{ post: PostView; threadPosts: PostView[] }> = ({
94
109
  </button>
95
110
  </>
96
111
  )}
97
- {currentAndAfter.map((tp) => renderThreadItem(tp, post.id))}
112
+ {currentAndAfter.map((tp) => renderThreadItem(tp, post.id, isPreview))}
98
113
  </div>
99
114
  );
100
115
  };
101
116
 
102
- export const PostPage: FC<PostPageProps> = ({ post, threadPosts }) => {
117
+ export const PostPage: FC<PostPageProps> = ({
118
+ post,
119
+ threadPosts,
120
+ isPreview = false,
121
+ }) => {
103
122
  return (
104
123
  <div
105
124
  data-post-view
@@ -107,9 +126,17 @@ export const PostPage: FC<PostPageProps> = ({ post, threadPosts }) => {
107
126
  data-thread-root-id={post.threadRootId ?? post.id}
108
127
  >
109
128
  {threadPosts && threadPosts.length > 1 ? (
110
- <ThreadDetail post={post} threadPosts={threadPosts} />
129
+ <ThreadDetail
130
+ post={post}
131
+ threadPosts={threadPosts}
132
+ isPreview={isPreview}
133
+ />
111
134
  ) : (
112
- <TimelineItemFromPost post={post} mode="detail" />
135
+ <TimelineItemFromPost
136
+ post={post}
137
+ mode="detail"
138
+ display={isPreview ? PREVIEW_DISPLAY : undefined}
139
+ />
113
140
  )}
114
141
  {/* Public integration slot — code injection (giscus, Webmentions, etc.) appends here. */}
115
142
  <div data-post-end />
@@ -15,6 +15,7 @@ import {
15
15
  CompactCollectionTags,
16
16
  PostMenuTriggerButton,
17
17
  } from "../shared/PostFooter.js";
18
+ import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
18
19
 
19
20
  // External link icon (shared by LinkCard)
20
21
  const ExternalLinkIcon = () => (
@@ -46,20 +47,11 @@ const SearchResultCard: FC<{
46
47
  }
47
48
  }
48
49
 
49
- const hasCollections = post.collections.length > 0;
50
+ const isChildPost = !!post.replyToId || !!post.threadRootId;
51
+ const visibleCollections = isChildPost ? [] : post.collections;
52
+ const hasCollections = visibleCollections.length > 0;
50
53
 
51
- // Data attributes required by the client-side post menu component
52
- const postAttrs = {
53
- "data-post": "",
54
- "data-format": post.format,
55
- "data-post-id": post.id,
56
- "data-post-slug": post.slug,
57
- "data-thread-root-id": post.threadRootId ?? post.id,
58
- ...(post.pinned ? { "data-post-pinned": "" } : {}),
59
- ...(post.featured ? { "data-post-featured": "" } : {}),
60
- "data-post-visibility": post.visibility,
61
- ...(post.threadRootId ? { "data-post-reply": "" } : {}),
62
- };
54
+ const postAttrs = getPostArticleAttributes(post);
63
55
 
64
56
  const footer = (
65
57
  <footer class="post-menu-footer mt-2">
@@ -69,7 +61,7 @@ const SearchResultCard: FC<{
69
61
  <time datetime={post.publishedAt}>{post.publishedAtFormatted}</time>
70
62
  </a>
71
63
  <CompactCollectionTags
72
- collections={post.collections}
64
+ collections={visibleCollections}
73
65
  showSeparator={hasCollections}
74
66
  />
75
67
  </div>
@@ -106,4 +106,74 @@ describe("ArchivePage", () => {
106
106
  expect(html).toContain("archive-chip-active");
107
107
  expect(html).toContain("Single posts");
108
108
  });
109
+
110
+ it("uses a recognizable quote mark for the quote format filter", () => {
111
+ const html = renderArchivePage({ filters: { format: "quote" } });
112
+
113
+ expect(html).toContain('class="lucide lucide-quote"');
114
+ expect(html).not.toContain('class="lucide lucide-text-quote"');
115
+ });
116
+
117
+ it("renders the feed button only when a feed href is provided", () => {
118
+ expect(renderArchivePage()).not.toContain('class="feed-link"');
119
+ expect(renderArchivePage({ feedHref: "/archive/feed" })).toContain(
120
+ 'class="feed-link"',
121
+ );
122
+ });
123
+
124
+ it("offers the updated sort and marks published as the default", () => {
125
+ const html = renderArchivePage();
126
+
127
+ expect(html).toContain('aria-label="Sort order"');
128
+ expect(html).toContain("sort=updated");
129
+ expect(html).toContain(
130
+ '<a href="/archive" class="archive-view-btn archive-view-btn-active" role="radio" aria-checked="true" aria-label="Sort by when each thread was published"',
131
+ );
132
+ });
133
+
134
+ it("marks the updated sort active and keeps a way back", () => {
135
+ const html = renderArchivePage({ filters: { sort: "updated" } });
136
+
137
+ expect(html).toContain(
138
+ '<a href="/archive?sort=updated" class="archive-view-btn archive-view-btn-active" role="radio" aria-checked="true" aria-label="Sort by when each thread was last added to"',
139
+ );
140
+ expect(html).toContain('<a href="/archive" class="archive-view-btn"');
141
+ });
142
+
143
+ // Icon-only controls need a hover tooltip too — aria-label only reaches
144
+ // assistive tech, and a bare clock/history pair is unreadable without one.
145
+ it("gives every toolbar toggle a tooltip matching its accessible name", () => {
146
+ const html = renderArchivePage();
147
+
148
+ for (const label of [
149
+ "Sort by when each thread was published",
150
+ "Sort by when each thread was last added to",
151
+ "Show as a grid of tiles",
152
+ "Show as a list with full posts",
153
+ ]) {
154
+ expect(html).toContain(`aria-label="${label}" title="${label}"`);
155
+ }
156
+ });
157
+
158
+ it("labels month headers as update months in the updated sort", () => {
159
+ expect(renderArchivePage()).toContain(
160
+ '<span class="archive-month-header-label">March 2026</span>',
161
+ );
162
+
163
+ const updated = renderArchivePage({ filters: { sort: "updated" } });
164
+ expect(updated).toContain(
165
+ '<span class="archive-month-header-label">Updated March 2026</span>',
166
+ );
167
+ expect(updated).toContain("newest changes first");
168
+ });
169
+
170
+ it("keeps the sort selection in filter and view links", () => {
171
+ const html = renderArchivePage({
172
+ filters: { sort: "updated" },
173
+ availableCollections: [{ slug: "tech", title: "Tech" }],
174
+ });
175
+
176
+ expect(html).toContain("collection=tech&amp;sort=updated");
177
+ expect(html).toContain("view=list&amp;sort=updated");
178
+ });
109
179
  });
@@ -0,0 +1,88 @@
1
+ import type { Context } from "hono";
2
+ import { renderToString } from "hono/jsx/dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+ import { I18nProvider } from "../../../i18n/context.js";
5
+ import { createI18n } from "../../../i18n/i18n.js";
6
+ import type { PostView } from "../../../types.js";
7
+ import { PostPage } from "../PostPage.js";
8
+
9
+ function createPostView(overrides: Partial<PostView> = {}): PostView {
10
+ return {
11
+ id: "pst_root",
12
+ permalink: "/root",
13
+ slug: "root",
14
+ bodyHtml: "<p>Post body</p>",
15
+ format: "note",
16
+ status: "published",
17
+ visibility: "public",
18
+ pinned: false,
19
+ featured: false,
20
+ publishedAt: "2026-07-18T00:00:00.000Z",
21
+ publishedAtFormatted: "Jul 18, 2026",
22
+ publishedAtTime: "08:00",
23
+ publishedAtRelative: "now",
24
+ updatedAt: "2026-07-18T00:00:00.000Z",
25
+ media: [],
26
+ collections: [],
27
+ isLastInThread: false,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function renderPostPage(post: PostView, threadPosts: PostView[]): string {
33
+ const i18n = createI18n("en");
34
+ const c = {
35
+ get(key: string) {
36
+ if (key === "i18n") return i18n;
37
+ return undefined;
38
+ },
39
+ } as unknown as Context;
40
+
41
+ I18nProvider({ c, children: "" });
42
+ return renderToString(PostPage({ post, threadPosts }));
43
+ }
44
+
45
+ function getArticleTag(html: string, postId: string): string {
46
+ const article = (html.match(/<article\b[^>]*>/g) ?? []).find((tag) =>
47
+ tag.includes(`data-post-id="${postId}"`),
48
+ );
49
+ if (!article) throw new Error(`Missing article for ${postId}`);
50
+ return article;
51
+ }
52
+
53
+ describe("PostPage", () => {
54
+ it("preserves Root-versus-Child action context in detail mode", () => {
55
+ const root = createPostView();
56
+ const child = createPostView({
57
+ id: "pst_child",
58
+ permalink: "/child",
59
+ slug: "child",
60
+ format: "link",
61
+ title: "Child link",
62
+ url: "https://example.com/child",
63
+ replyToId: root.id,
64
+ threadRootId: root.id,
65
+ });
66
+ const grandchild = createPostView({
67
+ id: "pst_grandchild",
68
+ permalink: "/grandchild",
69
+ slug: "grandchild",
70
+ format: "quote",
71
+ quoteText: "Child quote",
72
+ replyToId: child.id,
73
+ threadRootId: root.id,
74
+ isLastInThread: true,
75
+ });
76
+
77
+ const html = renderPostPage(child, [root, child, grandchild]);
78
+ const rootArticle = getArticleTag(html, root.id);
79
+ const childArticle = getArticleTag(html, child.id);
80
+ const grandchildArticle = getArticleTag(html, grandchild.id);
81
+
82
+ expect(rootArticle).not.toContain("data-post-reply");
83
+ expect(childArticle).toContain('data-post-reply=""');
84
+ expect(childArticle).toContain(`data-thread-root-id="${root.id}"`);
85
+ expect(grandchildArticle).toContain('data-post-reply=""');
86
+ expect(grandchildArticle).toContain(`data-thread-root-id="${root.id}"`);
87
+ });
88
+ });
@@ -124,7 +124,7 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
124
124
  i18n._(
125
125
  msg({
126
126
  message:
127
- "No collections yet. Start one to organize posts by topic.",
127
+ "No collections yet. Start one to organize threads by topic.",
128
128
  comment: "@context: Empty state message on collections page",
129
129
  }),
130
130
  )}
@@ -231,7 +231,9 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
231
231
  <div
232
232
  class="collection-directory-description prose"
233
233
  dangerouslySetInnerHTML={{
234
- __html: renderMarkdown(item.description),
234
+ __html: renderMarkdown(item.description, {
235
+ namespace: item.id,
236
+ }),
235
237
  }}
236
238
  />
237
239
  ) : (
@@ -279,24 +281,26 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
279
281
  <div
280
282
  class="collection-directory-description prose"
281
283
  dangerouslySetInnerHTML={{
282
- __html: renderMarkdown(collection.description),
284
+ __html: renderMarkdown(collection.description, {
285
+ namespace: collection.id,
286
+ }),
283
287
  }}
284
288
  />
285
289
  )}
286
290
  <p class="collection-directory-summary">
287
291
  <span class="collection-directory-meta">
288
- {collection.postCount}{" "}
289
- {collection.postCount === 1
292
+ {collection.threadCount}{" "}
293
+ {collection.threadCount === 1
290
294
  ? i18n._(
291
295
  msg({
292
- message: "entry",
293
- comment: "@context: Singular entry count label",
296
+ message: "thread",
297
+ comment: "@context: Singular thread count label",
294
298
  }),
295
299
  )
296
300
  : i18n._(
297
301
  msg({
298
- message: "entries",
299
- comment: "@context: Plural entry count label",
302
+ message: "threads",
303
+ comment: "@context: Plural thread count label",
300
304
  }),
301
305
  )}
302
306
  </span>
@@ -15,12 +15,14 @@ const escapeJson = (data: unknown) =>
15
15
 
16
16
  export interface CollectionsManagerProps {
17
17
  items: CollectionDirectoryItem[];
18
+ navigationCollectionIds?: string[];
18
19
  sitePathPrefix?: string;
19
20
  siteOrigin?: string;
20
21
  }
21
22
 
22
23
  export const CollectionsManager: FC<CollectionsManagerProps> = ({
23
24
  items,
25
+ navigationCollectionIds = [],
24
26
  sitePathPrefix = "",
25
27
  siteOrigin = "",
26
28
  }) => {
@@ -102,21 +104,21 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
102
104
  comment: "@context: Tooltip for divider delete button",
103
105
  }),
104
106
  ),
105
- entrySingular: i18n._(
107
+ threadSingular: i18n._(
106
108
  msg({
107
- message: "entry",
108
- comment: "@context: Singular entry count label",
109
+ message: "thread",
110
+ comment: "@context: Singular thread count label",
109
111
  }),
110
112
  ),
111
- entryPlural: i18n._(
113
+ threadPlural: i18n._(
112
114
  msg({
113
- message: "entries",
114
- comment: "@context: Plural entry count label",
115
+ message: "threads",
116
+ comment: "@context: Plural thread count label",
115
117
  }),
116
118
  ),
117
119
  emptyState: i18n._(
118
120
  msg({
119
- message: "No collections yet. Start one to organize posts by topic.",
121
+ message: "No collections yet. Start one to organize threads by topic.",
120
122
  comment: "@context: Empty state message on collections page",
121
123
  }),
122
124
  ),
@@ -126,6 +128,13 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
126
128
  comment: "@context: Toast after reordering collections",
127
129
  }),
128
130
  ),
131
+ notNow: i18n._(
132
+ msg({
133
+ message: "Not now",
134
+ comment:
135
+ "@context: Action to dismiss the post-create Collection navigation prompt",
136
+ }),
137
+ ),
129
138
  ...mutationLabels,
130
139
  };
131
140
 
@@ -312,6 +321,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
312
321
  <jant-collections-manager
313
322
  items={escapeJson(items)}
314
323
  labels={escapeJson(labels)}
324
+ navigation-collection-ids={escapeJson(navigationCollectionIds)}
315
325
  >
316
326
  <CollectionDirectory
317
327
  items={items}
@@ -0,0 +1,49 @@
1
+ import { msg } from "@lingui/core/macro";
2
+ import type { FC } from "hono/jsx";
3
+ import { useLingui } from "../../i18n/context.js";
4
+
5
+ interface DraftPreviewBarProps {
6
+ editHref: string;
7
+ }
8
+
9
+ /** Persistent page chrome that distinguishes an authenticated draft preview. */
10
+ export const DraftPreviewBar: FC<DraftPreviewBarProps> = ({ editHref }) => {
11
+ const { i18n } = useLingui();
12
+ const label = i18n._(
13
+ msg({
14
+ message: "Draft preview",
15
+ comment: "@context: Status label above a draft preview page",
16
+ }),
17
+ );
18
+
19
+ return (
20
+ <aside class="draft-preview-bar" aria-label={label} data-preview-status>
21
+ <div class="draft-preview-bar-inner">
22
+ <div class="draft-preview-bar-copy">
23
+ <span class="draft-preview-bar-label">
24
+ <span class="draft-preview-bar-dot" aria-hidden="true" />
25
+ {label}
26
+ </span>
27
+ <span class="draft-preview-bar-description">
28
+ {i18n._(
29
+ msg({
30
+ message: "This post isn’t published.",
31
+ comment:
32
+ "@context: Explanation in the draft preview status bar",
33
+ }),
34
+ )}
35
+ </span>
36
+ </div>
37
+ <a class="draft-preview-bar-action" href={editHref}>
38
+ {i18n._(
39
+ msg({
40
+ message: "Edit draft",
41
+ comment:
42
+ "@context: Action in the draft preview status bar that opens the editor",
43
+ }),
44
+ )}
45
+ </a>
46
+ </div>
47
+ </aside>
48
+ );
49
+ };