@jant/core 0.6.16 → 0.7.0

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