@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
@@ -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(
@@ -62,6 +62,7 @@ function buildFilterUrl(
62
62
  );
63
63
  }
64
64
  if (merged.view && merged.view !== "grid") params.set("view", merged.view);
65
+ if (merged.sort === "updated") params.set("sort", "updated");
65
66
 
66
67
  const qs = params.toString();
67
68
  return qs
@@ -127,7 +128,7 @@ function getFormatLabelPlural(format: string): string {
127
128
  const FORMAT_ICONS: Record<string, string> = {
128
129
  note: "notepad-text",
129
130
  link: "external-link",
130
- quote: "text-quote",
131
+ quote: "quote",
131
132
  };
132
133
 
133
134
  /** Icon name mapping for media kinds. */
@@ -463,34 +464,118 @@ const ChipMediaSelect: FC<{
463
464
  // View Toggle
464
465
  // =============================================================================
465
466
 
467
+ /**
468
+ * One option in a toolbar toggle. `label` is used for both the hover tooltip
469
+ * and the accessible name — an icon-only control has to explain itself to
470
+ * both audiences, and one string keeps them from drifting apart.
471
+ */
472
+ const ToggleOption: FC<{
473
+ href: string;
474
+ icon: string;
475
+ label: string;
476
+ active: boolean;
477
+ }> = ({ href, icon, label, active }) => (
478
+ <a
479
+ href={href}
480
+ class={`archive-view-btn${active ? " archive-view-btn-active" : ""}`}
481
+ role="radio"
482
+ aria-checked={active ? "true" : "false"}
483
+ aria-label={label}
484
+ title={label}
485
+ >
486
+ <Icon name={icon} class="[&>svg]:size-4" />
487
+ </a>
488
+ );
489
+
466
490
  const ViewToggle: FC<{
467
491
  filters: ArchiveFilters;
468
492
  sitePathPrefix?: string;
469
493
  }> = ({ filters, sitePathPrefix = "" }) => {
494
+ const { i18n } = useLingui();
470
495
  const currentView: ArchiveView = filters.view ?? "grid";
471
- const gridUrl = buildFilterUrl(filters, { view: undefined }, sitePathPrefix);
472
- const listUrl = buildFilterUrl(filters, { view: "list" }, sitePathPrefix);
473
496
 
474
497
  return (
475
- <div class="archive-view-toggle" role="radiogroup" aria-label="View mode">
476
- <a
477
- href={gridUrl}
478
- class={`archive-view-btn${currentView === "grid" ? " archive-view-btn-active" : ""}`}
479
- role="radio"
480
- aria-checked={currentView === "grid" ? "true" : "false"}
481
- aria-label="Grid view"
482
- >
483
- <Icon name="layout-grid" class="[&>svg]:size-4" />
484
- </a>
485
- <a
486
- href={listUrl}
487
- class={`archive-view-btn${currentView === "list" ? " archive-view-btn-active" : ""}`}
488
- role="radio"
489
- aria-checked={currentView === "list" ? "true" : "false"}
490
- aria-label="List view"
491
- >
492
- <Icon name="list" class="[&>svg]:size-4" />
493
- </a>
498
+ <div
499
+ class="archive-view-toggle"
500
+ role="radiogroup"
501
+ aria-label={i18n._(
502
+ msg({
503
+ message: "View mode",
504
+ comment: "@context: Archive grid/list toggle group label",
505
+ }),
506
+ )}
507
+ >
508
+ <ToggleOption
509
+ href={buildFilterUrl(filters, { view: undefined }, sitePathPrefix)}
510
+ icon="layout-grid"
511
+ active={currentView === "grid"}
512
+ label={i18n._(
513
+ msg({
514
+ message: "Show as a grid of tiles",
515
+ comment: "@context: Archive view option - grid",
516
+ }),
517
+ )}
518
+ />
519
+ <ToggleOption
520
+ href={buildFilterUrl(filters, { view: "list" }, sitePathPrefix)}
521
+ icon="list"
522
+ active={currentView === "list"}
523
+ label={i18n._(
524
+ msg({
525
+ message: "Show as a list with full posts",
526
+ comment: "@context: Archive view option - list",
527
+ }),
528
+ )}
529
+ />
530
+ </div>
531
+ );
532
+ };
533
+
534
+ /**
535
+ * Time-axis toggle. Sits next to the view toggle because it changes how the
536
+ * page is arranged, not which threads it contains — unlike the filter chips.
537
+ */
538
+ const SortToggle: FC<{
539
+ filters: ArchiveFilters;
540
+ sitePathPrefix?: string;
541
+ }> = ({ filters, sitePathPrefix = "" }) => {
542
+ const { i18n } = useLingui();
543
+ const sortsByActivity = filters.sort === "updated";
544
+
545
+ return (
546
+ <div
547
+ class="archive-view-toggle"
548
+ role="radiogroup"
549
+ aria-label={i18n._(
550
+ msg({
551
+ message: "Sort order",
552
+ comment: "@context: Archive sort toggle group label",
553
+ }),
554
+ )}
555
+ >
556
+ <ToggleOption
557
+ href={buildFilterUrl(filters, { sort: undefined }, sitePathPrefix)}
558
+ icon="clock"
559
+ active={!sortsByActivity}
560
+ label={i18n._(
561
+ msg({
562
+ message: "Sort by when each thread was published",
563
+ comment: "@context: Archive sort option - newest published first",
564
+ }),
565
+ )}
566
+ />
567
+ <ToggleOption
568
+ href={buildFilterUrl(filters, { sort: "updated" }, sitePathPrefix)}
569
+ icon="history"
570
+ active={sortsByActivity}
571
+ label={i18n._(
572
+ msg({
573
+ message: "Sort by when each thread was last added to",
574
+ comment:
575
+ "@context: Archive sort option - threads that recently gained a post first",
576
+ }),
577
+ )}
578
+ />
494
579
  </div>
495
580
  );
496
581
  };
@@ -948,7 +1033,10 @@ const FilterBar: FC<{
948
1033
  )}
949
1034
  </div>
950
1035
 
951
- <ViewToggle filters={filters} sitePathPrefix={sitePathPrefix} />
1036
+ <div class="archive-toolbar-toggles">
1037
+ <SortToggle filters={filters} sitePathPrefix={sitePathPrefix} />
1038
+ <ViewToggle filters={filters} sitePathPrefix={sitePathPrefix} />
1039
+ </div>
952
1040
  </div>
953
1041
  );
