@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
@@ -53,6 +53,13 @@ export interface AppVariables {
53
53
  * `c.var.rateLimiter.check(...)` instead of caring which impl is used.
54
54
  */
55
55
  rateLimiter: RateLimiter;
56
+ /**
57
+ * Language whose browsing view is being served, set by the language routes
58
+ * when the URL carries a language prefix (`/en/archive`). Undefined on the
59
+ * root view, which shows the primary language — or every language, when
60
+ * multilingual content is off.
61
+ */
62
+ viewLang?: string;
56
63
  }
57
64
 
58
65
  export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
@@ -37,11 +37,14 @@ export interface Bindings {
37
37
  SITE_LANGUAGE?: EnvBindingValue;
38
38
  DASHBOARD_LANGUAGE?: EnvBindingValue;
39
39
  MAIN_RSS_FEED?: EnvBindingValue;
40
+ ARCHIVE_DEFAULT_LAYOUT?: EnvBindingValue;
40
41
  TIME_ZONE?: EnvBindingValue;
41
42
  SITE_FOOTER?: EnvBindingValue;
42
43
  NOINDEX?: EnvBindingValue;
43
44
  PUBLIC_API_ENABLED?: EnvBindingValue;
44
45
  RSS_FEEDS_ENABLED?: EnvBindingValue;
46
+ DISCOVER?: EnvBindingValue;
47
+ DISCOVER_PING_URL?: EnvBindingValue;
45
48
  R2_PUBLIC_URL?: EnvBindingValue;
46
49
  IMAGE_TRANSFORM_URL?: EnvBindingValue;
47
50
  DEMO_EMAIL?: EnvBindingValue;
@@ -4,10 +4,12 @@
4
4
  * Single Source of Truth for all configuration fields.
5
5
  */
6
6
 
7
+ import type { DiscoverMode } from "../lib/discover.js";
7
8
  import {
8
9
  MAX_SITE_DESCRIPTION_LENGTH,
9
10
  MAX_SITE_FOOTER_LENGTH,
10
11
  MAX_SITE_NAME_LENGTH,
12
+ type ArchiveLayout,
11
13
  type FeedKind,
12
14
  } from "./constants.js";
13
15
 
@@ -125,13 +127,37 @@ export const CONFIG_FIELDS = {
125
127
  options: ["", "en", "zh-Hans", "zh-Hant"],
126
128
  },
127
129
  },
