@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -3,14 +3,17 @@
3
3
  */
4
4
 
5
5
  import type {
6
+ ArchiveLayout,
6
7
  Format,
7
8
  Status,
8
9
  Visibility,
9
10
  CollectionSortOrder,
11
+ SmartCollectionSortOrder,
10
12
  SystemNavKey,
11
13
  NavItemPlacement,
12
14
  TextAttachmentContentFormat,
13
15
  } from "./constants.js";
16
+ import type { PostFilterSelection } from "../lib/filter-dimensions.js";
14
17
 
15
18
  export type PostAttachmentInput =
16
19
  | {
@@ -93,6 +96,18 @@ export interface CreatePost {
93
96
  collectionEntries?: ThreadCollectionEntry[];
94
97
  replyToId?: string;
95
98
  quietReply?: boolean;
99
+ /**
100
+ * BCP 47 content language. Ignored for replies, which always inherit the
101
+ * Thread root's language so a language filter stays a plain column predicate.
102
+ * Omitted on single-language sites, where the column stays NULL.
103
+ */
104
+ language?: string | null;
105
+ /**
106
+ * Thread root ID this Post is a translation of. The service joins both into
107
+ * one translation group when the Post is created, minting the group on the
108
+ * source Post if it does not have one yet. Roots only.
109
+ */
110
+ translationOfId?: string;
96
111
  publishedAt?: number;
97
112
  attachments?: PostAttachmentInput[];
98
113
  }
@@ -119,6 +134,13 @@ export interface UpdatePost {
119
134
  collectionIds?: string[];
120
135
  collectionEntries?: ThreadCollectionEntry[];
121
136
  publishedAt?: number;
137
+ /**
138
+ * Content language. Applied to the whole Thread, like
139
+ * `PostService.setThreadLanguage` — a Thread is written in one language, and
140
+ * that is what keeps every language filter a plain column predicate. Ignored
141
+ * on replies, which follow their root.
142
+ */
143
+ language?: string | null;
122
144
  attachments?: PostAttachmentInput[];
123
145
  }
124
146
 
@@ -137,10 +159,20 @@ export type CreateNavItem =
137
159
  position?: string;
138
160
  }
139
161
  | {
162
+ // The URL is derived from the collection's slug and is not accepted
163
+ // here; an omitted label means the item follows the collection's title.
140
164
  type: "collection";
141
165
  collectionId: string;
142
- label: string;
143
- url: string;
166
+ label?: string;
167
+ placement?: NavItemPlacement;
168
+ position?: string;
169
+ }
170
+ | {
171
+ // Same shape as `collection`: the URL comes from the smart collection's
172
+ // slug, and an omitted label follows its title.
173
+ type: "smart_collection";
174
+ smartCollectionId: string;
175
+ label?: string;
144
176
  placement?: NavItemPlacement;
145
177
  position?: string;
146
178
  }
@@ -173,11 +205,41 @@ export interface UpdateCollection {
173
205
  sortOrder?: CollectionSortOrder;
174
206
  }
175
207
 
208
+ /**
209
+ * A smart collection's editable fields.
210
+ *
211
+ * `title` is required and stays required: the dialog asks for one, so no
212
+ * surface ever has to invent a name for an untitled smart collection in a page
213
+ * heading, a browser tab, a feed, or the directory.
214
+ */
215
+ export interface CreateSmartCollection {
216
+ slug: string;
217
+ title: string;
218
+ description?: string | null;
219
+ /** The conditions. Omitted or empty means every post. */
220
+ selection?: PostFilterSelection;
221
+ sort?: SmartCollectionSortOrder;
222
+ layout?: ArchiveLayout | null;
223
+ }
224
+
225
+ export interface UpdateSmartCollection {
226
+ slug?: string;
227
+ title?: string;
228
+ description?: string | null;
229
+ selection?: PostFilterSelection;
230
+ sort?: SmartCollectionSortOrder;
231
+ layout?: ArchiveLayout | null;
232
+ }
233
+
176
234
  export type CreateCollectionDirectoryEntry =
177
235
  | {
178
236
  type: "collection";
179
237
  collectionId: string;
180
238
  }
