@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
@@ -10,6 +10,11 @@
10
10
  */
11
11
 
12
12
  import type { MessageDescriptor } from "@lingui/core";
13
+ import {
14
+ getCjkFontFromLanguageTag,
15
+ isCjkSerifFont,
16
+ type CjkSerifFont,
17
+ } from "../i18n/detect.js";
13
18
 
14
19
  /**
15
20
  * A font theme definition with heading + body pairing.
@@ -29,29 +34,35 @@ export interface FontTheme {
29
34
  description: MessageDescriptor;
30
35
  }
31
36
 
32
- const DEFAULT_CJK_SERIF_FALLBACK =
33
- '"Songti SC", STSong, SimSun, "Songti TC", PMingLiU, MingLiU, "Noto Serif SC", "Noto Serif CJK SC", "Noto Serif TC", "Noto Serif CJK TC"';
37
+ const DEFAULT_CJK_SERIF_FALLBACK = '"Jant Language Fallback"';
38
+ const DEFAULT_CJK_SANS_FALLBACK = '"Jant Language Fallback"';
34
39
  const HANS_CJK_SERIF_FALLBACK =
35
40
  '"Songti SC", STSong, SimSun, "Noto Serif SC", "Noto Serif CJK SC", "Songti TC", PMingLiU, MingLiU, "Noto Serif TC", "Noto Serif CJK TC"';
41
+ const HANS_CJK_SANS_FALLBACK =
42
+ '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "Noto Sans CJK SC", "PingFang TC", "Hiragino Sans CNS", "Microsoft JhengHei", "Noto Sans TC", "Noto Sans CJK TC"';
36
43
  const HANT_CJK_SERIF_FALLBACK =
37
44
  '"Songti TC", PMingLiU, MingLiU, "Noto Serif TC", "Noto Serif CJK TC", "Songti SC", STSong, SimSun, "Noto Serif SC", "Noto Serif CJK SC"';
45
+ const HANT_CJK_SANS_FALLBACK =
46
+ '"PingFang TC", "Hiragino Sans CNS", "Microsoft JhengHei", "Noto Sans TC", "Noto Sans CJK TC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "Noto Sans CJK SC"';
38
47
  const JP_CJK_SERIF_FALLBACK =
39
- '"Hiragino Mincho ProN", "Hiragino Mincho Pro", "Yu Mincho", YuMincho, "Noto Serif JP", "Noto Serif CJK JP", "MS PMincho", "MS Mincho", serif';
48
+ '"Hiragino Mincho ProN", "Hiragino Mincho Pro", "Yu Mincho", YuMincho, "Noto Serif JP", "Noto Serif CJK JP", "MS PMincho", "MS Mincho"';
49
+ const JP_CJK_SANS_FALLBACK =
50
+ '"Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic", YuGothic, "Noto Sans JP", "Noto Sans CJK JP", Meiryo';
40
51
  const KO_CJK_SERIF_FALLBACK =
41
- '"Batang", "Noto Serif KR", "Noto Serif CJK KR", "NanumMyeongjo", serif';
52
+ 'Batang, "Noto Serif KR", "Noto Serif CJK KR", NanumMyeongjo';
53
+ const KO_CJK_SANS_FALLBACK =
54
+ '"Apple SD Gothic Neo", "Noto Sans KR", "Noto Sans CJK KR", "Malgun Gothic"';
42
55
  const CJK_SERIF_FALLBACK_VAR = "var(--font-cjk-serif-fallback)";
56
+ const CJK_SANS_FALLBACK_VAR = "var(--font-cjk-sans-fallback)";
43
57
 
44
58
  /** System sans-serif stack */
45
- const SANS =
46
- 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "PingFang TC", "PingFang SC", "Hiragino Sans CNS", "Hiragino Sans GB", "Microsoft JhengHei", "Microsoft YaHei", "Noto Sans CJK TC", "Noto Sans CJK SC", sans-serif';
59
+ const SANS = `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, ${CJK_SANS_FALLBACK_VAR}, sans-serif`;
47
60
 
48
61
  /** Humanist sans stack with self-hosted Latin and system CJK fallback */
49
- const HUMANIST_SANS =
50
- '"Source Sans 3 Variable", "Source Sans 3", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "PingFang TC", "PingFang SC", "Hiragino Sans CNS", "Hiragino Sans GB", "Microsoft JhengHei", "Microsoft YaHei", "Noto Sans CJK TC", "Noto Sans CJK SC", sans-serif';
62
+ const HUMANIST_SANS = `"Source Sans 3 Variable", "Source Sans 3", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, ${CJK_SANS_FALLBACK_VAR}, sans-serif`;
51
63
 