128
- CJK_SERIF_FONT: {
129
- defaultValue: "off",
130
+ // Multilingual content. Both keys are deliberately DB-only:
131
+ //
132
+ // - No `envKeys`: an env var could flip multilingual on without the confirm
133
+ // step that stamps existing posts with the primary language, and every
134
+ // unstamped post would vanish from the root view. Adding a language through
135
+ // env would likewise skip the URL-prefix conflict check.
136
+ // - No `editor`: `PUT /api/settings` and the generic Config Editor both gate
137
+ // on `"editor" in field`, so omitting it leaves exactly one writer — the
138
+ // language service, which owns the ordering these settings depend on.
139
+ //
140
+ // `configEditorLink` keeps them findable in settings search without making
141
+ // them directly writable.
142
+ MULTILINGUAL_ENABLED: {
143
+ defaultValue: "false",
130
144
  envOnly: false,
131
- envKeys: ["CJK_SERIF_FONT"],
132
- editor: {
133
- type: "enum",
134
- options: ["off", "zh-Hans", "zh-Hant", "ja", "ko"],
145
+ configEditorLink: {
146
+ type: "boolean",
147
+ settingsPath: "/settings/language",
148
+ display: "value",
149
+ },
150
+ },
151
+ // Comma-separated canonical BCP 47 tags, in switcher order. Never contains
152
+ // the primary language. Comma-separated rather than JSON because tags cannot
153
+ // contain commas: no escaping, readable in the settings table, order-preserving.
154
+ ADDITIONAL_LANGUAGES: {
155
+ defaultValue: "",
156
+ envOnly: false,
157
+ configEditorLink: {
158
+ type: "string",
159
+ settingsPath: "/settings/language",
160
+ display: "configured",
135
161
  },
136
162
  },
137
163
  MAIN_RSS_FEED: {
@@ -140,6 +166,15 @@ export const CONFIG_FIELDS = {
140
166
  envKeys: ["MAIN_RSS_FEED"],
141
167
  editor: { type: "enum", options: ["featured", "latest"] },
142
168
  },
169
+ // The archive is the widest of Featured / Latest / All, and the other two
170
+ // render as timelines. `list` keeps the trio consistent; `grid` turns the
171
+ // page back into a tile catalogue.
172
+ ARCHIVE_DEFAULT_LAYOUT: {
173
+ defaultValue: "list",
174
+ envOnly: false,
175
+ envKeys: ["ARCHIVE_DEFAULT_LAYOUT"],
176
+ editor: { type: "enum", options: ["list", "grid"] },
177
+ },
143
178
  // Environment-only (deployment/infrastructure config)
144
179
  DEFAULT_THEME: {
145
180
  defaultValue: "tufte",
@@ -192,7 +227,7 @@ export const CONFIG_FIELDS = {
192
227
  envKeys: ["DEMO_MODE"],
193
228
  },
194
229
  PAGE_SIZE: {
195
- defaultValue: "50",
230
+ defaultValue: "25",
196
231
  envOnly: false,
197
232
  envKeys: ["PAGE_SIZE"],
198
233
  editor: { type: "number", min: 1, max: 100, step: 1 },
@@ -432,6 +467,30 @@ export const CONFIG_FIELDS = {
432
467
  envKeys: ["RSS_FEEDS_ENABLED"],
433
468
  editor: { type: "boolean" },
434
469
  },
470
+ // No default value, and deliberately no `editor`. "Never chosen" has to stay
471
+ // tellable apart from an explicit choice — an unset site falls through to
472
+ // `noindex` and then to the deployment default, an explicit one does not —
473
+ // and Config Editor has no way to show a field whose default is "unset": it
474
+ // requires every editable field to resolve to a concrete valid value. The
475
+ // effective mode is derived in `lib/discover.ts`, and the checkbox in
476
+ // settings/general is where this is edited.
477
+ //
478
+ // The binding is read as this deployment's default rather than as the
479
+ // site's answer, which is how hosted Jant lists its fleet while a
480
+ // self-hosted site, with nothing set anywhere, stays out until asked.
481
+ DISCOVER: {
482
+ defaultValue: "",
483
+ envOnly: false,
484
+ envKeys: ["DISCOVER"],
485
+ },
486
+ // What the last announcement to the directory came to. State, not
487
+ // configuration — nobody edits this — so it is internal like
488
+ // `ONBOARDING_STATUS`. Shape and reasoning in `lib/discover-ping.ts`.
489
+ DISCOVER_ANNOUNCE_STATE: {
490
+ defaultValue: "",
491
+ envOnly: false,
492
+ internal: true,
493
+ },
435
494
  DISCOVERY_COMPOSE_OPEN_SHORTCUT_AT: {
436
495
  defaultValue: "",
437
496
  envOnly: false,
@@ -452,11 +511,6 @@ export const CONFIG_FIELDS = {
452
511
  envOnly: false,
453
512
  internal: true,
454
513
  },
455
- DELETE_CSRF_TOKEN: {
456
- defaultValue: "",
457
- envOnly: false,
458
- internal: true,
459
- },
460
514
 
461
515
  // GitHub Sync (DB-only, managed via GitHub Sync settings page)
462
516
  GITHUB_SYNC_ENABLED: {
@@ -683,6 +737,10 @@ export interface AppConfig {
683
737
  siteDescription: string;
684
738
  /** true only when description is set in DB or ENV (not just the default) */
685
739
  siteDescriptionExplicit: boolean;
740
+ /**
741
+ * The site's content language, and — once multilingual content is on — its
742
+ * primary language: the one served from the unprefixed root URLs.
743
+ */
686
744
  siteLanguage: string;
687
745
  /**
688
746
  * Admin dashboard UI locale. Empty string means "follow the content
@@ -690,14 +748,29 @@ export interface AppConfig {
690
748
  * the translated catalog locales ("en", "zh-Hans", "zh-Hant").
691
749
  */
692
750
  dashboardLanguage: string;
693
- /** Optional CJK font fallback when the content language has no profile. */
694
- cjkSerifFont: string;
751
+ /** Whether the site serves per-language browsing views. */
752
+ multilingualEnabled: boolean;
753
+ /**
754
+ * Canonical BCP 47 tags served under a URL prefix, in switcher order. Never
755
+ * includes `siteLanguage`. Retained when multilingual is switched off so
756
+ * turning it back on restores the same setup — and so the old prefixes can
757
+ * still redirect rather than 404.
758
+ */
759
+ additionalLanguages: string[];
695
760
  mainRssFeed: FeedKind;
761
+ /** Layout `/archive` uses when the URL names no explicit layout. */
762
+ archiveDefaultLayout: ArchiveLayout;
696
763
  /** Canonical IANA timezone identifier used for date/time display. */
697
764
  timeZone: string;
698
765
  siteFooter: string;
699
766
  showJantBrandingOnHome: boolean;
700
767
  noindex: boolean;
768
+ /**
769
+ * Effective Jant Discover mode this site's feeds declare. Derived from the
770
+ * `DISCOVER` setting plus the demo, `noindex`, and feeds-enabled rules in
771
+ * `lib/discover.ts` — never the stored value on its own.
772
+ */
773
+ discover: DiscoverMode;
701
774
  /** Whether published content can be read from JSON APIs without authentication. */
702
775
  publicApiEnabled: boolean;
703
776
  /** Whether the site publishes its Atom feed endpoints. */
@@ -757,8 +830,14 @@ export interface AppConfig {
757
830
  // Site appearance (DB internal)
758
831
  siteAvatar: string;
759
832
  showHeaderAvatar: boolean;
760
- /** Derived: getMediaUrl(siteAvatar, publicUrl) */
833
+ /** Derived: getMediaUrl(siteAvatar, publicUrl) — full resolution, for the
834
+ * social card, the settings editor, and anything that syncs the canonical
835
+ * asset. */
761
836
  siteAvatarUrl: string;
837
+ /** Derived: `siteAvatarUrl` sized for the header and drawer marks, which
838
+ * render it at 32px and 24px. Falls back to `siteAvatarUrl` when no image
839
+ * transform is configured. */
840
+ siteAvatarThumbUrl: string;
762
841
  faviconVersion: string;
763
842
 
764
843
  // Rate limiting (ENV only)
@@ -14,22 +14,102 @@ export type Visibility = (typeof VISIBILITIES)[number];
14
14
  export const FEED_KINDS = ["latest", "featured"] as const;
15
15
  export type FeedKind = (typeof FEED_KINDS)[number];
16
16
 
17
- export const SORT_ORDERS = [
18
- "newest",
19
- "oldest",
20
- "rating_desc",
21
- "rating_asc",
17
+ /**
18
+ * Layouts the archive page can render in. `list` renders the same timeline the
19
+ * rest of the nav trio uses; `grid` renders the tile catalogue.
20
+ *
21
+ * Named "layout", not "view": on the archive, `view` is reserved for the
22
+ * saved-selection concept, and the URL param follows the same split
23
+ * (`?layout=grid`, with `?view=` kept only as the legacy spelling).
24
+ */
25
+ export const ARCHIVE_LAYOUTS = ["list", "grid"] as const;
26
+ export type ArchiveLayout = (typeof ARCHIVE_LAYOUTS)[number];
27
+
28
+ /**
29
+ * Visibility values a signed-out reader may select on the archive.
30
+ *
31
+ * `featured` is a virtual member: it is a separate flag rather than a stored
32
+ * visibility, and it lives here because the filter bar presents the two as one
33
+ * dimension.
34
+ *
35
+ * `private` is deliberately absent. It names a set only the author can see, so
36
+ * a reader who asks for it has to be told so — handing back a different set
37
+ * under the same name is the one answer that is never right.
38
+ */
39
+ export const PUBLIC_ARCHIVE_VISIBILITIES = [
40
+ "public",
41
+ "featured",
42
+ "latest_hidden",
22
43
  ] as const;
23
- export type SortOrder = (typeof SORT_ORDERS)[number];
24
44
 
25
- export const COLLECTION_SORT_ORDERS = [
26
- "newest",
27
- "oldest",
28
- "rating_desc",
45
+ /** Every visibility the author can select, in filter-bar order. */
46
+ export const ARCHIVE_VISIBILITIES = [
47
+ ...PUBLIC_ARCHIVE_VISIBILITIES,
48
+ "private",
29
49
  ] as const;
30
- export type CollectionSortOrder = (typeof COLLECTION_SORT_ORDERS)[number];
31
50
 
32
- export const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
51
+ export type ArchiveVisibility = (typeof ARCHIVE_VISIBILITIES)[number];
52
+
53
+ /**
54
+ * The range a `year` filter may name, inclusive at both ends.
55
+ *
56
+ * Unix timestamps start in 1970, so anything at or below it is a parse failure
57
+ * rather than a very old archive.
58
+ *
59
+ * The ceiling is not pedantry. A year is translated into a pair of bounds with
60
+ * `Date.UTC`, which returns `NaN` past year 275760 — and a `NaN` bound is a
61
+ * comparison every row silently fails, so the page renders empty with nothing
62
+ * to explain it. Four digits is the largest year a post can honestly carry, and
63
+ * it is bounded in three places at once: the URL parser, the stored condition's
64
+ * validator, and the table CHECK in both dialects.
65
+ */
66
+ export const EARLIEST_FILTERABLE_YEAR = 1971;
67
+ export const LATEST_FILTERABLE_YEAR = 9999;
68
+
69
+ /**
70
+ * How a chronological query orders its results.
71
+ *
72
+ * `newest` and `oldest` read whichever time axis `PostFilters.sortBy` selects,
73
+ * so "newest" on the activity axis means "last gained a post" and on the
74
+ * published axis means "published last".
75
+ *
76
+ * Not to be confused with the archive's `?sort=`, which names a *time axis*
77
+ * (`published` / `updated`) and always runs newest-first. Two vocabularies, and
78
+ * mixing them is how month headers stop agreeing with the order beneath them.
79
+ */
80
+ export const SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
81
+ export type SortOrder = (typeof SORT_ORDERS)[number];
82
+
83
+ /**
84
+ * The orders a collection offers its readers — the same three, deliberately.
85
+ *
86
+ * Kept as its own name because the two vocabularies answer different questions
87
+ * (one orders a query, one is stored on a collection row and shown in a menu),
88
+ * and pointing them at one array is what keeps them from drifting apart the way
89
+ * the smart collection's fourth order once did.
90
+ */
91
+ export const COLLECTION_SORT_ORDERS = SORT_ORDERS;
92
+ export type CollectionSortOrder = SortOrder;
93
+
94
+ /**
95
+ * How a smart collection orders the posts its conditions gather.
96
+ *
97
+ * Identical to a manual collection's orders, and for the same reasons: on both,
98
+ * `newest` means the Thread that last gained a post, not the Thread published
99
+ * last. A smart collection briefly carried a fourth order named `updated` that
100
+ * said the same thing in different words; folding it into `newest` left one
101
+ * menu the reader can learn once.
102
+ */
103
+ export const SMART_COLLECTION_SORT_ORDERS = COLLECTION_SORT_ORDERS;
104
+ export type SmartCollectionSortOrder = CollectionSortOrder;
105
+
106
+ export const NAV_ITEM_TYPES = [
107
+ "link",
108
+ "system",
109
+ "collection",
110
+ "smart_collection",
111
+ "page",
112
+ ] as const;
33
113
  export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
34
114
 
35
115
  export const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
@@ -41,6 +121,7 @@ export const SYSTEM_NAV_KEY_VALUES = [
41
121
  "collections",
42
122
  "archive",
43
123
  "rss",
124
+ "subscribe",
44
125
  "settings",
45
126
  ] as const;
46
127
  export type SystemNavKey = (typeof SYSTEM_NAV_KEY_VALUES)[number];
@@ -59,14 +140,31 @@ export const SYSTEM_NAV_KEYS = {
59
140
  collections: {
60
141
  defaultLabel: "Collections",
61
142
  url: "/collections",
62
- defaultPlacement: "header",
143
+ // The header switches between how much of one list you see (Featured /
144
+ // Latest / All). Collections is a different axis — what a post is about —
145
+ // and readers reach that through the collection tags on posts they are
146
+ // already reading, so it starts one level down instead of widening the
147
+ // header for everyone.
148
+ defaultPlacement: "more",
63
149
  },
64
150
  archive: {
65
- defaultLabel: "Archive",
151
+ defaultLabel: "All",
66
152
  url: "/archive",
67
153
  defaultPlacement: "header",
68
154
  },
69
- rss: { defaultLabel: "Feed", url: "/feed", defaultPlacement: "more" },
155
+ // Two entries, two audiences. `rss` links straight at the Atom document, for
156
+ // an author whose readers already run a feed reader; `subscribe` links at the
157
+ // page that explains the feeds, for everyone else. Repointing `rss` at
158
+ // /subscribe instead of adding this would have destroyed the first case, and
159
+ // left a key named `rss` addressing an HTML page — nav data themes read.
160
+ rss: { defaultLabel: "RSS", url: "/feed", defaultPlacement: "more" },
161
+ subscribe: {
162
+ defaultLabel: "Subscribe",
163
+ url: "/subscribe",
164
+ // A once-in-a-reader's-lifetime utility page, not a section to come back
165
+ // and read, so it sits with Settings rather than beside Featured and All.
166
+ defaultPlacement: "more",
167
+ },
70
168
  settings: {
71
169
  defaultLabel: "Settings",
72
170
  url: "/settings",
@@ -81,6 +179,32 @@ export const SYSTEM_NAV_KEYS = {
81
179
  }
82
180
  >;
83
181
 
182
+ /**
183
+ * The system nav entries that exist only while RSS feeds do.
184
+ *
185
+ * `rss` addresses the Atom document; `subscribe` addresses the page that
186
+ * explains it, which 404s without feeds. Both disappear from the rendered
187
+ * navigation when feeds are switched off — while staying in the database, so
188
+ * placement and custom labels survive being switched back on.
189
+ */
190
+ export const FEED_NAV_KEYS = [
191
+ "rss",
192
+ "subscribe",
193
+ ] as const satisfies readonly SystemNavKey[];
194
+
195
+ /**
196
+ * Whether a system nav key is one of the feed entries.
197
+ *
198
+ * @param key - A system nav key, or nothing for a non-system item
199
+ * @returns Whether the entry depends on feeds being enabled
200
+ * @example
201
+ * isFeedNavKey("subscribe"); // true
202
+ * isFeedNavKey("archive"); // false
203
+ */
204
+ export function isFeedNavKey(key: string | null | undefined): boolean {
205
+ return (FEED_NAV_KEYS as readonly string[]).includes(key ?? "");
206
+ }
207
+
84
208
  export interface DefaultNavigationProfile {
85
209
  version: number;
86
210
  systemKeys: readonly SystemNavKey[];
@@ -96,9 +220,24 @@ export const DEFAULT_NAVIGATION_PROFILES = {
96
220
  version: 1,
97
221
  systemKeys: ["featured", "collections", "archive", "rss", "settings"],
98
222
  },
223
+ // Featured and All sit next to each other because they are the same list at
224
+ // two widths; version 1 split that pair with Collections, which reads as a
225
+ // third unrelated section rather than the wide end of one range.
226
+ 2: {
227
+ version: 2,
228
+ systemKeys: ["featured", "archive", "collections", "rss", "settings"],
229
+ },
230
+ // `rss` handed a browser a screen of Atom XML, which is a dead end for a
231
+ // reader who does not already know what to do with it. New sites point at
232
+ // /subscribe instead; existing rows keep `rss`, and an author who wants the
233
+ // new entry turns it on beside the old one in Appearance.
234
+ 3: {
235
+ version: 3,
236
+ systemKeys: ["featured", "archive", "collections", "subscribe", "settings"],
237
+ },
99
238
  } as const satisfies Record<number, DefaultNavigationProfile>;
100
239
 
101
- export const DEFAULT_NAVIGATION_PROFILE_VERSION = 1 as const;
240
+ export const DEFAULT_NAVIGATION_PROFILE_VERSION = 3 as const;
102
241
  export const DEFAULT_NAVIGATION_PROFILE =
103
242
  DEFAULT_NAVIGATION_PROFILES[DEFAULT_NAVIGATION_PROFILE_VERSION];
104
243
 
@@ -131,6 +270,23 @@ export type StorageDriver = (typeof STORAGE_DRIVERS)[number];
131
270
  export const PATH_KINDS = ["slug", "alias", "redirect", "archive"] as const;
132
271
  export type PathKind = (typeof PATH_KINDS)[number];
133
272
 
273
+ /**
274
+ * Row kinds the collections directory can hold.
275
+ *
276
+ * A `collection` row points at a collection and carries no text of its own; a
277
+ * `divider` and a `link` carry text and point at nothing. The table CHECK in
278
+ * both dialects is generated from this list, so a new kind is one edit here
279
+ * plus the shape branch that describes which columns it fills.
280
+ */
281
+ export const COLLECTION_DIRECTORY_ENTRY_TYPES = [
282
+ "collection",
283
+ "smart_collection",
284
+ "divider",
285
+ "link",
286
+ ] as const;
287
+ export type CollectionDirectoryEntryType =
288
+ (typeof COLLECTION_DIRECTORY_ENTRY_TYPES)[number];
289
+
134
290
  /** How long a collection nav item stays "fresh" after new content is added */
135
291
  export const COLLECTION_FRESHNESS_WINDOW_SECONDS = 48 * 60 * 60;
136
292
 
@@ -142,3 +298,19 @@ export type SiteDomainKind = (typeof SITE_DOMAIN_KINDS)[number];
142
298
 
143
299
  export const SITE_MEMBER_ROLES = ["owner", "admin", "editor"] as const;
144
300
  export type SiteMemberRole = (typeof SITE_MEMBER_ROLES)[number];
301
+
302
+ export const UPLOAD_SESSION_STATES = [
303
+ "pending",
304
+ "uploaded",
305
+ "completed",
306
+ "aborted",
307
+ "failed",
308
+ ] as const;
309
+ export type UploadSessionState = (typeof UPLOAD_SESSION_STATES)[number];
310
+
311
+ /** How a stored object is served: rendered in place, or downloaded. */
312
+ export const CONTENT_DISPOSITIONS = ["inline", "attachment"] as const;
313
+ export type ContentDisposition = (typeof CONTENT_DISPOSITIONS)[number];
314
+
315
+ export const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
316
+ export type GithubAppAccountType = (typeof GITHUB_APP_ACCOUNT_TYPES)[number];
@@ -3,10 +3,13 @@
3
3
  */
4
4
 
5
5
  import type {
6
+ CollectionDirectoryEntryType,
6
7
  Format,
7
8
  Status,
8
9
  Visibility,
9
10
  CollectionSortOrder,
11
+ SmartCollectionSortOrder,
12
+ ArchiveLayout,
10
13
  NavItemType,
11
14
  NavItemPlacement,
12
15
  SystemNavKey,
@@ -17,6 +20,10 @@ import type {
17
20
  SiteMemberRole,
18
21
  } from "./constants.js";
19
22
 
23
+ import type { PostFilterSelection } from "../lib/filter-dimensions.js";
24
+
25
+ export type { CollectionDirectoryEntryType };
26
+
20
27
  export interface Site {
21
28
  id: string;
22
29
  key: string;
@@ -66,6 +73,15 @@ export interface Post {
66
73
  previewProvider: string | null;
67
74
  replyToId: string | null;
68
75
  threadId: string;
76
+ /**
77
+ * BCP 47 content language, canonical form. Uniform across a Thread. `null`
78
+ * only until the site first enables multilingual content.
79
+ */
80
+ language: string | null;
81
+ /**
82
+ * Shared key for Posts that are translations of one another. Roots only.
83
+ */
84
+ translationGroupId: string | null;
69
85
  /** Reply published without announcing it on Latest. Always false on roots. */
70
86
  quietReply: boolean;
71
87
  publishedAt: number | null;
@@ -141,13 +157,60 @@ export interface CollectionDirectoryCollection extends Collection {
141
157
  recentActivityAt: number;
142
158
  }
143
159
 
144
- export type CollectionDirectoryEntryType = "collection" | "divider" | "link";
160
+ /**
161
+ * A collection whose members are decided by conditions rather than by tagging.
162
+ *
163
+ * To a reader this is a collection: a root-level address, a title, a
164
+ * description, a list of posts, a feed. The asymmetries are permanent and
165
+ * deliberate — nothing can be added to it by hand, nothing pinned, nothing
166
+ * reordered — so every "add this post to a collection" surface has to leave it
167
+ * out. See `selection` for what decides membership.
168
+ */
169
+ export interface SmartCollection {
170
+ id: string;
171
+ siteId: string;
172
+ slug: string;
173
+ title: string;
174
+ description: string | null;
175
+ /** The conditions, in the shared dimension vocabulary. Empty = every post. */
176
+ selection: PostFilterSelection;
177
+ sort: SmartCollectionSortOrder;
178
+ /** `null` follows the site's configured archive layout. */
179
+ layout: ArchiveLayout | null;
180
+ createdAt: number;
181
+ updatedAt: number;
182
+ }
183
+
184
+ export interface SmartCollectionDirectoryEntry extends SmartCollection {
185
+ threadCount: number;
186
+ /**
187
+ * Newest activity among the threads the conditions match, or the smart
188
+ * collection's own `updatedAt` when they match nothing.
189
+ *
190
+ * The same measure `CollectionDirectoryCollection` carries, on the same
191
+ * definition, because the two sit in one list and a reader compares them.
192
+ */
193
+ recentActivityAt: number;
194
+ }
195
+
196
+ /**
197
+ * One entry in the collections directory, either kind.
198
+ *
199
+ * The two are told apart by `kind`, not by which optional field happens to be
200
+ * present. Surfaces that treat them alike — the directory, the navigation
201
+ * picker, the command palette — take this; surfaces where the difference
202
+ * matters take one or the other.
203
+ */
204
+ export type AnyCollectionEntry =
205
+ | { kind: "manual"; collection: CollectionDirectoryCollection }
206
+ | { kind: "smart"; smartCollection: SmartCollectionDirectoryEntry };
145
207
 
146
208
  export interface CollectionDirectoryEntry {
147
209
  id: string;
148
210
  siteId: string;
149
211
  type: CollectionDirectoryEntryType;
150
212
  collectionId: string | null;
213
+ smartCollectionId: string | null;
151
214
  label: string | null;
152
215
  url: string | null;
153
216
  description: string | null;
@@ -163,10 +226,12 @@ export interface CollectionDirectoryItem {
163
226
  url?: string | null;
164
227
  description?: string | null;
165
228
  collection?: CollectionDirectoryCollection;
229
+ smartCollection?: SmartCollectionDirectoryEntry;
166
230
  }
167
231
 
168
232
  export interface CollectionsDirectoryData {
169
233
  collections: CollectionDirectoryCollection[];
234
+ smartCollections: SmartCollectionDirectoryEntry[];
170
235
  items: CollectionDirectoryItem[];
171
236
  directoryItems: CollectionDirectoryEntry[];
172
237
  }
@@ -183,9 +248,24 @@ export interface NavItem {
183
248
  type: NavItemType;
184
249
  systemKey?: SystemNavKey;
185
250
  collectionId?: string;
251
+ smartCollectionId?: string;
186
252
  postId?: string;
253
+ /**
254
+ * Author's override, or `""` when the item follows whatever it points at.
255
+ *
256
+ * Empty is the normal state: a page or collection item shows its target's
257
+ * current title, a built-in item shows its translated default. Only a label
258
+ * the author typed is stored here, and it then wins in every language view.
259
+ */
187
260
  label: string;
188
261
  url: string;
262
+ /**
263
+ * Current title of the post or collection this item points at, resolved at
264
+ * read time. The display label when `label` is empty — see
265
+ * `getNavItemDisplayLabel`. Absent for `link` and `system` items, which have
266
+ * no target row to follow.
267
+ */
268
+ targetTitle?: string;
189
269
  placement: NavItemPlacement;
190
270
  position: string;
191
271
  createdAt: number;
@@ -210,6 +290,7 @@ export interface PathRecord {
210
290
  kind: PathKind;
211
291
  postId: string | null;
212
292
  collectionId: string | null;
293
+ smartCollectionId: string | null;
213
294
  redirectToPath: string | null;
214
295
  redirectType: 301 | 302 | null;
215
296
  archiveQuery: string | null;
@@ -241,5 +322,10 @@ export interface ThreadTimelineContext {
241
322
  /** Published replies immediately before latest, in chronological order. */
242
323
  trailingReplies: Post[];
243
324
  latestReply: Post;
325
+ /**
326
+ * First reply the fold hides, which is where the gap link points. Null when
327
+ * nothing is hidden. Fetched for its address only — never rendered.
328
+ */
329
+ firstHiddenReply: Post | null;
244
330
  totalReplyCount: number;
245
331
  }