@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
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../../types.js";
15
15
  import { useLingui } from "../../i18n/context.js";
16
16
  import { sanitizeUrl } from "../../lib/url.js";
17
+ import { useViewer } from "../../lib/viewer-context.js";
17
18
  import { Icon } from "./Icon.js";
18
19
 
19
20
  interface PostFooterProps {
@@ -178,6 +179,7 @@ export const PostMenuTriggerButton: FC<{ className?: string }> = ({
178
179
 
179
180
  export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
180
181
  const { i18n } = useLingui();
182
+ const { isAuthor } = useViewer();
181
183
  const featuredLabel =
182
184
  post.featuredAtFormatted && post.featuredAtTime
183
185
  ? i18n._(
@@ -218,16 +220,21 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
218
220
  data-post-meta
219
221
  >
220
222
  <div class="post-footer-meta">
221
- <span
222
- class="post-footer-featured"
223
- tabindex={0}
224
- role="img"
225
- aria-label={featuredLabel}
226
- data-tooltip={featuredLabel}
227
- data-align="center"
228
- >
229
- <Icon name="featured-sparkle" />
230
- </span>
223
+ {/* Shown only under `article[data-post-featured]`, which the post menu
224
+ toggles in place — so the author's card keeps the icon ready and a
225
+ reader's carries it only when the post really is featured. */}
226
+ {(isAuthor || post.featured) && (
227
+ <span
228
+ class="post-footer-featured"
229
+ tabindex={0}
230
+ role="img"
231
+ aria-label={featuredLabel}
232
+ data-tooltip={featuredLabel}
233
+ data-align="center"
234
+ >
235
+ <Icon name="featured-sparkle" />
236
+ </span>
237
+ )}
231
238
  {showTimestamp && (
232
239
  <PostPublishedLink post={post} className="u-url post-footer-link" />
233
240
  )}
@@ -254,7 +261,9 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
254
261
  showIcon={detail}
255
262
  />
256
263
  </div>
257
- {!hideActions && (
264
+ {/* Both triggers are revealed by `body[data-authenticated]` alone, so for
265
+ a reader this whole group is markup nothing can ever surface. */}
266
+ {!hideActions && isAuthor && (
258
267
  <div class="post-menu-actions">
259
268
  {showReply && (
260
269
  <button
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ describeArchiveFilters,
4
+ getArchiveViewTitle,
5
+ hasActiveArchiveFilter,
6
+ } from "../archive-labels.js";
7
+ import { buildCollectionVocabulary } from "../../../lib/filter-dimensions.js";
8
+
9
+ const i18n = {
10
+ _: (descriptor: { message?: string } | string) =>
11
+ typeof descriptor === "string" ? descriptor : (descriptor.message ?? ""),
12
+ };
13
+
14
+ const ctx = {
15
+ collections: buildCollectionVocabulary([
16
+ { id: "col_books", slug: "books", title: "Books" },
17
+ ]),
18
+ };
19
+
20
+ describe("describeArchiveFilters", () => {
21
+ it("returns nothing when no filter is active", () => {
22
+ expect(describeArchiveFilters({}, i18n)).toEqual([]);
23
+ expect(hasActiveArchiveFilter({})).toBe(false);
24
+ });
25
+
26
+ it("leads with the collection, which identifies a view best", () => {
27
+ expect(
28
+ describeArchiveFilters(
29
+ { collection: ["col_books"], format: "quote", year: 2024 },
30
+ i18n,
31
+ ctx,
32
+ ),
33
+ ).toEqual(["Books", "Quotes", "2024"]);
34
+ });
35
+
36
+ it("lets title presence absorb the format it refines", () => {
37
+ expect(
38
+ describeArchiveFilters({ format: "note", title: false }, i18n),
39
+ ).toEqual(["Untitled"]);
40
+ expect(
41
+ describeArchiveFilters({ format: "note", title: true }, i18n),
42
+ ).toEqual(["Titled"]);
43
+ });
44
+
45
+ it("names a single media kind but summarises several", () => {
46
+ expect(describeArchiveFilters({ media: ["image"] }, i18n)).toEqual([
47
+ "Images",
48
+ ]);
49
+ expect(describeArchiveFilters({ media: ["image", "video"] }, i18n)).toEqual(
50
+ ["With media"],
51
+ );
52
+ expect(describeArchiveFilters({ media: "none" }, i18n)).toEqual([
53
+ "Without media",
54
+ ]);
55
+ expect(describeArchiveFilters({ media: "any" }, i18n)).toEqual([
56
+ "With media",
57
+ ]);
58
+ });
59
+
60
+ it("describes the thread and visibility dimensions", () => {
61
+ expect(describeArchiveFilters({ replies: true }, i18n)).toEqual([
62
+ "Threads",
63
+ ]);
64
+ expect(describeArchiveFilters({ replies: false }, i18n)).toEqual([
65
+ "Single posts",
66
+ ]);
67
+ expect(describeArchiveFilters({ visibility: "private" }, i18n)).toEqual([
68
+ "Private",
69
+ ]);
70
+ });
71
+
72
+ it("says nothing about a collection it cannot name", () => {
73
+ // The id resolves to no title here, so the dimension is left undescribed
74
+ // rather than described with a raw id.
75
+ expect(describeArchiveFilters({ collection: ["col_gone"] }, i18n)).toEqual(
76
+ [],
77
+ );
78
+ });
79
+ });
80
+
81
+ describe("getArchiveViewTitle", () => {
82
+ it("names the unfiltered view", () => {
83
+ expect(getArchiveViewTitle({}, i18n)).toBe("All posts");
84
+ });
85
+
86
+ it("stops at two parts, since a tab truncates from the right", () => {
87
+ expect(
88
+ getArchiveViewTitle(
89
+ {
90
+ collection: ["col_books"],
91
+ format: "quote",
92
+ year: 2024,
93
+ replies: true,
94
+ },
95
+ i18n,
96
+ ctx,
97
+ ),
98
+ ).toBe("Books, Quotes");
99
+ });
100
+ });
@@ -1,7 +1,7 @@
1
1
  import { renderToString } from "hono/jsx/dom/server";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import type { MediaView } from "../../../types.js";
4
- import { MediaGallery } from "../MediaGallery.js";
4
+ import { getGalleryRowHeight, MediaGallery } from "../MediaGallery.js";
5
5
 
6
6
  const HASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
7
7
 
@@ -51,7 +51,7 @@ describe("MediaGallery", () => {
51
51
 
52
52
  expect(html).toContain("aspect-ratio:900/1600");
53
53
  expect(html).toMatch(
54
- /width:min\(100%, ?calc\(24rem ?\* ?0\.5625\), ?var\(--layout-content-width\)\)/,
54
+ /width:min\(100%, ?calc\(24rem ?\* ?0\.5625\), ?var\(--layout-reading-width\)\)/,
55
55
  );
56
56
  expect(html).not.toContain("object-contain");
57
57
  });
@@ -140,4 +140,212 @@ describe("MediaGallery", () => {
140
140
  expect(html).not.toContain("media-gallery-nav");
141
141
  expect(html).not.toContain('tabindex="0"');
142
142
  });
143
+
144
+ // Attachments render beside `e-content`, so without these a parser reading
145
+ // the h-entry sees a post with no media. Placement matters as much as
146
+ // presence: mf2 takes a `u-*` value from the element's own attribute, so a
147
+ // mark on the wrong element publishes the wrong URL.
148
+ describe("microformats", () => {
149
+ it("marks the image thumbnail, not the link to the original", () => {
150
+ const html = renderToString(
151
+ MediaGallery({
152
+ attachments: [
153
+ createMediaView({
154
+ width: 1600,
155
+ height: 900,
156
+ altText: "A harbour at dusk",
157
+ }),
158
+ ],
159
+ }),
160
+ );
161
+
162
+ expect(html).toMatch(
163
+ /<img[^>]*src="\/media\/thumb\.jpg"[^>]*class="u-photo /,
164
+ );
165
+ expect(html).toContain('alt="A harbour at dusk"');
166
+ // The anchor holds the full-size original — a click target, not the
167
+ // post's photo.
168
+ expect(html).not.toMatch(/<a[^>]*class="u-photo/);
169
+ });
170
+
171
+ it("marks a video by its file URL and its poster by the poster frame", () => {
172
+ const html = renderToString(
173
+ MediaGallery({
174
+ attachments: [
175
+ createMediaView({
176
+ id: "media-video",
177
+ url: "/media/clip.mp4",
178
+ mimeType: "video/mp4",
179
+ posterUrl: "/media/clip.jpg",
180
+ width: 1280,
181
+ height: 720,
182
+ durationSeconds: 120,
183
+ }),
184
+ ],
185
+ }),
186
+ );
187
+
188
+ expect(html).toMatch(
189
+ /<a[^>]*href="\/media\/clip\.mp4"[^>]*class="u-video/,
190
+ );
191
+ expect(html).toMatch(/<video[^>]*class="u-photo /);
192
+ });
193
+
194
+ it("leaves a posterless video unmarked as a photo", () => {
195
+ // The fallback poster is a blurhash data URL. Publishing one as
196
+ // `u-photo` would hand a consumer a smear instead of a picture.
197
+ const html = renderToString(
198
+ MediaGallery({
199
+ attachments: [
200
+ createMediaView({
201
+ id: "media-video",
202
+ url: "/media/clip.mp4",
203
+ mimeType: "video/mp4",
204
+ blurhash: HASH,
205
+ width: 1280,
206
+ height: 720,
207
+ durationSeconds: 120,
208
+ }),
209
+ ],
210
+ }),
211
+ );
212
+
213
+ expect(html).toContain("u-video");
214
+ expect(html).not.toContain("u-photo");
215
+ });
216
+
217
+ it("marks audio on the source element that carries the URL", () => {
218
+ const html = renderToString(
219
+ MediaGallery({
220
+ attachments: [
221
+ createMediaView({
222
+ id: "media-audio",
223
+ url: "/media/track.mp3",
224
+ mimeType: "audio/mpeg",
225
+ }),
226
+ ],
227
+ }),
228
+ );
229
+
230
+ // `<audio>` has no `src` of its own here, so the mark belongs on
231
+ // `<source>` or it would resolve to nothing.
232
+ expect(html).toMatch(
233
+ /<source[^>]*class="u-audio"[^>]*src="\/media\/track\.mp3"/,
234
+ );
235
+ });
236
+
237
+ it("leaves documents and attached texts unmarked", () => {
238
+ const html = renderToString(
239
+ MediaGallery({
240
+ attachments: [
241
+ createMediaView({
242
+ id: "media-doc",
243
+ url: "/media/report.pdf",
244
+ mimeType: "application/pdf",
245
+ originalName: "report.pdf",
246
+ }),
247
+ createMediaView({
248
+ id: "media-text",
249
+ url: "/media/notes.md",
250
+ mimeType: "text/markdown",
251
+ summary: "Field notes",
252
+ }),
253
+ ],
254
+ }),
255
+ );
256
+
257
+ expect(html).not.toContain("u-photo");
258
+ expect(html).not.toContain("u-video");
259
+ expect(html).not.toContain("u-audio");
260
+ });
261
+ });
262
+ });
263
+
264
+ describe("getGalleryRowHeight", () => {
265
+ it("holds the widest item to the strip budget instead of the opener's wish", () => {
266
+ // A portrait screenshot asks for 320 / 0.46 = 692px of height. Granting it
267
+ // would make the 16:9 video beside it 1231px wide — wider than the whole
268
+ // content column. The 520px per-item budget decides instead.
269
+ expect(getGalleryRowHeight([1170 / 2532, 16 / 9])).toBe(293);
270
+ });
271
+
272
+ it("lets a portrait opener run to the cap when nothing after it is wide", () => {
273
+ expect(getGalleryRowHeight([1170 / 2532, 900 / 1600])).toBe(384);
274
+ });
275
+
276
+ it("never exceeds the 24rem height a lone visual already gets", () => {
277
+ // Otherwise a second attachment would make the first picture grow.
278
+ expect(getGalleryRowHeight([9 / 16, 9 / 16, 9 / 16])).toBe(384);
279
+ });
280
+
281
+ it("floors an all-landscape strip rather than letting it go squat", () => {
282
+ expect(getGalleryRowHeight([16 / 9, 16 / 9])).toBe(240);
283
+ });
284
+
285
+ it("falls back to the floor when the strip carries no visuals", () => {
286
+ expect(getGalleryRowHeight([])).toBe(240);
287
+ });
288
+ });
289
+
290
+ describe("MediaGallery row geometry", () => {
291
+ const portrait = createMediaView({ id: "m-1", width: 1170, height: 2532 });
292
+ const video = createMediaView({
293
+ id: "m-2",
294
+ url: "/media/clip.mp4",
295
+ thumbnailUrl: "/media/clip.mp4",
296
+ mimeType: "video/mp4",
297
+ width: 1920,
298
+ height: 1080,
299
+ });
300
+ const text = createMediaView({
301
+ id: "m-3",
302
+ mimeType: "text/html; charset=utf-8",
303
+ summary: "haha hahah",
304
+ chars: 10,
305
+ });
306
+
307
+ it("sizes pictures the same whether or not a text file rides along", () => {
308
+ const withoutText = renderToString(
309
+ MediaGallery({ attachments: [portrait, video] }),
310
+ );
311
+ const withText = renderToString(
312
+ MediaGallery({ attachments: [portrait, video, text] }),
313
+ );
314
+
315
+ expect(withoutText).toContain("height:293px");
316
+ expect(withText).toContain("height:293px");
317
+ // ...and the video stays inside its 520px budget in both.
318
+ expect(withoutText).toContain("width:521px");
319
+ expect(withText).toContain("width:521px");
320
+ });
321
+
322
+ it("crops a clip in a row like the pictures beside it, and letterboxes a lone one", () => {
323
+ // `.media-video-wrap video` in site-media.css sets no fit, so the markup
324
+ // is the only place it is decided — for a clip that autoplays too.
325
+ const shortClip = createMediaView({
326
+ ...video,
327
+ id: "m-4",
328
+ durationSeconds: 8,
329
+ size: 2_000_000,
330
+ });
331
+
332
+ for (const clip of [video, shortClip]) {
333
+ const inRow = renderToString(
334
+ MediaGallery({ attachments: [portrait, clip] }),
335
+ );
336
+ const alone = renderToString(MediaGallery({ attachments: [clip] }));
337
+
338
+ expect(inRow).toMatch(/<video[^>]*class="[^"]*\bobject-cover\b/);
339
+ expect(alone).toMatch(/<video[^>]*class="[^"]*\bobject-contain\b/);
340
+ }
341
+ });
342
+
343
+ it("sizes non-visual cards from the row height, 3:4 portrait", () => {
344
+ const html = renderToString(
345
+ MediaGallery({ attachments: [portrait, video, text] }),
346
+ );
347
+
348
+ // 293 * 0.75, rounded.
349
+ expect(html).toContain("width:220px;height:293px");
350
+ });
143
351
  });
@@ -1,8 +1,8 @@
1
1
  import type { I18n, MessageDescriptor } from "@lingui/core";
2
2
  import { describe, expect, it } from "vitest";
3
+ import { SYSTEM_NAV_KEY_VALUES } from "../../../types.js";
3
4
  import {
4
5
  getNavItemDisplayLabel,
5
- getSystemNavDescription,
6
6
  getSystemNavDisplayLabel,
7
7
  } from "../navigation-labels.js";
8
8
 
@@ -13,6 +13,17 @@ const i18n = {
13
13
  } satisfies Pick<I18n, "_">;
14
14
 
15
15
  describe("getNavItemDisplayLabel", () => {
16
+ it("gives every system key a default label", () => {
17
+ for (const key of SYSTEM_NAV_KEY_VALUES) {
18
+ const label = getNavItemDisplayLabel(
19
+ { type: "system", systemKey: key, label: "", url: `/${key}` },
20
+ i18n,
21
+ );
22
+
23
+ expect(label, `system key "${key}" has no default label`).not.toBe("");
24
+ }
25
+ });
26
+
16
27
  it("translates system items with empty label (default)", () => {
17
28
  expect(
18
29
  getNavItemDisplayLabel(
@@ -48,7 +59,7 @@ describe("getNavItemDisplayLabel", () => {
48
59
  { type: "system", systemKey: "rss", label: "", url: "/feed" },
49
60
  i18n,
50
61
  ),
51
- ).toBe("translated:Feed");
62
+ ).toBe("translated:RSS");
52
63
  });
53
64
 
54
65
  it("translates prefixed public archive system items", () => {
@@ -63,7 +74,7 @@ describe("getNavItemDisplayLabel", () => {
63
74
  i18n,
64
75
  "/blog",
65
76
  ),
66
- ).toBe("translated:Archive");
77
+ ).toBe("translated:All");
67
78
  });
68
79
 
69
80
  it("uses custom label for system item when label is non-empty", () => {
@@ -131,24 +142,7 @@ describe("system nav labels", () => {
131
142
  expect(getSystemNavDisplayLabel("collections", i18n)).toBe(
132
143
  "translated:Collections",
133
144
  );
134
- expect(getSystemNavDisplayLabel("archive", i18n)).toBe(
135
- "translated:Archive",
136
- );
137
- expect(getSystemNavDisplayLabel("rss", i18n)).toBe("translated:Feed");
138
- });
139
-
140
- it("translates system nav descriptions", () => {
141
- expect(getSystemNavDescription("latest", i18n)).toBe(
142
- "translated:Link to your latest posts. Your homepage shows this feed.",
143
- );
144
- expect(getSystemNavDescription("featured", i18n)).toBe(
145
- "translated:Link to posts you've marked as featured.",
146
- );
147
- expect(getSystemNavDescription("archive", i18n)).toBe(
148
- "translated:Link to the post archive",
149
- );
150
- expect(getSystemNavDescription("rss", i18n)).toBe(
151
- "translated:Add a link to your main RSS feed. Change what /feed returns in General.",
152
- );
145
+ expect(getSystemNavDisplayLabel("archive", i18n)).toBe("translated:All");
146
+ expect(getSystemNavDisplayLabel("rss", i18n)).toBe("translated:RSS");
153
147
  });
154
148
  });
@@ -8,40 +8,54 @@ describe("getPageNumbers", () => {
8
8
  expect(getPageNumbers(3, 7)).toEqual([1, 2, 3, 4, 5, 6, 7]);
9
9
  });
10
10
 
11
- it("shows ellipsis for gaps in large page ranges", () => {
12
- // Page 1 of 20: 1, 2, ..., 20
13
- const result = getPageNumbers(1, 20);
14
- expect(result).toEqual([1, 2, 0, 20]);
11
+ it("shows the first five pages near the start", () => {
12
+ // Page 1 of 19: 1, 2, 3, 4, 5, ..., 19
13
+ expect(getPageNumbers(1, 19)).toEqual([1, 2, 3, 4, 5, 0, 19]);
14
+ expect(getPageNumbers(2, 19)).toEqual([1, 2, 3, 4, 5, 0, 19]);
15
+ // Page 4 shows page 2 rather than an ellipsis standing in for it alone
16
+ expect(getPageNumbers(4, 19)).toEqual([1, 2, 3, 4, 5, 0, 19]);
15
17
  });
16
18
 
17
19
  it("shows ellipsis on both sides for middle pages", () => {
18
- // Page 10 of 20: 1, ..., 9, 10, 11, ..., 20
19
- const result = getPageNumbers(10, 20);
20
- expect(result).toEqual([1, 0, 9, 10, 11, 0, 20]);
20
+ // Page 10 of 19: 1, ..., 9, 10, 11, ..., 19
21
+ expect(getPageNumbers(5, 19)).toEqual([1, 0, 4, 5, 6, 0, 19]);
22
+ expect(getPageNumbers(10, 19)).toEqual([1, 0, 9, 10, 11, 0, 19]);
23
+ expect(getPageNumbers(15, 19)).toEqual([1, 0, 14, 15, 16, 0, 19]);
21
24
  });
22
25
 
23
- it("shows ellipsis only on right for early pages", () => {
24
- // Page 3 of 20: 1, 2, 3, 4, ..., 20
25
- const result = getPageNumbers(3, 20);
26
- expect(result).toEqual([1, 2, 3, 4, 0, 20]);
26
+ it("shows the last five pages near the end", () => {
27
+ // Page 19 of 19: 1, ..., 15, 16, 17, 18, 19
28
+ expect(getPageNumbers(16, 19)).toEqual([1, 0, 15, 16, 17, 18, 19]);
29
+ expect(getPageNumbers(18, 19)).toEqual([1, 0, 15, 16, 17, 18, 19]);
30
+ expect(getPageNumbers(19, 19)).toEqual([1, 0, 15, 16, 17, 18, 19]);
27
31
  });
28
32
 
29
- it("shows ellipsis only on left for late pages", () => {
30
- // Page 18 of 20: 1, ..., 17, 18, 19, 20
31
- const result = getPageNumbers(18, 20);
32
- expect(result).toEqual([1, 0, 17, 18, 19, 20]);
33
+ it("handles the smallest range that needs an ellipsis", () => {
34
+ expect(getPageNumbers(4, 8)).toEqual([1, 2, 3, 4, 5, 0, 8]);
35
+ expect(getPageNumbers(5, 8)).toEqual([1, 0, 4, 5, 6, 7, 8]);
33
36
  });
34
37
 
35
- it("handles last page", () => {
36
- // Page 20 of 20: 1, ..., 19, 20
37
- const result = getPageNumbers(20, 20);
38
- expect(result).toEqual([1, 0, 19, 20]);
39
- });
38
+ it("keeps seven slots and never hides a single page behind an ellipsis", () => {
39
+ for (const totalPages of [8, 9, 19, 120]) {
40
+ for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
41
+ const pages = getPageNumbers(currentPage, totalPages);
42
+ const shown = pages.filter((page) => page !== 0);
40
43
 
41
- it("handles page 2 of large range", () => {
42
- // Page 2 of 20: 1, 2, 3, ..., 20
43
- const result = getPageNumbers(2, 20);
44
- expect(result).toEqual([1, 2, 3, 0, 20]);
44
+ expect(pages).toHaveLength(7);
45
+ expect(shown[0]).toBe(1);
46
+ expect(shown[shown.length - 1]).toBe(totalPages);
47
+ expect(shown).toContain(currentPage);
48
+ pages.forEach((page, i) => {
49
+ const prev = pages[i - 1] ?? Number.NaN;
50
+ if (page === 0) {
51
+ const next = pages[i + 1] ?? Number.NaN;
52
+ expect(next - prev).toBeGreaterThanOrEqual(3);
53
+ } else if (i > 0 && prev !== 0) {
54
+ expect(page).toBe(prev + 1);
55
+ }
56
+ });
57
+ }
58
+ }
45
59
  });
46
60
  });
47
61
 
@@ -49,11 +49,13 @@ function renderPostFooter(
49
49
  detail = false,
50
50
  locale: "en" = "en",
51
51
  display?: PostFooterDisplayOptions,
52
+ isAuthenticated = true,
52
53
  ): string {
53
54
  const i18n = createI18n(locale);
54
55
  const c = {
55
56
  get(key: string) {
56
57
  if (key === "i18n") return i18n;
58
+ if (key === "isAuthenticated") return isAuthenticated;
57
59
  return undefined;
58
60
  },
59
61
  } as unknown as Context;
@@ -159,4 +161,34 @@ describe("PostFooter", () => {
159
161
  expect(html).not.toContain("data-reply-trigger");
160
162
  expect(html).toContain("data-post-menu-trigger");
161
163
  });
164
+
165
+ it("omits the action group for a reader, who can never see it", () => {
166
+ const html = renderPostFooter(
167
+ createPostView(),
168
+ false,
169
+ "en",
170
+ undefined,
171
+ false,
172
+ );
173
+
174
+ expect(html).not.toContain("data-post-menu-trigger");
175
+ expect(html).not.toContain("data-reply-trigger");
176
+ expect(html).not.toContain("post-menu-actions");
177
+ // The reader still gets the footer itself.
178
+ expect(html).toContain('href="/hello-world"');
179
+ });
180
+
181
+ it("keeps the featured mark for the author and drops it for a reader", () => {
182
+ const featured = createPostView({ featured: true });
183
+
184
+ expect(renderPostFooter(createPostView())).toContain(
185
+ "post-footer-featured",
186
+ );
187
+ expect(
188
+ renderPostFooter(createPostView(), false, "en", undefined, false),
189
+ ).not.toContain("post-footer-featured");
190
+ expect(renderPostFooter(featured, false, "en", undefined, false)).toContain(
191
+ "post-footer-featured",
192
+ );
193
+ });
162
194
  });