52
64
  /** Narrower newsroom sans for headlines and labels */
53
- const NEWSROOM_SANS =
54
- '"News Cycle", "Franklin Gothic Medium", "Arial Narrow", "Helvetica Neue", Helvetica, Arial, "PingFang TC", "PingFang SC", "Hiragino Sans CNS", "Hiragino Sans GB", "Microsoft JhengHei", "Microsoft YaHei", "Noto Sans CJK TC", "Noto Sans CJK SC", sans-serif';
65
+ const NEWSROOM_SANS = `"News Cycle", "Franklin Gothic Medium", "Arial Narrow", "Helvetica Neue", Helvetica, Arial, ${CJK_SANS_FALLBACK_VAR}, sans-serif`;
55
66
 
56
67
  /**
57
68
  * Editorial serif stack
@@ -81,8 +92,7 @@ const TUFTE_SERIF = `et-book, Palatino, "Palatino Linotype", "Palatino LT STD",
81
92
  *
82
93
  * Futura (macOS); Century Gothic (Windows); clean geometric proportions
83
94
  */
84
- const GEOMETRIC_SANS =
85
- '"Avenir Next", Avenir, Futura, "Century Gothic", Montserrat, "Noto Sans", "PingFang TC", "PingFang SC", "Hiragino Sans CNS", "Hiragino Sans GB", "Microsoft JhengHei", "Microsoft YaHei", "Noto Sans CJK TC", "Noto Sans CJK SC", sans-serif';
95
+ const GEOMETRIC_SANS = `"Avenir Next", Avenir, Futura, "Century Gothic", Montserrat, "Noto Sans", ${CJK_SANS_FALLBACK_VAR}, sans-serif`;
86
96
 
87
97
  /**
88
98
  * Resolve all CSS variables a font theme contributes.
@@ -107,35 +117,73 @@ export function getFontThemeCssVariables(
107
117
  }
108
118
 
109
119
  /**
110
- * Resolve runtime CJK serif fallbacks for the configured CJK font setting.
120
+ * Resolve the language font profile for the current content language.
111
121
  *
112
- * @param cjkSerifFont - CJK serif font setting ("off", "zh-Hans", "zh-Hant", "ja", "ko")
113
- * @returns CSS variable overrides for CJK sites
122
+ * An adapted content language always wins. The explicit CJK setting is a
123
+ * fallback for sites whose primary content language does not have a profile.
114
124
  *
125
+ * @param siteLanguage - BCP 47 content language tag
126
+ * @param cjkFallback - Explicit CJK fallback setting
127
+ * @returns Resolved CJK font profile, or `undefined` for the default stack
115
128
  * @example
116
- * ```typescript
117
- * const vars = getCjkSerifCssVariables("zh-Hans");
118
- * // => { "--font-cjk-serif-fallback": '"Songti SC", ...' }
119
- * ```
129
+ * resolveCjkFontProfile("zh-TW", "zh-Hans") // "zh-Hant"
130
+ */
131
+ export function resolveCjkFontProfile(
132
+ siteLanguage?: string,
133
+ cjkFallback?: string,
134
+ ): Exclude<CjkSerifFont, "off"> | undefined {
135
+ const contentProfile = siteLanguage
136
+ ? getCjkFontFromLanguageTag(siteLanguage)
137
+ : undefined;
138
+ if (contentProfile) return contentProfile;
139
+
140
+ return isCjkSerifFont(cjkFallback) && cjkFallback !== "off"
141
+ ? cjkFallback
142
+ : undefined;
143
+ }
144
+
145
+ /**
146
+ * Build serif and sans fallback variables for the resolved language profile.
147
+ *
148
+ * @param siteLanguage - BCP 47 content language tag
149
+ * @param cjkFallback - Explicit CJK fallback setting
150
+ * @returns CSS variables consumed by every font theme
151
+ * @example
152
+ * getCjkFontCssVariables("en", "zh-Hans")
153
+ * // => { "--font-cjk-serif-fallback": '"Songti SC", ...', ... }
120
154
  */