954
1042
  };
@@ -1258,6 +1346,7 @@ export const ArchivePage: FC<ArchivePageProps> = ({
1258
1346
  }) => {
1259
1347
  const { i18n } = useLingui();
1260
1348
  const currentView: ArchiveView = filters.view ?? "grid";
1349
+ const sortsByActivity = filters.sort === "updated";
1261
1350
  const paginationBaseUrl = buildFilterUrl(filters, {}, sitePathPrefix);
1262
1351
  const totalCountUnit =
1263
1352
  totalCount === 1
@@ -1295,6 +1384,18 @@ export const ArchivePage: FC<ArchivePageProps> = ({
1295
1384
  >
1296
1385
  <span class="archive-page-summary-count">{totalCount}</span>{" "}
1297
1386
  {totalCountUnit}
1387
+ {sortsByActivity && (
1388
+ <>
1389
+ {" · "}
1390
+ {i18n._(
1391
+ msg({
1392
+ message: "newest changes first",
1393
+ comment:
1394
+ "@context: Archive page meta note explaining the active sort order",
1395
+ }),
1396
+ )}
1397
+ </>
1398
+ )}
1298
1399
  {feedHref && (
1299
1400
  <>
1300
1401
  {" "}
@@ -1347,7 +1448,13 @@ export const ArchivePage: FC<ArchivePageProps> = ({
1347
1448
  <div key={`grid-${group.year}-${group.month}`} class="contents">
1348
1449
  <ArchiveMonthHeader
1349
1450
  class={`archive-month-header${groupIndex > 0 ? " archive-month-header-spaced" : ""}`}
1350
- label={group.label}
1451
+ // Not translated, on purpose: `group.label` is built with
1452
+ // a hardcoded en-US month name, so a translated wrapper
1453
+ // would only produce "更新于 August 2026". Both halves
1454
+ // become translatable together, or neither does.
1455
+ label={
1456
+ sortsByActivity ? `Updated ${group.label}` : group.label
1457
+ }
1351
1458
  count={group.totalCount}
1352
1459
  />
1353
1460
  {group.posts.map((post) => (