@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. 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
  }