@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
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Link Card
3
3
  *
4
- * Author commentary renders as first-class prose (same level as Note body).
5
- * The link reference (domain, title, preview) sits in a compact card below,
6
- * keeping the author's voice visually distinct from the referenced content.
4
+ * The link reference (domain, title, preview) comes first, then the author's
5
+ * commentary as first-class prose (same level as a Note body) — the reader
6
+ * sees what is being referenced before what is said about it. Feed, compact
7
+ * and detail differ only in heading level, never in that order.
7
8
  */
8
9
 
9
10
  import type { FC } from "hono/jsx";
10
11
  import type { TimelineCardProps } from "../../types.js";
12
+ import { EmptyPostContent } from "../shared/EmptyPostContent.js";
11
13
  import { StarRating } from "../shared/StarRating.js";
12
14
  import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
13
15
  import { PostFooter } from "../shared/PostFooter.js";
@@ -32,6 +34,14 @@ export const LinkCard: FC<TimelineCardProps> = ({
32
34
  const safeUrl = post.url ? sanitizeUrl(post.url) : "";
33
35
  const domain = safeUrl ? extractDisplayDomain(safeUrl) : null;
34
36
 
37
+ /* The h-entry's own `u-url` is the permalink, emitted once by PostFooter.
38
+ The title points at the referenced article instead, which is what
39
+ `u-bookmark-of` means — and only when there is one: without a link the
40
+ href falls back to the permalink, and a post cannot bookmark itself. */
41
+ const titleLinkClass = safeUrl
42
+ ? "u-bookmark-of feed-link-title-link"
43
+ : "feed-link-title-link";
44
+
35
45
  const domainEl =
36
46
  domain &&
37
47
  (safeUrl ? (
@@ -51,6 +61,33 @@ export const LinkCard: FC<TimelineCardProps> = ({
51
61
  </div>
52
62
  ));
53
63
 
64
+ const titleLinkEl = post.title && (
65
+ <a
66
+ href={safeUrl || post.permalink}
67
+ class={titleLinkClass}
68
+ target={safeUrl ? "_blank" : undefined}
69
+ rel={safeUrl ? "noopener noreferrer" : undefined}
70
+ >
71
+ {post.title}
72
+ </a>
73
+ );
74
+
75
+ /* Detail promotes the title to the page heading and carries the rating with
76
+ it; feed and compact keep it a card heading. Only the wrapper differs —
77
+ the link itself, and where the title sits in the reference, do not. */
78
+ const titleEl =
79
+ post.title &&
80
+ (isDetail ? (
81
+ <div class="post-header-block">
82
+ <h1 class="p-name post-detail-title feed-link-title">{titleLinkEl}</h1>
83
+ {showHeaderRating && <StarRating rating={post.rating} />}
84
+ </div>
85
+ ) : (
86
+ <h2 class={`p-name feed-link-title ${isCompact ? "text-sm" : ""}`}>
87
+ {titleLinkEl}
88
+ </h2>
89
+ ));
90
+
54
91
  const previewEl = !isCompact && post.previewImageUrl && (
55
92
  <LinkPreview
56
93
  imageUrl={post.previewImageUrl}
@@ -68,6 +105,11 @@ export const LinkCard: FC<TimelineCardProps> = ({
68
105
  />
69
106
  );
70
107
 
108
+ /* No title and no commentary leaves the entry with neither `p-name` nor
109
+ `e-content`, which is the shape a parser fills in by guessing. The title
110
+ is the `p-name`, so this only ever renders when there is none. */
111
+ const emptyContentEl = !post.title && !bodyEl && <EmptyPostContent />;
112
+
71
113
  const mediaEl = !isCompact && post.media.length > 0 && (
72
114
  <div class="mt-3" data-post-media>
73
115
  <MediaGallery attachments={post.media} postPermalink={post.permalink} />
@@ -78,22 +120,14 @@ export const LinkCard: FC<TimelineCardProps> = ({
78
120
  <StarRating rating={post.rating} />
79
121
  );
80
122
 
81
- /* -- Link reference: domain + title + preview (feed & compact only) -- */
123
+ /* -- Link reference: domain + title + preview, in that order in every mode.
124
+ The preview is part of what is being referenced, so the author's
125
+ commentary always follows it — a detail page that led with the body read
126
+ as a comment on something the reader had not seen yet. -- */
82
127
  const linkRef = (
83
128
  <>
84
129
  {domainEl}
85
- {post.title && (
86
- <h2 class={`p-name feed-link-title ${isCompact ? "text-sm" : ""}`}>
87
- <a
88
- href={safeUrl || post.permalink}
89
- class="u-url feed-link-title-link"
90
- target={safeUrl ? "_blank" : undefined}
91
- rel={safeUrl ? "noopener noreferrer" : undefined}
92
- >
93
- {post.title}
94
- </a>
95
- </h2>
96
- )}
130
+ {titleEl}
97
131
  {previewEl}
98
132
  </>
99
133
  );
@@ -104,39 +138,15 @@ export const LinkCard: FC<TimelineCardProps> = ({
104
138
  {...(isDetail ? { "data-page": "post" } : {})}
105
139
  {...getPostArticleAttributes(post)}
106
140
  >
107
- {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
108
- {isDetail ? (
109
- <>
110
- {domainEl}
111
- {post.title && (
112
- <div class="post-header-block">
113
- <h1 class="p-name post-detail-title feed-link-title">
114
- <a
115
- href={safeUrl || post.permalink}
116
- class="u-url feed-link-title-link"
117
- target={safeUrl ? "_blank" : undefined}
118
- rel={safeUrl ? "noopener noreferrer" : undefined}
119
- >
120
- {post.title}
121
- </a>
122
- </h1>
123
- {showHeaderRating && <StarRating rating={post.rating} />}
124
- </div>
125
- )}
126
- {bodyEl}
127
- {previewEl}
128
- </>
129
- ) : isCompact ? (
130
- linkRef
131
- ) : (
132
- <>
133
- {linkRef}
134
- {bodyEl}
135
- </>
141
+ {!isCompact && !display?.hideStatusBadges && (
142
+ <PostStatusBadges post={post} />
136
143
  )}
144
+ {linkRef}
145
+ {bodyEl}
137
146
  {(() => {
138
147
  const tail = (
139
148
  <>
149
+ {emptyContentEl}
140
150
  {mediaEl}
141
151
  {ratingEl}
142
152
  <PostFooter
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { FC } from "hono/jsx";
9
+ import { getLinkPreviewProviderLabel } from "../../lib/link-preview.js";
9
10
  import { Icon } from "../shared/Icon.js";
10
11
 
11
12
  interface LinkPreviewProps {
@@ -15,12 +16,6 @@ interface LinkPreviewProps {
15
16
  provider?: string;
16
17
  }
17
18
 
18
- const PROVIDER_LABELS: Record<string, string> = {
19
- youtube: "YouTube",
20
- vimeo: "Vimeo",
21
- bilibili: "Bilibili",
22
- };
23
-
24
19
  export const LinkPreview: FC<LinkPreviewProps> = ({
25
20
  imageUrl,
26
21
  linkUrl,
@@ -28,7 +23,7 @@ export const LinkPreview: FC<LinkPreviewProps> = ({
28
23
  provider,
29
24
  }) => {
30
25
  const isVideo = kind === "video";
31
- const providerLabel = provider ? PROVIDER_LABELS[provider] : undefined;
26
+ const providerLabel = getLinkPreviewProviderLabel(provider);
32
27
 
33
28
  return (
34
29
  <a
@@ -9,6 +9,7 @@ import { msg } from "@lingui/core/macro";
9
9
  import type { FC } from "hono/jsx";
10
10
  import { useLingui } from "../../i18n/context.js";
11
11
  import type { TimelineCardProps } from "../../types.js";
12
+ import { EmptyPostContent } from "../shared/EmptyPostContent.js";
12
13
  import { MediaGallery } from "../shared/MediaGallery.js";
13
14
  import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
14
15
  import { StarRating } from "../shared/StarRating.js";
@@ -89,7 +90,9 @@ export const NoteCard: FC<TimelineCardProps> = ({
89
90
  {...(isDetail ? { "data-page": "post" } : {})}
90
91
  {...getPostArticleAttributes(post)}
91
92
  >
92
- {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
93
+ {!isCompact && !display?.hideStatusBadges && (
94
+ <PostStatusBadges post={post} />
95
+ )}
93
96
  {isArticle &&
94
97
  (isDetail ? (
95
98
  <div class="post-header-block post-header-block-detail">
@@ -104,20 +107,22 @@ export const NoteCard: FC<TimelineCardProps> = ({
104
107
  </div>
105
108
  ) : (
106
109
  <h2
107
- class={`p-name ${isCompact ? "text-sm mb-1" : "feed-note-title"}`}
110
+ class={`p-name ${isCompact ? "feed-compact-title text-sm mb-1" : "feed-note-title"}`}
108
111
  >
109
- <a href={post.permalink} class="u-url hover:underline">
112
+ <a href={post.permalink} class="hover:underline">
110
113
  {post.title}
111
114
  </a>
112
115
  </h2>
113
116
  ))}
114
- {displayHtml && (
117
+ {displayHtml ? (
115
118
  <div
116
119
  class={`e-content prose ${isCompact ? "prose-sm" : isDetail || showFullBody ? "post-detail-body" : isArticle ? "post-body-summary" : ""}`}
117
120
  data-post-body
118
121
  {...(clampNote ? { "data-note-clamp": "" } : {})}
119
122
  dangerouslySetInnerHTML={{ __html: displayHtml }}
120
123
  />
124
+ ) : (
125
+ !isArticle && <EmptyPostContent />
121
126
  )}
122
127
  {(() => {
123
128
  const tail = (
@@ -2,30 +2,116 @@
2
2
  * Post Status Badges
3
3
  *
4
4
  * Renders top-of-card status indicators that should stay visually prominent.
5
- * All badges are always rendered in the DOM; visibility is driven by CSS
6
- * selectors on the parent article's data attributes. This lets the post menu
7
- * toggle badges instantly without a page reload. Featured is rendered in the
8
- * footer meta instead.
5
+ * Visibility is driven by CSS selectors on the parent article's data
6
+ * attributes, so for the signed-in author every badge ships in the DOM whether
7
+ * or not it currently applies: the post menu toggles `data-post-pinned` and
8
+ * friends on the article, and the matching badge appears without a reload.
9
+ *
10
+ * An anonymous reader has no menu and cannot change any of that, and never
11
+ * receives a draft or private post to begin with, so the same card would carry
12
+ * four permanently invisible badges. There we emit only the badges that
13
+ * actually apply — usually none at all. Featured is rendered in the footer
14
+ * meta instead.
9
15
  */
10
16
 
17
+ import { msg } from "@lingui/core/macro";
11
18
  import type { FC } from "hono/jsx";
19
+ import { useLingui } from "../../i18n/context.js";
20
+ import { useViewer } from "../../lib/viewer-context.js";
21
+ import type { PostView } from "../../types.js";
12
22
  import { Icon } from "../shared/Icon.js";
13
23
 
14
- export const PostStatusBadges: FC = () => {
24
+ type PostStatusBadgeSource = Pick<
25
+ PostView,
26
+ "status" | "visibility" | "pinned" | "pinnedInCollection"
27
+ >;
28
+
29
+ export const PostStatusBadges: FC<{ post: PostStatusBadgeSource }> = ({
30
+ post,
31
+ }) => {
32
+ const { i18n } = useLingui();
33
+ const { isAuthor } = useViewer();
34
+
35
+ const isPinned = post.pinned;
36
+ const isPinnedInCollection = Boolean(post.pinnedInCollection);
37
+ const isPrivate = post.visibility === "private";
38
+ const isDraft = post.status === "draft";
39
+
40
+ // The author's card keeps every badge so the menu can reveal one instantly.
41
+ // A reader's card carries only what is true right now, and nothing when the
42
+ // post has no status to announce.
43
+ if (
44
+ !isAuthor &&
45
+ !isPinned &&
46
+ !isPinnedInCollection &&
47
+ !isPrivate &&
48
+ !isDraft
49
+ ) {
50
+ return null;
51
+ }
52
+
53
+ const showPinned = isAuthor || isPinned;
54
+ const showPinnedInCollection = isAuthor || isPinnedInCollection;
55
+ const showPrivate = isAuthor || isPrivate;
56
+ const showDraft = isAuthor || isDraft;
57
+
58
+ const pinnedLabel = i18n._(
59
+ msg({
60
+ message: "Pinned",
61
+ comment: "@context: Post status badge for a pinned post",
62
+ }),
63
+ );
64
+
15
65
  return (
16
66
  <div class="post-status-badges">
17
- <span class="post-status-badge post-status-pinned">
18
- <Icon name="post-status-pin" />
19
- Pinned
20
- </span>
21
- <span class="post-status-badge post-status-pinned-in-collection">
22
- <Icon name="post-status-pin" />
23
- Pinned
24
- </span>
25
- <span class="post-status-badge post-status-private">
26
- <Icon name="post-status-private" />
27
- Private
28
- </span>
67
+ {showPinned && (
68
+ <span class="post-status-badge post-status-pinned">
69
+ <Icon name="post-status-pin" />
70
+ {pinnedLabel}
71
+ </span>
72
+ )}
73
+ {showPinnedInCollection && (
74
+ <span class="post-status-badge post-status-pinned-in-collection">
75
+ <Icon name="post-status-pin" />
76
+ {pinnedLabel}
77
+ </span>
78
+ )}
79
+ {showPrivate && (
80
+ <span class="post-status-badge post-status-private">
81
+ <Icon name="post-status-private" />
82
+ {i18n._(
83
+ msg({
84
+ message: "Private",
85
+ comment: "@context: Post status badge for a private post",
86
+ }),
87
+ )}
88
+ </span>
89
+ )}
90
+ {/* The one badge that is also a control: a draft's most likely next
91
+ action is "finish it", so the label itself opens the editor. */}
92
+ {showDraft && (
93
+ <button
94
+ type="button"
95
+ class="post-status-badge post-status-draft"
96
+ data-draft-continue
97
+ title={i18n._(
98
+ msg({
99
+ message: "Continue writing this draft",
100
+ comment:
101
+ "@context: Tooltip on the draft badge, which opens the editor",
102
+ }),
103
+ )}
104
+ >
105
+ <Icon name="post-status-draft" />
106
+ {i18n._(
107
+ msg({
108
+ message: "Draft",
109
+ comment:
110
+ "@context: Post status badge for an unpublished draft, shown to the author only",
111
+ }),
112
+ )}
113
+ </button>
114
+ )}
29
115
  </div>
30
116
  );
31
117
  };
@@ -3,11 +3,15 @@
3
3
  *
4
4
  * Left-border accent blockquote with full date in footer.
5
5
  *
6
- * Fields:
7
- * - quoteText: the quoted text
8
- * - title: attribution (who said it)
9
- * - url: source link
10
- * - bodyHtml: commentary
6
+ * Fields, and the microformats2 property each maps to:
7
+ * - quoteText: the quoted text — the h-cite's `p-content`
8
+ * - title: attribution (who said it) — the h-cite's `p-name`
9
+ * - url: source link — the h-cite's `u-url`
10
+ * - bodyHtml: commentary — the h-entry's `e-content`
11
+ *
12
+ * `p-name` for the attribution is deliberately loose: the field may hold a
13
+ * person, a work, or a site, and a person would strictly be a `p-author` with
14
+ * a nested h-card. `p-name` covers all three without inventing that h-card.
11
15
  */
12
16
 
13
17
  import type { FC } from "hono/jsx";
@@ -17,6 +21,7 @@ import { PostFooter } from "../shared/PostFooter.js";
17
21
  import { PostStatusBadges } from "./PostStatusBadges.js";
18
22
  import { sanitizeUrl, extractDisplayDomain } from "../../lib/url.js";
19
23
  import { DecorativeQuoteMark } from "../shared/DecorativeQuoteMark.js";
24
+ import { EmptyPostContent } from "../shared/EmptyPostContent.js";
20
25
  import { MediaGallery } from "../shared/MediaGallery.js";
21
26
  import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
22
27
 
@@ -30,6 +35,9 @@ export const QuoteCard: FC<TimelineCardProps> = ({
30
35
  const articleClass = `h-entry post-menu-target${isCompact ? " feed-compact" : isDetail ? " py-6" : " feed-quote-post"}`;
31
36
  const safeUrl = post.url ? sanitizeUrl(post.url) : "";
32
37
  const commentaryHtml = post.bodyHtml ?? null;
38
+ const showCite =
39
+ !!post.quoteText || (!isCompact && !!(post.title || safeUrl));
40
+ const showCommentary = !isCompact && !!commentaryHtml;
33
41
 
34
42
  return (
35
43
  <article
@@ -37,36 +45,53 @@ export const QuoteCard: FC<TimelineCardProps> = ({
37
45
  {...(isDetail ? { "data-page": "post" } : {})}
38
46
  {...getPostArticleAttributes(post)}
39
47
  >
40
- {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
41
- {post.quoteText && (
42
- <blockquote class={`feed-quote${isCompact ? "" : " feed-quote-card"}`}>
43
- {!isCompact && <DecorativeQuoteMark class="feed-quote-mark" />}
44
- <div
45
- class={`e-content feed-quote-content${isCompact ? " text-sm" : isDetail ? " post-detail-quote" : ""}`}
46
- >
47
- {post.quoteText}
48
- </div>
49
- </blockquote>
48
+ {!isCompact && !display?.hideStatusBadges && (
49
+ <PostStatusBadges post={post} />
50
50
  )}
51
- {!isCompact && (post.title || safeUrl) && (
52
- <div class="feed-quote-attribution">
53
- {safeUrl ? (
54
- <a
55
- href={safeUrl}
56
- class="feed-quote-source"
57
- target="_blank"
58
- rel="noopener noreferrer"
51
+ {showCite && (
52
+ /* The h-cite spans both the quoted text and its attribution, because
53
+ mf2 properties bind to descendants: a `u-url` on the source link
54
+ left outside would attach to the h-entry and shadow the permalink,
55
+ and `p-name` would become this post's own name. `<figure>` with a
56
+ `<figcaption>` is also how HTML pairs a blockquote with its source. */
57
+ <figure class="feed-quote-cite h-cite">
58
+ {post.quoteText && (
59
+ <blockquote
60
+ class={`feed-quote${isCompact ? "" : " feed-quote-card"}`}
59
61
  >
60
- {post.title || extractDisplayDomain(safeUrl) || safeUrl}
61
- </a>
62
- ) : (
63
- <span>{post.title}</span>
62
+ {!isCompact && <DecorativeQuoteMark class="feed-quote-mark" />}
63
+ <div
64
+ class={`p-content feed-quote-content${isCompact ? " text-sm" : isDetail ? " post-detail-quote" : ""}`}
65
+ >
66
+ {post.quoteText}
67
+ </div>
68
+ </blockquote>
69
+ )}
70
+ {!isCompact && (post.title || safeUrl) && (
71
+ <figcaption class="feed-quote-attribution">
72
+ {safeUrl ? (
73
+ <a
74
+ href={safeUrl}
75
+ class="u-url p-name feed-quote-source"
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ >
79
+ {post.title || extractDisplayDomain(safeUrl) || safeUrl}
80
+ </a>
81
+ ) : (
82
+ <span class="p-name">{post.title}</span>
83
+ )}
84
+ </figcaption>
64
85
  )}
65
- </div>
86
+ </figure>
66
87
  )}
67
- {!isCompact && commentaryHtml && (
88
+ {/* Nothing quoted and nothing said leaves the entry with no `p-name`,
89
+ no `e-content`, and no nested h-cite — the shape a parser fills in by
90
+ guessing. */}
91
+ {!showCite && !showCommentary && <EmptyPostContent />}
92
+ {showCommentary && (
68
93
  <div
69
- class={`feed-quote-commentary prose${isDetail ? " post-detail-body" : " text-muted-foreground"}`}
94
+ class={`e-content feed-quote-commentary prose${isDetail ? " post-detail-body" : " text-muted-foreground"}`}
70
95
  data-post-body
71
96
  dangerouslySetInnerHTML={{ __html: commentaryHtml }}
72
97
  />
@@ -36,6 +36,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
36
36
  leadingReplies,
37
37
  trailingReplies,
38
38
  latestReply,
39
+ gapHref,
39
40
  totalReplyCount,
40
41
  }) => {
41
42
  const { i18n } = useLingui();
@@ -81,7 +82,10 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
81
82
  });
82
83
  const renderedLeadingReplies = dedupeReplies(leadingReplies);
83
84
  const renderedTrailingReplies = dedupeReplies(trailingReplies);
84
- const gapHref = renderedLeadingReplies[0]?.permalink ?? latestReply.permalink;
85
+ // The gap opens the first post it hides — the rule in `lib/thread-fold.ts`,
86
+ // which the feed follows too. The fallback only covers a gap target that
87
+ // went unpublished between the count and the fetch.
88
+ const gapLinkHref = gapHref ?? latestReply.permalink;
85
89
 
86
90
  // Always render the collapsible shell + toggle: the cap and fade are a
87
91
  // constant "this is context" affordance. The toggle's *initial* visibility
@@ -116,7 +120,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
116
120
  const gapItem =
117
121
  hiddenCount > 0 ? (
118
122
  <div class="thread-item thread-item-gap">
119
- <a href={gapHref} class="thread-gap-link">
123
+ <a href={gapLinkHref} class="thread-gap-link">
120
124
  {hiddenPostsLabel}
121
125
  </a>
122
126
  </div>
@@ -1,7 +1,7 @@
1
- import { readFileSync } from "node:fs";
2
1
  import type { Context } from "hono";
3
2
  import { renderToString } from "hono/jsx/dom/server";
4
3
  import { describe, expect, it } from "vitest";
4
+ import { readComponentCss } from "../../../__tests__/helpers/component-css.js";
5
5
  import { I18nProvider } from "../../../i18n/context.js";
6
6
  import { createI18n } from "../../../i18n/i18n.js";
7
7
  import type { PostView, TimelineItemView } from "../../../types.js";
@@ -36,8 +36,7 @@ function createPostView(overrides: Partial<PostView> = {}): PostView {
36
36
 
37
37
  function renderWithI18n(
38
38
  render: () =>
39
- | ReturnType<typeof ThreadPreview>
40
- | ReturnType<typeof CuratedThreadPreview>,
39
+ ReturnType<typeof ThreadPreview> | ReturnType<typeof CuratedThreadPreview>,
41
40
  ) {
42
41
  const i18n = createI18n("en");
43
42
  const c = {
@@ -157,10 +156,7 @@ describe("getThreadPreviewState", () => {
157
156
  });
158
157
 
159
158
  it("keeps thread preview items shrinkable within the grid track", () => {
160
- const css = readFileSync(
161
- new URL("../../../styles/ui.css", import.meta.url),
162
- "utf8",
163
- );
159
+ const css = readComponentCss();
164
160
 
165
161
  expect(css).toMatch(
166
162
  /\.thread-item\s*\{[\s\S]*min-width:\s*0;[\s\S]*max-width:\s*100%;/,
@@ -168,17 +164,14 @@ describe("getThreadPreviewState", () => {
168
164
  });
169
165
 
170
166
  it("adds extra mobile inset before the thread rail reaches the viewport edge", () => {
171
- const css = readFileSync(
172
- new URL("../../../styles/ui.css", import.meta.url),
173
- "utf8",
174
- );
167
+ const css = readComponentCss();
175
168
 
176
169
  expect(css).toMatch(
177
- /@media\s*\(max-width:\s*760px\)\s*\{[\s\S]*\.thread-group-preview,\s*\.thread-group-detail\s*\{[\s\S]*--site-thread-rail-indent:\s*8px;[\s\S]*--site-thread-rail-line-left:\s*-11px;/,
170
+ /@media\s*\(max-width:\s*699px\)\s*\{[\s\S]*\.thread-group-preview,\s*\.thread-group-detail\s*\{[\s\S]*--site-thread-rail-indent:\s*8px;[\s\S]*--site-thread-rail-line-left:\s*-11px;/,
178
171
  );
179
172
  // dot-left is no longer hardcoded — derived via calc() from line-left
180
173
  expect(css).not.toMatch(
181
- /@media\s*\(max-width:\s*760px\)\s*\{[\s\S]*--site-thread-rail-dot-left:/,
174
+ /@media\s*\(max-width:\s*699px\)\s*\{[\s\S]*--site-thread-rail-dot-left:/,
182
175
  );
183
176
  });
184
177
 
@@ -400,7 +393,7 @@ describe("getThreadPreviewState", () => {
400
393
  expect(toggleTag(html)).not.toContain("hidden");
401
394
  });
402
395
 
403
- it("points the hidden-posts gap link to the second reply so the detail page opens just above the hidden range", () => {
396
+ it("points the hidden-posts gap link at the first post it hides", () => {
404
397
  const html = renderWithI18n(() =>
405
398
  ThreadPreview({
406
399
  rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),
@@ -427,16 +420,20 @@ describe("getThreadPreviewState", () => {
427
420
  bodyHtml: "<p>Latest</p>",
428
421
  isLastInThread: true,
429
422
  }),
423
+ gapHref: "/post-3",
430
424
  totalReplyCount: 4,
431
425
  }),
432
426
  );
433
427
 
434
428
  expect(html).toMatch(
435
- /<a[^>]*\bhref="\/post-2"[^>]*\bclass="thread-gap-link"|<a[^>]*\bclass="thread-gap-link"[^>]*\bhref="\/post-2"/,
429
+ /<a[^>]*\bhref="\/post-3"[^>]*\bclass="thread-gap-link"|<a[^>]*\bclass="thread-gap-link"[^>]*\bhref="\/post-3"/,
436
430
  );
437
431
  });
438
432
 
439
- it("falls back to the latest reply for the gap link when there is no second reply", () => {
433
+ // The gap target comes from the service, one rank past the leading window.
434
+ // It can be missing where the count and the fetch disagree — a reply
435
+ // unpublished between them — and the link still has to go somewhere.
436
+ it("falls back to the latest reply when no gap target arrives", () => {
440
437
  const html = renderWithI18n(() =>
441
438
  ThreadPreview({
442
439
  rootPost: createPostView({ bodyHtml: "<p>Root</p>" }),