@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
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readAllStylesheets } from "./helpers/component-css.js";
3
+
4
+ /**
5
+ * The sanctioned width breakpoints, mirroring the ledger comment in
6
+ * `styles/tokens.css`. Each number is one job; a job never gets a second
7
+ * number.
8
+ *
9
+ * This is not bookkeeping for its own sake. The composer once switched to its
10
+ * full-screen sheet at 700px while its own controls took their touch sizing at
11
+ * 760px, so a 750px window rendered a thumb-sized Post button (46px, against
12
+ * 33px one pixel wider) inside a windowed dialog. Nothing was wrong in either
13
+ * rule on its own — they had simply drifted to two numbers for one switch, and
14
+ * no test could see it.
15
+ */
16
+ const LEDGER = [
17
+ 480, // Header hamburger; the composer's post-meta pill sheds its words.
18
+ 580, // Nav collapse tier-sm.
19
+ 640, // Media grid and lightbox chrome.
20
+ 700, // The phone switch.
21
+ 780, // Nav collapse tier-md.
22
+ 960, // Nav collapse tier-lg.
23
+ 1024, // Tufte two-column → single-column.
24
+ 1079, // Collections sidebar drops out.
25
+ 1200, // Header search collapses to an icon.
26
+ ] as const;
27
+
28
+ /**
29
+ * Height breakpoints are a separate axis with a separate job: a short viewport
30
+ * has no room for the composer's chrome regardless of how wide it is. One
31
+ * number, stated on both sides.
32
+ */
33
+ const HEIGHT_LEDGER = [760] as const;
34
+
35
+ interface Condition {
36
+ /** The stylesheet the condition appears in. */
37
+ file: string;
38
+ /** 1-indexed line of the `@media` prelude the condition sits in. */
39
+ line: number;
40
+ /** `min` or `max`. */
41
+ bound: "min" | "max";
42
+ /** `width` or `height`. */
43
+ axis: "width" | "height";
44
+ /** The pixel value as written. */
45
+ value: number;
46
+ /** The condition as written, for failure messages. */
47
+ text: string;
48
+ }
49
+
50
+ /**
51
+ * Every `(min-width: …)` / `(max-width: …)` / `(min-height: …)` /
52
+ * `(max-height: …)` condition in an `@media` prelude across the hand-written
53
+ * stylesheets.
54
+ *
55
+ * `@media` only. A `@container` query carrying the same words measures its
56
+ * container, not the viewport, so it answers to the component it wraps rather
57
+ * than to this ledger — the archive filter bar splits on its own inline size
58
+ * at 39rem and is right to.
59
+ *
60
+ * A prelude may span several lines (the composer's full-screen sheet lists
61
+ * three conditions), so the line number is counted to the condition itself
62
+ * rather than to the `@media` that opens it.
63
+ */
64
+ function collectConditions(): Condition[] {
65
+ const found: Condition[] = [];
66
+
67
+ for (const { name, css } of readAllStylesheets()) {
68
+ const preludes = /@media\b([^{;]*)/g;
69
+ let prelude: RegExpExecArray | null;
70
+
71
+ while ((prelude = preludes.exec(css)) !== null) {
72
+ const offset = prelude.index + "@media".length;
73
+ const pattern = /\((min|max)-(width|height):\s*([^)]+)\)/g;
74
+ let match: RegExpExecArray | null;
75
+
76
+ while ((match = pattern.exec(prelude[1])) !== null) {
77
+ const [text, bound, axis, raw] = match;
78
+ const px = /^(\d+(?:\.\d+)?)px$/.exec(raw.trim());
79
+
80
+ found.push({
81
+ file: name,
82
+ line: css.slice(0, offset + match.index).split("\n").length,
83
+ bound: bound as "min" | "max",
84
+ axis: axis as "width" | "height",
85
+ // A non-px unit lands as NaN and fails the ledger check by name,
86
+ // which is the point: `rem` in a media query resolves against the
87
+ // initial font-size rather than the root this document sets, so the
88
+ // number it reads as is not the number it is.
89
+ value: px ? Number(px[1]) : Number.NaN,
90
+ text,
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ return found;
97
+ }
98
+
99
+ /**
100
+ * The breakpoint a condition belongs to. `min-width: 700px` and
101
+ * `max-width: 699px` are the two sides of the same 700px switch, so the `max`
102
+ * side is stated one pixel below the number it divides on.
103
+ */
104
+ function breakpointOf(condition: Condition): number {
105
+ return condition.bound === "max" ? condition.value + 1 : condition.value;
106
+ }
107
+
108
+ describe("stylesheet breakpoints", () => {
109
+ it("uses only the breakpoints on the ledger", () => {
110
+ const strays = collectConditions()
111
+ .filter((condition) => {
112
+ const ledger = condition.axis === "width" ? LEDGER : HEIGHT_LEDGER;
113
+ // Both spellings of a breakpoint are accepted here — `max-width: 640px`
114
+ // and `max-width: 639px` are the same switch. Which one a file may use
115
+ // is settled by the overlap test below.
116
+ return !(
117
+ ledger.includes(condition.value as never) ||
118
+ ledger.includes(breakpointOf(condition) as never)
119
+ );
120
+ })
121
+ .map(
122
+ (condition) =>
123
+ `${condition.file}:${condition.line} ${condition.text}` +
124
+ ` — not on the ledger in styles/tokens.css`,
125
+ );
126
+
127
+ expect(strays).toEqual([]);
128
+ });
129
+
130
+ it("never lets both sides of a breakpoint match the same viewport", () => {
131
+ const conditions = collectConditions();
132
+ const overlaps: string[] = [];
133
+
134
+ for (const axis of ["width", "height"] as const) {
135
+ const onAxis = conditions.filter((condition) => condition.axis === axis);
136
+
137
+ for (const value of new Set(
138
+ onAxis.filter((c) => c.bound === "min").map((c) => c.value),
139
+ )) {
140
+ // `min-*: N` and `max-*: N` both match at exactly N. Wherever a
141
+ // breakpoint is written from both sides, the lower side has to sit one
142
+ // pixel below it — otherwise a viewport of exactly N gets the phone
143
+ // layout and the desktop spacing at once.
144
+ const clash = onAxis.filter(
145
+ (condition) => condition.bound === "max" && condition.value === value,
146
+ );
147
+
148
+ overlaps.push(
149
+ ...clash.map(
150
+ (condition) =>
151
+ `${condition.file}:${condition.line} ${condition.text}` +
152
+ ` overlaps (min-${axis}: ${value}px) at exactly ${value}px` +
153
+ ` — write it as (max-${axis}: ${value - 1}px)`,
154
+ ),
155
+ );
156
+ }
157
+ }
158
+
159
+ expect(overlaps).toEqual([]);
160
+ });
161
+
162
+ it("keeps the phone switch on one number", () => {
163
+ // The regression this file exists for. Touch sizing and the mobile type
164
+ // scale used to ride on 760px while the layout they sit inside switched at
165
+ // 700px; anything between the two was a desktop window wearing phone
166
+ // controls. Any width breakpoint in the 700s that is not the nav ladder's
167
+ // 780px is that drift coming back.
168
+ const nearMisses = collectConditions()
169
+ .filter(
170
+ (condition) =>
171
+ condition.axis === "width" &&
172
+ condition.value > 700 &&
173
+ condition.value < 780,
174
+ )
175
+ .map(
176
+ (condition) =>
177
+ `${condition.file}:${condition.line} ${condition.text}` +
178
+ ` — the phone switch is 700px; see the ledger in styles/tokens.css`,
179
+ );
180
+
181
+ expect(nearMisses).toEqual([]);
182
+ });
183
+ });
package/src/app.tsx CHANGED
@@ -16,11 +16,13 @@ import { hostedDomainCheckRoutes } from "./routes/hosted/domain-check.js";
16
16
 
17
17
  // Routes - Pages
18
18
  import { homeRoutes } from "./routes/pages/home.js";
19
+ import { languageRoutes } from "./routes/pages/language.js";
19
20
  import { pageRoutes } from "./routes/pages/page.js";
20
21
  import { collectionRoutes } from "./routes/pages/collection.js";
21
22
  import { archiveRoutes } from "./routes/pages/archive.js";
22
23
  import { searchRoutes } from "./routes/pages/search.js";
23
24
  import { featuredRoutes } from "./routes/pages/featured.js";
25
+ import { subscribeRoutes } from "./routes/pages/subscribe.js";
24
26
  import { latestRoutes } from "./routes/pages/latest.js";
25
27
  import { collectionsPageRoutes } from "./routes/pages/collections.js";
26
28
  import { newPostRoutes } from "./routes/pages/new.js";
@@ -37,6 +39,7 @@ import { postsApiRoutes } from "./routes/api/posts.js";
37
39
  import { attachmentsApiRoutes } from "./routes/api/attachments.js";
38
40
  import { navItemsApiRoutes } from "./routes/api/nav-items.js";
39
41
  import { collectionsApiRoutes } from "./routes/api/collections.js";
42
+ import { smartCollectionsApiRoutes } from "./routes/api/smart-collections.js";
40
43
  import { settingsApiRoutes } from "./routes/api/settings.js";
41
44
  import { uploadApiRoutes } from "./routes/api/upload.js";
42
45
  import { uploadsApiRoutes } from "./routes/api/uploads.js";
@@ -59,6 +62,7 @@ import { internalPostBodyHtmlRoutes } from "./routes/api/internal/post-body-html
59
62
  import { internalUploadsRoutes } from "./routes/api/internal/uploads.js";
60
63
  import { publicArchiveApiRoutes } from "./routes/api/public/archive.js";
61
64
  import { publicPostsApiRoutes } from "./routes/api/public/posts.js";
65
+ import { discoverApiRoutes } from "./routes/api/discover.js";
62
66
  // Routes - Compose
63
67
  import { composeRoutes } from "./routes/compose.js";
64
68
 
@@ -71,7 +75,7 @@ import { siteSkillRoutes } from "./routes/site-skill.js";
71
75
  // Middleware
72
76
  import { requireAuth } from "./middleware/auth.js";
73
77
  import { attachSession } from "./middleware/session.js";
74
- import { defaultCacheControl } from "./middleware/cache-control.js";
78
+ import { defaultCacheControl, noStore } from "./middleware/cache-control.js";
75
79
  import { requireRssFeedsEnabled } from "./middleware/public-content-access.js";
76
80
  import { requireOnboarding } from "./middleware/onboarding.js";
77
81
  import { errorHandler } from "./middleware/error-handler.js";
@@ -90,6 +94,10 @@ import {
90
94
  import { isAssetPath } from "./lib/asset-path.js";
91
95
  import { getHostedCanonicalRedirect } from "./lib/hosted-domain.js";
92
96
  import { stripSitePathPrefix, toPublicHref } from "./lib/url.js";
97
+ import {
98
+ matchesIfNoneMatch,
99
+ withConditionalResponse,
100
+ } from "./lib/http-cache.js";
93
101
  import { withWorkerResponseCache } from "./lib/worker-response-cache.js";
94
102
  import { createRequestRuntime } from "./runtime/index.js";
95
103
  import { getInstanceReadiness } from "./runtime/readiness.js";
@@ -265,22 +273,30 @@ export function createApp(): App {
265
273
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
266
274
  const defaultFetch = app.fetch.bind(app);
267
275
 
268
- app.fetch = (request, env, executionCtx) => {
276
+ app.fetch = async (request, env, executionCtx) => {
269
277
  const bindings = env as Bindings | undefined;
270
278
  const preparedRequest = prepareRequestForRouting(
271
279
  request,
272
280
  getConfiguredSingleSitePathPrefix(bindings),
273
281
  );
274
282
  if (preparedRequest instanceof Response) {
275
- return Promise.resolve(preparedRequest);
283
+ return preparedRequest;
276
284
  }
277
- return withWorkerResponseCache({
278
- bindings,
279
- executionCtx,
285
+ // Revalidation sits outside the response cache, so a reader holding the
286
+ // current version is answered from a cache hit as cheaply as from a miss,
287
+ // and a miss still stores the full document on its way out.
288
+ return withConditionalResponse(
280
289
  request,
281
- next: () =>
282
- Promise.resolve(defaultFetch(preparedRequest, bindings, executionCtx)),
283
- });
290
+ await withWorkerResponseCache({
291
+ bindings,
292
+ executionCtx,
293
+ request,
294
+ next: () =>
295
+ Promise.resolve(
296
+ defaultFetch(preparedRequest, bindings, executionCtx),
297
+ ),
298
+ }),
299
+ );
284
300
  };
285
301
 
286
302
  // Global error handler: maps DomainError → HTTP responses
@@ -307,7 +323,11 @@ export function createApp(): App {
307
323
  if (startupConfigError) {
308
324
  return c.html(startupConfigError, 500);
309
325
  }
310
- const runtime = await createRequestRuntime(c.env, publicRequestUrl);
326
+ const runtime = await createRequestRuntime(
327
+ c.env,
328
+ publicRequestUrl,
329
+ c.req.raw,
330
+ );
311
331
  c.set("services", runtime.services);
312
332
  c.set("servicesForSite", runtime.servicesForSite);
313
333
  c.set("hostedHandoff", runtime.hostedHandoff);
@@ -324,12 +344,35 @@ export function createApp(): App {
324
344
  // downstream handlers don't each call auth.api.getSession themselves.
325
345
  app.use("*", attachSession());
326
346
 
327
- // Default every response without an explicit Cache-Control to
328
- // `private, no-store`. Jant pages are auth-variant, so a shared/CDN cache
329
- // must never store them; routes serving genuinely public resources (media,
330
- // feeds, sitemaps, favicons) set their own Cache-Control and are untouched.
347
+ // Default every response without an explicit Cache-Control: `private,
348
+ // no-cache` for an anonymous reader, `private, no-store` once signed in.
349
+ // Jant pages are auth-variant, so a shared/CDN cache must never store them
350
+ // — that is what `private` is for in both — while a reader's own browser
351
+ // keeps the page and its back/forward cache. Routes serving genuinely public
352
+ // resources (media, feeds, sitemaps, favicons) set their own Cache-Control
353
+ // and are untouched.
331
354
  app.use("*", defaultCacheControl());
332
355
 
356
+ // Credential surfaces override that default. Each of these is served to
357
+ // someone with no session yet, so the anonymous branch above would let the
358
+ // browser keep it — and its URL — on disk: a reset link's one-time token, a
359
+ // sign-in form, an SSO handoff. One list rather than a `use()` beside each
360
+ // route group, because it has to be registered up here anyway: `/api/auth/*`
361
+ // ends in a terminal handler that never yields, so a middleware registered
362
+ // after it never runs. The handlers themselves live under "Auth routes"
363
+ // below and in `app.all("/api/auth/*")`; a new one belongs in both places.
364
+ for (const path of [
365
+ "/setup",
366
+ "/signin",
367
+ "/signout",
368
+ "/reset",
369
+ "/__sso",
370
+ "/__dev/login",
371
+ "/api/auth/*",
372
+ ]) {
373
+ app.use(path, noStore());
374
+ }
375
+
333
376
  app.use("*", async (c, next) => {
334
377
  const redirectUrl = await getHostedCanonicalRedirect({
335
378
  currentSite: c.var.currentSite,
@@ -371,8 +414,10 @@ export function createApp(): App {
371
414
  const storage = c.var.storage;
372
415
  if (!storage) return c.notFound();
373
416
 
417
+ // The outer conditional layer would answer this too, but only after the
418
+ // object has been fetched and rendered; matching here skips that work.
374
419
  const etag = `"${media.updatedAt}"`;
375
- if (c.req.header("If-None-Match") === etag) {
420
+ if (matchesIfNoneMatch(c.req.header("If-None-Match"), etag)) {
376
421
  return new Response(null, { status: 304, headers: { ETag: etag } });
377
422
  }
378
423
 
@@ -529,15 +574,18 @@ export function createApp(): App {
529
574
  // API Routes
530
575
  app.route("/api/public/posts", publicPostsApiRoutes);
531
576
  app.route("/api/public/archive", publicArchiveApiRoutes);
577
+ app.route("/api/discover", discoverApiRoutes);
532
578
  app.route("/api/posts", postsApiRoutes);
533
579
  app.route("/api/nav-items", navItemsApiRoutes);
534
580
  app.route("/api/collections", collectionsApiRoutes);
581
+ app.route("/api/smart-collections", smartCollectionsApiRoutes);
535
582
  app.route("/api/settings", settingsApiRoutes);
536
583
  app.route("/api/custom-urls", customUrlsApiRoutes);
537
584
  app.route("/api/export", exportApiRoutes);
538
585
  app.route("/api/github-sync", githubSyncAdminRoutes);
539
586
 
540
- // Auth routes
587
+ // Auth routes. Their paths are also listed in the `noStore()` block above,
588
+ // which is what keeps a credential page out of the browser's disk cache.
541
589
  app.route("/", setupRoutes);
542
590
  app.route("/", signinRoutes);
543
591
  app.route("/", resetRoutes);
@@ -571,6 +619,7 @@ export function createApp(): App {
571
619
  app.route("/", newPostRoutes);
572
620
  app.route("/archive", archiveRoutes);
573
621
  app.route("/featured", featuredRoutes);
622
+ app.route("/subscribe", subscribeRoutes);
574
623
  app.route("/latest", latestRoutes);
575
624
  app.route("/", partialPageRoutes);
576
625
  app.route("/_", brandRoutes);
@@ -579,6 +628,12 @@ export function createApp(): App {
579
628
  app.route("/collections", collectionRoutes);
580
629
  app.route("/", homeRoutes);
581
630
 
631
+ // Per-language views of every surface above. Mounted here on purpose: after
632
+ // the static route groups, so `/archive` is the archive rather than a
633
+ // language named "archive", and before the catch-all, so `/en` is a language
634
+ // view rather than a lookup for a post whose slug is "en".
635
+ app.route("/:lang", languageRoutes);
636
+
582
637
  // Custom page catch-all (must be last)
583
638
  app.route("/", pageRoutes);
584
639
 
package/src/auth.ts CHANGED
@@ -66,7 +66,24 @@ export function createAuth(
66
66
  },
67
67
  },
68
68
  session: {
69
- expiresIn: 3600 * 24 * 30, // 30 days
69
+ // An idle window, not a hard cap. `attachSession` writes better-auth's
70
+ // re-issued cookie back to the browser, so any visit inside the window
71
+ // pushes both the stored session and the cookie out another 90 days —
72
+ // a single author on their own machine should effectively never be
73
+ // asked to sign in again.
74
+ expiresIn: 3600 * 24 * 90, // 90 days
75
+ // How stale a session may get before a read renews it. Same as
76
+ // better-auth's default, but stated here because it sets the renewal
77
+ // cadence and that shouldn't be invisible library behaviour.
78
+ updateAge: 3600 * 24, // 1 day
79
+ // Turns off better-auth's re-authentication gate, whose default lets a
80
+ // session read `/list-sessions` only within 24 hours of sign-in — and it
81
+ // measures that from `createdAt`, which renewal never moves, so the
82
+ // Sessions page would answer "Session is not fresh" from day two onward.
83
+ // The gate also guards nothing here: revoking a session, the destructive
84
+ // half of that page, is not behind it, and the endpoints that would care
85
+ // (delete-user, unlink-account) are not part of Jant.
86
+ freshAge: 0,
70
87
  cookieCache: {
71
88
  enabled: true,
72
89
  maxAge: 60 * 5, // 5 minutes
@@ -11,6 +11,10 @@ vi.mock("../confirm.js", () => ({
11
11
  showConfirmDialog: vi.fn(),
12
12
  }));
13
13
 
14
+ vi.mock("../collection-dialog-host.js", () => ({
15
+ openCollectionDialog: vi.fn(),
16
+ }));
17
+
14
18
  function createMarkup() {
15
19
  document.body.innerHTML = `
16
20
  <div id="toast-container"></div>
@@ -29,9 +33,9 @@ function createMarkup() {
29
33
  More actions
30
34
  </button>
31
35
  <div data-collection-page-menu hidden>
32
- <a href="/collections/original-slug/edit?returnTo=%2Foriginal-slug" role="menuitem">
36
+ <button type="button" role="menuitem" data-collection-page-action="edit">
33
37
  Edit
34
- </a>
38
+ </button>
35
39
  <button
36
40
  type="button"
37
41
  role="menuitem"
@@ -132,6 +136,51 @@ describe("collection detail page actions", () => {
132
136
  expect(window.location.pathname).toBe("/collections");
133
137
  });
134
138
 
139
+ it("edits the collection in the dialog, and follows a moved address", async () => {
140
+ createMarkup();
141
+ const { openCollectionDialog } =
142
+ await import("../collection-dialog-host.js");
143
+ vi.mocked(openCollectionDialog).mockResolvedValue({
144
+ changed: true,
145
+ collection: { id: "collection-1", slug: "renamed", title: "Renamed" },
146
+ });
147
+ await import("../collection-page-actions.js");
148
+
149
+ document
150
+ .querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
151
+ ?.click();
152
+ document
153
+ .querySelector<HTMLElement>("[data-collection-page-action='edit']")
154
+ ?.click();
155
+ await flushAsyncWork();
156
+
157
+ expect(openCollectionDialog).toHaveBeenCalledWith({
158
+ collectionId: "collection-1",
159
+ });
160
+ expect(window.location.pathname).toBe("/renamed");
161
+ });
162
+
163
+ it("leaves the page where it is when the dialog is dismissed", async () => {
164
+ createMarkup();
165
+ const { openCollectionDialog } =
166
+ await import("../collection-dialog-host.js");
167
+ vi.mocked(openCollectionDialog).mockResolvedValue({ changed: false });
168
+ const reload = vi.fn();
169
+ vi.spyOn(window.location, "reload").mockImplementation(reload);
170
+ await import("../collection-page-actions.js");
171
+
172
+ document
173
+ .querySelector<HTMLElement>("[data-collection-page-action='toggle-menu']")
174
+ ?.click();
175
+ document
176
+ .querySelector<HTMLElement>("[data-collection-page-action='edit']")
177
+ ?.click();
178
+ await flushAsyncWork();
179
+
180
+ expect(reload).not.toHaveBeenCalled();
181
+ expect(window.location.pathname).toBe("/original-slug");
182
+ });
183
+
135
184
  it("adds the collection to navigation and reveals the settings action", async () => {
136
185
  createMarkup();
137
186
  const fetchMock = vi.fn().mockResolvedValue(
@@ -0,0 +1,196 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ /**
4
+ * Selecting a photo used to fetch the 3MB HEIC decoder and read the whole
5
+ * file into memory just to check four bytes of it, then decode the processed
6
+ * image a second time for its blurhash. The bridge now sniffs the header
7
+ * itself, imports `heic-to` only for a real HEIC, and takes the blurhash the
8
+ * processor already sampled.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, it, vi } from "vitest";
12
+ import type {
13
+ ComposeAttachment,
14
+ ComposeLabels,
15
+ } from "../components/compose-types.js";
16
+ import type { JantComposeEditor } from "../components/jant-compose-editor.js";
17
+
18
+ const processToFile = vi.fn();
19
+ const uploadViaSession = vi.fn();
20
+ const heicTo = vi.fn();
21
+ const heicToImports = { count: 0 };
22
+
23
+ vi.mock("../image-processor.js", () => ({
24
+ ImageProcessor: {
25
+ processToFile: (...args: unknown[]) => processToFile(...args),
26
+ },
27
+ }));
28
+
29
+ vi.mock("../upload-session.js", () => ({
30
+ uploadViaSession: (...args: unknown[]) => uploadViaSession(...args),
31
+ }));
32
+
33
+ vi.mock("heic-to", () => {
34
+ heicToImports.count += 1;
35
+ return { heicTo: (...args: unknown[]) => heicTo(...args) };
36
+ });
37
+
38
+ await import("../compose-bridge.js");
39
+ await import("../components/jant-compose-editor.js");
40
+
41
+ const PROCESSED = new File(["webp"], "photo.webp", { type: "image/webp" });
42
+ const BLURHASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
43
+
44
+ function jpegFile() {
45
+ return new File(
46
+ [new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0])],
47
+ "photo.jpg",
48
+ {
49
+ type: "image/jpeg",
50
+ },
51
+ );
52
+ }
53
+
54
+ function heicFile() {
55
+ const bytes = new Uint8Array(16);
56
+ bytes.set([...new TextEncoder().encode("ftyp")], 4);
57
+ bytes.set([...new TextEncoder().encode("heic")], 8);
58
+ return new File([bytes], "IMG_0001.HEIC", { type: "image/heic" });
59
+ }
60
+
61
+ function selectFile(file: File) {
62
+ document.dispatchEvent(
63
+ new CustomEvent("jant:files-selected", {
64
+ detail: { files: [{ file, clientId: "attachment-1" }] },
65
+ }),
66
+ );
67
+ }
68
+
69
+ async function settle() {
70
+ for (let i = 0; i < 10; i++) {
71
+ await new Promise((resolve) => setTimeout(resolve, 0));
72
+ }
73
+ }
74
+
75
+ describe("compose bridge image upload", () => {
76
+ beforeEach(() => {
77
+ processToFile.mockReset().mockResolvedValue({
78
+ file: PROCESSED,
79
+ width: 1200,
80
+ height: 900,
81
+ blurhash: BLURHASH,
82
+ });
83
+ uploadViaSession.mockReset().mockResolvedValue({
84
+ id: "med_1",
85
+ filename: "photo.webp",
86
+ url: "/media/photo.webp",
87
+ mimeType: "image/webp",
88
+ size: 4,
89
+ });
90
+ heicTo
91
+ .mockReset()
92
+ .mockResolvedValue(new Blob(["jpeg"], { type: "image/jpeg" }));
93
+ });
94
+
95
+ it("uploads a JPEG with the processor's blurhash and never loads heic-to", async () => {
96
+ const file = jpegFile();
97
+
98
+ selectFile(file);
99
+ await settle();
100
+
101
+ expect(heicToImports.count).toBe(0);
102
+ expect(heicTo).not.toHaveBeenCalled();
103
+ expect(processToFile).toHaveBeenCalledWith(file);
104
+ expect(uploadViaSession).toHaveBeenCalledTimes(1);
105
+ expect(uploadViaSession.mock.calls[0][0]).toBe(PROCESSED);
106
+ expect(uploadViaSession.mock.calls[0][1]).toMatchObject({
107
+ width: 1200,
108
+ height: 900,
109
+ blurhash: BLURHASH,
110
+ });
111
+ });
112
+
113
+ it("converts a file whose bytes say HEIC before processing it", async () => {
114
+ selectFile(heicFile());
115
+ await settle();
116
+
117
+ expect(heicToImports.count).toBe(1);
118
+ expect(heicTo).toHaveBeenCalledWith(
119
+ expect.objectContaining({ type: "image/jpeg" }),
120
+ );
121
+ const converted = processToFile.mock.calls[0][0] as File;
122
+ expect(converted.name).toBe("IMG_0001.jpg");
123
+ expect(converted.type).toBe("image/jpeg");
124
+ expect(uploadViaSession.mock.calls[0][1]).toMatchObject({
125
+ blurhash: BLURHASH,
126
+ });
127
+ });
128
+
129
+ it("reports to the editor that took the post over, not the one it started in", async () => {
130
+ // Entering or leaving thread mode renders the post in a new editor. An
131
+ // upload still running then used to keep reporting to the old one, so the
132
+ // new editor showed "uploading" forever and the file never reached the
133
+ // local draft.
134
+ let finishUpload: (value: unknown) => void = () => {};
135
+ uploadViaSession.mockReset().mockReturnValue(
136
+ new Promise((resolve) => {
137
+ finishUpload = resolve;
138
+ }),
139
+ );
140
+ const editor = async () => {
141
+ const el = document.createElement(
142
+ "jant-compose-editor",
143
+ ) as JantComposeEditor;
144
+ el.labels = {} as ComposeLabels;
145
+ document.body.appendChild(el);
146
+ await el.updateComplete;
147
+ return el;
148
+ };
149
+ const file = jpegFile();
150
+ const pending: ComposeAttachment = {
151
+ clientId: "attachment-1",
152
+ file,
153
+ previewUrl: "blob:preview",
154
+ posterUrl: null,
155
+ status: "pending",
156
+ progress: null,
157
+ mediaId: null,
158
+ remoteUrl: null,
159
+ alt: "",
160
+ error: null,
161
+ summary: null,
162
+ chars: null,
163
+ };
164
+
165
+ const started = await editor();
166
+ started._attachments = [pending];
167
+ started._attachmentOrder = [pending.clientId];
168
+ started.dispatchEvent(
169
+ new CustomEvent("jant:files-selected", {
170
+ bubbles: true,
171
+ detail: { files: [{ file, clientId: pending.clientId }] },
172
+ }),
173
+ );
174
+ await settle();
175
+ expect(uploadViaSession).toHaveBeenCalledTimes(1);
176
+
177
+ started.remove();
178
+ const next = await editor();
179
+ next.takeOver(started);
180
+
181
+ finishUpload({
182
+ id: "med_1",
183
+ filename: "photo.webp",
184
+ url: "/media/photo.webp",
185
+ mimeType: "image/webp",
186
+ size: 4,
187
+ });
188
+ await settle();
189
+
190
+ expect(next._attachments[0]).toMatchObject({
191
+ status: "done",
192
+ mediaId: "med_1",
193
+ remoteUrl: "/media/photo.webp",
194
+ });
195
+ });
196
+ });