@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
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Content language detection.
3
+ *
4
+ * The detector is a suggestion engine, so these tests care about two things:
5
+ * that it is right on the cases it claims to be reliable on (Hangul, kana,
6
+ * Simplified vs Traditional over a sentence), and that it refuses rather than
7
+ * guesses everywhere else — a wrong confident answer costs the author an edit
8
+ * they may never notice they need.
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ detectContentLanguage,
14
+ detectScript,
15
+ readContentLanguage,
16
+ suggestPostLanguage,
17
+ } from "../lang-detect.js";
18
+
19
+ describe("detectScript", () => {
20
+ it("reads Hangul as Korean", () => {
21
+ expect(detectScript("안녕하세요, 반갑습니다")).toBe("ko");
22
+ });
23
+
24
+ it("reads Korean mixed with hanja as Korean", () => {
25
+ // Korean borrows Han characters but never kana, so Hangul settles it.
26
+ expect(detectScript("韓國語 문법 정리")).toBe("ko");
27
+ });
28
+
29
+ it("reads kana as Japanese", () => {
30
+ expect(detectScript("これはテストです")).toBe("ja");
31
+ expect(detectScript("日本語の文章を書く")).toBe("ja");
32
+ });
33
+
34
+ it("reads Simplified-only characters as Simplified", () => {
35
+ expect(detectScript("国学说这时会对后")).toBe("zh-Hans");
36
+ });
37
+
38
+ it("reads Traditional-only characters as Traditional", () => {
39
+ expect(detectScript("國學說這時會對後")).toBe("zh-Hant");
40
+ });
41
+
42
+ it("decides a mixed passage by majority", () => {
43
+ // A Traditional post that quotes a short Simplified phrase.
44
+ expect(detectScript("這篇文章談的是國學說法,裡頭引了一句「这个」")).toBe(
45
+ "zh-Hant",
46
+ );
47
+ });
48
+
49
+ it("names the script but not the variant when the two sides tie", () => {
50
+ // Three distinctive characters each. Which variant is a coin toss; that
51
+ // the text is Chinese is not in doubt.
52
+ expect(detectScript("這個說这个说")).toBe("han");
53
+ });
54
+
55
+ it("names the script but not the variant for characters shared by both", () => {
56
+ // "我今天很好" is identical in Simplified and Traditional.
57
+ expect(detectScript("我今天很好")).toBe("han");
58
+ // Repetition across paragraphs adds length, never a distinguishing
59
+ // character — the case that first showed this up.
60
+ expect(detectScript("哈哈哈哈哈哈哈啊哈哈哈\n\n".repeat(5))).toBe("han");
61
+ });
62
+
63
+ it("reads Latin script as Latin, accents included", () => {
64
+ expect(detectScript("Hello there")).toBe("latin");
65
+ expect(detectScript("Café crème à Paris")).toBe("latin");
66
+ });
67
+
68
+ it("refuses text with no letters at all", () => {
69
+ expect(detectScript("123 — !!! 🙂")).toBeNull();
70
+ expect(detectScript("")).toBeNull();
71
+ });
72
+
73
+ it("refuses a fragment, whichever script it is in", () => {
74
+ // The first few characters of a post decide nothing — an answer here would
75
+ // change on the next keystroke, which is worse than no answer.
76
+ expect(detectScript("H")).toBeNull();
77
+ expect(detectScript("Hello")).toBeNull();
78
+ expect(detectScript("这会")).toBeNull();
79
+ expect(detectScript("こん")).toBeNull();
80
+ });
81
+
82
+ it("answers once there are a couple of words", () => {
83
+ expect(detectScript("Hello there")).toBe("latin");
84
+ expect(detectScript("这是中文")).toBe("zh-Hans");
85
+ });
86
+
87
+ it("reads a passage by what it is mostly in", () => {
88
+ // A Han character used to speak for the whole post no matter how much
89
+ // English surrounded it.
90
+ expect(
91
+ detectScript(
92
+ "The word 说 comes up in every other sentence of this chapter.",
93
+ ),
94
+ ).toBe("latin");
95
+ // ...and the reverse still holds: a Chinese passage quoting a English
96
+ // word is Chinese.
97
+ expect(detectScript("这篇讲的是国学时间,里头引了 React")).toBe("zh-Hans");
98
+ });
99
+ });
100
+
101
+ describe("detectContentLanguage", () => {
102
+ const zhEn = { languages: ["zh-Hans", "en"], fallback: "zh-Hans" };
103
+
104
+ it("picks the language matching the detected script", () => {
105
+ expect(detectContentLanguage("这是中文", zhEn)).toBe("zh-Hans");
106
+ expect(detectContentLanguage("This is English", zhEn)).toBe("en");
107
+ });
108
+
109
+ it("matches Simplified and Traditional as distinct languages", () => {
110
+ const both = {
111
+ languages: ["zh-Hans", "zh-Hant", "en"],
112
+ fallback: "zh-Hans",
113
+ };
114
+
115
+ expect(detectContentLanguage("國學說這時會對後", both)).toBe("zh-Hant");
116
+ expect(detectContentLanguage("国学说这时会对后", both)).toBe("zh-Hans");
117
+ });
118
+
119
+ it("matches a language tag that only implies its script", () => {
120
+ // The site configured zh-TW, not zh-Hant.
121
+ expect(
122
+ detectContentLanguage("國學說這時", {
123
+ languages: ["en", "zh-TW"],
124
+ fallback: "en",
125
+ }),
126
+ ).toBe("zh-TW");
127
+ });
128
+
129
+ it("will not choose between two non-CJK languages", () => {
130
+ // The Latin alphabet does not distinguish English from French.
131
+ expect(
132
+ detectContentLanguage("Bonjour tout le monde", {
133
+ languages: ["en", "fr", "ja"],
134
+ fallback: "en",
135
+ }),
136
+ ).toBe("en");
137
+ });
138
+
139
+ it("reads shared Han as the site's one Chinese language", () => {
140
+ // Text written the same way in both variants. With one of the two
141
+ // published there is nothing left to be ambiguous about.
142
+ expect(detectContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", zhEn)).toBe(
143
+ "zh-Hans",
144
+ );
145
+ expect(
146
+ detectContentLanguage("我今天很好", {
147
+ languages: ["zh-TW", "en"],
148
+ fallback: "en",
149
+ }),
150
+ ).toBe("zh-TW");
151
+ });
152
+
153
+ it("reads shared Han as Simplified when that is the only language", () => {
154
+ // Nothing to weigh it against: every candidate the site has is Simplified
155
+ // Chinese, so text that is certainly Chinese is certainly that.
156
+ expect(
157
+ readContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", {
158
+ languages: ["zh-Hans"],
159
+ }),
160
+ ).toBe("zh-Hans");
161
+ });
162
+
163
+ it("will not choose between Simplified and Traditional", () => {
164
+ // Both published, and the text distinguishes neither: the fallback is the
165
+ // only honest answer.
166
+ expect(
167
+ readContentLanguage("我今天很好", {
168
+ languages: ["zh-Hans", "zh-Hant", "en"],
169
+ }),
170
+ ).toBeNull();
171
+ });
172
+
173
+ it("falls back when the site does not publish the detected script", () => {
174
+ expect(
175
+ detectContentLanguage("これは日本語です", {
176
+ languages: ["zh-Hans", "en"],
177
+ fallback: "zh-Hans",
178
+ }),
179
+ ).toBe("zh-Hans");
180
+ });
181
+
182
+ it("never returns a language outside the configured set", () => {
183
+ const samples = [
184
+ "안녕하세요",
185
+ "これはテスト",
186
+ "國學說這時",
187
+ "国学说这时",
188
+ "Hello",
189
+ "12345",
190
+ "",
191
+ ];
192
+
193
+ for (const sample of samples) {
194
+ expect(zhEn.languages, sample).toContain(
195
+ detectContentLanguage(sample, zhEn),
196
+ );
197
+ }
198
+ });
199
+ });
200
+
201
+ describe("suggestPostLanguage", () => {
202
+ const site = { languages: ["zh-Hans", "en"], primary: "zh-Hans" };
203
+
204
+ it("reads the language out of the text", () => {
205
+ expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
206
+ expect(suggestPostLanguage({ ...site, text: "这是中文" })).toBe("zh-Hans");
207
+ });
208
+
209
+ it("falls back to the primary language for empty text", () => {
210
+ expect(suggestPostLanguage({ ...site, text: " " })).toBe("zh-Hans");
211
+ expect(suggestPostLanguage(site)).toBe("zh-Hans");
212
+ });
213
+
214
+ it("keeps the primary language until the text says otherwise", () => {
215
+ // Half a word in: the composer shows the language of the page the author
216
+ // is writing from, and stays there until there is something to read.
217
+ expect(suggestPostLanguage({ ...site, text: "Hel" })).toBe("zh-Hans");
218
+ expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
219
+ });
220
+
221
+ it("returns null on a site that is not multilingual", () => {
222
+ // Nothing is stamped before the author turns the feature on.
223
+ expect(
224
+ suggestPostLanguage({
225
+ languages: [],
226
+ primary: "zh-Hans",
227
+ text: "Hello there",
228
+ }),
229
+ ).toBeNull();
230
+ });
231
+ });
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Media } from "../../types.js";
3
+ import { buildMediaMap } from "../media-helpers.js";
4
+
5
+ const R2_PUBLIC_URL = "https://cdn.example.com";
6
+ const IMAGE_TRANSFORM_URL = "https://example.com/cdn-cgi/image";
7
+
8
+ function makeMedia(overrides: Partial<Media> = {}): Media {
9
+ return {
10
+ id: "med_01j0000000000000000000000",
11
+ siteId: "sit_test00000000000000000000000",
12
+ postId: "pst_01j0000000000000000000000",
13
+ filename: "clip.mp4",
14
+ originalName: "clip.mp4",
15
+ mimeType: "video/mp4",
16
+ size: 4_000_000,
17
+ storageKey: "media/clip.mp4",
18
+ provider: "r2",
19
+ width: 1920,
20
+ height: 1080,
21
+ durationSeconds: 12,
22
+ alt: null,
23
+ position: "a0",
24
+ blurhash: null,
25
+ waveform: null,
26
+ posterKey: "posters/clip-poster.png",
27
+ summary: null,
28
+ chars: null,
29
+ mediaKind: "video",
30
+ createdAt: 1706745600,
31
+ updatedAt: 1706745600,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function firstAttachment(media: Media) {
37
+ const map = buildMediaMap(
38
+ new Map([["pst_01j0000000000000000000000", [media]]]),
39
+ R2_PUBLIC_URL,
40
+ IMAGE_TRANSFORM_URL,
41
+ );
42
+ const list = map.get("pst_01j0000000000000000000000");
43
+ expect(list).toHaveLength(1);
44
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked above
45
+ return list![0]!;
46
+ }
47
+
48
+ describe("buildMediaMap", () => {
49
+ it("sizes a video poster through the image transform", () => {
50
+ // A poster is a still frame at the source video's resolution, so an
51
+ // untransformed one puts the full-size original in the timeline. This is
52
+ // the same treatment `toMediaView` gives the field.
53
+ expect(firstAttachment(makeMedia()).posterUrl).toBe(
54
+ "https://example.com/cdn-cgi/image/width=640,quality=80,format=auto,fit=scale-down/https://cdn.example.com/posters/clip-poster.png",
55
+ );
56
+ });
57
+
58
+ it("leaves the poster URL untransformed when no transform is configured", () => {
59
+ const map = buildMediaMap(
60
+ new Map([["pst_01j0000000000000000000000", [makeMedia()]]]),
61
+ R2_PUBLIC_URL,
62
+ );
63
+ expect(map.get("pst_01j0000000000000000000000")?.[0]?.posterUrl).toBe(
64
+ "https://cdn.example.com/posters/clip-poster.png",
65
+ );
66
+ });
67
+
68
+ it("has no poster URL without a poster key", () => {
69
+ expect(
70
+ firstAttachment(makeMedia({ posterKey: null })).posterUrl,
71
+ ).toBeNull();
72
+ });
73
+
74
+ it("leaves a video's own URL alone", () => {
75
+ // Only images go through the transform; asking it to resize an mp4
76
+ // returns nothing useful.
77
+ expect(firstAttachment(makeMedia()).previewUrl).toBe(
78
+ "https://cdn.example.com/media/clip.mp4",
79
+ );
80
+ });
81
+ });
@@ -1,6 +1,10 @@
1
1
  import type { Context } from "hono";
2
2
  import { describe, expect, it } from "vitest";
3
- import { getNavigationData } from "../navigation.js";
3
+ import type { NavItemView } from "../../types.js";
4
+ import {
5
+ collectNavigationCollectionIds,
6
+ getNavigationData,
7
+ } from "../navigation.js";
4
8
 
5
9
  describe("getNavigationData", () => {
6
10
  it("renders site footer markdown through the shared pipeline", async () => {
@@ -99,6 +103,61 @@ describe("getNavigationData", () => {
99
103
  expect(result.links[1]?.url).toBe("/");
100
104
  });
101
105
 
106
+ it("hides the built-in Subscribe item when feeds are disabled", async () => {
107
+ const context = {
108
+ var: {
109
+ publicPath: "/",
110
+ appConfig: {
111
+ siteName: "Jant",
112
+ sitePathPrefix: "",
113
+ siteOrigin: "https://example.com",
114
+ siteDescription: "",
115
+ siteDescriptionExplicit: false,
116
+ siteAvatarUrl: "",
117
+ showHeaderAvatar: false,
118
+ siteFooter: "",
119
+ rssFeedsEnabled: false,
120
+ },
121
+ services: {
122
+ navItems: {
123
+ list: async () => [
124
+ {
125
+ id: "nav_subscribe",
126
+ type: "system",
127
+ systemKey: "subscribe",
128
+ label: "Subscribe",
129
+ url: "/subscribe",
130
+ placement: "header",
131
+ position: "a0",
132
+ createdAt: 1,
133
+ updatedAt: 1,
134
+ },
135
+ {
136
+ id: "nav_archive",
137
+ type: "system",
138
+ systemKey: "archive",
139
+ label: "All",
140
+ url: "/archive",
141
+ placement: "header",
142
+ position: "a1",
143
+ createdAt: 1,
144
+ updatedAt: 1,
145
+ },
146
+ ],
147
+ },
148
+ collections: { list: async () => [] },
149
+ },
150
+ isAuthenticated: false,
151
+ session: null,
152
+ },
153
+ } as unknown as Context;
154
+
155
+ const result = await getNavigationData(context);
156
+
157
+ // The page it addresses 404s without feeds, so the entry has to go too.
158
+ expect(result.links.map((link) => link.url)).toEqual(["/archive"]);
159
+ });
160
+
102
161
  it("hides only the built-in RSS item when feeds are disabled", async () => {
103
162
  const context = {
104
163
  var: {
@@ -153,4 +212,79 @@ describe("getNavigationData", () => {
153
212
 
154
213
  expect(result.links.map((link) => link.id)).toEqual(["nav_custom"]);
155
214
  });
215
+
216
+ // A post page's URL carries no language prefix, so its chrome is scoped by
217
+ // the post's language instead of the request path.
218
+ it("scopes the base path to an explicit language", async () => {
219
+ const makeContext = (languageScope: string) =>
220
+ ({
221
+ var: {
222
+ publicPath: "/some-post",
223
+ appConfig: {
224
+ siteName: "Jant",
225
+ sitePathPrefix: "",
226
+ siteDescription: "",
227
+ siteDescriptionExplicit: false,
228
+ siteAvatarUrl: "",
229
+ showHeaderAvatar: false,
230
+ siteFooter: "",
231
+ siteLanguage: "zh-Hans",
232
+ additionalLanguages: ["ja"],
233
+ multilingualEnabled: true,
234
+ rssFeedsEnabled: true,
235
+ },
236
+ services: {
237
+ navItems: {
238
+ list: async () => [
239
+ {
240
+ id: "nav_latest",
241
+ type: "system",
242
+ systemKey: "latest",
243
+ label: "Latest",
244
+ url: "/",
245
+ placement: "header",
246
+ position: "a0",
247
+ createdAt: 1,
248
+ updatedAt: 1,
249
+ },
250
+ ],
251
+ },
252
+ collections: {
253
+ listByRecentActivity: async () => [],
254
+ },
255
+ },
256
+ isAuthenticated: false,
257
+ session: null,
258
+ },
259
+ }) as unknown as Context;
260
+
261
+ const japanese = await getNavigationData(makeContext("ja"), {
262
+ languageScope: "ja",
263
+ });
264
+ expect(japanese.basePath).toBe("/ja");
265
+ expect(japanese.links[0]?.url).toBe("/ja");
266
+
267
+ const primary = await getNavigationData(makeContext("zh-Hans"), {
268
+ languageScope: "zh-Hans",
269
+ });
270
+ expect(primary.basePath).toBe("");
271
+ expect(primary.links[0]?.url).toBe("/");
272
+ });
273
+ });
274
+
275
+ describe("collectNavigationCollectionIds", () => {
276
+ // The directory used to read only `collection` items, so a smart collection
277
+ // already in the navigation still offered "Add to Navigation".
278
+ it("names both kinds of collection and ignores everything else", () => {
279
+ const links = [
280
+ { id: "nav_1", type: "system", systemKey: "home" },
281
+ { id: "nav_2", type: "collection", collectionId: "col_1" },
282
+ { id: "nav_3", type: "smart_collection", smartCollectionId: "smc_1" },
283
+ { id: "nav_4", type: "link", url: "https://example.com" },
284
+ { id: "nav_5", type: "collection" },
285
+ { id: "nav_6", type: "smart_collection" },
286
+ ] as unknown as NavItemView[];
287
+
288
+ expect(collectNavigationCollectionIds(links)).toEqual(["col_1", "smc_1"]);
289
+ });
156
290
  });
@@ -31,9 +31,9 @@ describe("resolveConfig", () => {
31
31
  expect(config.publicApiEnabled).toBe(true);
32
32
  expect(config.rssFeedsEnabled).toBe(true);
33
33
  expect(config.demoMode).toBe(false);
34
- expect(config.pageSize).toBe(50);
35
- expect(config.searchPageSize).toBe(50);
36
- expect(config.archivePageSize).toBe(50);
34
+ expect(config.pageSize).toBe(25);
35
+ expect(config.searchPageSize).toBe(25);
36
+ expect(config.archivePageSize).toBe(25);
37
37
  expect(config.rssFeedLimit).toBe(50);
38
38
  expect(config.rssPublishDelaySeconds).toBe(300);
39
39
  });
@@ -81,6 +81,34 @@ describe("resolveConfig", () => {
81
81
  expect(config.mainRssFeed).toBe("featured");
82
82
  });
83
83
 
84
+ it("resolves archiveDefaultLayout from DB, env, and defaults", () => {
85
+ const defaultConfig = resolveConfig(makeEnv(), {});
86
+ expect(defaultConfig.archiveDefaultLayout).toBe("list");
87
+
88
+ const envConfig = resolveConfig(
89
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "grid" }),
90
+ {},
91
+ );
92
+ expect(envConfig.archiveDefaultLayout).toBe("grid");
93
+
94
+ const dbConfig = resolveConfig(
95
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "list" }),
96
+ {
97
+ ARCHIVE_DEFAULT_LAYOUT: "grid",
98
+ },
99
+ );
100
+ expect(dbConfig.archiveDefaultLayout).toBe("grid");
101
+ });
102
+
103
+ it("falls back when the archive layout setting is invalid", () => {
104
+ const config = resolveConfig(
105
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "carousel" }),
106
+ {},
107
+ );
108
+
109
+ expect(config.archiveDefaultLayout).toBe("list");
110
+ });
111
+
84
112
  it("normalizes legacy time zone values from the database", () => {
85
113
  const config = resolveConfig(makeEnv(), { TIME_ZONE: "Beijing" });
86
114
 
@@ -172,6 +200,39 @@ describe("resolveConfig", () => {
172
200
  expect(config.siteAvatarUrl).toBe(
173
201
  "https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
174
202
  );
203
+ // Without a transform there is nothing to size it down with, so the
204
+ // header falls back to the canonical asset.
205
+ expect(config.siteAvatarThumbUrl).toBe(config.siteAvatarUrl);
206
+ });
207
+
208
+ it("sizes the header avatar down when an image transform is configured", () => {
209
+ const config = resolveConfig(
210
+ makeEnv({
211
+ R2_PUBLIC_URL: "https://r2.example.com",
212
+ IMAGE_TRANSFORM_URL: "https://r2.example.com/cdn-cgi/image",
213
+ STORAGE_DRIVER: "r2",
214
+ }),
215
+ {
216
+ SITE_AVATAR:
217
+ "media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
218
+ },
219
+ );
220
+
221
+ // The canonical URL stays full-resolution: it is what the social card and
222
+ // the settings editor need.
223
+ expect(config.siteAvatarUrl).toBe(
224
+ "https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
225
+ );
226
+ expect(config.siteAvatarThumbUrl).toBe(
227
+ "https://r2.example.com/cdn-cgi/image/width=128,quality=80,format=auto,fit=scale-down/https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
228
+ );
229
+ });
230
+
231
+ it("leaves both avatar URLs empty without a stored avatar", () => {
232
+ const config = resolveConfig(makeEnv({}), {});
233
+
234
+ expect(config.siteAvatarUrl).toBe("");
235
+ expect(config.siteAvatarThumbUrl).toBe("");
175
236
  });
176
237
 
177
238
  it("returns empty siteAvatarUrl when no avatar set", () => {
@@ -9,11 +9,12 @@ import {
9
9
  CreatePostApiSchema,
10
10
  UpdatePostSchema,
11
11
  UpdatePostApiSchema,
12
- SetupSchema,
12
+ SetupAccountSchema,
13
13
  SigninSchema,
14
14
  UpdateSiteSettingsSchema,
15
15
  normalizeEmail,
16
16
  parseFormData,
17
+ PostFilterSelectionSchema,
17
18
  parseFormDataOptional,
18
19
  validateAttachmentCount,
19
20
  } from "../schemas.js";
@@ -75,10 +76,9 @@ describe("normalizeEmail", () => {
75
76
  });
76
77
  });
77
78
 
78
- describe("SetupSchema", () => {
79
+ describe("SetupAccountSchema", () => {
79
80
  it("normalizes email before returning parsed data", () => {
80
- const result = SetupSchema.parse({
81
- siteName: "Jant",
81
+ const result = SetupAccountSchema.parse({
82
82
  email: " Admin@Example.COM ",
83
83
  password: "password123",
84
84
  });
@@ -816,3 +816,41 @@ describe("validateAttachmentCount", () => {
816
816
  );
817
817
  });
818
818
  });
819
+
820
+ describe("PostFilterSelectionSchema", () => {
821
+ it("accepts a selection a smart collection may store", () => {
822
+ const result = PostFilterSelectionSchema.safeParse({
823
+ format: "note",
824
+ media: "any",
825
+ visibility: "featured",
826
+ collection: ["col_01m0f291t3fzvte3vj2g8d611z"],
827
+ });
828
+ expect(result.success).toBe(true);
829
+ });
830
+
831
+ it("accepts the empty selection, which means every post", () => {
832
+ expect(PostFilterSelectionSchema.safeParse({}).success).toBe(true);
833
+ });
834
+
835
+ it("refuses the private visibility a smart collection can never name", () => {
836
+ expect(
837
+ PostFilterSelectionSchema.safeParse({ visibility: "private" }).success,
838
+ ).toBe(false);
839
+ });
840
+
841
+ it("refuses a key that is not a dimension", () => {
842
+ expect(
843
+ PostFilterSelectionSchema.safeParse({ language: "en" }).success,
844
+ ).toBe(false);
845
+ });
846
+
847
+ it("names the condition it rejected", () => {
848
+ const result = PostFilterSelectionSchema.safeParse({ year: 1970 });
849
+ expect(result.success).toBe(false);
850
+ if (result.success) return;
851
+ expect(result.error.issues[0]?.path).toEqual(["year"]);
852
+ expect(result.error.issues[0]?.message).toBe(
853
+ "Invalid value for the year condition",
854
+ );
855
+ });
856
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderSiteUnavailablePage } from "../site-unavailable-page.js";
3
+
4
+ describe("renderSiteUnavailablePage", () => {
5
+ it("links back to the configured hosting provider", () => {
6
+ const html = renderSiteUnavailablePage({
7
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud-jant.localtest.me",
8
+ HOSTED_CONTROL_PLANE_PROVIDER_NAME: "Jant Cloud",
9
+ SITE_RESOLUTION_MODE: "host-based",
10
+ });
11
+
12
+ expect(html).toContain("This site is offline");
13
+ expect(html).toContain(
14
+ '<a href="https://cloud-jant.localtest.me/app">Jant Cloud</a>',
15
+ );
16
+ });
17
+
18
+ it("stays provider-neutral when no control plane is configured", () => {
19
+ const html = renderSiteUnavailablePage({});
20
+
21
+ expect(html).toContain("There's nothing to read here right now");
22
+ expect(html).toContain("sign in to your hosting provider");
23
+ expect(html).not.toContain("<a ");
24
+ });
25
+ });