@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
@@ -5,6 +5,10 @@ vi.stubGlobal("__JANT_DEV__", false);
5
5
  vi.stubGlobal("__JANT_VERSION__", "test-version");
6
6
  vi.stubGlobal("__CLIENT_JS_FILE__", "/_assets/client.js");
7
7
  vi.stubGlobal("__CLIENT_AUTH_JS_FILE__", "/_assets/client-auth.js");
8
+ vi.stubGlobal("__CLIENT_COMPOSE_PRELOAD__", [
9
+ "/_assets/client-compose.js",
10
+ "/_assets/chunks/create-editor.js",
11
+ ]);
8
12
 
9
13
  function createContext(
10
14
  mainRssFeed: "featured" | "latest",
@@ -17,8 +21,11 @@ function createContext(
17
21
  themeId?: string;
18
22
  defaultThemeId?: string;
19
23
  siteLanguage?: string;
20
- cjkSerifFont?: string;
24
+ /** Language of the page being rendered (post language or view language). */
25
+ lang?: string;
21
26
  rssFeedsEnabled?: boolean;
27
+ storageDriver?: string;
28
+ r2PublicUrl?: string;
22
29
  },
23
30
  ) {
24
31
  const values = {
@@ -29,15 +36,16 @@ function createContext(
29
36
  siteUrl: overrides?.siteUrl ?? "https://example.com",
30
37
  siteAvatarUrl: overrides?.siteAvatarUrl,
31
38
  siteLanguage: overrides?.siteLanguage ?? "en",
32
- cjkSerifFont: overrides?.cjkSerifFont ?? "off",
33
39
  noindex: false,
34
40
  customCSS: "",
35
41
  themeMode: overrides?.themeMode ?? "auto",
36
42
  themeId: overrides?.themeId ?? overrides?.defaultThemeId ?? "linen",
37
43
  defaultThemeId: overrides?.defaultThemeId ?? "linen",
38
44
  assetBasePath: overrides?.assetBasePath ?? "/_assets",
45
+ storageDriver: overrides?.storageDriver ?? "r2",
46
+ r2PublicUrl: overrides?.r2PublicUrl,
39
47
  },
40
- lang: "en",
48
+ lang: overrides?.lang ?? overrides?.siteLanguage ?? "en",
41
49
  i18n: {
42
50
  _: (descriptor: { message?: string }) => descriptor.message ?? "",
43
51
  },
@@ -320,6 +328,72 @@ describe("BaseLayout", () => {
320
328
  expect(html).not.toContain('href="/feed"');
321
329
  });
322
330
 
331
+ it("preloads the composer's files on signed-in pages, under the asset base path", async () => {
332
+ const { BaseLayout } = await loadBaseLayout();
333
+ const html = renderToString(
334
+ BaseLayout({
335
+ title: "Jant",
336
+ c: createContext("featured", {
337
+ sitePathPrefix: "/blog",
338
+ siteUrl: "https://example.com/blog",
339
+ assetBasePath: "/blog/_assets",
340
+ }),
341
+ isAuthenticated: true,
342
+ children: "Test",
343
+ }),
344
+ );
345
+
346
+ expect(html).toContain(`src="/blog/_assets/client-auth.js"`);
347
+ expect(html).toContain(
348
+ `<link rel="modulepreload" href="/blog/_assets/client-compose.js"`,
349
+ );
350
+ expect(html).toContain(
351
+ `<link rel="modulepreload" href="/blog/_assets/chunks/create-editor.js"`,
352
+ );
353
+ });
354
+
355
+ it("preloads nothing for readers, who never open the composer", async () => {
356
+ const { BaseLayout } = await loadBaseLayout();
357
+ const html = renderToString(
358
+ BaseLayout({
359
+ title: "Jant",
360
+ c: createContext("featured"),
361
+ children: "Test",
362
+ }),
363
+ );
364
+
365
+ expect(html).toContain(`src="/_assets/client.js"`);
366
+ expect(html).not.toContain("modulepreload");
367
+ });
368
+
369
+ it("links the author stylesheet only once someone is signed in", async () => {
370
+ const { BaseLayout } = await loadBaseLayout();
371
+ const render = (isAuthenticated: boolean) =>
372
+ renderToString(
373
+ BaseLayout({
374
+ title: "Jant",
375
+ c: createContext("featured", {
376
+ sitePathPrefix: "/blog",
377
+ siteUrl: "https://example.com/blog",
378
+ assetBasePath: "/blog/_assets",
379
+ }),
380
+ ...(isAuthenticated ? { isAuthenticated } : {}),
381
+ children: "Test",
382
+ }),
383
+ );
384
+
385
+ // The composer, editor and settings CSS is the bulk of the hand-written
386
+ // component styles and none of it renders for a visitor; shipping it on a
387
+ // public page would put it back on the critical path of every page view.
388
+ expect(render(false)).not.toContain("client-author.css");
389
+ expect(render(true)).toContain(
390
+ '<link rel="stylesheet" href="/blog/_assets/client-author.css"',
391
+ );
392
+ // Both pages still share the one reader stylesheet.
393
+ expect(render(false)).toContain(`href="/blog/_assets/client.css"`);
394
+ expect(render(true)).toContain(`href="/blog/_assets/client.css"`);
395
+ });
396
+
323
397
  it("uses the public asset base path from appConfig in production", async () => {
324
398
  const { BaseLayout } = await loadBaseLayout();
325
399
  const html = renderToString(
@@ -339,48 +413,153 @@ describe("BaseLayout", () => {
339
413
  expect(html).toContain('data-asset-base-path="/blog/_assets"');
340
414
  });
341
415
 
342
- it("loads the content language font profile when the CJK fallback is off", async () => {
416
+ it("preconnects to the asset host when assets are served from a CDN", async () => {
343
417
  const { BaseLayout } = await loadBaseLayout();
344
418
  const html = renderToString(
345
419
  BaseLayout({
346
420
  title: "Jant",
347
421
  c: createContext("featured", {
348
- siteLanguage: "zh-Hant",
349
- cjkSerifFont: "off",
422
+ siteUrl: "https://example.com",
423
+ assetBasePath: "https://cdn.example.com/jant",
350
424
  }),
351
425
  children: "Test",
352
426
  }),
353
427
  );
354
428
 
355
- expect(html).toContain("client-cjk-tc.css");
356
- expect(html).not.toContain("client-cjk.css");
429
+ expect(html).toContain(
430
+ '<link rel="preconnect" href="https://cdn.example.com"/>',
431
+ );
432
+ expect(html).toContain(
433
+ '<link rel="preconnect" href="https://cdn.example.com" crossorigin="anonymous"/>',
434
+ );
435
+ // Before the stylesheet it warms the connection for.
436
+ expect(html.indexOf('rel="preconnect"')).toBeLessThan(
437
+ html.indexOf('rel="stylesheet"'),
438
+ );
439
+ });
440
+
441
+ it("preconnects to the media host as well when it is a separate one", async () => {
442
+ const { BaseLayout } = await loadBaseLayout();
443
+ const html = renderToString(
444
+ BaseLayout({
445
+ title: "Jant",
446
+ c: createContext("featured", {
447
+ siteUrl: "https://example.com",
448
+ assetBasePath: "https://cdn.example.com/jant",
449
+ r2PublicUrl: "https://media.example.com",
450
+ }),
451
+ children: "Test",
452
+ }),
453
+ );
454
+
455
+ expect(html).toContain(
456
+ '<link rel="preconnect" href="https://media.example.com"/>',
457
+ );
458
+ expect(html).not.toContain(
459
+ '<link rel="preconnect" href="https://media.example.com" crossorigin="anonymous"/>',
460
+ );
357
461
  });
358
462
 
359
- it("uses the manual CJK fallback only for an unadapted content language", async () => {
463
+ it("preconnects once to a host that serves both assets and media", async () => {
360
464
  const { BaseLayout } = await loadBaseLayout();
361
- const fallbackHtml = renderToString(
465
+ const html = renderToString(
466
+ BaseLayout({
467
+ title: "Jant",
468
+ c: createContext("featured", {
469
+ siteUrl: "https://example.com",
470
+ assetBasePath: "https://cdn.example.com/_assets",
471
+ r2PublicUrl: "https://cdn.example.com/media",
472
+ }),
473
+ children: "Test",
474
+ }),
475
+ );
476
+
477
+ expect(html.match(/rel="preconnect"/g)).toHaveLength(2);
478
+ });
479
+
480
+ it("preconnects to nothing when assets and media share the site's origin", async () => {
481
+ const { BaseLayout } = await loadBaseLayout();
482
+ const sameOrigin = renderToString(
362
483
  BaseLayout({
363
484
  title: "Jant",
364
485
  c: createContext("featured", {
365
- siteLanguage: "en",
366
- cjkSerifFont: "zh-Hans",
486
+ siteUrl: "https://example.com",
487
+ assetBasePath: "https://example.com/_assets",
488
+ r2PublicUrl: "https://example.com/media",
367
489
  }),
368
490
  children: "Test",
369
491
  }),
370
492
  );
371
- const contentLanguageHtml = renderToString(
493
+ const samePath = renderToString(
494
+ BaseLayout({
495
+ title: "Jant",
496
+ c: createContext("featured", { assetBasePath: "/_assets" }),
497
+ children: "Test",
498
+ }),
499
+ );
500
+
501
+ expect(sameOrigin).not.toContain('rel="preconnect"');
502
+ expect(samePath).not.toContain('rel="preconnect"');
503
+ });
504
+
505
+ it("loads the CJK stylesheet for the site language", async () => {
506
+ const { BaseLayout } = await loadBaseLayout();
507
+ const html = renderToString(
508
+ BaseLayout({
509
+ title: "Jant",
510
+ c: createContext("featured", { siteLanguage: "zh-Hant" }),
511
+ children: "Test",
512
+ }),
513
+ );
514
+
515
+ expect(html).toContain("client-cjk-tc.css");
516
+ expect(html).not.toContain("client-cjk.css");
517
+ });
518
+
519
+ it("follows the page language rather than the site language", async () => {
520
+ const { BaseLayout } = await loadBaseLayout();
521
+ // A Traditional Chinese post on a Simplified Chinese site: the Han glyphs
522
+ // must come from the post's own language, not the site's.
523
+ const html = renderToString(
372
524
  BaseLayout({
373
525
  title: "Jant",
374
526
  c: createContext("featured", {
375
- siteLanguage: "zh-Hant",
376
- cjkSerifFont: "zh-Hans",
527
+ siteLanguage: "zh-Hans",
528
+ lang: "zh-Hant",
377
529
  }),
378
530
  children: "Test",
379
531
  }),
380
532
  );
381
533
 
382
- expect(fallbackHtml).toContain("client-cjk.css");
383
- expect(contentLanguageHtml).toContain("client-cjk-tc.css");
534
+ expect(html).toContain("client-cjk-tc.css");
535
+ expect(html).not.toContain("client-cjk.css");
536
+ });
537
+
538
+ it("emits the CJK font variables for the page language", async () => {
539
+ const { BaseLayout } = await loadBaseLayout();
540
+ const html = renderToString(
541
+ BaseLayout({
542
+ title: "Jant",
543
+ c: createContext("featured", { siteLanguage: "en", lang: "ja" }),
544
+ children: "Test",
545
+ }),
546
+ );
547
+
548
+ expect(html).toContain("--font-cjk-serif-fallback");
549
+ expect(html).toContain("client-cjk-jp.css");
550
+ });
551
+
552
+ it("emits no CJK font variables for a non-CJK page language", async () => {
553
+ const { BaseLayout } = await loadBaseLayout();
554
+ const html = renderToString(
555
+ BaseLayout({
556
+ title: "Jant",
557
+ c: createContext("featured", { siteLanguage: "en" }),
558
+ children: "Test",
559
+ }),
560
+ );
561
+
562
+ expect(html).not.toContain("--font-cjk-serif-fallback");
384
563
  });
385
564
 
386
565
  it("exposes the active theme id on the root html element", async () => {
@@ -1,124 +1,148 @@
1
+ /**
2
+ * The site chrome around every public page.
3
+ *
4
+ * The header's links are language-scoped: a page inside a language view —
5
+ * /en/archive, or a Japanese post at its language-neutral URL — hands the
6
+ * header a `basePath` carrying the view's prefix, and the logo, drawer brand,
7
+ * and search icon must stay inside that view rather than leading back to the
8
+ * primary language.
9
+ */
10
+
1
11
  import type { Context } from "hono";
2
12
  import { renderToString } from "hono/jsx/dom/server";
3
13
  import { describe, expect, it } from "vitest";
4
14
  import { I18nProvider } from "../../../i18n/context.js";
5
15
  import { createI18n } from "../../../i18n/i18n.js";
6
- import type { NavItemView } from "../../../types.js";
7
- import { SiteLayout } from "../SiteLayout.js";
8
-
9
- function createNavItem(
10
- index: number,
11
- placement: NavItemView["placement"],
12
- ): NavItemView {
13
- return {
14
- id: `nav_${index}`,
15
- type: "link",
16
- label: `Link ${index}`,
17
- url: `/link-${index}`,
18
- placement,
19
- isActive: false,
20
- isExternal: false,
21
- };
22
- }
16
+ import type { SiteLayoutProps } from "../../../types.js";
17
+ import { SiteHeader, SiteLayout } from "../SiteLayout.js";
23
18
 
24
- function renderSiteLayout(
25
- props: Partial<Parameters<typeof SiteLayout>[0]> = {},
26
- ): string {
19
+ function provideI18n(): void {
27
20
  const i18n = createI18n("en");
28
21
  const c = {
29
22
  get(key: string) {
30
- if (key === "i18n") return i18n;
31
- return undefined;
23
+ return key === "i18n" ? i18n : undefined;
32
24
  },
33
25
  } as unknown as Context;
34
-
35
26
  I18nProvider({ c, children: "" });
27
+ }
28
+
29
+ function renderHeader(
30
+ props: Partial<Parameters<typeof SiteHeader>[0]> = {},
31
+ ): string {
32
+ provideI18n();
36
33
 
37
34
  return renderToString(
38
- SiteLayout({
35
+ SiteHeader({
39
36
  siteName: "Jant",
40
37
  links: [],
41
38
  currentPath: "/",
42
- children: "Feed",
43
39
  ...props,
44
40
  }),
45
41
  );
46
42
  }
47
43
 
48
- describe("SiteLayout", () => {
49
- it.each([
50
- [3, "sm"],
51
- [4, "md"],
52
- [5, "lg"],
53
- ] as const)(
54
- "reveals the More divider at the first breakpoint that overflows %i header links",
55
- (headerLinkCount, tier) => {
56
- const headerLinks = Array.from({ length: headerLinkCount }, (_, index) =>
57
- createNavItem(index, "header"),
58
- );
59
- const html = renderSiteLayout({
60
- links: [...headerLinks, createNavItem(headerLinkCount, "more")],
61
- });
44
+ function renderLayout(props: Partial<SiteLayoutProps> = {}): string {
45
+ provideI18n();
62
46
 
63
- expect(html).toContain(
64
- `class="site-header-more-divider site-header-more-divider-responsive site-header-more-divider-show-${tier}"`,
65
- );
66
- },
47
+ return renderToString(
48
+ SiteLayout({
49
+ siteName: "Jant",
50
+ links: [],
51
+ currentPath: "/",
52
+ children: "",
53
+ ...props,
54
+ }),
67
55
  );
56
+ }
68
57
 
69
- it("omits the More divider when no header link can overflow", () => {
70
- const html = renderSiteLayout({
71
- links: [
72
- createNavItem(0, "header"),
73
- createNavItem(1, "header"),
74
- createNavItem(2, "more"),
75
- ],
76
- });
58
+ describe("SiteHeader", () => {
59
+ it("keeps the logo and search inside a language view", () => {
60
+ const html = renderHeader({ basePath: "/ja", currentPath: "/ja" });
77
61
 
78
- expect(html).not.toContain("site-header-more-divider");
62
+ expect(html).toContain('href="/ja" class="site-logo"');
63
+ expect(html).toContain('href="/ja/search"');
64
+ expect(html).toContain('href="/ja" class="site-nav-drawer-brand"');
79
65
  });
80
66
 
81
- it("renders the mobile compose FAB for authenticated timeline pages", () => {
82
- const html = renderSiteLayout({
83
- currentPath: "/latest",
84
- isAuthenticated: true,
85
- });
86
-
87
- expect(html).toContain('class="site-mobile-compose-fab"');
88
- expect(html).toContain('aria-label="New post"');
89
- expect(html).toContain("openNew()");
67
+ it("marks a language view's home as the home page", () => {
68
+ expect(renderHeader({ basePath: "/ja", currentPath: "/ja" })).toContain(
69
+ "site-header-top-home",
70
+ );
71
+ expect(
72
+ renderHeader({ basePath: "/ja", currentPath: "/ja/archive" }),
73
+ ).not.toContain("site-header-top-home");
90
74
  });
91
75
 
92
- it("does not render the mobile compose FAB for collection pages", () => {
93
- const html = renderSiteLayout({
94
- currentPath: "/collections/writing",
95
- isAuthenticated: true,
96
- children: "Collection",
97
- });
76
+ it("links to the root without a base path", () => {
77
+ const html = renderHeader({});
98
78
 
99
- expect(html).not.toContain('class="site-mobile-compose-fab"');
79
+ expect(html).toContain('href="/" class="site-logo"');
80
+ expect(html).toContain('href="/search"');
100
81
  });
101
82
 
102
- it("renders the mobile compose FAB with collection context on collection detail pages", () => {
103
- const html = renderSiteLayout({
104
- currentPath: "/collections/writing",
105
- isAuthenticated: true,
106
- composeCollectionId: "col_abc123",
107
- children: "Collection",
83
+ it("composes the deployment prefix with the language prefix", () => {
84
+ const html = renderHeader({
85
+ sitePathPrefix: "/blog",
86
+ basePath: "/blog/ja",
87
+ currentPath: "/blog/ja",
108
88
  });
109
89
 
110
- expect(html).toContain('class="site-mobile-compose-fab"');
111
- expect(html).toContain("col_abc123");
112
- expect(html).toContain("collectionId");
90
+ expect(html).toContain('href="/blog/ja" class="site-logo"');
91
+ expect(html).toContain('href="/blog/ja/search"');
113
92
  });
93
+ });
114
94
 
115
- it("does not render the mobile compose FAB for signed-out readers", () => {
116
- const html = renderSiteLayout({
117
- currentPath: "/featured",
118
- isAuthenticated: false,
119
- children: "Feed",
120
- });
95
+ describe("SiteLayout", () => {
96
+ // The floating compose button is fixed to the bottom of a phone screen, so
97
+ // the page has to keep room for it after its last element — the footer and
98
+ // the Jant credit included, not only the posts. The modifier that keeps the
99
+ // room and the button itself must never render apart.
100
+ it.each([
101
+ {
102
+ name: "an author's home page",
103
+ props: { isAuthenticated: true, currentPath: "/" },
104
+ expected: true,
105
+ },
106
+ {
107
+ name: "an author's collection page",
108
+ props: {
109
+ isAuthenticated: true,
110
+ currentPath: "/notes",
111
+ composeCollectionId: "col_01h455vb4pex5vsknk084sn02q",
112
+ },
113
+ expected: true,
114
+ },
115
+ {
116
+ name: "an author's archive",
117
+ props: { isAuthenticated: true, currentPath: "/archive" },
118
+ expected: false,
119
+ },
120
+ {
121
+ name: "an author's home page without the composer",
122
+ props: {
123
+ isAuthenticated: true,
124
+ currentPath: "/",
125
+ showComposeDialog: false,
126
+ },
127
+ expected: false,
128
+ },
129
+ {
130
+ name: "a reader's home page",
131
+ props: { isAuthenticated: false, currentPath: "/" },
132
+ expected: false,
133
+ },
134
+ ])(
135
+ "keeps room for the compose button exactly when it shows: $name",
136
+ ({ props, expected }) => {
137
+ const html = renderLayout({
138
+ siteFooterHtml: "<p>Footer</p>",
139
+ ...props,
140
+ });
121
141
 
122
- expect(html).not.toContain('class="site-mobile-compose-fab"');
123
- });
142
+ expect(html.includes('class="site-mobile-compose-fab"')).toBe(expected);
143
+ expect(
144
+ html.includes('class="site-page site-page-mobile-compose-enabled"'),
145
+ ).toBe(expected);
146
+ },
147
+ );
124
148
  });