@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml } from "./html.js";
2
+ import { getJantDocsUrl } from "./jant-docs.js";
2
3
  import {
3
4
  getAuthSecret,
4
5
  getHostedControlPlaneBaseUrl,
@@ -94,8 +95,7 @@ function getAuthSecretErrorHtml(kind: AuthSecretIssueKind): string {
94
95
  return renderConfigurationErrorPage({
95
96
  title: titleByKind[kind],
96
97
  bodyHtml: `${leadByKind[kind]}${runtimeInstructions}`,
97
- docsHref:
98
- "https://github.com/jant-me/jant/blob/main/docs/configuration.md#required",
98
+ docsHref: getJantDocsUrl("configuration#required"),
99
99
  });
100
100
  }
101
101
 
@@ -219,8 +219,7 @@ function getHostBasedConfigurationErrorHtml(
219
219
  return renderConfigurationErrorPage({
220
220
  title: "Hosted configuration is incomplete",
221
221
  bodyHtml: `<p>Jant is running with <code>SITE_RESOLUTION_MODE=host-based</code>, so hosted control-plane integration must be configured before the instance can serve requests.</p><ul>${itemsHtml}</ul>`,
222
- docsHref:
223
- "https://github.com/jant-me/jant/blob/main/docs/configuration.md#hosted-control-plane-integration-variables",
222
+ docsHref: getJantDocsUrl("configuration"),
224
223
  });
225
224
  }
226
225
 
@@ -243,8 +242,7 @@ export function getRuntimeConfigurationErrorPage(message: string): string {
243
242
  return renderConfigurationErrorPage({
244
243
  title: "Configuration Error",
245
244
  bodyHtml: `<p>${escapeHtml(message)}</p><p>Update your environment or instance data, then restart Jant.</p>`,
246
- docsHref:
247
- "https://github.com/jant-me/jant/blob/main/docs/configuration.md#site-resolution",
245
+ docsHref: getJantDocsUrl("configuration"),
248
246
  });
249
247
  }
250
248
 
@@ -432,6 +432,13 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
432
432
  // uploaded object checksums during finalize, so only enable response
433
433
  // checksum verification when a request explicitly asks for it.
434
434
  responseChecksumValidation: "WHEN_REQUIRED",
435
+ // Presigning runs with an empty body, so the SDK's default
436
+ // "WHEN_SUPPORTED" request checksums bake CRC32-of-nothing
437
+ // (x-amz-checksum-crc32=AAAAAA==) into every presigned PUT URL. The
438
+ // browser then uploads real bytes and the storage provider rejects
439
+ // the mismatch. Only send a request checksum when the caller asked
440
+ // for one (presignPut passes ChecksumSHA256 explicitly).
441
+ requestChecksumCalculation: "WHEN_REQUIRED",
435
442
  });
436
443
  return {
437
444
  send: (cmd: unknown) => client.send(cmd as never),
@@ -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` returns each Thread in Thread
110
+ * order, the same `threadOrder` the window functions rank by, so once the root
111
+ * is set aside 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
  };
@@ -242,7 +272,7 @@ async function buildCuratedThreadItems(
242
272
 
243
273
  return orderedThreads.reduce<TimelineItemView[]>((items, thread) => {
244
274
  const rootEntry = thread.posts[0];
245
- if (!rootEntry || rootEntry.position !== 0) {
275
+ if (!rootEntry || rootEntry.post.id !== rootEntry.post.threadId) {
246
276
  return items;
247
277
  }
248
278
 
@@ -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;
@@ -11,8 +11,14 @@ import { serializeMarkdownDocument } from "./markdown-manager.js";
11
11
  /**
12
12
  * Converts a Tiptap JSON document to a Markdown string.
13
13
  *
14
- * @param json - Tiptap JSON string or parsed document object
14
+ * Throws rather than returning an empty string: a caller writing the result
15
+ * somewhere (an export, a text attachment) would otherwise replace the
16
+ * content with nothing and say nothing.
17
+ *
18
+ * @param json - Tiptap JSON document string
15
19
  * @returns Markdown string
20
+ * @throws {SyntaxError} When `json` is not JSON
21
+ * @throws {Error} When the root node is not a `doc`
16
22
  *
17
23
  * @example
18
24
  * ```ts
@@ -21,11 +27,11 @@ import { serializeMarkdownDocument } from "./markdown-manager.js";
21
27
  * ```
22
28
  */
23
29
  export function tiptapJsonToMarkdown(json: string): string {
24
- try {
25
- const doc = JSON.parse(json) as JSONContent;
26
- if (doc.type !== "doc") return "";
27
- return serializeMarkdownDocument(doc).trimEnd();
28
- } catch {
29
- return "";
30
+ const doc = JSON.parse(json) as JSONContent;
31
+ if (doc.type !== "doc") {
32
+ throw new Error(
33
+ `A TipTap body's root must be a doc node, not ${JSON.stringify(doc.type)}.`,
34
+ );
30
35
  }
36
+ return serializeMarkdownDocument(doc).trimEnd();
31
37
  }
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";