@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
@@ -12,6 +12,7 @@ import {
12
12
  resolveDatabasePath,
13
13
  resolvePublicRequestUrl,
14
14
  } from "../runtime.js";
15
+ import type { App } from "../../types/app-context.js";
15
16
  import type { Bindings } from "../../types.js";
16
17
 
17
18
  const tempDirs: string[] = [];
@@ -204,6 +205,80 @@ describe("resolveNodeAssetRoot", () => {
204
205
  ).rejects.toThrow("single-site mode found multiple sites in the database:");
205
206
  });
206
207
 
208
+ // A deploy check tells a container that just started from one a rollback
209
+ // left running by this stamp, so it has to be the handler's start and not a
210
+ // time taken per request.
211
+ it("stamps the bindings with when the handler started serving", async () => {
212
+ const root = await mkdtemp(join(tmpdir(), "jant-node-started-at-"));
213
+ tempDirs.push(root);
214
+ const databasePath = join(root, "data", "jant.sqlite");
215
+ await migrate({ DATABASE_URL: `file:${databasePath}` } as Bindings);
216
+
217
+ const before = Math.floor(Date.now() / 1000);
218
+ const handler = await createNodeRequestHandler({
219
+ assetRoot: null,
220
+ env: { DATABASE_URL: `file:${databasePath}` } as Bindings,
221
+ app: {
222
+ fetch: (_request: Request, env: Bindings) =>
223
+ Response.json({ startedAt: env.NODE_STARTED_AT }),
224
+ } as unknown as App,
225
+ });
226
+ const after = Math.floor(Date.now() / 1000);
227
+
228
+ try {
229
+ const read = async () =>
230
+ (
231
+ (await (
232
+ await handler.fetch(new Request("http://127.0.0.1:3000/readyz"))
233
+ ).json()) as { startedAt: number }
234
+ ).startedAt;
235
+ const first = await read();
236
+
237
+ expect(first).toBeGreaterThanOrEqual(before);
238
+ expect(first).toBeLessThanOrEqual(after);
239
+ expect(await read()).toBe(first);
240
+ } finally {
241
+ await handler.close();
242
+ }
243
+ });
244
+
245
+ // The single-site readiness check blames SITE_RESOLUTION_MODE for extra
246
+ // sites, which on a hosted database are just its tenants.
247
+ it("starts in host-based mode when the database holds several sites", async () => {
248
+ const root = await mkdtemp(join(tmpdir(), "jant-node-host-based-sites-"));
249
+ tempDirs.push(root);
250
+ const databasePath = join(root, "data", "jant.sqlite");
251
+
252
+ await migrate({
253
+ DATABASE_URL: `file:${databasePath}`,
254
+ } as Bindings);
255
+
256
+ const sqlite = new Database(databasePath);
257
+ try {
258
+ sqlite
259
+ .prepare(
260
+ `
261
+ INSERT INTO site (id, key, status, created_at, updated_at)
262
+ VALUES
263
+ ('sit_primary00000000000000000000', 'primary', 'active', 1774200000, 1774200000),
264
+ ('sit_extra000000000000000000000', 'extra', 'active', 1774200001, 1774200001)
265
+ `,
266
+ )
267
+ .run();
268
+ } finally {
269
+ sqlite.close();
270
+ }
271
+
272
+ const handler = await createNodeRequestHandler({
273
+ assetRoot: null,
274
+ env: {
275
+ DATABASE_URL: `file:${databasePath}`,
276
+ ...VALID_HOST_BASED_NODE_ENV,
277
+ } as Bindings,
278
+ });
279
+ await handler.close();
280
+ });
281
+
207
282
  it("fails fast when host-based mode is missing hosted control-plane config", async () => {
208
283
  const root = await mkdtemp(join(tmpdir(), "jant-node-host-based-config-"));
209
284
  tempDirs.push(root);
package/src/node/index.ts CHANGED
@@ -6,9 +6,12 @@ export { createNodeRequestHandler } from "./request-handler.js";
6
6
  export {
7
7
  createNodeCliRuntime,
8
8
  createNodeRequestRuntime,
9
+ setUpNodeInstance,
9
10
  } from "../runtime/node.js";
11
+ export type { CliSiteSelector } from "../runtime/site.js";
10
12
  export { createExportService } from "../services/export.js";
11
13
  export { createStorageDriver } from "../lib/storage.js";
14
+ export { getConfiguredStorageDriver } from "../lib/env.js";
12
15
  export { resolveConfig } from "../lib/resolve-config.js";
13
16
  export { buildThemeStyle } from "../lib/theme.js";
14
17
  export { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
@@ -31,6 +31,7 @@ import {
31
31
  shouldTrustProxy,
32
32
  } from "../lib/env.js";
33
33
  import { getHostBasedStartupConfigurationIssues } from "../lib/startup-config.js";
34
+ import { now } from "../lib/time.js";
34
35
  import { createSiteService } from "../services/site.js";
35
36
  import type { App } from "../types/app-context.js";
36
37
  import type { Bindings } from "../types/bindings.js";
@@ -616,7 +617,8 @@ export async function createNodeRequestHandler(options?: {
616
617
  `Host-based startup configuration is invalid:\n- ${hostBasedStartupIssues.map((issue) => issue.message).join("\n- ")}`,
617
618
  );
618
619
  }
619
- const { bindings, close } = await createNodeBindings(env);
620
+ const { bindings: databaseBindings, close } = await createNodeBindings(env);
621
+ const bindings: Bindings = { ...databaseBindings, NODE_STARTED_AT: now() };
620
622
  try {
621
623
  await assertNodeSiteResolutionReady(bindings);
622
624
 
package/src/preset.css CHANGED
@@ -55,6 +55,9 @@ html {
55
55
  * text when a serif font theme is active (Tufte, Bookish, etc.).
56
56
  */
57
57
  @layer components {
58
+ /* Every BaseCoat button variant, not only the default size: a
59
+ `btn-sm-outline` is as much a UI control as a `btn-outline`, and one left
60
+ off this list silently picks up the theme's content font instead. */
58
61
  .btn,
59
62
  .btn-primary,
60
63
  .btn-secondary,
@@ -62,6 +65,41 @@ html {
62
65
  .btn-ghost,
63
66
  .btn-link,
64
67
  .btn-destructive,
68
+ .btn-sm,
69
+ .btn-sm-primary,
70
+ .btn-sm-secondary,
71
+ .btn-sm-outline,
72
+ .btn-sm-ghost,
73
+ .btn-sm-link,
74
+ .btn-sm-destructive,
75
+ .btn-lg,
76
+ .btn-lg-primary,
77
+ .btn-lg-secondary,
78
+ .btn-lg-outline,
79
+ .btn-lg-ghost,
80
+ .btn-lg-link,
81
+ .btn-lg-destructive,
82
+ .btn-icon,
83
+ .btn-icon-primary,
84
+ .btn-icon-secondary,
85
+ .btn-icon-outline,
86
+ .btn-icon-ghost,
87
+ .btn-icon-link,
88
+ .btn-icon-destructive,
89
+ .btn-sm-icon,
90
+ .btn-sm-icon-primary,
91
+ .btn-sm-icon-secondary,
92
+ .btn-sm-icon-outline,
93
+ .btn-sm-icon-ghost,
94
+ .btn-sm-icon-link,
95
+ .btn-sm-icon-destructive,
96
+ .btn-lg-icon,
97
+ .btn-lg-icon-primary,
98
+ .btn-lg-icon-secondary,
99
+ .btn-lg-icon-outline,
100
+ .btn-lg-icon-ghost,
101
+ .btn-lg-icon-link,
102
+ .btn-lg-icon-destructive,
65
103
  .badge,
66
104
  .badge-primary,
67
105
  .badge-secondary,
@@ -191,6 +229,12 @@ html {
191
229
  font-size: var(--type-body-size);
192
230
  line-height: var(--type-body-leading);
193
231
  letter-spacing: var(--type-body-tracking);
232
+ /* Authored text can contain a run with no break opportunity — a long URL, an
233
+ ID, a word typed without spaces. Such a run does not wrap on its own and
234
+ spills past the content column across the page. `break-word` splits it only
235
+ when it cannot fit a line by itself, so ordinary text keeps its natural
236
+ line breaks and intrinsic sizing is unaffected. */
237
+ overflow-wrap: break-word;
194
238
 
195
239
  :where(h1, h2, h3, h4, h5, h6) {
196
240
  font-family: var(--font-heading);
@@ -529,6 +573,30 @@ html {
529
573
  );
530
574
  }
531
575
 
576
+ /*
577
+ * Author text outside `.prose`
578
+ *
579
+ * Titles, quote bodies, and their attributions render authored text without
580
+ * the prose wrapper, so they need the same last-resort break. Without it a
581
+ * single unbroken run overflows the fixed-width content column below.
582
+ */
583
+ .post-detail-title,
584
+ .post-detail-quote,
585
+ .feed-note-title,
586
+ .feed-link-title,
587
+ .feed-quote-content {
588
+ overflow-wrap: break-word;
589
+ }
590
+
591
+ /* Attribution and domain lay their parts out with flex, so each part is a flex
592
+ item that refuses to shrink below its min-content width. `break-word` leaves
593
+ min-content at the longest word and the item still overflows; `anywhere` also
594
+ shrinks min-content, which is what lets the row wrap. */
595
+ .feed-quote-attribution,
596
+ .feed-link-domain {
597
+ overflow-wrap: anywhere;
598
+ }
599
+
532
600
  /*
533
601
  * Tufte content-width constraint.
534
602
  *
@@ -554,7 +622,7 @@ html {
554
622
  [data-post-body].prose,
555
623
  [data-post-meta],
556
624
  [data-post-end] {
557
- width: var(--layout-content-width);
625
+ width: var(--layout-reading-width);
558
626
  }
559
627
 
560
628
  .post-detail-title {
@@ -571,7 +639,7 @@ html {
571
639
  actions follow the gallery's right edge. */
572
640
  .post-attached-group {
573
641
  width: fit-content;
574
- min-width: var(--layout-content-width);
642
+ min-width: var(--layout-reading-width);
575
643
  max-width: 100%;
576
644
  }
577
645
 
@@ -579,26 +647,12 @@ html {
579
647
  width: 100%;
580
648
  }
581
649
 
650
+ /* Everything above already tracks the reading column through the token; only
651
+ the title needs saying twice, because its desktop measure is narrower than
652
+ the column (Tufte sets titles short) and there is no gutter left to keep it
653
+ short for down here. */
582
654
  @media (max-width: 1024px) {
583
- [data-page="search"],
584
- .site-home-header,
585
- .post-detail-title,
586
- .post-detail-body,
587
- .post-body-summary,
588
- .feed-note-title,
589
- .feed-link-title,
590
- .feed-quote,
591
- .feed-quote-attribution,
592
- .feed-quote-commentary,
593
- .feed-link-domain,
594
- .feed-continue-link,
595
- [data-post-body].prose,
596
- [data-post-meta],
597
- [data-post-end] {
598
- width: min(100%, 35rem);
599
- }
600
-
601
- .post-attached-group {
602
- min-width: min(100%, 35rem);
655
+ .post-detail-title {
656
+ width: var(--layout-reading-width);
603
657
  }
604
658
  }
@@ -0,0 +1,156 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../__tests__/helpers/app.js";
3
+ import { archiveRoutes } from "../pages/archive.js";
4
+ import { collectionRoutes } from "../pages/collection.js";
5
+ import { collectionsPageRoutes } from "../pages/collections.js";
6
+ import { featuredRoutes } from "../pages/featured.js";
7
+ import { latestRoutes } from "../pages/latest.js";
8
+ import { pageRoutes } from "../pages/page.js";
9
+ import { feedRoutes } from "../feed/feed.js";
10
+
11
+ /**
12
+ * The validator every feed surface carries.
13
+ *
14
+ * Six route modules assemble a feed, and each used to build its own Response.
15
+ * They now share `renderFeed`, so what these tests hold in place is that the
16
+ * sharing is real — a tag on every feed, not just on `/feed`, whose tests are
17
+ * the ones anyone remembers to run.
18
+ *
19
+ * Turning that tag into a `304` belongs to `withConditionalResponse` at the
20
+ * edge of the app, and is covered where it lives.
21
+ */
22
+
23
+ const ETAG = /^"[0-9a-f]{32}"$/;
24
+
25
+ function setup() {
26
+ const testApp = createTestApp({ rssPublishDelaySeconds: 0 });
27
+ const { app } = testApp;
28
+
29
+ app.use("*", async (c, next) => {
30
+ c.set("publicPath", c.req.path);
31
+ c.set("publicRequestUrl", c.req.url);
32
+ await next();
33
+ });
34
+
35
+ app.route("/feed", feedRoutes);
36
+ app.route("/latest", latestRoutes);
37
+ app.route("/featured", featuredRoutes);
38
+ app.route("/archive", archiveRoutes);
39
+ app.route("/collections", collectionsPageRoutes);
40
+ app.route("/collections", collectionRoutes);
41
+ // The root namespace matches any slug, so it is mounted last.
42
+ app.route("/", pageRoutes);
43
+
44
+ return testApp;
45
+ }
46
+
47
+ /**
48
+ * Seed every surface with an entry.
49
+ *
50
+ * An empty feed has no content timestamp to date itself by and falls back to
51
+ * the current time, so its bytes — and its tag — change on every poll. Feeds
52
+ * under test therefore all carry content.
53
+ */
54
+ async function seed(services: ReturnType<typeof createTestApp>["services"]) {
55
+ await services.posts.create({
56
+ format: "note",
57
+ title: "Featured note",
58
+ bodyMarkdown: "Featured body",
59
+ status: "published",
60
+ featured: true,
61
+ });
62
+ await services.posts.create({
63
+ format: "quote",
64
+ title: "A quote",
65
+ quoteText: "Something worth keeping",
66
+ bodyMarkdown: "Quote body",
67
+ status: "published",
68
+ });
69
+
70
+ for (const [slug, title] of [
71
+ ["reading", "Reading"],
72
+ ["movies", "Movies"],
73
+ ] as const) {
74
+ const collection = await services.collections.create({ slug, title });
75
+ const post = await services.posts.create({
76
+ format: "note",
77
+ title: `${title} log`,
78
+ bodyMarkdown: `${title} body`,
79
+ status: "published",
80
+ });
81
+ await services.collections.addThread(collection.id, post.id);
82
+ }
83
+
84
+ await services.smartCollections.create({
85
+ slug: "quotes",
86
+ title: "Quotes",
87
+ selection: { format: "quote" },
88
+ });
89
+ }
90
+
91
+ const FEED_PATHS = [
92
+ ["site main feed", "/feed"],
93
+ ["latest", "/latest/feed"],
94
+ ["featured", "/featured/feed"],
95
+ ["archive", "/archive/feed"],
96
+ ["single collection", "/reading/feed"],
97
+ ["collection selection", "/collections/reading+movies/feed"],
98
+ ["smart collection", "/quotes/feed"],
99
+ ] as const;
100
+
101
+ describe("feed conditional GET", () => {
102
+ let app: ReturnType<typeof createTestApp>["app"];
103
+ let services: ReturnType<typeof createTestApp>["services"];
104
+
105
+ beforeEach(async () => {
106
+ const testApp = setup();
107
+ app = testApp.app;
108
+ services = testApp.services;
109
+ await seed(services);
110
+ });
111
+
112
+ describe.each(FEED_PATHS)("%s (%s)", (_name, path) => {
113
+ it("serves the feed with a strong validator", async () => {
114
+ const res = await app.request(path);
115
+
116
+ expect(res.status).toBe(200);
117
+ expect(res.headers.get("ETag")).toMatch(ETAG);
118
+ expect(res.headers.get("Cache-Control")).toBe("public, max-age=60");
119
+ expect(res.headers.get("Content-Type")).toBe(
120
+ "application/atom+xml; charset=utf-8",
121
+ );
122
+ expect(await res.text()).toContain("<feed");
123
+ });
124
+
125
+ it("re-derives the same tag for an unchanged feed", async () => {
126
+ const first = await app.request(path);
127
+ const second = await app.request(path);
128
+
129
+ expect(second.headers.get("ETag")).toBe(first.headers.get("ETag"));
130
+ });
131
+ });
132
+
133
+ it("issues a new tag once the feed changes", async () => {
134
+ const before = await app.request("/latest/feed");
135
+ const staleTag = before.headers.get("ETag") as string;
136
+
137
+ await services.posts.create({
138
+ format: "note",
139
+ title: "Published after the poll",
140
+ bodyMarkdown: "New body",
141
+ status: "published",
142
+ });
143
+
144
+ const after = await app.request("/latest/feed");
145
+
146
+ expect(after.headers.get("ETag")).not.toBe(staleTag);
147
+ expect(await after.text()).toContain("Published after the poll");
148
+ });
149
+
150
+ it("gives two different feeds two different tags", async () => {
151
+ const latest = await app.request("/latest/feed");
152
+ const featured = await app.request("/featured/feed");
153
+
154
+ expect(featured.headers.get("ETag")).not.toBe(latest.headers.get("ETag"));
155
+ });
156
+ });
@@ -45,6 +45,30 @@ describe("Collections API Routes", () => {
45
45
  expect(body.directoryItems[0].collectionId).toBe(col.id);
46
46
  });
47
47
 
48
+ it("returns smart collections with the same two measures", async () => {
49
+ const { app, services } = createTestApp();
50
+ app.route("/api/collections", collectionsApiRoutes);
51
+
52
+ await services.smartCollections.create({
53
+ slug: "quotes",
54
+ title: "Quotes",
55
+ selection: { format: "quote" },
56
+ });
57
+ const quote = await services.posts.create({
58
+ format: "quote",
59
+ quoteText: "Worth keeping",
60
+ });
61
+
62
+ const res = await app.request("/api/collections");
63
+ const body = await res.json();
64
+
65
+ expect(body.smartCollections).toHaveLength(1);
66
+ expect(body.smartCollections[0].threadCount).toBe(1);
67
+ expect(body.smartCollections[0].recentActivityAt).toBe(
68
+ quote.lastActivityAt,
69
+ );
70
+ });
71
+
48
72
  it("returns divider labels", async () => {
49
73
  const { app, services } = createTestApp({ authenticated: true });
50
74
  app.route("/api/collections", collectionsApiRoutes);
@@ -143,6 +167,61 @@ describe("Collections API Routes", () => {
143
167
  });
144
168
  });
145
169
 
170
+ describe("GET /api/collections/slug", () => {
171
+ it("answers whether an address is free", async () => {
172
+ const { app, services } = createTestApp({ authenticated: true });
173
+ app.route("/api/collections", collectionsApiRoutes);
174
+
175
+ await services.collections.create({ slug: "tech", title: "Tech" });
176
+
177
+ const taken = await app.request(
178
+ "/api/collections/slug?mode=check&slug=tech",
179
+ );
180
+ expect(taken.status).toBe(200);
181
+ expect(await taken.json()).toEqual({ slug: "tech", available: false });
182
+
183
+ const free = await app.request(
184
+ "/api/collections/slug?mode=check&slug=reading",
185
+ );
186
+ expect(await free.json()).toEqual({ slug: "reading", available: true });
187
+ });
188
+
189
+ it("ignores the address the named collection already holds", async () => {
190
+ const { app, services } = createTestApp({ authenticated: true });
191
+ app.route("/api/collections", collectionsApiRoutes);
192
+
193
+ const collection = await services.collections.create({
194
+ slug: "tech",
195
+ title: "Tech",
196
+ });
197
+
198
+ const res = await app.request(
199
+ `/api/collections/slug?mode=check&slug=tech&collectionId=${collection.id}`,
200
+ );
201
+ expect(await res.json()).toEqual({ slug: "tech", available: true });
202
+ });
203
+
204
+ it("refuses a reserved address rather than calling it taken", async () => {
205
+ const { app } = createTestApp({ authenticated: true });
206
+ app.route("/api/collections", collectionsApiRoutes);
207
+
208
+ const res = await app.request(
209
+ "/api/collections/slug?mode=check&slug=settings",
210
+ );
211
+ expect(res.status).toBe(400);
212
+ });
213
+
214
+ it("returns 401 when not authenticated", async () => {
215
+ const { app } = createTestApp({ authenticated: false });
216
+ app.route("/api/collections", collectionsApiRoutes);
217
+
218
+ const res = await app.request(
219
+ "/api/collections/slug?mode=check&slug=tech",
220
+ );
221
+ expect(res.status).toBe(401);
222
+ });
223
+ });
224
+
146
225
  describe("POST /api/collections", () => {
147
226
  it("returns 401 when not authenticated", async () => {
148
227
  const { app } = createTestApp({ authenticated: false });
@@ -458,6 +537,41 @@ describe("Collections API Routes", () => {
458
537
  expect(reordered[1]?.id).toBe(itemC?.id);
459
538
  expect(reordered[2]?.id).toBe(itemB?.id);
460
539
  });
540
+
541
+ it("moves a smart collection, which the directory names by its own id", async () => {
542
+ const { app, services } = createTestApp({ authenticated: true });
543
+ app.route("/api/collections", collectionsApiRoutes);
544
+
545
+ const a = await services.collections.create({ slug: "a", title: "A" });
546
+ const b = await services.collections.create({ slug: "b", title: "B" });
547
+ const quotes = await services.smartCollections.create({
548
+ slug: "quotes",
549
+ title: "Quotes",
550
+ });
551
+
552
+ const items = await services.collections.listDirectoryItems();
553
+
554
+ // The shape the drag surface sends: a smart collection id beside a
555
+ // directory row id.
556
+ const res = await app.request(
557
+ `/api/collections/directory-items/${quotes.id}/move`,
558
+ {
559
+ method: "PUT",
560
+ headers: { "Content-Type": "application/json" },
561
+ body: JSON.stringify({
562
+ after: items[0]?.id ?? "",
563
+ before: items[1]?.id ?? "",
564
+ }),
565
+ },
566
+ );
567
+
568
+ expect(res.status).toBe(200);
569
+
570
+ const reordered = await services.collections.listDirectoryItems();
571
+ expect(
572
+ reordered.map((item) => item.collectionId ?? item.smartCollectionId),
573
+ ).toEqual([a.id, quotes.id, b.id]);
574
+ });
461
575
  });
462
576
 
463
577
  describe("POST /api/collections/:id/threads", () => {