@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
@@ -22,6 +22,21 @@ interface TiptapNode {
22
22
  attrs?: Record<string, unknown>;
23
23
  }
24
24
 
25
+ /**
26
+ * Summary limits for titled, article-style posts — the excerpt is a teaser
27
+ * and the reader continues on the post's own page.
28
+ */
29
+ export const ARTICLE_SUMMARY_MAX_BLOCKS = 5;
30
+ export const ARTICLE_SUMMARY_MAX_CHARS = 500;
31
+ /**
32
+ * Larger limits for untitled notes — the body itself is the content, so the
33
+ * preview only exists to cap runaway length.
34
+ */
35
+ export const NOTE_SUMMARY_MAX_BLOCKS = 10;
36
+ export const NOTE_SUMMARY_MAX_CHARS = 1500;
37
+ /** Don't truncate an untitled note just to hide a tail under this many chars. */
38
+ export const NOTE_SUMMARY_MIN_HIDDEN_CHARS = 200;
39
+
25
40
  /**
26
41
  * Block node types that carry user-visible content for summary extraction.
27
42
  * Structural nodes (horizontalRule, moreBreak, image) are excluded.
@@ -348,3 +363,38 @@ export function extractSummaryHtml(
348
363
  breakAtIndex: lastSelectedIdx + 1,
349
364
  };
350
365
  }
366
+
367
+ /**
368
+ * Extract the summary the site's timeline shows for a post, at the boundary
369
+ * the timeline uses.
370
+ *
371
+ * Wraps {@link extractSummaryHtml} with the limits that decide where a card
372
+ * stops: a titled post is an article whose excerpt is a teaser, an untitled one
373
+ * is a note whose body is the content and only needs a cap. Callers that need
374
+ * that boundary — the feed renderer, the theme exporter — share this rather
375
+ * than repeating five numbers each.
376
+ *
377
+ * @param bodyJson - TipTap JSON body, or null/undefined for a post without one
378
+ * @param hasTitle - Whether the post has a title, which makes it an article
379
+ * @param renderOptions - Renderer options, notably the footnote namespace
380
+ * @returns The truncated HTML and whether content continues, or null when
381
+ * there is no TipTap document to truncate
382
+ * @example
383
+ * extractTimelineSummary(post.body, !!post.title, { namespace: post.id });
384
+ * // { html: "<p>Intro</p>", hasMore: true }
385
+ */
386
+ export function extractTimelineSummary(
387
+ bodyJson: string | null | undefined,
388
+ hasTitle: boolean,
389
+ renderOptions: TiptapRenderOptions = {},
390
+ ): { html: string; hasMore: boolean } | null {
391
+ if (!bodyJson) return null;
392
+ const result = extractSummaryHtml(
393
+ bodyJson,
394
+ hasTitle ? ARTICLE_SUMMARY_MAX_BLOCKS : NOTE_SUMMARY_MAX_BLOCKS,
395
+ hasTitle ? ARTICLE_SUMMARY_MAX_CHARS : NOTE_SUMMARY_MAX_CHARS,
396
+ hasTitle ? 0 : NOTE_SUMMARY_MIN_HIDDEN_CHARS,
397
+ renderOptions,
398
+ );
399
+ return result ? { html: result.html, hasMore: result.hasMore } : null;
400
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Thread Fold
3
+ *
4
+ * How much of a thread the timeline shows, and how much it folds away.
5
+ *
6
+ * A thread appears in two places that cannot select its posts the same way.
7
+ * The site paints a page of timeline items and must not load whole threads to
8
+ * do it, so `getThreadTimelineContext` ranks replies in SQL and hands the
9
+ * buckets down. The feed already holds every reply — `<content>` carries the
10
+ * whole chain — so it slices the array it has. Two access paths, one rule.
11
+ *
12
+ * This module owns what they share: the thresholds, and the arithmetic that
13
+ * turns a selection into a hidden count. Whoever changes the shape of the fold
14
+ * changes it here, and both surfaces move together.
15
+ *
16
+ * ## The fold
17
+ *
18
+ * ```
19
+ * root · reply 1 · reply 2 · [N more posts] · 3rd-newest · 2nd-newest · newest
20
+ * ```
21
+ *
22
+ * Six posts at most. A thread that fits arrives whole, with no gap at all —
23
+ * the gap appears only once something is actually left out.
24
+ *
25
+ * ## Where the gap points
26
+ *
27
+ * **At the first post it hides.** The link stands for a run of posts, and it
28
+ * opens the earliest of them, so a reader following "3 more posts" lands on
29
+ * the first of those three.
30
+ *
31
+ * It used to open the last *visible* post instead, on the reasoning that the
32
+ * detail page would then start just above the missing stretch, like a
33
+ * bookmark. That was a deliberate choice and it reads well on the site — but
34
+ * it makes the link mean something other than what its label says, and the
35
+ * feed cannot follow it: `<jant:thread>`'s `gap` attribute is documented as
36
+ * the first hidden post, and a consumer joining it against the `<jant:post>`
37
+ * rows would be handed a post that is on screen. One rule beats two, so both
38
+ * surfaces open the first hidden post.
39
+ */
40
+
41
+ import type { Post, PostView } from "../types.js";
42
+
43
+ /**
44
+ * Replies kept from the start of a thread, as context for the newest post.
45
+ *
46
+ * Also fixes where the SQL path's rank cut-off falls, so the two selections
47
+ * stay the same fold.
48
+ */
49
+ export const THREAD_LEADING_REPLIES = 2;
50
+
51
+ /**
52
+ * Replies kept from the end of a thread, the newest one included — it is the
53
+ * hero, and the two before it are its immediate context.
54
+ */
55
+ export const THREAD_TRAILING_REPLIES = 3;
56
+
57
+ /** Anything with an id, which is all the dedupe needs. */
58
+ type Identified = Pick<Post, "id"> | Pick<PostView, "id">;
59
+
60
+ /**
61
+ * How many replies a fold leaves unshown.
62
+ *
63
+ * Deduplicated, because on a short thread the buckets overlap: with four
64
+ * replies the first is also the third-newest, and counting it twice would
65
+ * report posts hidden that are on screen.
66
+ *
67
+ * @param selection - The replies the fold shows, and the thread's total
68
+ * @returns Replies the fold hides, never negative
69
+ * @example
70
+ * getThreadHiddenCount({ leadingReplies: [r1, r2], trailingReplies: [r6, r7],
71
+ * latestReply: r8, totalReplyCount: 8 }); // 3
72
+ */
73
+ export function getThreadHiddenCount({
74
+ leadingReplies,
75
+ trailingReplies,
76
+ latestReply,
77
+ totalReplyCount,
78
+ }: {
79
+ leadingReplies: Identified[];
80
+ trailingReplies: Identified[];
81
+ latestReply: Identified;
82
+ totalReplyCount: number;
83
+ }): number {
84
+ const shown = new Set(
85
+ [...leadingReplies, ...trailingReplies, latestReply].map((post) => post.id),
86
+ );
87
+ return Math.max(0, totalReplyCount - shown.size);
88
+ }
89
+
90
+ /** What the fold shows of a thread, and what it hides. */
91
+ export interface ThreadFold<T> {
92
+ /** Earliest replies, kept as context above the gap. */
93
+ leadingReplies: T[];
94
+ /** Replies just before the newest, kept as its immediate context. */
95
+ trailingReplies: T[];
96
+ /** The newest reply — the hero the timeline builds the card around. */
97
+ latestReply: T;
98
+ /** First reply the fold hides, which is where the gap link points. */
99
+ firstHiddenReply: T | null;
100
+ /** How many replies the fold hides. */
101
+ hiddenCount: number;
102
+ }
103
+
104
+ /**
105
+ * Fold a thread whose replies are already in hand.
106
+ *
107
+ * Takes the same slices the SQL path ranks for: the first
108
+ * `THREAD_LEADING_REPLIES`, and the last `THREAD_TRAILING_REPLIES` with the
109
+ * newest as the hero. `getPublishedThreads` orders replies by
110
+ * `(threadId, createdAt, id)` ascending, which is the window function's own
111
+ * `ORDER BY`, so position in this array *is* the rank — no approximation.
112
+ *
113
+ * @param replies - Every published reply, oldest first
114
+ * @returns The fold, or null when the thread has no replies
115
+ * @example
116
+ * foldThreadReplies([r1, r2, r3, r4, r5, r6, r7, r8]);
117
+ * // leading [r1, r2], trailing [r6, r7], latest r8, firstHidden r3, hidden 3
118
+ */
119
+ export function foldThreadReplies<T extends Identified>(
120
+ replies: T[],
121
+ ): ThreadFold<T> | null {
122
+ const latestReply = replies.at(-1);
123
+ if (!latestReply) return null;
124
+
125
+ // A short thread's windows overlap, and the hero is inside both of them on
126
+ // the shortest ones: with a single reply, that reply is the newest *and* the
127
+ // whole leading window. Each list therefore drops whatever an earlier one
128
+ // already shows, hero first — the same order `ThreadPreview` dedupes in when
129
+ // it walks the buckets the SQL path hands it.
130
+ const leadingReplies = replies
131
+ .slice(0, THREAD_LEADING_REPLIES)
132
+ .filter((reply) => reply.id !== latestReply.id);
133
+
134
+ const leadingIds = new Set(leadingReplies.map((reply) => reply.id));
135
+ const trailingReplies = replies
136
+ .slice(-THREAD_TRAILING_REPLIES, -1)
137
+ .filter(
138
+ (reply) => reply.id !== latestReply.id && !leadingIds.has(reply.id),
139
+ );
140
+
141
+ const shown = new Set([
142
+ ...leadingIds,
143
+ ...trailingReplies.map((reply) => reply.id),
144
+ latestReply.id,
145
+ ]);
146
+
147
+ return {
148
+ leadingReplies,
149
+ trailingReplies,
150
+ latestReply,
151
+ // In thread order, so the gap link lands on the earliest post it stands
152
+ // for rather than somewhere in the middle of the run it hides.
153
+ firstHiddenReply: replies.find((reply) => !shown.has(reply.id)) ?? null,
154
+ hiddenCount: getThreadHiddenCount({
155
+ leadingReplies,
156
+ trailingReplies,
157
+ latestReply,
158
+ totalReplyCount: replies.length,
159
+ }),
160
+ };
161
+ }
package/src/lib/time.ts CHANGED
@@ -223,3 +223,35 @@ export function formatYearMonth(timestamp: number, timeZone = "UTC"): string {
223
223
  const month = parts.find((part) => part.type === "month")?.value ?? "01";
224
224
  return `${year}-${month}`;
225
225
  }
226
+
227
+ /**
228
+ * Formats a "YYYY-MM" grouping key as a readable month label.
229
+ *
230
+ * The label is always en-US, matching the archive's other month copy — the
231
+ * grid header and the list marker read from this one function so their labels
232
+ * cannot drift apart.
233
+ *
234
+ * @param yearMonth - Year-month string in "YYYY-MM" format
235
+ * @returns Month label such as "February 2024", or null when the key is malformed
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const label = formatYearMonthLabel("2024-02");
240
+ * // Returns: "February 2024"
241
+ * ```
242
+ */
243
+ export function formatYearMonthLabel(yearMonth: string): string | null {
244
+ const [year, month] = yearMonth.split("-");
245
+ if (!year || !month) return null;
246
+
247
+ const yearNumber = parseInt(year, 10);
248
+ const monthNumber = parseInt(month, 10);
249
+ if (!Number.isInteger(yearNumber) || !Number.isInteger(monthNumber)) {
250
+ return null;
251
+ }
252
+
253
+ return new Date(yearNumber, monthNumber - 1).toLocaleDateString("en-US", {
254
+ year: "numeric",
255
+ month: "long",
256
+ });
257
+ }
@@ -15,9 +15,24 @@ import type {
15
15
  import type { AppVariables } from "../types/app-context.js";
16
16
  import { buildMediaMap } from "./media-helpers.js";
17
17
  import { createMediaContext, resolveDraftTailId, toPostView } from "./view.js";
18
+ import { getViewLang } from "./view-language.js";
19
+ import { toPublicPath } from "./url.js";
18
20
 
19
21
  type Env = { Bindings: Bindings; Variables: AppVariables };
20
22
 
23
+ /**
24
+ * Language filter for the view the request is being served in.
25
+ *
26
+ * Read from the context rather than passed in, so a caller cannot forget it
27
+ * and quietly leak another language's posts into a language view.
28
+ *
29
+ * @param c - Hono context
30
+ * @returns Canonical tag to filter on, or undefined on the root view
31
+ */
32
+ function viewLangFilter(c: Context<Env>): string | undefined {
33
+ return getViewLang(c) ?? undefined;
34
+ }
35
+
21
36
  /**
22
37
  * Result from assembling a timeline page.
23
38
  */
@@ -88,11 +103,15 @@ async function buildTimelineItems(
88
103
 
89
104
  // Batch load media for the bounded leading/trailing thread context.
90
105
  const contextPostIds = new Set<string>();
106
+ const gapPostIds = new Set<string>();
91
107
  for (const ctx of threadContexts.values()) {
92
108
  contextPostIds.add(ctx.latestReply.id);
93
109
  for (const reply of [...ctx.leadingReplies, ...ctx.trailingReplies]) {
94
110
  contextPostIds.add(reply.id);
95
111
  }
112
+ // The gap target is never rendered, so it needs no media or collections —
113
+ // only the alias that decides its address.
114
+ if (ctx.firstHiddenReply) gapPostIds.add(ctx.firstHiddenReply.id);
96
115
  }
97
116
  const [contextMediaMap, contextCollectionsMap, contextAliasesMap] =
98
117
  contextPostIds.size > 0
@@ -112,7 +131,12 @@ async function buildTimelineItems(
112
131
  c.var.services.collections.getCollectionsByPostIds([
113
132
  ...contextPostIds,
114
133
  ]),
115
- c.var.services.paths.getPostAliases([...contextPostIds]),
134
+ // Gap targets ride along here and nowhere else: their address is
135
+ // all the gap link needs.
136
+ c.var.services.paths.getPostAliases([
137
+ ...contextPostIds,
138
+ ...gapPostIds,
139
+ ]),
116
140
  ])
117
141
  : [new Map(), new Map(), new Map<string, string[]>()];
118
142
 
@@ -178,6 +202,12 @@ async function buildTimelineItems(
178
202
  leadingReplies: leadingReplyViews,
179
203
  trailingReplies: trailingReplyViews,
180
204
  latestReply: latestReplyView,
205
+ gapHref: threadCtx.firstHiddenReply
206
+ ? toPublicPath(
207
+ `/${firstContextAlias(threadCtx.firstHiddenReply.id) ?? threadCtx.firstHiddenReply.slug}`,
208
+ mediaCtx.sitePathPrefix,
209
+ )
210
+ : undefined,
181
211
  totalReplyCount: threadCtx.totalReplyCount,
182
212
  },
183
213
  };
@@ -344,18 +374,22 @@ export async function assembleTimeline(
344
374
  const excludePrivate = !(options?.isAuthenticated ?? false);
345
375
 
346
376
  // Count + list are independent — run in parallel
377
+ const lang = viewLangFilter(c);
378
+
347
379
  const [totalCount, posts] = await Promise.all([
348
380
  c.var.services.posts.count({
349
381
  status: "published",
350
382
  excludeReplies: true,
351
383
  excludeLatestHidden: true,
352
384
  excludePrivate,
385
+ lang,
353
386
  }),
354
387
  c.var.services.posts.list({
355
388
  status: "published",
356
389
  excludeReplies: true,
357
390
  excludeLatestHidden: true,
358
391
  excludePrivate,
392
+ lang,
359
393
  limit: pageSize,
360
394
  offset,
361
395
  }),
@@ -420,14 +454,18 @@ export async function assembleFeaturedTimeline(
420
454
  const offset = (page - 1) * pageSize;
421
455
  const excludePrivate = !(options?.isAuthenticated ?? false);
422
456
 
457
+ const lang = viewLangFilter(c);
458
+
423
459
  const [totalCount, rootIds] = await Promise.all([
424
460
  c.var.services.posts.countFeaturedThreadRoots({
425
461
  status: "published",
426
462
  excludePrivate,
463
+ lang,
427
464
  }),
428
465
  c.var.services.posts.listFeaturedThreadRootIds({
429
466
  status: "published",
430
467
  excludePrivate,
468
+ lang,
431
469
  limit: pageSize,
432
470
  offset,
433
471
  }),
@@ -482,6 +520,7 @@ export async function assembleCollectionTimeline(
482
520
  const page = Math.max(1, options.page ?? 1);
483
521
  const offset = (page - 1) * pageSize;
484
522
  const excludePrivate = !(options.isAuthenticated ?? false);
523
+ const lang = viewLangFilter(c);
485
524
 
486
525
  const [totalCount, rootIds] = await Promise.all([
487
526
  c.var.services.posts.countCollectionThreadRootsForCollections(
@@ -489,6 +528,7 @@ export async function assembleCollectionTimeline(
489
528
  {
490
529
  status: "published",
491
530
  excludePrivate,
531
+ lang,
492
532
  },
493
533
  ),
494
534
  c.var.services.posts.listCollectionThreadRootIdsForCollections(
@@ -496,6 +536,7 @@ export async function assembleCollectionTimeline(
496
536
  {
497
537
  status: "published",
498
538
  excludePrivate,
539
+ lang,
499
540
  sortOrder: options.sortOrder,
500
541
  limit: pageSize,
501
542
  offset,
@@ -48,8 +48,7 @@ export interface TiptapRenderOptions {
48
48
  }
49
49
 
50
50
  export type TiptapRenderResult =
51
- | { ok: true; html: string }
52
- | { ok: false; error: string };
51
+ { ok: true; html: string } | { ok: false; error: string };
53
52
 
54
53
  interface FootnoteReferencePlan {
55
54
  definitionId: string;
package/src/lib/upload.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Shared file validation and storage key generation for upload routes.
5
5
  */
6
6
 
7
- import type { MediaKind } from "../types/constants.js";
7
+ import type { ContentDisposition, MediaKind } from "../types/constants.js";
8
8
  import { createEntityId } from "./ids.js";
9
9
 
10
10
  const MEDIA_ROOT_PREFIX = "media";
@@ -161,16 +161,6 @@ const MIME_CATEGORY_MAP = new Map<string, MediaCategory>([
161
161
  ...CODE_MIME_TYPES.map((t) => [t, "code" as const] as const),
162
162
  ]);
163
163
 
164
- /**
165
- * Accept string for file inputs. Accepts all file types.
166
- *
167
- * @example
168
- * ```ts
169
- * <input type="file" accept={UPLOAD_ACCEPT} />
170
- * ```
171
- */
172
- export const UPLOAD_ACCEPT = "*/*";
173
-
174
164
  export type MediaCategory =
175
165
  | "image"
176
166
  | "video"
@@ -183,7 +173,7 @@ export type MediaCategory =
183
173
  | "3d"
184
174
  | "code";
185
175
 
186
- export type UploadContentDisposition = "inline" | "attachment";
176
+ export type UploadContentDisposition = ContentDisposition;
187
177
 
188
178
  export interface StoredUploadPolicy {
189
179
  contentDisposition: UploadContentDisposition;
@@ -326,10 +316,22 @@ export function imageExtensionForMimeType(contentType: string): string | null {
326
316
  return IMAGE_MIME_EXTENSIONS[contentType] ?? null;
327
317
  }
328
318
 
319
+ /** ISO BMFF `ftyp` major brands that mean a HEIC/HEIF still image. */
320
+ const HEIC_FTYP_BRANDS = new Set([
321
+ "heic",
322
+ "heix",
323
+ "hevc",
324
+ "hevx",
325
+ "mif1",
326
+ "msf1",
327
+ ]);
328
+
329
329
  /**
330
330
  * Identify an image format from its leading bytes (magic numbers), so a remote
331
331
  * sideload can trust the actual content rather than the server's content-type
332
- * header. Recognizes every {@link isAllowedSideloadImageType} format.
332
+ * header. Recognizes every {@link isAllowedSideloadImageType} format, plus
333
+ * HEIC/HEIF — which no browser decodes natively and no sideload accepts, but
334
+ * which the composer converts before upload once it knows what it holds.
333
335
  *
334
336
  * @param bytes - The leading bytes of the file (≥ ~256 bytes recommended)
335
337
  * @returns The detected image MIME type, or null if unrecognized
@@ -367,6 +369,7 @@ export function sniffImageMimeType(bytes: Uint8Array): string | null {
367
369
  if (bytes.length >= 12 && readAscii(bytes, 4, 4) === "ftyp") {
368
370
  const brand = readAscii(bytes, 8, 4);
369
371
  if (brand === "avif" || brand === "avis") return "image/avif";
372
+ if (HEIC_FTYP_BRANDS.has(brand)) return "image/heic";
370
373
  }
371
374
  if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
372
375
  return "image/bmp";