@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
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import type { Context } from "hono";
3
3
  import { renderToString } from "hono/jsx/dom/server";
4
4
  import { describe, expect, it } from "vitest";
5
+ import { readComponentCss } from "../../../__tests__/helpers/component-css.js";
5
6
  import type { MediaView, PostView } from "../../../types.js";
6
7
  import { I18nProvider } from "../../../i18n/context.js";
7
8
  import { createI18n } from "../../../i18n/i18n.js";
@@ -50,11 +51,13 @@ function renderWithI18n(
50
51
  | ReturnType<typeof NoteCard>
51
52
  | ReturnType<typeof LinkCard>
52
53
  | ReturnType<typeof QuoteCard>,
54
+ isAuthenticated = true,
53
55
  ) {
54
56
  const i18n = createI18n("en");
55
57
  const c = {
56
58
  get(key: string) {
57
59
  if (key === "i18n") return i18n;
60
+ if (key === "isAuthenticated") return isAuthenticated;
58
61
  return undefined;
59
62
  },
60
63
  } as unknown as Context;
@@ -91,6 +94,274 @@ describe("timeline cards", () => {
91
94
  expect(detailHtml).not.toContain("feed-card");
92
95
  });
93
96
 
97
+ it("keeps the link preview above the commentary in every mode", () => {
98
+ const post = createPostView({
99
+ format: "link",
100
+ previewImageUrl: "https://example.com/preview.jpg",
101
+ previewKind: "video",
102
+ previewProvider: "youtube",
103
+ });
104
+
105
+ for (const mode of ["feed", "detail"] as const) {
106
+ const html = renderWithI18n(LinkCard({ post, mode }));
107
+
108
+ // The preview is part of the reference, so commentary follows it —
109
+ // detail used to invert this and read as a note about nothing.
110
+ expect(html.indexOf('class="link-preview"')).toBeGreaterThan(-1);
111
+ expect(html.indexOf('class="link-preview"')).toBeLessThan(
112
+ html.indexOf("data-post-body"),
113
+ );
114
+ expect(html.indexOf("feed-link-title")).toBeLessThan(
115
+ html.indexOf('class="link-preview"'),
116
+ );
117
+ }
118
+ });
119
+
120
+ it("marks a link post's referenced article as u-bookmark-of, never as the entry's own u-url", () => {
121
+ const post = createPostView({ format: "link" });
122
+
123
+ const feedHtml = renderWithI18n(LinkCard({ post, mode: "feed" }));
124
+ const compactHtml = renderWithI18n(LinkCard({ post, mode: "compact" }));
125
+ const detailHtml = renderWithI18n(LinkCard({ post, mode: "detail" }));
126
+
127
+ for (const html of [feedHtml, compactHtml, detailHtml]) {
128
+ expect(html).toContain(
129
+ '<a href="https://example.com/article" class="u-bookmark-of feed-link-title-link"',
130
+ );
131
+ }
132
+
133
+ // The entry's own URL is the permalink, and it is emitted exactly once —
134
+ // a second `u-url` on the referenced article would make parsers read that
135
+ // article as this post.
136
+ expect(feedHtml.match(/u-url/g)).toHaveLength(1);
137
+ expect(detailHtml.match(/u-url/g)).toHaveLength(1);
138
+ expect(feedHtml).toContain('class="u-url post-footer-link"');
139
+ expect(detailHtml).toContain('class="u-url post-footer-link"');
140
+ });
141
+
142
+ it("omits u-bookmark-of when a link post has no referenced article", () => {
143
+ const post = createPostView({ format: "link", url: undefined });
144
+
145
+ const feedHtml = renderWithI18n(LinkCard({ post, mode: "feed" }));
146
+ const detailHtml = renderWithI18n(LinkCard({ post, mode: "detail" }));
147
+
148
+ // The title falls back to the permalink; a post does not bookmark itself.
149
+ for (const html of [feedHtml, detailHtml]) {
150
+ expect(html).toContain('<a href="/post-1" class="feed-link-title-link"');
151
+ expect(html).not.toContain("u-bookmark-of");
152
+ expect(html.match(/u-url/g)).toHaveLength(1);
153
+ }
154
+ });
155
+
156
+ it("scopes a quote post's citation to an h-cite and keeps e-content on the commentary", () => {
157
+ const post = createPostView({ format: "quote" });
158
+
159
+ const feedHtml = renderWithI18n(QuoteCard({ post, mode: "feed" }));
160
+ const detailHtml = renderWithI18n(QuoteCard({ post, mode: "detail" }));
161
+
162
+ for (const html of [feedHtml, detailHtml]) {
163
+ expect(html).toContain('<figure class="feed-quote-cite h-cite">');
164
+ // The quotation is the citation's content, not the entry's.
165
+ expect(html).toContain('class="p-content feed-quote-content');
166
+ expect(html).not.toContain("e-content feed-quote-content");
167
+ expect(html).toContain('class="e-content feed-quote-commentary prose');
168
+
169
+ // Source URL and attribution belong to the cited work, so they have to
170
+ // sit inside the h-cite — outside it they would land on the h-entry.
171
+ const cite = html.slice(
172
+ html.indexOf('<figure class="feed-quote-cite h-cite">'),
173
+ html.indexOf("</figure>"),
174
+ );
175
+ expect(cite).toContain(
176
+ '<a href="https://example.com/article" class="u-url p-name feed-quote-source"',
177
+ );
178
+ expect(cite).toContain('<figcaption class="feed-quote-attribution">');
179
+ }
180
+ });
181
+
182
+ it("keeps a quote attribution without a source link addressable as p-name", () => {
183
+ const post = createPostView({ format: "quote", url: undefined });
184
+
185
+ const feedHtml = renderWithI18n(QuoteCard({ post, mode: "feed" }));
186
+
187
+ expect(feedHtml).toContain('<span class="p-name">Card title</span>');
188
+ expect(feedHtml.match(/u-url/g)).toHaveLength(1);
189
+ });
190
+
191
+ it("leaves note card microformats untouched", () => {
192
+ const post = createPostView({ format: "note" });
193
+
194
+ const feedHtml = renderWithI18n(NoteCard({ post, mode: "feed" }));
195
+ const detailHtml = renderWithI18n(NoteCard({ post, mode: "detail" }));
196
+
197
+ expect(feedHtml).toContain('<h2 class="p-name feed-note-title">');
198
+ expect(feedHtml).toContain('<a href="/post-1" class="hover:underline"');
199
+ expect(feedHtml).toContain('class="e-content prose');
200
+ expect(feedHtml).not.toContain("u-bookmark-of");
201
+ expect(feedHtml).not.toContain("h-cite");
202
+
203
+ expect(detailHtml).toContain('<h1 class="p-name post-detail-title">');
204
+ expect(detailHtml).toContain('class="u-url post-header-meta-link"');
205
+ expect(detailHtml).toContain('class="e-content prose post-detail-body"');
206
+ expect(detailHtml).not.toContain("u-bookmark-of");
207
+ expect(detailHtml).not.toContain("h-cite");
208
+ });
209
+
210
+ it("gives every card exactly one u-url, pointing at its permalink", () => {
211
+ // A titled note's heading used to link the permalink as a second `u-url`.
212
+ // Same value as the footer's, so no parser read the wrong post — but the
213
+ // entry's URL is one fact, and the footer is where all three cards state it.
214
+ const cards = [
215
+ NoteCard({ post: createPostView({ format: "note" }), mode: "feed" }),
216
+ NoteCard({ post: createPostView({ format: "note" }), mode: "detail" }),
217
+ NoteCard({
218
+ post: createPostView({ format: "note", title: undefined }),
219
+ mode: "feed",
220
+ }),
221
+ LinkCard({ post: createPostView({ format: "link" }), mode: "feed" }),
222
+ LinkCard({ post: createPostView({ format: "link" }), mode: "detail" }),
223
+ QuoteCard({ post: createPostView({ format: "quote" }), mode: "feed" }),
224
+ QuoteCard({ post: createPostView({ format: "quote" }), mode: "detail" }),
225
+ ];
226
+
227
+ for (const card of cards) {
228
+ // A quote's source link is a `u-url` too, but it belongs to the nested
229
+ // h-cite. Drop the citation before counting what the entry claims.
230
+ const entry = renderWithI18n(card).replace(
231
+ /<figure class="feed-quote-cite h-cite">[\s\S]*?<\/figure>/,
232
+ "",
233
+ );
234
+ expect(entry.match(/u-url/g)).toHaveLength(1);
235
+ expect(entry).toMatch(/<a href="\/post-1" class="u-url /);
236
+ }
237
+ });
238
+
239
+ it("states empty content outright when a card has no name and nothing to say", () => {
240
+ // The author's card carries every status badge in the DOM, hidden with
241
+ // CSS, which a microformats parser does not run. With no `p-name` and no
242
+ // `e-content` to go on, it names the entry after whatever text it finds —
243
+ // the badge labels. An explicit empty `e-content` says what is true and
244
+ // stops that.
245
+ const empty = { title: undefined, bodyHtml: undefined, media: [] };
246
+
247
+ for (const mode of ["feed", "detail", "compact"] as const) {
248
+ const cards = {
249
+ note: NoteCard({
250
+ post: createPostView({ format: "note", ...empty }),
251
+ mode,
252
+ }),
253
+ link: LinkCard({
254
+ post: createPostView({ format: "link", ...empty }),
255
+ mode,
256
+ }),
257
+ quote: QuoteCard({
258
+ post: createPostView({
259
+ format: "quote",
260
+ ...empty,
261
+ quoteText: undefined,
262
+ url: undefined,
263
+ }),
264
+ mode,
265
+ }),
266
+ };
267
+ for (const [format, card] of Object.entries(cards)) {
268
+ const html = renderWithI18n(card);
269
+ expect(`${format}/${mode}: ${html}`).toContain(
270
+ '<div class="e-content"></div>',
271
+ );
272
+ }
273
+ }
274
+ });
275
+
276
+ it("keeps stray h-* utilities out of post markup", () => {
277
+ // A microformats2 parser reads every `h-`-prefixed class as a microformat
278
+ // root, so a Tailwind height utility inside a card becomes a phantom child
279
+ // microformat on the entry. Only real roots may appear.
280
+ const withMedia = createPostView({
281
+ format: "note",
282
+ media: [createMediaView(), createMediaView({ id: "media-2" })],
283
+ });
284
+ const cards = {
285
+ "note with one image": NoteCard({
286
+ post: { ...withMedia, media: [createMediaView()] },
287
+ mode: "feed",
288
+ }),
289
+ "note with a gallery": NoteCard({ post: withMedia, mode: "feed" }),
290
+ "link post": LinkCard({
291
+ post: createPostView({ format: "link" }),
292
+ mode: "detail",
293
+ }),
294
+ "quote post": QuoteCard({
295
+ post: createPostView({ format: "quote" }),
296
+ mode: "feed",
297
+ }),
298
+ };
299
+
300
+ for (const [label, card] of Object.entries(cards)) {
301
+ const html = renderWithI18n(card);
302
+ const roots = [...html.matchAll(/class="([^"]*)"/g)]
303
+ .flatMap((m) => m[1].split(/\s+/))
304
+ .filter((c) => /^h-/.test(c));
305
+ const unexpected = roots.filter((c) => c !== "h-entry" && c !== "h-cite");
306
+ expect(`${label}: ${unexpected.join(", ")}`).toBe(`${label}: `);
307
+ }
308
+ });
309
+
310
+ it("omits the empty content marker when the card carries a name or content", () => {
311
+ const cards = {
312
+ "titled note": NoteCard({
313
+ post: createPostView({ format: "note", bodyHtml: undefined }),
314
+ mode: "feed",
315
+ }),
316
+ "untitled note with a body": NoteCard({
317
+ post: createPostView({ format: "note", title: undefined }),
318
+ mode: "feed",
319
+ }),
320
+ "titled link": LinkCard({
321
+ post: createPostView({ format: "link", bodyHtml: undefined }),
322
+ mode: "feed",
323
+ }),
324
+ // A quotation is a nested h-cite, which is enough on its own.
325
+ "quote without commentary": QuoteCard({
326
+ post: createPostView({ format: "quote", bodyHtml: undefined }),
327
+ mode: "feed",
328
+ }),
329
+ };
330
+
331
+ for (const [label, card] of Object.entries(cards)) {
332
+ const html = renderWithI18n(card);
333
+ expect(`${label}: ${html}`).not.toContain(
334
+ '<div class="e-content"></div>',
335
+ );
336
+ }
337
+ });
338
+
339
+ it("styles post titles by their own classes, never by their microformats class", () => {
340
+ const css = readComponentCss();
341
+
342
+ // `.p-name` is parsed API; styling it would silently retypeset any element
343
+ // that later needs the property — the quote attribution, for one.
344
+ expect(css).not.toMatch(/^\s*\.p-name\s*\{/m);
345
+ expect(css).toMatch(
346
+ /\.post-detail-title,\s*\.feed-compact-title\s*\{[^}]*font-family:\s*var\(--font-heading\);/,
347
+ );
348
+ // Compact note titles have no other class carrying the heading face.
349
+ const compactHtml = renderWithI18n(
350
+ NoteCard({ post: createPostView({ format: "note" }), mode: "compact" }),
351
+ );
352
+ expect(compactHtml).toContain("feed-compact-title");
353
+ });
354
+
355
+ it("keeps embedded quotes on the host panel's type scale", () => {
356
+ const css = readComponentCss();
357
+
358
+ // The quote body dropped `.e-content`, which is how this rule used to
359
+ // reach it; without the explicit class it would out-size the panel.
360
+ expect(css).toMatch(
361
+ /\.compose-reply-context-body\s*:is\(\s*\.prose,\s*\.e-content,\s*\.feed-quote,\s*\.feed-quote-content\s*\)\s*\{[^}]*font-size:\s*inherit;/,
362
+ );
363
+ });
364
+
94
365
  it("renders quote attachments in feed and detail modes", () => {
95
366
  const post = createPostView({ format: "quote" });
96
367
 
@@ -108,10 +379,7 @@ describe("timeline cards", () => {
108
379
  new URL("../../../styles/tokens.css", import.meta.url),
109
380
  "utf8",
110
381
  );
111
- const css = readFileSync(
112
- new URL("../../../styles/ui.css", import.meta.url),
113
- "utf8",
114
- );
382
+ const css = readComponentCss();
115
383
  const exportCss = readFileSync(
116
384
  new URL(
117
385
  "../../../services/export-theme/styles/main.css",
@@ -171,10 +439,7 @@ describe("timeline cards", () => {
171
439
  new URL("../../../preset.css", import.meta.url),
172
440
  "utf8",
173
441
  );
174
- const uiCss = readFileSync(
175
- new URL("../../../styles/ui.css", import.meta.url),
176
- "utf8",
177
- );
442
+ const uiCss = readComponentCss();
178
443
  const exportCss = readFileSync(
179
444
  new URL(
180
445
  "../../../services/export-theme/styles/main.css",
@@ -291,10 +556,7 @@ describe("timeline cards", () => {
291
556
  new URL("../../../preset.css", import.meta.url),
292
557
  "utf8",
293
558
  );
294
- const uiCss = readFileSync(
295
- new URL("../../../styles/ui.css", import.meta.url),
296
- "utf8",
297
- );
559
+ const uiCss = readComponentCss();
298
560
  const exportCss = readFileSync(
299
561
  new URL(
300
562
  "../../../services/export-theme/styles/main.css",
@@ -346,10 +608,7 @@ describe("timeline cards", () => {
346
608
  });
347
609
 
348
610
  it("keeps quote footers on the shared card spacing baseline", () => {
349
- const css = readFileSync(
350
- new URL("../../../styles/ui.css", import.meta.url),
351
- "utf8",
352
- );
611
+ const css = readComponentCss();
353
612
 
354
613
  expect(css).toContain(".feed-quote-post [data-post-meta] {");
355
614
  expect(css).not.toContain(
@@ -358,10 +617,7 @@ describe("timeline cards", () => {
358
617
  });
359
618
 
360
619
  it("uses inset-note styling for compose editor blockquotes", () => {
361
- const css = readFileSync(
362
- new URL("../../../styles/ui.css", import.meta.url),
363
- "utf8",
364
- );
620
+ const css = readComponentCss();
365
621
 
366
622
  expect(css).toMatch(
367
623
  /\.compose-tiptap-body\s+\.tiptap\s+blockquote\s*\{[\s\S]*background:\s*linear-gradient\(/,
@@ -378,10 +634,7 @@ describe("timeline cards", () => {
378
634
  });
379
635
 
380
636
  it("keeps quote compose aligned with quote-card typography", () => {
381
- const css = readFileSync(
382
- new URL("../../../styles/ui.css", import.meta.url),
383
- "utf8",
384
- );
637
+ const css = readComponentCss();
385
638
 
386
639
  expect(css).toMatch(
387
640
  /\.compose-quote-wrap\s*\{[\s\S]*background-color:\s*color-mix\(/,
@@ -393,10 +646,7 @@ describe("timeline cards", () => {
393
646
  });
394
647
 
395
648
  it("keeps reply context previews on thread-context type tokens", () => {
396
- const uiCss = readFileSync(
397
- new URL("../../../styles/ui.css", import.meta.url),
398
- "utf8",
399
- );
649
+ const uiCss = readComponentCss();
400
650
  const tokenCss = readFileSync(
401
651
  new URL("../../../styles/tokens.css", import.meta.url),
402
652
  "utf8",
@@ -416,10 +666,7 @@ describe("timeline cards", () => {
416
666
  });
417
667
 
418
668
  it("resets cloned reply previews away from feed/detail width constraints", () => {
419
- const uiCss = readFileSync(
420
- new URL("../../../styles/ui.css", import.meta.url),
421
- "utf8",
422
- );
669
+ const uiCss = readComponentCss();
423
670
  const unlayeredCssStart = uiCss.indexOf(" * Home description");
424
671
  const resetRule = uiCss.match(
425
672
  /\.compose-reply-context-body\s*:is\([^{}]*\[data-post-body\]\.prose[^{}]*\)\s*\{[^{}]*width:\s*100%;[^{}]*max-width:\s*none;[^{}]*\}/,
@@ -487,10 +734,7 @@ describe("timeline cards", () => {
487
734
  });
488
735
 
489
736
  it("keeps titled detail headers spaced as one reading group", () => {
490
- const css = readFileSync(
491
- new URL("../../../styles/ui.css", import.meta.url),
492
- "utf8",
493
- );
737
+ const css = readComponentCss();
494
738
 
495
739
  expect(css).toMatch(
496
740
  /\.post-header-block\s*\{[\s\S]*margin-bottom:\s*1\.7rem;/,
@@ -501,10 +745,7 @@ describe("timeline cards", () => {
501
745
  });
502
746
 
503
747
  it("keeps detail dates quiet while preserving standalone footer spacing", () => {
504
- const css = readFileSync(
505
- new URL("../../../styles/ui.css", import.meta.url),
506
- "utf8",
507
- );
748
+ const css = readComponentCss();
508
749
 
509
750
  expect(css).toMatch(
510
751
  /\.post-header-meta-link\s*\{[^}]*font-size:\s*var\(--type-ui-hint\);/,
@@ -518,13 +759,10 @@ describe("timeline cards", () => {
518
759
  });
519
760
 
520
761
  it("adds breathing room when a quote source leads directly into its footer", () => {
521
- const css = readFileSync(
522
- new URL("../../../styles/ui.css", import.meta.url),
523
- "utf8",
524
- );
762
+ const css = readComponentCss();
525
763
 
526
764
  expect(css).toMatch(
527
- /\.feed-quote-attribution:has\(\+ \.post-menu-footer\)\s*\{[^}]*margin-bottom:\s*0\.45rem;/,
765
+ /\.feed-quote-cite:has\(\+ \.post-menu-footer\) \.feed-quote-attribution\s*\{[^}]*margin-bottom:\s*0\.45rem;/,
528
766
  );
529
767
  });
530
768
 
@@ -542,10 +780,7 @@ describe("timeline cards", () => {
542
780
  });
543
781
 
544
782
  it("progressively enhances canonical endnotes on wide detail and timeline posts", () => {
545
- const css = readFileSync(
546
- new URL("../../../styles/ui.css", import.meta.url),
547
- "utf8",
548
- );
783
+ const css = readComponentCss();
549
784
  const tokens = readFileSync(
550
785
  new URL("../../../styles/tokens.css", import.meta.url),
551
786
  "utf8",
@@ -764,4 +999,34 @@ describe("timeline cards", () => {
764
999
  expect(html).not.toContain("data-reply-trigger");
765
1000
  expect(html).toContain("data-post-menu-trigger");
766
1001
  });
1002
+
1003
+ it("ships every status badge to the author so the menu can reveal one", () => {
1004
+ const html = renderWithI18n(
1005
+ NoteCard({ post: createPostView({ format: "note" }), mode: "feed" }),
1006
+ );
1007
+
1008
+ expect(html).toContain("post-status-pinned");
1009
+ expect(html).toContain("post-status-private");
1010
+ expect(html).toContain("post-status-draft");
1011
+ });
1012
+
1013
+ it("gives a reader only the badges that apply", () => {
1014
+ const plain = renderWithI18n(
1015
+ NoteCard({ post: createPostView({ format: "note" }), mode: "feed" }),
1016
+ false,
1017
+ );
1018
+ // Nothing to announce, so not even the wrapper.
1019
+ expect(plain).not.toContain("post-status-badges");
1020
+
1021
+ const pinned = renderWithI18n(
1022
+ NoteCard({
1023
+ post: createPostView({ format: "note", pinned: true }),
1024
+ mode: "feed",
1025
+ }),
1026
+ false,
1027
+ );
1028
+ expect(pinned).toContain("post-status-pinned");
1029
+ expect(pinned).not.toContain("post-status-private");
1030
+ expect(pinned).not.toContain("post-status-draft");
1031
+ });
767
1032
  });
@@ -1,5 +1,19 @@
1
1
  import type { PostView } from "../../types.js";
2
+ import { getThreadHiddenCount } from "../../lib/thread-fold.js";
2
3
 
4
+ /**
5
+ * What the preview needs to know about its own fold.
6
+ *
7
+ * The count itself lives in `lib/thread-fold.ts`, where the feed reads it too —
8
+ * a card and a feed entry that disagreed about how many posts are hidden would
9
+ * be describing different threads.
10
+ *
11
+ * @param selection - The replies this preview shows, and the thread's total
12
+ * @returns The hidden-post count the gap link announces
13
+ * @example
14
+ * getThreadPreviewState({ leadingReplies, trailingReplies, latestReply,
15
+ * totalReplyCount: 8 }); // { hiddenCount: 3 }
16
+ */
3
17
  export function getThreadPreviewState({
4
18
  leadingReplies,
5
19
  trailingReplies,
@@ -11,13 +25,13 @@ export function getThreadPreviewState({
11
25
  latestReply: PostView;
12
26
  totalReplyCount: number;
13
27
  }) {
14
- const visibleReplyIds = new Set(
15
- [...leadingReplies, ...trailingReplies, latestReply].map((post) => post.id),
16
- );
17
- const hiddenCount = Math.max(0, totalReplyCount - visibleReplyIds.size);
18
-
19
28
  return {
20
- hiddenCount,
29
+ hiddenCount: getThreadHiddenCount({
30
+ leadingReplies,
31
+ trailingReplies,
32
+ latestReply,
33
+ totalReplyCount,
34
+ }),
21
35
  };
22
36
  }
23
37
 
@@ -12,8 +12,7 @@
12
12
  import type { MessageDescriptor } from "@lingui/core";
13
13
  import {
14
14
  getCjkFontFromLanguageTag,
15
- isCjkSerifFont,
16
- type CjkSerifFont,
15
+ type CjkFontProfile,
17
16
  } from "../i18n/detect.js";
18
17
 
19
18
  /**
@@ -34,8 +33,6 @@ export interface FontTheme {
34
33
  description: MessageDescriptor;
35
34
  }
36
35
 
37
- const DEFAULT_CJK_SERIF_FALLBACK = '"Jant Language Fallback"';
38
- const DEFAULT_CJK_SANS_FALLBACK = '"Jant Language Fallback"';
39
36
  const HANS_CJK_SERIF_FALLBACK =
40
37
  '"Songti SC", STSong, SimSun, "Noto Serif SC", "Noto Serif CJK SC", "Songti TC", PMingLiU, MingLiU, "Noto Serif TC", "Noto Serif CJK TC"';
41
38
  const HANS_CJK_SANS_FALLBACK =
@@ -52,6 +49,20 @@ const KO_CJK_SERIF_FALLBACK =
52
49
  'Batang, "Noto Serif KR", "Noto Serif CJK KR", NanumMyeongjo';
53
50
  const KO_CJK_SANS_FALLBACK =
54
51
  '"Apple SD Gothic Neo", "Noto Sans KR", "Noto Sans CJK KR", "Malgun Gothic"';
52
+ /*
53
+ * Default stacks for a page whose language names no CJK profile.
54
+ *
55
+ * A font stack cannot express "no opinion": every Latin family in a theme stack
56
+ * lacks Han coverage, and `ui-serif` / `serif` resolve to Times or New York,
57
+ * which lack it too — so a placeholder name leaves Chinese, Japanese and Korean
58
+ * text to the OS last-resort font (PingFang SC on Apple platforms), rendering a
59
+ * serif page in sans. The default therefore has to be real. Simplified comes
60
+ * first because it is the most common CJK content here; a page that knows its
61
+ * language gets the profile stacks above instead, which put the right glyph
62
+ * shapes first.
63
+ */
64
+ const DEFAULT_CJK_SERIF_FALLBACK = `${HANS_CJK_SERIF_FALLBACK}, ${JP_CJK_SERIF_FALLBACK}, ${KO_CJK_SERIF_FALLBACK}`;
65
+ const DEFAULT_CJK_SANS_FALLBACK = `${HANS_CJK_SANS_FALLBACK}, ${JP_CJK_SANS_FALLBACK}, ${KO_CJK_SANS_FALLBACK}`;
55
66
  const CJK_SERIF_FALLBACK_VAR = "var(--font-cjk-serif-fallback)";
56
67
  const CJK_SANS_FALLBACK_VAR = "var(--font-cjk-sans-fallback)";
57
68
 
@@ -117,46 +128,43 @@ export function getFontThemeCssVariables(
117
128
  }
118
129
 
119
130
  /**
120
- * Resolve the language font profile for the current content language.
131
+ * Resolve the CJK font profile for a content language.
121
132
  *
122
- * An adapted content language always wins. The explicit CJK setting is a
123
- * fallback for sites whose primary content language does not have a profile.
133
+ * Purely derived from the language — Simplified, Traditional, Japanese and
134
+ * Korean want different Han glyph shapes, and the language tag is the only
135
+ * thing that knows which. The page passes the language it is actually
136
+ * rendering: a post's own language on a post page, the view language on a
137
+ * language-filtered list, the site language otherwise.
124
138
  *
125
- * @param siteLanguage - BCP 47 content language tag
126
- * @param cjkFallback - Explicit CJK fallback setting
127
- * @returns Resolved CJK font profile, or `undefined` for the default stack
139
+ * @param language - BCP 47 content language tag
140
+ * @returns Resolved CJK font profile, or `undefined` to keep the token default
128
141
  * @example
129
- * resolveCjkFontProfile("zh-TW", "zh-Hans") // "zh-Hant"
142
+ * resolveCjkFontProfile("zh-TW") // "zh-Hant"
143
+ * resolveCjkFontProfile("en") // undefined
130
144
  */
131
145
  export function resolveCjkFontProfile(
132
- siteLanguage?: string,
133
- cjkFallback?: string,
134
- ): Exclude<CjkSerifFont, "off"> | undefined {
135
- const contentProfile = siteLanguage
136
- ? getCjkFontFromLanguageTag(siteLanguage)
137
- : undefined;
138
- if (contentProfile) return contentProfile;
139
-
140
- return isCjkSerifFont(cjkFallback) && cjkFallback !== "off"
141
- ? cjkFallback
142
- : undefined;
146
+ language?: string,
147
+ ): CjkFontProfile | undefined {
148
+ return language ? getCjkFontFromLanguageTag(language) : undefined;
143
149
  }
144
150
 
145
151
  /**
146
152
  * Build serif and sans fallback variables for the resolved language profile.
147
153
  *
148
- * @param siteLanguage - BCP 47 content language tag
149
- * @param cjkFallback - Explicit CJK fallback setting
150
- * @returns CSS variables consumed by every font theme
154
+ * Empty for a language with no profile: `tokens.css` already carries a real
155
+ * script-neutral stack in both variables, so there is nothing to override.
156
+ *
157
+ * @param language - BCP 47 content language tag
158
+ * @returns CSS variables consumed by every font theme, empty when the token
159
+ * defaults already apply
151
160
  * @example
152
- * getCjkFontCssVariables("en", "zh-Hans")
161
+ * getCjkFontCssVariables("zh-Hans")
153
162
  * // => { "--font-cjk-serif-fallback": '"Songti SC", ...', ... }
154
163
  */
155
164
  export function getCjkFontCssVariables(
156
- siteLanguage?: string,
157
- cjkFallback?: string,
165
+ language?: string,
158
166
  ): Record<string, string> {
159
- switch (resolveCjkFontProfile(siteLanguage, cjkFallback)) {
167
+ switch (resolveCjkFontProfile(language)) {
160
168
  case "zh-Hans":
161
169
  return {
162
170
  "--font-cjk-serif-fallback": HANS_CJK_SERIF_FALLBACK,
@@ -182,7 +190,9 @@ export function getCjkFontCssVariables(
182
190
  }
183
191
  }
184
192
 
193
+ /** The `--font-cjk-serif-fallback` value `tokens.css` ships as its default. */
185
194
  export const DEFAULT_FONT_CJK_SERIF_FALLBACK = DEFAULT_CJK_SERIF_FALLBACK;
195
+ /** The `--font-cjk-sans-fallback` value `tokens.css` ships as its default. */
186
196
  export const DEFAULT_FONT_CJK_SANS_FALLBACK = DEFAULT_CJK_SANS_FALLBACK;
187
197
 
188
198
  export const BUILTIN_FONT_THEMES: FontTheme[] = [