@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. 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,29 +70,33 @@ 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";
73
+ export type { ArchiveLayout, ArchiveVisibility };
51
74
 
52
- /** View mode for the archive page. */
53
- export type ArchiveView = "grid" | "list";
75
+ /**
76
+ * Time axis for the archive page.
77
+ *
78
+ * `published` (default) orders and buckets by when a thread was first
79
+ * published — the stable historical record. `updated` switches the whole axis
80
+ * to thread activity, so a thread moves to the month it last gained a post.
81
+ * Edits are not activity.
82
+ */
83
+ export type ArchiveSort = "published" | "updated";
54
84
 
55
- /** 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
+ */
56
94
  export interface ArchiveFilters {
57
- year?: number;
58
- collectionSlug?: string;
59
- collectionTitle?: string;
60
- format?: Format;
61
- mediaKinds?: MediaKind[];
62
- hasMedia?: boolean;
63
- hasTitle?: boolean;
64
- /** true = threads (roots with replies), false = single posts (no replies) */
65
- hasReplies?: boolean;
66
- visibility?: ArchiveVisibility;
67
- view?: ArchiveView;
95
+ selection: PostFilterSelection;
96
+ /** Omitted when the site's configured default layout is active */
97
+ layout?: ArchiveLayout;
98
+ /** Omitted when the default `published` axis is active */
99
+ sort?: ArchiveSort;
68
100
  }
69
101
 
70
102
  /** Props for the archive page component */
@@ -73,14 +105,30 @@ export interface ArchivePageProps {
73
105
  groups: ArchiveGroup[];
74
106
  /** Flat timeline items used by list view (skips month grouping) */
75
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;
76
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;
77
119
  currentPage: number;
78
120
  totalPages: number;
79
121
  filters: ArchiveFilters;
80
122
  availableYears: number[];
81
- availableCollections: { slug: string; title: string }[];
123
+ availableCollections: { id: string; slug: string; title: string }[];
82
124
  isAuthenticated: boolean;
83
- 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;
84
132
  timeZone?: string;
85
133
  /** Href for the RSS feed matching current filters */
86
134
  feedHref?: string;
@@ -93,7 +141,8 @@ export interface SearchPageProps {
93
141
  error?: string;
94
142
  hasMore: boolean;
95
143
  page: number;
96
- sitePathPrefix?: string;
144
+ /** Public path this page's own URLs are built from. See `ArchivePageProps`. */
145
+ basePath?: string;
97
146
  isAuthenticated?: boolean;
98
147
  }
99
148
 
@@ -111,25 +160,80 @@ export interface CollectionPageProps {
111
160
  showRatingSort: boolean;
112
161
  isAuthenticated: boolean;
113
162
  isInNavigation?: boolean;
163
+ /** Deployment path prefix. Used for links to admin surfaces. */
114
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
+ };
115
181
  /** Href for this collection selection's Atom feed when feeds are enabled. */
116
182
  feedHref?: string;
117
183
  }
118
184
 
119
- /** Props for the collection editor page component */
120
- export interface CollectionEditorPageProps {
121
- mode: "create" | "edit";
122
- collection?: Collection;
123
- 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. */
124
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;
125
212
  }
126
213
 
127
214
  /** Props for the collections list page component */
128
215
  export interface CollectionsPageProps {
129
216
  items: CollectionDirectoryItem[];
130
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
+ */
131
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. */
132
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;
133
237
  siteOrigin?: string;
134
238
  }
135
239
 
@@ -147,6 +251,38 @@ export interface FeedData {
147
251
  title?: string;
148
252
  selfUrl: string;
149
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[];
150
286
  }
151
287
 
152
288
  // =============================================================================
@@ -188,6 +324,8 @@ export interface ThreadPreviewProps {
188
324
  leadingReplies: PostView[];
189
325
  trailingReplies: PostView[];
190
326
  latestReply: PostView;
327
+ /** Where the gap link points: the first post the fold hides. */
328
+ gapHref?: string;
191
329
  totalReplyCount: number;
192
330
  }
193
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. */
@@ -102,6 +107,12 @@ export interface PostView {
102
107
  threadRootId?: string;
103
108
  /** Whether this post is the last (most recent) in its thread. Controls reply button visibility. */
104
109
  isLastInThread: boolean;
110
+ /**
111
+ * TypeID of an unpublished draft that already ends this thread, set only for
112
+ * the signed-in author on surfaces that do not render drafts. Replying here
113
+ * would fork the chain, so the reply affordance resumes that draft instead.
114
+ */
115
+ draftTailId?: string;
105
116
  /** Number of published replies in this thread when relevant to the current view. */
106
117
  replyCount?: number;
107
118
 
@@ -157,9 +168,13 @@ export interface NavItemView {
157
168
  type: NavItemType;
158
169
  systemKey?: SystemNavKey;
159
170
  collectionId?: string;
171
+ smartCollectionId?: string;
160
172
  postId?: string;
173
+ /** Author's override, or `""` when the item follows its target's title */
161
174
  label: string;
162
175
  url: string;
176
+ /** Target's current title — the display label when `label` is empty */
177
+ targetTitle?: string;
163
178
  /** "header" = visible in nav bar, "more" = under More dropdown */
164
179
  placement: NavItemPlacement;
165
180
  /** Pre-computed based on currentPath */
@@ -228,6 +243,8 @@ export interface TimelineItemView {
228
243
  leadingReplies: PostView[];
229
244
  trailingReplies: PostView[];
230
245
  latestReply: PostView;
246
+ /** Where the gap link points: the first post the fold hides. */
247
+ gapHref?: string;
231
248
  totalReplyCount: number;
232
249
  };
233
250
  curatedThread?: {
@@ -261,14 +278,45 @@ export interface ArchiveGroup {
261
278
  /**
262
279
  * Site Layout Props
263
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
+
264
307
  export interface SiteLayoutProps {
265
308
  siteName: string;
266
309
  links: NavItemView[];
267
310
  currentPath: string;
268
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;
269
317
  isAuthenticated?: boolean;
270
318
  collections?: Collection[];
271
- siteAvatarUrl?: string;
319
+ siteAvatarThumbUrl?: string;
272
320
  showHeaderAvatar?: boolean;
273
321
  siteDescriptionHtml?: string;
274
322
  siteFooterHtml?: string;
@@ -281,4 +329,16 @@ export interface SiteLayoutProps {
281
329
  slashCommandDiscovered?: boolean;
282
330
  /** When set, the mobile compose FAB pre-selects this collection. */
283
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;
284
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", () => {