@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
@@ -7,17 +7,23 @@ import { run } from "../../../bin/commands/reset-password.js";
7
7
  import { migrate } from "../runtime.js";
8
8
  import type { Bindings } from "../../types.js";
9
9
  import { createBootstrapService } from "../../services/bootstrap.js";
10
+ import { createSiteService } from "../../services/site.js";
10
11
  import { createNodeDatabase } from "../../db/index.js";
11
12
 
12
13
  describe("jant reset-password", () => {
13
14
  const tempDirs: string[] = [];
14
- const originalEnv = process.env.DATABASE_URL;
15
+ const originalEnv = {
16
+ DATABASE_URL: process.env.DATABASE_URL,
17
+ SITE_RESOLUTION_MODE: process.env.SITE_RESOLUTION_MODE,
18
+ };
15
19
 
16
20
  afterEach(async () => {
17
- if (originalEnv === undefined) {
18
- delete process.env.DATABASE_URL;
19
- } else {
20
- process.env.DATABASE_URL = originalEnv;
21
+ for (const [key, value] of Object.entries(originalEnv)) {
22
+ if (value === undefined) {
23
+ delete process.env[key];
24
+ } else {
25
+ process.env[key] = value;
26
+ }
21
27
  }
22
28
 
23
29
  await Promise.all(
@@ -37,7 +43,14 @@ describe("jant reset-password", () => {
37
43
 
38
44
  const sqlite = new Database(databasePath);
39
45
  try {
40
- const bootstrap = createBootstrapService(createNodeDatabase(sqlite));
46
+ const db = createNodeDatabase(sqlite);
47
+ // Each setup screen is its own request, bound to the site it resolved:
48
+ // none before the account step, the one it created after.
49
+ const bootstrapForRequest = async () =>
50
+ createBootstrapService(
51
+ db,
52
+ (await createSiteService(db).resolveSingleSite()).site.id,
53
+ );
41
54
  await sqlite
42
55
  .prepare(
43
56
  `
@@ -53,10 +66,17 @@ describe("jant reset-password", () => {
53
66
  new Date().toISOString(),
54
67
  new Date().toISOString(),
55
68
  );
56
- await bootstrap.completeInitialSetup({
69
+ await (
70
+ await bootstrapForRequest()
71
+ ).provisionOwnerAccount({
57
72
  ownerUserId: "usr_reset_password_test",
58
- siteName: "Reset Password Test",
59
73
  });
74
+ await (
75
+ await bootstrapForRequest()
76
+ ).completeSiteSetup(
77
+ { siteName: "Reset Password Test" },
78
+ { oldLanguage: "" },
79
+ );
60
80
  } finally {
61
81
  sqlite.close();
62
82
  }
@@ -88,4 +108,57 @@ describe("jant reset-password", () => {
88
108
  expect.stringMatching(/^Visit: \/reset\?token=[a-f0-9]{64}$/),
89
109
  );
90
110
  });
111
+
112
+ // A hosted database holds every tenant, so the token goes to the site the
113
+ // operator named — and nowhere when they named none.
114
+ it("writes the token to the site --site names on a host-based database", async () => {
115
+ const root = await mkdtemp(join(tmpdir(), "jant-reset-password-hosted-"));
116
+ tempDirs.push(root);
117
+
118
+ const databasePath = join(root, "jant.sqlite");
119
+ await migrate({ DATABASE_URL: `file:${databasePath}` } as Bindings);
120
+ process.env.DATABASE_URL = `file:${databasePath}`;
121
+ process.env.SITE_RESOLUTION_MODE = "host-based";
122
+
123
+ const sqlite = new Database(databasePath);
124
+ try {
125
+ sqlite
126
+ .prepare(
127
+ `
128
+ INSERT INTO site (id, key, status, created_at, updated_at)
129
+ VALUES
130
+ ('sit_alpha000000000000000000000', 'alpha', 'active', 1774200000, 1774200000),
131
+ ('sit_beta0000000000000000000000', 'beta', 'active', 1774200001, 1774200001)
132
+ `,
133
+ )
134
+ .run();
135
+ } finally {
136
+ sqlite.close();
137
+ }
138
+
139
+ vi.spyOn(console, "log").mockImplementation(() => {});
140
+
141
+ await expect(run([])).rejects.toThrow(
142
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.",
143
+ );
144
+ await run(["--site", "beta"]);
145
+
146
+ const sqliteReadonly = new Database(databasePath, { readonly: true });
147
+ try {
148
+ const owners = sqliteReadonly
149
+ .prepare(
150
+ `
151
+ SELECT site_id
152
+ FROM "site_setting"
153
+ WHERE "key" = 'PASSWORD_RESET_TOKEN'
154
+ `,
155
+ )
156
+ .pluck()
157
+ .all();
158
+
159
+ expect(owners).toEqual(["sit_beta0000000000000000000000"]);
160
+ } finally {
161
+ sqliteReadonly.close();
162
+ }
163
+ });
91
164
  });
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { loadNodeEnvFile } from "../../../bin/lib/node-env.js";
5
+ import { findNodeEnvPath, loadNodeEnvFile } from "../../../bin/lib/node-env.js";
6
6
  import {
7
7
  formatRuntimeBanner,
8
8
  resolveCliRuntime,
@@ -106,6 +106,53 @@ describe("loadNodeEnvFile", () => {
106
106
  });
107
107
  });
108
108
 
109
+ describe("findNodeEnvPath", () => {
110
+ let tmpDir: string;
111
+
112
+ beforeEach(() => {
113
+ tmpDir = mkdtempSync(join(tmpdir(), "jant-env-path-"));
114
+ });
115
+
116
+ it("finds .env.node in the given directory when JANT_ENV_FILE is unset", () => {
117
+ const envPath = join(tmpDir, ".env.node");
118
+ writeFileSync(envPath, "DATABASE_URL=file:./jant.sqlite\n");
119
+
120
+ expect(findNodeEnvPath(tmpDir, {})).toBe(envPath);
121
+ });
122
+
123
+ it("loads nothing when JANT_ENV_FILE is empty", () => {
124
+ writeFileSync(
125
+ join(tmpDir, ".env.node"),
126
+ "DATABASE_URL=file:./local.sqlite\n",
127
+ );
128
+
129
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "" })).toBeNull();
130
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: " " })).toBeNull();
131
+ });
132
+
133
+ it("uses the file JANT_ENV_FILE names, resolved against the directory", () => {
134
+ writeFileSync(
135
+ join(tmpDir, ".env.node"),
136
+ "DATABASE_URL=file:./local.sqlite\n",
137
+ );
138
+ const overridePath = join(tmpDir, "staging.env");
139
+ writeFileSync(overridePath, "DATABASE_URL=postgres://example/db\n");
140
+
141
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "staging.env" })).toBe(
142
+ overridePath,
143
+ );
144
+ expect(findNodeEnvPath(tmpDir, { JANT_ENV_FILE: overridePath })).toBe(
145
+ overridePath,
146
+ );
147
+ });
148
+
149
+ it("throws when JANT_ENV_FILE names a file that does not exist", () => {
150
+ expect(() =>
151
+ findNodeEnvPath(tmpDir, { JANT_ENV_FILE: "missing.env" }),
152
+ ).toThrow(/JANT_ENV_FILE points at "missing.env"/);
153
+ });
154
+ });
155
+
109
156
  describe("formatRuntimeBanner", () => {
110
157
  const originalLog = console.log;
111
158
  beforeEach(() => {
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { resolveCliSite } from "../../../bin/lib/site-selection.js";
2
+ import {
3
+ parseCliSiteSelector,
4
+ resolveCliSite,
5
+ } from "../../../bin/lib/site-selection.js";
3
6
 
4
7
  function createSiteRow(id, key) {
5
8
  return {
@@ -11,6 +14,8 @@ function createSiteRow(id, key) {
11
14
  };
12
15
  }
13
16
 
17
+ const HOST_BASED_ENV = { SITE_RESOLUTION_MODE: "host-based" };
18
+
14
19
  describe("CLI site selection", () => {
15
20
  it("resolves a host-scoped site in host-based mode", async () => {
16
21
  const query = vi.fn(async (sql) => {
@@ -26,9 +31,7 @@ describe("CLI site selection", () => {
26
31
  query,
27
32
  },
28
33
  {
29
- env: {
30
- SITE_RESOLUTION_MODE: "host-based",
31
- },
34
+ env: HOST_BASED_ENV,
32
35
  host: "demo.jant.blog",
33
36
  },
34
37
  );
@@ -54,9 +57,7 @@ describe("CLI site selection", () => {
54
57
  query,
55
58
  },
56
59
  {
57
- env: {
58
- SITE_RESOLUTION_MODE: "host-based",
59
- },
60
+ env: HOST_BASED_ENV,
60
61
  url: "https://demo.jant.blog/base/",
61
62
  },
62
63
  );
@@ -64,49 +65,91 @@ describe("CLI site selection", () => {
64
65
  expect(resolved.site.key).toBe("demo");
65
66
  });
66
67
 
67
- it("falls back to the only site in host-based mode", async () => {
68
- const query = vi.fn(async (sql) => {
69
- if (sql.includes('FROM "site_domain"')) {
70
- return [];
71
- }
72
-
73
- return [createSiteRow("sit_only", "only")];
74
- });
68
+ it("resolves --site by key or by id", async () => {
69
+ const sites = [
70
+ createSiteRow("sit_one", "one"),
71
+ createSiteRow("sit_two", "two"),
72
+ ];
73
+ const query = vi.fn(async (sql) =>
74
+ sites.filter(
75
+ (site) =>
76
+ sql.includes(`"id" = '${site.id}'`) ||
77
+ sql.includes(`"key" = '${site.key}'`),
78
+ ),
79
+ );
75
80
 
76
- const resolved = await resolveCliSite(
77
- {
78
- query,
79
- },
80
- {
81
- env: {
82
- SITE_RESOLUTION_MODE: "host-based",
83
- },
84
- },
81
+ const byKey = await resolveCliSite(
82
+ { query },
83
+ { env: HOST_BASED_ENV, site: "two" },
84
+ );
85
+ const byId = await resolveCliSite(
86
+ { query },
87
+ { env: HOST_BASED_ENV, site: "sit_one" },
85
88
  );
86
89
 
87
- expect(resolved.site.id).toBe("sit_only");
90
+ expect(byKey.site.id).toBe("sit_two");
91
+ expect(byId.site.id).toBe("sit_one");
88
92
  });
89
93
 
90
- it("requires explicit selection for host-based multi-site instances", async () => {
94
+ // Unreachable through the managed API, whose keys cannot contain `_`, but a
95
+ // hand-edited row must not make `--site` pick a tenant by accident.
96
+ it("prefers the id match when a key equals another site's id", async () => {
91
97
  const query = vi.fn(async () => [
92
- createSiteRow("sit_one", "one"),
93
- createSiteRow("sit_two", "two"),
98
+ createSiteRow("sit_other", "sit_target"),
99
+ createSiteRow("sit_target", "target"),
94
100
  ]);
95
101
 
102
+ const resolved = await resolveCliSite(
103
+ { query },
104
+ { env: HOST_BASED_ENV, site: "sit_target" },
105
+ );
106
+
107
+ expect(resolved.site.key).toBe("target");
108
+ });
109
+
110
+ it("names the --site value that matched nothing", async () => {
111
+ const query = vi.fn(async () => []);
112
+
113
+ await expect(
114
+ resolveCliSite({ query }, { env: HOST_BASED_ENV, site: "missing" }),
115
+ ).rejects.toThrow("No site found for --site missing.");
116
+ });
117
+
118
+ // A hosted database holds every tenant. Falling back to "the only site"
119
+ // would work until the day a second tenant signs up, so host-based mode
120
+ // never guesses — it fails before running a single query.
121
+ it("requires a site flag in host-based mode, however many sites exist", async () => {
122
+ const query = vi.fn(async () => [createSiteRow("sit_only", "only")]);
123
+
124
+ await expect(
125
+ resolveCliSite({ query }, { env: HOST_BASED_ENV }),
126
+ ).rejects.toThrow(
127
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.",
128
+ );
129
+ expect(query).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it("rejects more than one site flag instead of picking one", async () => {
133
+ const query = vi.fn(async () => []);
134
+
96
135
  await expect(
97
136
  resolveCliSite(
98
- {
99
- query,
100
- },
101
- {
102
- env: {
103
- SITE_RESOLUTION_MODE: "host-based",
104
- },
105
- },
137
+ { query },
138
+ { env: HOST_BASED_ENV, site: "one", host: "two.jant.blog" },
106
139
  ),
107
- ).rejects.toThrow(
108
- "host-based mode requires --site, --host, or --url when the database contains multiple sites.",
140
+ ).rejects.toThrow("Choose only one of --site, --host, or --url.");
141
+ expect(query).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("uses the only site in single-site mode", async () => {
145
+ const query = vi.fn(async () => [createSiteRow("sit_only", "only")]);
146
+
147
+ const resolved = await resolveCliSite(
148
+ { query },
149
+ { env: { SITE_RESOLUTION_MODE: "single-site" } },
109
150
  );
151
+
152
+ expect(resolved.site.id).toBe("sit_only");
110
153
  });
111
154
 
112
155
  it("returns an actionable message for single-site multi-site instances", async () => {
@@ -131,3 +174,50 @@ describe("CLI site selection", () => {
131
174
  );
132
175
  });
133
176
  });
177
+
178
+ describe("parseCliSiteSelector", () => {
179
+ it("returns null when no site flag was passed", () => {
180
+ expect(parseCliSiteSelector({ pathPrefix: "/blog" })).toBeNull();
181
+ });
182
+
183
+ it("reads --site as a key or id", () => {
184
+ expect(parseCliSiteSelector({ site: " demo " })).toEqual({
185
+ kind: "site",
186
+ idOrKey: "demo",
187
+ });
188
+ });
189
+
190
+ it("splits --url into its host and path prefix", () => {
191
+ expect(parseCliSiteSelector({ url: "https://demo.jant.blog/" })).toEqual({
192
+ kind: "host",
193
+ host: "demo.jant.blog",
194
+ pathPrefix: null,
195
+ });
196
+ expect(
197
+ parseCliSiteSelector({ url: "https://demo.jant.blog/blog/" }),
198
+ ).toEqual({
199
+ kind: "host",
200
+ host: "demo.jant.blog",
201
+ pathPrefix: "/blog",
202
+ });
203
+ });
204
+
205
+ it("pairs --host with --path-prefix", () => {
206
+ expect(
207
+ parseCliSiteSelector({ host: "demo.jant.blog", pathPrefix: "blog/" }),
208
+ ).toEqual({
209
+ kind: "host",
210
+ host: "demo.jant.blog",
211
+ pathPrefix: "/blog",
212
+ });
213
+ });
214
+
215
+ it("rejects --url together with --host", () => {
216
+ expect(() =>
217
+ parseCliSiteSelector({
218
+ host: "demo.jant.blog",
219
+ url: "https://demo.jant.blog/",
220
+ }),
221
+ ).toThrow("Choose only one of --site, --host, or --url.");
222
+ });
223
+ });
@@ -204,6 +204,43 @@ describe("resolveNodeAssetRoot", () => {
204
204
  ).rejects.toThrow("single-site mode found multiple sites in the database:");
205
205
  });
206
206
 
207
+ // The single-site readiness check blames SITE_RESOLUTION_MODE for extra
208
+ // sites, which on a hosted database are just its tenants.
209
+ it("starts in host-based mode when the database holds several sites", async () => {
210
+ const root = await mkdtemp(join(tmpdir(), "jant-node-host-based-sites-"));
211
+ tempDirs.push(root);
212
+ const databasePath = join(root, "data", "jant.sqlite");
213
+
214
+ await migrate({
215
+ DATABASE_URL: `file:${databasePath}`,
216
+ } as Bindings);
217
+
218
+ const sqlite = new Database(databasePath);
219
+ try {
220
+ sqlite
221
+ .prepare(
222
+ `
223
+ INSERT INTO site (id, key, status, created_at, updated_at)
224
+ VALUES
225
+ ('sit_primary00000000000000000000', 'primary', 'active', 1774200000, 1774200000),
226
+ ('sit_extra000000000000000000000', 'extra', 'active', 1774200001, 1774200001)
227
+ `,
228
+ )
229
+ .run();
230
+ } finally {
231
+ sqlite.close();
232
+ }
233
+
234
+ const handler = await createNodeRequestHandler({
235
+ assetRoot: null,
236
+ env: {
237
+ DATABASE_URL: `file:${databasePath}`,
238
+ ...VALID_HOST_BASED_NODE_ENV,
239
+ } as Bindings,
240
+ });
241
+ await handler.close();
242
+ });
243
+
207
244
  it("fails fast when host-based mode is missing hosted control-plane config", async () => {
208
245
  const root = await mkdtemp(join(tmpdir(), "jant-node-host-based-config-"));
209
246
  tempDirs.push(root);
package/src/node/index.ts CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  createNodeCliRuntime,
8
8
  createNodeRequestRuntime,
9
9
  } from "../runtime/node.js";
10
+ export type { CliSiteSelector } from "../runtime/site.js";
10
11
  export { createExportService } from "../services/export.js";
11
12
  export { createStorageDriver } from "../lib/storage.js";
12
13
  export { resolveConfig } from "../lib/resolve-config.js";
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
  }