@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -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 {
@@ -97,7 +98,11 @@ export const CompactCollectionTags: FC<{
97
98
  interface PostPublishedLinkProps {
98
99
  post: Pick<
99
100
  PostView,
100
- "permalink" | "publishedAt" | "publishedAtFormatted" | "publishedAtTime"
101
+ | "permalink"
102
+ | "publishedAt"
103
+ | "publishedAtFormatted"
104
+ | "publishedAtTime"
105
+ | "status"
101
106
  >;
102
107
  className: string;
103
108
  }
@@ -107,17 +112,33 @@ export const PostPublishedLink: FC<PostPublishedLinkProps> = ({
107
112
  className,
108
113
  }) => {
109
114
  const { i18n } = useLingui();
110
- const publishedLabel = i18n._(
111
- msg({
112
- message: "Published on {date} at {time}",
113
- comment:
114
- "@context: Tooltip text for the published timestamp in post metadata",
115
- }),
116
- {
117
- date: post.publishedAtFormatted,
118
- time: post.publishedAtTime,
119
- },
120
- );
115
+ // A draft has never been published; `publishedAt` is standing in for its
116
+ // last edit. The link still matters (it is how you open the post), so keep
117
+ // it and tell the truth about what the date means.
118
+ const publishedLabel =
119
+ post.status === "draft"
120
+ ? i18n._(
121
+ msg({
122
+ message: "Last edited on {date} at {time}",
123
+ comment:
124
+ "@context: Tooltip text for the timestamp on an unpublished draft",
125
+ }),
126
+ {
127
+ date: post.publishedAtFormatted,
128
+ time: post.publishedAtTime,
129
+ },
130
+ )
131
+ : i18n._(
132
+ msg({
133
+ message: "Published on {date} at {time}",
134
+ comment:
135
+ "@context: Tooltip text for the published timestamp in post metadata",
136
+ }),
137
+ {
138
+ date: post.publishedAtFormatted,
139
+ time: post.publishedAtTime,
140
+ },
141
+ );
121
142
 
122
143
  return (
123
144
  <a href={post.permalink} class={className}>
@@ -158,6 +179,7 @@ export const PostMenuTriggerButton: FC<{ className?: string }> = ({
158
179
 
159
180
  export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
160
181
  const { i18n } = useLingui();
182
+ const { isAuthor } = useViewer();
161
183
  const featuredLabel =
162
184
  post.featuredAtFormatted && post.featuredAtTime
163
185
  ? i18n._(
@@ -180,6 +202,9 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
180
202
  );
181
203
  const safeExternalUrl =
182
204
  post.format === "link" && post.url ? sanitizeUrl(post.url) : "";
205
+ // Drafts get an ordinary footer — the Draft badge above already says what
206
+ // they are, and their lifecycle actions live in the post menu next to Edit
207
+ // and Delete rather than in a bespoke button row.
183
208
  const showTimestamp = !display?.hideTimestamp;
184
209
  const hideActions = !!display?.hideActions;
185
210
  const hideReply = !!display?.hideReply;
@@ -195,16 +220,21 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
195
220
  data-post-meta
196
221
  >
197
222
  <div class="post-footer-meta">
198
- <span
199
- class="post-footer-featured"
200
- tabindex={0}
201
- role="img"
202
- aria-label={featuredLabel}
203
- data-tooltip={featuredLabel}
204
- data-align="center"
205
- >
206
- <Icon name="featured-sparkle" />
207
- </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
+ )}
208
238
  {showTimestamp && (
209
239
  <PostPublishedLink post={post} className="u-url post-footer-link" />
210
240
  )}
@@ -231,7 +261,9 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
231
261
  showIcon={detail}
232
262
  />
233
263
  </div>
234
- {!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 && (
235
267
  <div class="post-menu-actions">
236
268
  {showReply && (
237
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
  });