121
- export function getCjkSerifCssVariables(
122
- cjkSerifFont?: string,
155
+ export function getCjkFontCssVariables(
156
+ siteLanguage?: string,
157
+ cjkFallback?: string,
123
158
  ): Record<string, string> {
124
- switch (cjkSerifFont) {
159
+ switch (resolveCjkFontProfile(siteLanguage, cjkFallback)) {
125
160
  case "zh-Hans":
126
- return { "--font-cjk-serif-fallback": HANS_CJK_SERIF_FALLBACK };
161
+ return {
162
+ "--font-cjk-serif-fallback": HANS_CJK_SERIF_FALLBACK,
163
+ "--font-cjk-sans-fallback": HANS_CJK_SANS_FALLBACK,
164
+ };
127
165
  case "zh-Hant":
128
- return { "--font-cjk-serif-fallback": HANT_CJK_SERIF_FALLBACK };
166
+ return {
167
+ "--font-cjk-serif-fallback": HANT_CJK_SERIF_FALLBACK,
168
+ "--font-cjk-sans-fallback": HANT_CJK_SANS_FALLBACK,
169
+ };
129
170
  case "ja":
130
- return { "--font-cjk-serif-fallback": JP_CJK_SERIF_FALLBACK };
171
+ return {
172
+ "--font-cjk-serif-fallback": JP_CJK_SERIF_FALLBACK,
173
+ "--font-cjk-sans-fallback": JP_CJK_SANS_FALLBACK,
174
+ };
131
175
  case "ko":
132
- return { "--font-cjk-serif-fallback": KO_CJK_SERIF_FALLBACK };
176
+ return {
177
+ "--font-cjk-serif-fallback": KO_CJK_SERIF_FALLBACK,
178
+ "--font-cjk-sans-fallback": KO_CJK_SANS_FALLBACK,
179
+ };
133
180
  default:
134
181
  return {};
135
182
  }
136
183
  }
137
184
 
138
185
  export const DEFAULT_FONT_CJK_SERIF_FALLBACK = DEFAULT_CJK_SERIF_FALLBACK;
186
+ export const DEFAULT_FONT_CJK_SANS_FALLBACK = DEFAULT_CJK_SANS_FALLBACK;
139
187
 
140
188
  export const BUILTIN_FONT_THEMES: FontTheme[] = [
141
189
  {
@@ -33,6 +33,7 @@ import {
33
33
  IS_VITE_DEV,
34
34
  } from "../../lib/version.js";
35
35
  import { I18nProvider } from "../../i18n/index.js";
36
+ import { resolveCjkFontProfile } from "../font-themes.js";
36
37
  import { resetIconCollector } from "../shared/icon-collector.js";
37
38
  import { Icon } from "../shared/Icon.js";
38
39
  import { IconSprite } from "../shared/IconSprite.js";
@@ -177,20 +178,24 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
177
178
  const resolvedClientBundle =
178
179
  clientBundle ?? (isAuthenticated ? "full" : "public");
179
180
  const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
181
+ const cjkFontProfile = resolveCjkFontProfile(
182
+ appConfig?.siteLanguage ?? resolvedLang,
183
+ cjkSerifFont,
184
+ );
180
185
  const cjkStylesheetPath =
181
- cjkSerifFont === "zh-Hans"
186
+ cjkFontProfile === "zh-Hans"
182
187
  ? IS_VITE_DEV
183
188
  ? assetPath("/src/style-cjk.css")
184
189
  : toPublicAssetPath(CLIENT_CJK_CSS_FILE, assetBasePath)
185
- : cjkSerifFont === "zh-Hant"
190
+ : cjkFontProfile === "zh-Hant"
186
191
  ? IS_VITE_DEV
187
192
  ? assetPath("/src/style-cjk-tc.css")
188
193
  : toPublicAssetPath(CLIENT_CJK_TC_CSS_FILE, assetBasePath)
189
- : cjkSerifFont === "ja"
194
+ : cjkFontProfile === "ja"
190
195
  ? IS_VITE_DEV
191
196
  ? assetPath("/src/style-cjk-jp.css")
192
197
  : toPublicAssetPath(CLIENT_CJK_JP_CSS_FILE, assetBasePath)
193
- : cjkSerifFont === "ko"
198
+ : cjkFontProfile === "ko"
194
199
  ? IS_VITE_DEV
195
200
  ? assetPath("/src/style-cjk-kr.css")
196
201
  : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath)
@@ -248,11 +253,14 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
248
253
  socialImageHeightValue !== undefined &&
249
254
  socialImageWidthValue > socialImageHeightValue &&
250
255
  socialImageWidthValue >= 300;
251
- const mainFeedHref = appConfig ? toPublicPath("/feed", sitePathPrefix) : null;
252
- const latestFeedHref = appConfig
256
+ const feedsEnabled = appConfig?.rssFeedsEnabled === true;
257
+ const mainFeedHref = feedsEnabled
258
+ ? toPublicPath("/feed", sitePathPrefix)
259
+ : null;
260
+ const latestFeedHref = feedsEnabled
253
261
  ? toPublicPath("/latest/feed", sitePathPrefix)
254
262
  : null;
255
- const featuredFeedHref = appConfig
263
+ const featuredFeedHref = feedsEnabled
256
264
  ? toPublicPath("/featured/feed", sitePathPrefix)
257
265
  : null;
258
266
  const mainFeedTitle =
@@ -451,7 +459,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
451
459
  {...(isAuthenticated ? { "data-authenticated": true } : {})}
452
460
  >
453
461
  {content}
454
- <div id="toast-container" class="toast-container" popover="manual">
462
+ <div
463
+ id="toast-container"
464
+ class="toast-container"
465
+ popover="manual"
466
+ aria-live="polite"
467
+ aria-relevant="additions text"
468
+ >
455
469
  {toast && (
456
470
  <div
457
471
  class={`toast ${toast.type === "error" ? "toast-error" : "toast-success"}`}
@@ -16,15 +16,20 @@ function createContext(
16
16
  themeMode?: "auto" | "light" | "dark";
17
17
  themeId?: string;
18
18
  defaultThemeId?: string;
19
+ siteLanguage?: string;
20
+ cjkSerifFont?: string;
21
+ rssFeedsEnabled?: boolean;
19
22
  },
20
23
  ) {
21
24
  const values = {
22
25
  appConfig: {
23
26
  mainRssFeed,
27
+ rssFeedsEnabled: overrides?.rssFeedsEnabled ?? true,
24
28
  sitePathPrefix: overrides?.sitePathPrefix ?? "",
25
29
  siteUrl: overrides?.siteUrl ?? "https://example.com",
26
30
  siteAvatarUrl: overrides?.siteAvatarUrl,
27
- siteLanguage: "en",
31
+ siteLanguage: overrides?.siteLanguage ?? "en",
32
+ cjkSerifFont: overrides?.cjkSerifFont ?? "off",
28
33
  noindex: false,
29
34
  customCSS: "",
30
35
  themeMode: overrides?.themeMode ?? "auto",
@@ -301,6 +306,20 @@ describe("BaseLayout", () => {
301
306
  expect(html).not.toContain('href="/latest/feed"');
302
307
  });
303
308
 
309
+ it("omits feed autodiscovery when feed publishing is disabled", async () => {
310
+ const { BaseLayout } = await loadBaseLayout();
311
+ const html = renderToString(
312
+ BaseLayout({
313
+ title: "Jant",
314
+ c: createContext("featured", { rssFeedsEnabled: false }),
315
+ children: "Test",
316
+ }),
317
+ );
318
+
319
+ expect(html).not.toContain('type="application/atom+xml"');
320
+ expect(html).not.toContain('href="/feed"');
321
+ });
322
+
304
323
  it("uses the public asset base path from appConfig in production", async () => {
305
324
  const { BaseLayout } = await loadBaseLayout();
306
325
  const html = renderToString(
@@ -320,6 +339,50 @@ describe("BaseLayout", () => {
320
339
  expect(html).toContain('data-asset-base-path="/blog/_assets"');
321
340
  });
322
341
 
342
+ it("loads the content language font profile when the CJK fallback is off", async () => {
343
+ const { BaseLayout } = await loadBaseLayout();
344
+ const html = renderToString(
345
+ BaseLayout({
346
+ title: "Jant",
347
+ c: createContext("featured", {
348
+ siteLanguage: "zh-Hant",
349
+ cjkSerifFont: "off",
350
+ }),
351
+ children: "Test",
352
+ }),
353
+ );
354
+
355
+ expect(html).toContain("client-cjk-tc.css");
356
+ expect(html).not.toContain("client-cjk.css");
357
+ });
358
+
359
+ it("uses the manual CJK fallback only for an unadapted content language", async () => {
360
+ const { BaseLayout } = await loadBaseLayout();
361
+ const fallbackHtml = renderToString(
362
+ BaseLayout({
363
+ title: "Jant",
364
+ c: createContext("featured", {
365
+ siteLanguage: "en",
366
+ cjkSerifFont: "zh-Hans",
367
+ }),
368
+ children: "Test",
369
+ }),
370
+ );
371
+ const contentLanguageHtml = renderToString(
372
+ BaseLayout({
373
+ title: "Jant",
374
+ c: createContext("featured", {
375
+ siteLanguage: "zh-Hant",
376
+ cjkSerifFont: "zh-Hans",
377
+ }),
378
+ children: "Test",
379
+ }),
380
+ );
381
+
382
+ expect(fallbackHtml).toContain("client-cjk.css");
383
+ expect(contentLanguageHtml).toContain("client-cjk-tc.css");
384
+ });
385
+
323
386
  it("exposes the active theme id on the root html element", async () => {
324
387
  const { BaseLayout } = await loadBaseLayout();
325
388
  const html = renderToString(
@@ -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>