239
+ | {
240
+ type: "smart_collection";
241
+ smartCollectionId: string;
242
+ }
181
243
  | {
182
244
  type: "divider";
183
245
  label?: string | null;
@@ -2,15 +2,28 @@
2
2
  * Page-Level Props & Feed Data Types
3
3
  */
4
4
 
5
- import type { CollectionSortOrder, Format, MediaKind } from "./constants.js";
6
- import type { Collection, CollectionDirectoryItem } from "./entities.js";
5
+ import type {
6
+ ArchiveLayout,
7
+ ArchiveVisibility,
8
+ CollectionSortOrder,
9
+ SmartCollectionSortOrder,
10
+ } from "./constants.js";
11
+ import type { PostFilterSelection } from "../lib/filter-dimensions.js";
12
+ import type {
13
+ Collection,
14
+ CollectionDirectoryItem,
15
+ SmartCollection,
16
+ } from "./entities.js";
7
17
  import type {
8
18
  PostView,
9
19
  FeedPostView,
20
+ LanguageAlternate,
21
+ LanguageSwitcherOption,
10
22
  TimelineItemView,
11
23
  SearchResultView,
12
24
  ArchiveGroup,
13
25
  } from "./views.js";
26
+ import type { DiscoverMode } from "../lib/discover.js";
14
27
 
15
28
  // =============================================================================
16
29
  // Page-Level Props
@@ -26,12 +39,27 @@ export interface HomePageProps {
26
39
  signinUrl: string;
27
40
  }
28
41
 
42
+ /** A link to another language's version of a post. */
43
+ export interface PostTranslationLinkView {
44
+ href: string;
45
+ /** The language's own name for itself, e.g. "日本語". */
46
+ label: string;
47
+ /** Canonical BCP 47 tag. */
48
+ lang: string;
49
+ }
50
+
29
51
  /** Props for the single post page component */
30
52
  export interface PostPageProps {
31
53
  post: PostView;
32
54
  threadPosts?: PostView[];
33
55
  /** Hide owner-only actions while preserving the public post rendering. */
34
56
  isPreview?: boolean;
57
+ /**
58
+ * Other-language versions of this post. Rendered as one quiet sentence
59
+ * after the post — a reader who cannot read this one needs a way out that
60
+ * does not involve guessing at the language switcher.
61
+ */
62
+ translations?: PostTranslationLinkView[];
35
63
  }
36
64
 
37
65
  /** Props for the featured page component */
@@ -42,15 +70,7 @@ export interface FeaturedPageProps {
42
70
  baseUrl: string;
43
71
  }
44
72
 
45
- /** Visibility filter values for the archive page (includes "featured" as a virtual value). */
46
- export type ArchiveVisibility =
47
- | "public"
48
- | "latest_hidden"
49
- | "private"
50
- | "featured";
51
-
52
- /** View mode for the archive page. */
53
- export type ArchiveView = "grid" | "list";
73
+ export type { ArchiveLayout, ArchiveVisibility };
54
74
 
55
75
  /**
56
76
  * Time axis for the archive page.
@@ -62,19 +82,19 @@ export type ArchiveView = "grid" | "list";
62
82
  */
63
83
  export type ArchiveSort = "published" | "updated";
64
84
 
65
- /** Filters currently active on the archive page */
85
+ /**
86
+ * What the archive page is currently showing.
87
+ *
88
+ * The selection itself is the shared dimension vocabulary — the chip bar builds
89
+ * its links by handing an edited copy back to the same serializer the route
90
+ * parses with, so a chip can never spell a filter the route cannot read.
91
+ * `layout` and `sort` sit beside it because they shape the rendering, not the
92
+ * result set.
93
+ */
66
94
  export interface ArchiveFilters {
67
- year?: number;
68
- collectionSlug?: string;
69
- collectionTitle?: string;
70
- format?: Format;
71
- mediaKinds?: MediaKind[];
72
- hasMedia?: boolean;
73
- hasTitle?: boolean;
74
- /** true = threads (roots with replies), false = single posts (no replies) */
75
- hasReplies?: boolean;
76
- visibility?: ArchiveVisibility;
77
- view?: ArchiveView;
95
+ selection: PostFilterSelection;
96
+ /** Omitted when the site's configured default layout is active */
97
+ layout?: ArchiveLayout;
78
98
  /** Omitted when the default `published` axis is active */
79
99
  sort?: ArchiveSort;
80
100
  }
@@ -85,14 +105,30 @@ export interface ArchivePageProps {
85
105
  groups: ArchiveGroup[];
86
106
  /** Flat timeline items used by list view (skips month grouping) */
87
107
  items?: TimelineItemView[];
108
+ /**
109
+ * Layout to use when the URL names none. Comes from the site's
110
+ * `ARCHIVE_DEFAULT_LAYOUT` setting.
111
+ */
112
+ defaultLayout?: ArchiveLayout;
88
113
  totalCount: number;
114
+ /**
115
+ * Matches the same view with every filter cleared. Lets the count say how
116
+ * much the active filter removed; omitted when nothing is filtered.
117
+ */
118
+ baselineCount?: number;
89
119
  currentPage: number;
90
120
  totalPages: number;
91
121
  filters: ArchiveFilters;
92
122
  availableYears: number[];
93
- availableCollections: { slug: string; title: string }[];
123
+ availableCollections: { id: string; slug: string; title: string }[];
94
124
  isAuthenticated: boolean;
95
- sitePathPrefix?: string;
125
+ /**
126
+ * Public path this page's own URLs are built from — the deployment prefix
127
+ * plus, in a language view, that language's prefix (`/blog/en`). Every link
128
+ * the archive generates is a link back to itself with different filters, so
129
+ * this is the only prefix it needs.
130
+ */
131
+ basePath?: string;
96
132
  timeZone?: string;
97
133
  /** Href for the RSS feed matching current filters */
98
134
  feedHref?: string;
@@ -105,7 +141,8 @@ export interface SearchPageProps {
105
141
  error?: string;
106
142
  hasMore: boolean;
107
143
  page: number;
108
- sitePathPrefix?: string;
144
+ /** Public path this page's own URLs are built from. See `ArchivePageProps`. */
145
+ basePath?: string;
109
146
  isAuthenticated?: boolean;
110
147
  }
111
148
 
@@ -123,25 +160,80 @@ export interface CollectionPageProps {
123
160
  showRatingSort: boolean;
124
161
  isAuthenticated: boolean;
125
162
  isInNavigation?: boolean;
163
+ /** Deployment path prefix. Used for links to admin surfaces. */
126
164
  sitePathPrefix?: string;
165
+ /**
166
+ * Public path prefix for reader-facing links, which in a language view also
167
+ * carries that language's prefix. Defaults to `sitePathPrefix`.
168
+ */
169
+ basePath?: string;
170
+ /**
171
+ * Set when the collection is empty in this language but not in the others.
172
+ * The empty state then says which language is missing and offers the ones
173
+ * that have something — an empty page with no way out is a dead end.
174
+ */
175
+ emptyInLanguage?: {
176
+ /** The current language's own name for itself. */
177
+ languageLabel: string;
178
+ /** This collection in each of the site's other languages. */
179
+ alternatives: LanguageSwitcherOption[];
180
+ };
127
181
  /** Href for this collection selection's Atom feed when feeds are enabled. */
128
182
  feedHref?: string;
129
183
  }
130
184
 
131
- /** Props for the collection editor page component */
132
- export interface CollectionEditorPageProps {
133
- mode: "create" | "edit";
134
- collection?: Collection;
135
- cancelHref: string;
185
+ /** Props for the smart collection page component */
186
+ export interface SmartCollectionPageProps {
187
+ smartCollection: SmartCollection;
188
+ items: TimelineItemView[];
189
+ totalThreadCount: number;
190
+ currentPage: number;
191
+ totalPages: number;
192
+ pagePath: string;
193
+ baseUrl: string;
194
+ currentSort: SmartCollectionSortOrder;
195
+ defaultSort: SmartCollectionSortOrder;
196
+ showRatingSort: boolean;
197
+ /** One sentence naming what the conditions gather. Shown to every reader. */
198
+ conditionSummary: string;
199
+ /** Archive URL showing the same posts, for a reader who wants to narrow. */
200
+ conditionHref: string;
201
+ isAuthenticated: boolean;
202
+ isInNavigation?: boolean;
203
+ /** Deployment path prefix. Used for links to admin surfaces. */
136
204
  sitePathPrefix?: string;
205
+ /**
206
+ * Public path prefix for reader-facing links, which in a language view also
207
+ * carries that language's prefix. Defaults to `sitePathPrefix`.
208
+ */
209
+ basePath?: string;
210
+ /** Href for this smart collection's Atom feed when feeds are enabled. */
211
+ feedHref?: string;
137
212
  }
138
213
 
139
214
  /** Props for the collections list page component */
140
215
  export interface CollectionsPageProps {
141
216
  items: CollectionDirectoryItem[];
142
217
  isAuthenticated: boolean;
218
+ /**
219
+ * TypeIDs of the collections and smart collections already in the site
220
+ * navigation. Both kinds share one list: the directory offers them the
221
+ * same action, and their ID prefixes keep them apart.
222
+ */
143
223
  navigationCollectionIds?: string[];
224
+ /**
225
+ * Whether each collection offers its feed. `/subscribe` lists only the
226
+ * site-wide feeds, so the directory is where a reader is handed a
227
+ * collection's — beside the collection itself.
228
+ */
229
+ feedsEnabled?: boolean;
230
+ /** Deployment path prefix. Used for links to admin surfaces. */
144
231
  sitePathPrefix?: string;
232
+ /**
233
+ * Public path prefix for reader-facing links, which in a language view also
234
+ * carries that language's prefix. Defaults to `sitePathPrefix`.
235
+ */
236
+ basePath?: string;
145
237
  siteOrigin?: string;
146
238
  }
147
239
 
@@ -159,6 +251,38 @@ export interface FeedData {
159
251
  title?: string;
160
252
  selfUrl: string;
161
253
  posts: FeedPostView[];
254
+ /**
255
+ * The site's avatar, for `atom:icon`. Readers show it beside the feed, and
256
+ * a directory listing the site has no other machine-readable place to find
257
+ * it. Omitted when the site has no avatar; Atom has no empty-icon form.
258
+ */
259
+ siteIconUrl?: string;
260
+ /**
261
+ * Effective Jant Discover mode, declared in the feed header. Omitted means
262
+ * the feed says nothing about Discover — which a crawler reads as "this
263
+ * site predates the protocol", not as `none`.
264
+ */
265
+ discover?: DiscoverMode;
266
+ /** Absolute URL of the feed a Discover crawler should poll, if any. */
267
+ discoverFeedUrl?: string | null;
268
+ /**
269
+ * Absolute URL of the site's featured feed, named beside `discoverFeedUrl`
270
+ * under `latest` so a directory can show featured posts on one list and
271
+ * everything on another. Null under `featured`, where `discoverFeedUrl`
272
+ * already is that feed, and under `none`.
273
+ */
274
+ discoverFeaturedFeedUrl?: string | null;
275
+ /**
276
+ * Absolute URL of the endpoint a directory asks whether posts it holds are
277
+ * still in these feeds, scoped to this feed's language view. Null under
278
+ * `none`.
279
+ */
280
+ discoverStatusUrl?: string | null;
281
+ /**
282
+ * The same feed in the site's other languages. Empty on a single-language
283
+ * site.
284
+ */
285
+ languageAlternates?: LanguageAlternate[];
162
286
  }
163
287
 
164
288
  // =============================================================================
@@ -200,6 +324,8 @@ export interface ThreadPreviewProps {
200
324
  leadingReplies: PostView[];
201
325
  trailingReplies: PostView[];
202
326
  latestReply: PostView;
327
+ /** Where the gap link points: the first post the fold hides. */
328
+ gapHref?: string;
203
329
  totalReplyCount: number;
204
330
  }
205
331
 
@@ -68,6 +68,11 @@ export interface PostView {
68
68
  /** 24-hour featured time, e.g. "23:05" */
69
69
  featuredAtTime?: string;
70
70
  rating?: number;
71
+ /**
72
+ * BCP 47 content language, on Thread roots of a multilingual site. Absent on
73
+ * replies (they follow the root) and on sites that publish one language.
74
+ */
75
+ language?: string;
71
76
 
72
77
  // Link preview
73
78
  /** Preview kind: "video", "image", etc. */
@@ -163,9 +168,13 @@ export interface NavItemView {
163
168
  type: NavItemType;
164
169
  systemKey?: SystemNavKey;
165
170
  collectionId?: string;
171
+ smartCollectionId?: string;
166
172
  postId?: string;
173
+ /** Author's override, or `""` when the item follows its target's title */
167
174
  label: string;
168
175
  url: string;
176
+ /** Target's current title — the display label when `label` is empty */
177
+ targetTitle?: string;
169
178
  /** "header" = visible in nav bar, "more" = under More dropdown */
170
179
  placement: NavItemPlacement;
171
180
  /** Pre-computed based on currentPath */
@@ -234,6 +243,8 @@ export interface TimelineItemView {
234
243
  leadingReplies: PostView[];
235
244
  trailingReplies: PostView[];
236
245
  latestReply: PostView;
246
+ /** Where the gap link points: the first post the fold hides. */
247
+ gapHref?: string;
237
248
  totalReplyCount: number;
238
249
  };
239
250
  curatedThread?: {
@@ -267,14 +278,45 @@ export interface ArchiveGroup {
267
278
  /**
268
279
  * Site Layout Props
269
280
  */
281
+ /** One `<link rel="alternate" hreflang>` target. */
282
+ export interface LanguageAlternate {
283
+ /** BCP 47 tag, or `x-default` for the entry point. */
284
+ hreflang: string;
285
+ /** Absolute URL. */
286
+ href: string;
287
+ }
288
+
289
+ /** One entry in the site's language switcher. */
290
+ export interface LanguageSwitcherOption {
291
+ /** Canonical BCP 47 tag. */
292
+ lang: string;
293
+ /** The language's own name for itself, e.g. "日本語". */
294
+ label: string;
295
+ /** Where switching to this language takes the reader. */
296
+ href: string;
297
+ /** Whether this is the language currently on screen. */
298
+ isCurrent: boolean;
299
+ /**
300
+ * Whether this is the site's primary language — the one served from the
301
+ * root. The switcher's trigger stays silent on it and names every other
302
+ * language, so a reader always knows when they are off the default.
303
+ */
304
+ isPrimary: boolean;
305
+ }
306
+
270
307
  export interface SiteLayoutProps {
271
308
  siteName: string;
272
309
  links: NavItemView[];
273
310
  currentPath: string;
274
311
  sitePathPrefix?: string;
312
+ /**
313
+ * Public base for language-scoped chrome links (logo, search, home
314
+ * detection). `sitePathPrefix` plus the language-view prefix, when any.
315
+ */
316
+ basePath?: string;
275
317
  isAuthenticated?: boolean;
276
318
  collections?: Collection[];
277
- siteAvatarUrl?: string;
319
+ siteAvatarThumbUrl?: string;
278
320
  showHeaderAvatar?: boolean;
279
321
  siteDescriptionHtml?: string;
280
322
  siteFooterHtml?: string;
@@ -287,4 +329,16 @@ export interface SiteLayoutProps {
287
329
  slashCommandDiscovered?: boolean;
288
330
  /** When set, the mobile compose FAB pre-selects this collection. */
289
331
  composeCollectionId?: string;
332
+ /**
333
+ * Languages this site publishes in, for the header's language switcher.
334
+ * Empty on a single-language site, which renders no switcher at all.
335
+ */
336
+ languageSwitcher?: LanguageSwitcherOption[];
337
+ /** Languages offered in the composer. Empty on a single-language site. */
338
+ composeLanguages?: Array<{ tag: string; label: string }>;
339
+ /**
340
+ * Content language of the page the composer opens from. The composer's
341
+ * automatic language resolves to this unless detection disagrees.
342
+ */
343
+ composeContextLanguage?: string | null;
290
344
  }
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
1
3
  import { describe, it, expect } from "vitest";
2
4
  import {
3
5
  BUILTIN_FONT_THEMES,
@@ -93,7 +95,7 @@ describe("BUILTIN_FONT_THEMES", () => {
93
95
  });
94
96
 
95
97
  it("routes zh-Hans content to simplified serif and sans fallbacks", () => {
96
- const variables = getCjkFontCssVariables("zh-Hans", "off");
98
+ const variables = getCjkFontCssVariables("zh-Hans");
97
99
  const serif = variables["--font-cjk-serif-fallback"];
98
100
  const sans = variables["--font-cjk-sans-fallback"];
99
101
 
@@ -105,7 +107,7 @@ describe("BUILTIN_FONT_THEMES", () => {
105
107
  });
106
108
 
107
109
  it("routes zh-Hant content to traditional serif and sans fallbacks", () => {
108
- const variables = getCjkFontCssVariables("zh-Hant", "off");
110
+ const variables = getCjkFontCssVariables("zh-Hant");
109
111
  const serif = variables["--font-cjk-serif-fallback"];
110
112
  const sans = variables["--font-cjk-sans-fallback"];
111
113
 
@@ -117,31 +119,69 @@ describe("BUILTIN_FONT_THEMES", () => {
117
119
  });
118
120
 
119
121
  it("routes Japanese content to the Japanese font profile", () => {
120
- const variables = getCjkFontCssVariables("ja", "off");
122
+ const variables = getCjkFontCssVariables("ja");
121
123
 
122
124
  expect(variables["--font-cjk-serif-fallback"]).toContain('"Noto Serif JP"');
123
125
  expect(variables["--font-cjk-sans-fallback"]).toContain('"Noto Sans JP"');
124
126
  });
125
127
 
126
128
  it("routes Korean content to the Korean font profile", () => {
127
- const variables = getCjkFontCssVariables("ko", "off");
129
+ const variables = getCjkFontCssVariables("ko");
128
130
 
129
131
  expect(variables["--font-cjk-serif-fallback"]).toContain('"Noto Serif KR"');
130
132
  expect(variables["--font-cjk-sans-fallback"]).toContain('"Noto Sans KR"');
131
133
  });
132
134
 
133
- it("uses the explicit CJK fallback for an unadapted content language", () => {
134
- expect(resolveCjkFontProfile("en", "zh-Hans")).toBe("zh-Hans");
135
+ it("derives the profile from the language alone", () => {
136
+ expect(resolveCjkFontProfile("zh-Hant")).toBe("zh-Hant");
137
+ expect(resolveCjkFontProfile("zh-CN")).toBe("zh-Hans");
138
+ expect(resolveCjkFontProfile("ja-JP")).toBe("ja");
135
139
  });
136
140
 
137
- it("lets an adapted content language override the manual fallback", () => {
138
- expect(resolveCjkFontProfile("zh-Hant", "zh-Hans")).toBe("zh-Hant");
141
+ it("has no profile for a language without CJK typography", () => {
142
+ expect(resolveCjkFontProfile("en")).toBeUndefined();
143
+ expect(resolveCjkFontProfile()).toBeUndefined();
139
144
  });
140
145
 
141
- it("keeps the default profile neutral", () => {
142
- expect(getCjkFontCssVariables("en", "off")).toEqual({});
143
- expect(DEFAULT_FONT_CJK_SERIF_FALLBACK).toBe('"Jant Language Fallback"');
144
- expect(DEFAULT_FONT_CJK_SANS_FALLBACK).toBe('"Jant Language Fallback"');
146
+ it("leaves a language without a profile on the token defaults", () => {
147
+ expect(getCjkFontCssVariables("en")).toEqual({});
148
+ });
149
+
150
+ it("keeps the default stacks real, script-neutral and generic-free", () => {
151
+ for (const stack of [
152
+ DEFAULT_FONT_CJK_SERIF_FALLBACK,
153
+ DEFAULT_FONT_CJK_SANS_FALLBACK,
154
+ ]) {
155
+ // A placeholder family name is skipped by the browser, which drops CJK
156
+ // text to the OS last-resort font — the whole point of these constants.
157
+ expect(stack).not.toContain("Jant Language Fallback");
158
+ // The generics belong to the theme stack, after this slot.
159
+ expect(stack).not.toMatch(/\b(?:ui-)?(?:serif|sans-serif)\b/);
160
+ // Every script the profiles cover is reachable from the default too.
161
+ for (const family of ["SC", "TC", "JP", "KR"]) {
162
+ expect(stack).toContain(family);
163
+ }
164
+ }
165
+ expect(DEFAULT_FONT_CJK_SERIF_FALLBACK).toMatch(/^"Songti SC",/);
166
+ expect(DEFAULT_FONT_CJK_SANS_FALLBACK).toMatch(/^"PingFang SC",/);
167
+ });
168
+
169
+ it("ships the same defaults in tokens.css", () => {
170
+ const tokens = readFileSync(
171
+ fileURLToPath(new URL("../../styles/tokens.css", import.meta.url)),
172
+ "utf8",
173
+ );
174
+ const declared = (name: string) => {
175
+ const match = tokens.match(new RegExp(`--${name}:([^;]*);`));
176
+ expect(match, `--${name} is missing from tokens.css`).not.toBeNull();
177
+ return match![1].trim().replace(/\s+/g, " ");
178
+ };
179
+ expect(declared("font-cjk-serif-fallback")).toBe(
180
+ DEFAULT_FONT_CJK_SERIF_FALLBACK,
181
+ );
182
+ expect(declared("font-cjk-sans-fallback")).toBe(
183
+ DEFAULT_FONT_CJK_SANS_FALLBACK,
184
+ );
145
185
  });
146
186
 
147
187
  it("puts language slots into every serif and sans theme stack", () => {