@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -211,6 +211,27 @@
211
211
  "when": 1786008671529,
212
212
  "tag": "0029_rare_hairball",
213
213
  "breakpoints": true
214
+ },
215
+ {
216
+ "idx": 30,
217
+ "version": "7",
218
+ "when": 1786069149825,
219
+ "tag": "0030_abandoned_nighthawk",
220
+ "breakpoints": true
221
+ },
222
+ {
223
+ "idx": 31,
224
+ "version": "7",
225
+ "when": 1787282384249,
226
+ "tag": "0031_strong_ultimatum",
227
+ "breakpoints": true
228
+ },
229
+ {
230
+ "idx": 32,
231
+ "version": "7",
232
+ "when": 1787425868269,
233
+ "tag": "0032_furry_multiple_man",
234
+ "breakpoints": true
214
235
  }
215
236
  ]
216
237
  }
@@ -19,26 +19,37 @@ import {
19
19
  customType,
20
20
  } from "drizzle-orm/pg-core";
21
21
  import { sql } from "drizzle-orm";
22
- import { SYSTEM_NAV_KEY_VALUES } from "../../types/constants.js";
23
-
24
- const SITE_STATUSES = ["active", "suspended"] as const;
25
- const SITE_DOMAIN_KINDS = ["primary", "alias"] as const;
26
- const SITE_MEMBER_ROLES = ["owner", "admin", "editor"] as const;
27
- const FORMATS = ["note", "link", "quote"] as const;
28
- const STATUSES = ["draft", "published"] as const;
29
- const VISIBILITIES = ["public", "latest_hidden", "private"] as const;
30
- const COLLECTION_SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
31
- const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
32
- const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
33
- const UPLOAD_SESSION_STATES = [
34
- "pending",
35
- "uploaded",
36
- "completed",
37
- "aborted",
38
- "failed",
39
- ] as const;
40
- const CONTENT_DISPOSITIONS = ["inline", "attachment"] as const;
22
+ import {
23
+ ARCHIVE_LAYOUTS,
24
+ COLLECTION_DIRECTORY_ENTRY_TYPES,
25
+ COLLECTION_SORT_ORDERS,
26
+ CONTENT_DISPOSITIONS,
27
+ EARLIEST_FILTERABLE_YEAR,
28
+ FORMATS,
29
+ GITHUB_APP_ACCOUNT_TYPES,
30
+ LATEST_FILTERABLE_YEAR,
31
+ NAV_ITEM_PLACEMENTS,
32
+ NAV_ITEM_TYPES,
33
+ PATH_KINDS,
34
+ PUBLIC_ARCHIVE_VISIBILITIES,
35
+ SITE_DOMAIN_KINDS,
36
+ SITE_MEMBER_ROLES,
37
+ SITE_STATUSES,
38
+ SMART_COLLECTION_SORT_ORDERS,
39
+ STATUSES,
40
+ SYSTEM_NAV_KEY_VALUES,
41
+ UPLOAD_SESSION_STATES,
42
+ VISIBILITIES,
43
+ } from "../../types/constants.js";
41
44
 
45
+ /**
46
+ * Render a value list as the `IN (...)` body of a CHECK constraint.
47
+ *
48
+ * Every list it is handed comes from `types/constants.ts`. Declaring one
49
+ * locally instead is how a CHECK once shipped missing an enum value: the
50
+ * shared list grew, the schema's private copy did not, and the generated
51
+ * constraint rejected rows the application treats as valid.
52
+ */
42
53
  function sqlTextEnum(values: readonly string[]) {
43
54
  return sql.raw(values.map((value) => `'${value}'`).join(", "));
44
55
  }
@@ -179,6 +190,26 @@ export const posts = pgTable(
179
190
  previewProvider: text("preview_provider"),
180
191
  replyToId: text("reply_to_id"),
181
192
  threadId: text("thread_id").notNull(),
193
+ /**
194
+ * BCP 47 content language in canonical form (`en`, `zh-Hans`).
195
+ *
196
+ * Uniform across a Thread: replies inherit the Root's value on creation and
197
+ * a language change rewrites every row in the Thread. Language filters can
198
+ * therefore be plain column predicates even on member-grained queries.
199
+ *
200
+ * NULL only exists before multilingual content is first enabled; enabling
201
+ * stamps every NULL row with the primary language.
202
+ */
203
+ language: text("language"),
204
+ /**
205
+ * Shared translation-group key (TypeID, `tgr_` prefix). Rows sharing a value
206
+ * are translations of one another — one row per language, no direction.
207
+ *
208
+ * Only ever set on Thread Roots (`thread_id = id`); enforced in the post
209
+ * service so both dialects share one rule (see the SQLite schema for why a
210
+ * table CHECK is not an option there).
211
+ */
212
+ translationGroupId: text("translation_group_id"),
182
213
  /**
183
214
  * The author added this reply without announcing it. Persisted so both
184
215
  * activity timestamps below stay recomputable from the rows alone.
@@ -259,6 +290,12 @@ export const posts = pgTable(
259
290
  .where(
260
291
  sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
261
292
  ),
293
+ // One Post per language inside a translation group. The `site_id` prefix
294
+ // matches the indexing convention here and lets the same index serve
295
+ // "list the members of this group" without a second index.
296
+ uniqueIndex("uq_post_site_translation_group_language")
297
+ .on(table.siteId, table.translationGroupId, table.language)
298
+ .where(sql`${table.translationGroupId} IS NOT NULL`),
262
299
  index("idx_post_search_document").using("gin", table.searchDocument),
263
300
  index("idx_post_search_text_trgm").using(
264
301
  "gin",
@@ -459,6 +496,82 @@ export const collections = pgTable(
459
496
  ],
460
497
  );
461
498
 
499
+ // =============================================================================
500
+ // Smart Collections
501
+ // =============================================================================
502
+
503
+ /**
504
+ * A collection whose members are decided by conditions rather than by tagging.
505
+ *
506
+ * See the SQLite schema for why this is its own table and what `NULL` means in
507
+ * a condition column. The two dialects must stay identical: Drizzle ignores a
508
+ * column it does not know about, so a field added on one side and forgotten on
509
+ * the other loses data silently.
510
+ */
511
+ export const smartCollections = pgTable(
512
+ "smart_collection",
513
+ {
514
+ id: text("id").primaryKey(),
515
+ siteId: text("site_id")
516
+ .notNull()
517
+ .references(() => sites.id, { onDelete: "cascade" }),
518
+ title: text("title").notNull(),
519
+ description: text("description"),
520
+
521
+ // --- Conditions (see lib/filter-dimensions.ts) --------------------------
522
+ format: text("format", { enum: FORMATS }),
523
+ year: integer("year"),
524
+ // No foreign key, alone among this schema's id columns. See the SQLite
525
+ // schema for why the refusal is held in the service instead.
526
+ collectionId: text("collection_id"),
527
+ /** `any` | `none` | comma-joined MEDIA_KINDS. One folded vocabulary. */
528
+ media: text("media"),
529
+ hasTitle: boolean("has_title"),
530
+ hasReplies: boolean("has_replies"),
531
+ visibility: text("visibility", { enum: PUBLIC_ARCHIVE_VISIBILITIES }),
532
+
533
+ // --- Presentation (not conditions) --------------------------------------
534
+ sort: text("sort", { enum: SMART_COLLECTION_SORT_ORDERS })
535
+ .notNull()
536
+ .default("newest"),
537
+ /** NULL follows the site's `ARCHIVE_DEFAULT_LAYOUT`. */
538
+ layout: text("layout", { enum: ARCHIVE_LAYOUTS }),
539
+
540
+ createdAt: integer("created_at").notNull(),
541
+ updatedAt: integer("updated_at").notNull(),
542
+ },
543
+ (table) => [
544
+ check(
545
+ "chk_smart_collection_format",
546
+ sql`${table.format} IS NULL OR ${table.format} IN (${sqlTextEnum(FORMATS)})`,
547
+ ),
548
+ check(
549
+ "chk_smart_collection_visibility",
550
+ sql`${table.visibility} IS NULL OR ${table.visibility} IN (${sqlTextEnum(PUBLIC_ARCHIVE_VISIBILITIES)})`,
551
+ ),
552
+ check(
553
+ "chk_smart_collection_sort",
554
+ sql`${table.sort} IN (${sqlTextEnum(SMART_COLLECTION_SORT_ORDERS)})`,
555
+ ),
556
+ check(
557
+ "chk_smart_collection_layout",
558
+ sql`${table.layout} IS NULL OR ${table.layout} IN (${sqlTextEnum(ARCHIVE_LAYOUTS)})`,
559
+ ),
560
+ check(
561
+ "chk_smart_collection_year",
562
+ sql`${table.year} IS NULL OR ${table.year} BETWEEN ${sql.raw(String(EARLIEST_FILTERABLE_YEAR))} AND ${sql.raw(String(LATEST_FILTERABLE_YEAR))}`,
563
+ ),
564
+ index("idx_smart_collection_site_created_at").on(
565
+ table.siteId,
566
+ table.createdAt,
567
+ ),
568
+ index("idx_smart_collection_site_collection_id").on(
569
+ table.siteId,
570
+ table.collectionId,
571
+ ),
572
+ ],
573
+ );
574
+
462
575
  // =============================================================================
463
576
  // Path Registry (slug + alias + redirect)
464
577
  // =============================================================================
@@ -480,6 +593,10 @@ export const pathRegistry = pgTable(
480
593
  collectionId: text("collection_id").references(() => collections.id, {
481
594
  onDelete: "cascade",
482
595
  }),
596
+ smartCollectionId: text("smart_collection_id").references(
597
+ () => smartCollections.id,
598
+ { onDelete: "cascade" },
599
+ ),
483
600
  redirectToPath: text("redirect_to_path"),
484
601
  redirectType: integer("redirect_type"),
485
602
  archiveQuery: text("archive_query"),
@@ -489,7 +606,7 @@ export const pathRegistry = pgTable(
489
606
  (table) => [
490
607
  check(
491
608
  "chk_path_registry_kind",
492
- sql`${table.kind} IN ('slug', 'alias', 'redirect', 'archive')`,
609
+ sql`${table.kind} IN (${sqlTextEnum(PATH_KINDS)})`,
493
610
  ),
494
611
  uniqueIndex("uq_path_registry_site_path").on(table.siteId, table.path),
495
612
  uniqueIndex("uq_path_registry_site_post_slug")
@@ -498,18 +615,28 @@ export const pathRegistry = pgTable(
498
615
  uniqueIndex("uq_path_registry_site_collection_slug")
499
616
  .on(table.siteId, table.collectionId)
500
617
  .where(sql`${table.kind} = 'slug' AND ${table.collectionId} IS NOT NULL`),
618
+ uniqueIndex("uq_path_registry_site_smart_collection_slug")
619
+ .on(table.siteId, table.smartCollectionId)
620
+ .where(
621
+ sql`${table.kind} = 'slug' AND ${table.smartCollectionId} IS NOT NULL`,
622
+ ),
501
623
  index("idx_path_registry_site_post_id").on(table.siteId, table.postId),
502
624
  index("idx_path_registry_site_collection_id").on(
503
625
  table.siteId,
504
626
  table.collectionId,
505
627
  ),
628
+ index("idx_path_registry_site_smart_collection_id").on(
629
+ table.siteId,
630
+ table.smartCollectionId,
631
+ ),
506
632
  check(
507
633
  "chk_path_registry_shape",
508
634
  sql`(
509
635
  ${table.kind} IN ('slug', 'alias')
510
636
  AND (
511
- (${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL)
512
- OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL)
637
+ (${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NULL)
638
+ OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL AND ${table.smartCollectionId} IS NULL)
639
+ OR (${table.postId} IS NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NOT NULL)
513
640
  )
514
641
  AND ${table.redirectToPath} IS NULL
515
642
  AND ${table.redirectType} IS NULL
@@ -518,6 +645,7 @@ export const pathRegistry = pgTable(
518
645
  ${table.kind} = 'redirect'
519
646
  AND ${table.postId} IS NULL
520
647
  AND ${table.collectionId} IS NULL
648
+ AND ${table.smartCollectionId} IS NULL
521
649
  AND ${table.redirectToPath} IS NOT NULL
522
650
  AND ${table.redirectType} IN (301, 302)
523
651
  AND ${table.archiveQuery} IS NULL
@@ -525,6 +653,7 @@ export const pathRegistry = pgTable(
525
653
  ${table.kind} = 'archive'
526
654
  AND ${table.postId} IS NULL
527
655
  AND ${table.collectionId} IS NULL
656
+ AND ${table.smartCollectionId} IS NULL
528
657
  AND ${table.redirectToPath} IS NULL
529
658
  AND ${table.redirectType} IS NULL
530
659
  AND ${table.archiveQuery} IS NOT NULL
@@ -544,10 +673,16 @@ export const collectionDirectoryItems = pgTable(
544
673
  siteId: text("site_id")
545
674
  .notNull()
546
675
  .references(() => sites.id, { onDelete: "cascade" }),
547
- type: text("type", { enum: ["collection", "divider", "link"] }).notNull(),
676
+ type: text("type", {
677
+ enum: COLLECTION_DIRECTORY_ENTRY_TYPES,
678
+ }).notNull(),
548
679
  collectionId: text("collection_id").references(() => collections.id, {
549
680
  onDelete: "cascade",
550
681
  }),
682
+ smartCollectionId: text("smart_collection_id").references(
683
+ () => smartCollections.id,
684
+ { onDelete: "cascade" },
685
+ ),
551
686
  label: text("label"),
552
687
  url: text("url"),
553
688
  description: text("description"),
@@ -558,12 +693,16 @@ export const collectionDirectoryItems = pgTable(
558
693
  (table) => [
559
694
  check(
560
695
  "chk_collection_directory_item_type",
561
- sql`${table.type} IN ('collection', 'divider', 'link')`,
696
+ sql`${table.type} IN (${sqlTextEnum(COLLECTION_DIRECTORY_ENTRY_TYPES)})`,
562
697
  ),
563
698
  index("idx_collection_directory_item_site_collection_id").on(
564
699
  table.siteId,
565
700
  table.collectionId,
566
701
  ),
702
+ index("idx_collection_directory_item_site_smart_collection_id").on(
703
+ table.siteId,
704
+ table.smartCollectionId,
705
+ ),
567
706
  uniqueIndex("uq_collection_directory_item_site_position").on(
568
707
  table.siteId,
569
708
  table.position,
@@ -573,27 +712,41 @@ export const collectionDirectoryItems = pgTable(
573
712
  .where(
574
713
  sql`${table.type} = 'collection' AND ${table.collectionId} IS NOT NULL`,
575
714
  ),
715
+ uniqueIndex("uq_collection_directory_item_site_smart_collection_once")
716
+ .on(table.siteId, table.smartCollectionId)
717
+ .where(
718
+ sql`${table.type} = 'smart_collection' AND ${table.smartCollectionId} IS NOT NULL`,
719
+ ),
576
720
  check(
577
721
  "chk_collection_directory_item_shape",
578
722
  sql`(
579
723
  ${table.type} = 'collection'
580
724
  AND ${table.collectionId} IS NOT NULL
725
+ AND ${table.smartCollectionId} IS NULL
726
+ AND ${table.label} IS NULL
727
+ AND ${table.url} IS NULL
728
+ ) OR (
729
+ ${table.type} = 'smart_collection'
730
+ AND ${table.collectionId} IS NULL
731
+ AND ${table.smartCollectionId} IS NOT NULL
581
732
  AND ${table.label} IS NULL
582
733
  AND ${table.url} IS NULL
583
734
  ) OR (
584
735
  ${table.type} = 'divider'
585
736
  AND ${table.collectionId} IS NULL
737
+ AND ${table.smartCollectionId} IS NULL
586
738
  AND ${table.url} IS NULL
587
739
  ) OR (
588
740
  ${table.type} = 'link'
589
741
  AND ${table.collectionId} IS NULL
742
+ AND ${table.smartCollectionId} IS NULL
590
743
  AND ${table.label} IS NOT NULL
591
744
  AND ${table.url} IS NOT NULL
592
745
  )`,
593
746
  ),
594
747
  check(
595
748
  "chk_collection_directory_item_label",
596
- sql`${table.type} <> 'collection' OR ${table.label} IS NULL`,
749
+ sql`${table.type} NOT IN ('collection', 'smart_collection') OR ${table.label} IS NULL`,
597
750
  ),
598
751
  check(
599
752
  "chk_collection_directory_item_description",
@@ -659,6 +812,10 @@ export const navItems = pgTable(
659
812
  collectionId: text("collection_id").references(() => collections.id, {
660
813
  onDelete: "cascade",
661
814
  }),
815
+ smartCollectionId: text("smart_collection_id").references(
816
+ () => smartCollections.id,
817
+ { onDelete: "cascade" },
818
+ ),
662
819
  postId: text("post_id").references(() => posts.id, {
663
820
  onDelete: "cascade",
664
821
  }),
@@ -688,21 +845,31 @@ export const navItems = pgTable(
688
845
  ${table.type} = 'link'
689
846
  AND ${table.systemKey} IS NULL
690
847
  AND ${table.collectionId} IS NULL
848
+ AND ${table.smartCollectionId} IS NULL
691
849
  AND ${table.postId} IS NULL
692
850
  ) OR (
693
851
  ${table.type} = 'system'
694
852
  AND ${table.systemKey} IS NOT NULL
695
853
  AND ${table.collectionId} IS NULL
854
+ AND ${table.smartCollectionId} IS NULL
696
855
  AND ${table.postId} IS NULL
697
856
  ) OR (
698
857
  ${table.type} = 'collection'
699
858
  AND ${table.systemKey} IS NULL
700
859
  AND ${table.collectionId} IS NOT NULL
860
+ AND ${table.smartCollectionId} IS NULL
861
+ AND ${table.postId} IS NULL
862
+ ) OR (
863
+ ${table.type} = 'smart_collection'
864
+ AND ${table.systemKey} IS NULL
865
+ AND ${table.collectionId} IS NULL
866
+ AND ${table.smartCollectionId} IS NOT NULL
701
867
  AND ${table.postId} IS NULL
702
868
  ) OR (
703
869
  ${table.type} = 'page'
704
870
  AND ${table.systemKey} IS NULL
705
871
  AND ${table.collectionId} IS NULL
872
+ AND ${table.smartCollectionId} IS NULL
706
873
  AND ${table.postId} IS NOT NULL
707
874
  )`,
708
875
  ),
@@ -713,6 +880,9 @@ export const navItems = pgTable(
713
880
  uniqueIndex("uq_nav_item_site_collection_id")
714
881
  .on(table.siteId, table.collectionId)
715
882
  .where(sql`${table.collectionId} IS NOT NULL`),
883
+ uniqueIndex("uq_nav_item_site_smart_collection_id")
884
+ .on(table.siteId, table.smartCollectionId)
885
+ .where(sql`${table.smartCollectionId} IS NOT NULL`),
716
886
  uniqueIndex("uq_nav_item_site_post_id")
717
887
  .on(table.siteId, table.postId)
718
888
  .where(sql`${table.postId} IS NOT NULL`),
@@ -860,8 +1030,6 @@ export const verification = pgTable("verification", {
860
1030
  // GitHub App Installations (junction table: installation ↔ site, many-to-many)
861
1031
  // ---------------------------------------------------------------------------
862
1032
 
863
- const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
864
-
865
1033
  export const githubAppInstallation = pgTable(
866
1034
  "github_app_installation",
867
1035
  {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Who may see a post — one definition, shared by every surface that counts or
3
+ * lists them.
4
+ *
5
+ * A reply carries no visibility of its own: it inherits the Thread root's, so
6
+ * hiding a root hides the whole thread. Every query that filters by visibility
7
+ * has to resolve that inheritance the same way, and a query that forgets is not
8
+ * loud about it — it returns *more* rows, which looks like working code.
9
+ *
10
+ * That is how the collections directory came to count drafts and private
11
+ * threads into the number it showed a signed-out reader while the collection
12
+ * page, one click away, counted them out. Subtracting one number from the other
13
+ * told a stranger how much unpublished material a collection held.
14
+ *
15
+ * The rule these helpers encode: **the same reader gets the same set on every
16
+ * surface.** A page and the directory entry that links to it must apply this
17
+ * from one place, not two.
18
+ */
19
+
20
+ import { sql, type SQL, type SQLWrapper } from "drizzle-orm";
21
+ import type { Status, Visibility } from "../types/constants.js";
22
+
23
+ /** The `post` columns the visibility rules read, in either dialect's bundle. */
24
+ export interface PostVisibilityColumns {
25
+ status: SQLWrapper;
26
+ visibility: SQLWrapper;
27
+ threadId: SQLWrapper;
28
+ language: SQLWrapper;
29
+ }
30
+
31
+ /**
32
+ * A post's visibility after Thread-root inheritance.
33
+ *
34
+ * Replies store `NULL` and answer with their root's value, so this is the
35
+ * expression to compare against — never the raw column.
36
+ *
37
+ * @param post - The post table's `visibility` and `threadId` columns
38
+ * @param siteId - Site the query is scoped to
39
+ * @returns COALESCE expression yielding a {@link Visibility} value
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const visibility = buildEffectiveVisibilityExpr(posts, siteId);
44
+ * conditions.push(sql`${visibility} != 'private'`);
45
+ * ```
46
+ */
47
+ export function buildEffectiveVisibilityExpr(
48
+ post: Pick<PostVisibilityColumns, "visibility" | "threadId">,
49
+ siteId: string,
50
+ ): SQL<string> {
51
+ return sql<string>`coalesce(
52
+ ${post.visibility},
53
+ (SELECT root.visibility FROM post AS root WHERE root.id = ${post.threadId} AND root.site_id = ${siteId})
54
+ )`;
55
+ }
56
+
57
+ /** What a given reader is allowed to see, in the shape every caller passes. */
58
+ export interface ReaderVisibilityOptions {
59
+ /** Usually `"published"`. Omitted only where drafts are the subject. */
60
+ status?: Status;
61
+ /** Narrow to exactly one visibility, as the archive's filter bar does. */
62
+ visibility?: Visibility;
63
+ /** True for a signed-out reader. */
64
+ excludePrivate?: boolean;
65
+ /** True where `latest_hidden` posts are held back, as on Latest. */
66
+ excludeLatestHidden?: boolean;
67
+ /** Restrict to one content language, for a per-language view. */
68
+ lang?: string;
69
+ }
70
+
71
+ /**
72
+ * The WHERE clauses that narrow posts to what one reader may see.
73
+ *
74
+ * Returns clauses only for the options that are set, so it composes with
75
+ * whatever else a query filters on. Safe in a LEFT JOIN's `ON` clause, which is
76
+ * how a directory keeps listing a collection whose posts this reader cannot
77
+ * see, showing it as empty rather than dropping the row.
78
+ *
79
+ * @param post - The post table's columns
80
+ * @param siteId - Site the query is scoped to
81
+ * @param options - What this reader may see
82
+ * @returns Conditions to spread into `and(...)`
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const conditions = buildReaderVisibilityConditions(posts, siteId, {
87
+ * status: "published",
88
+ * excludePrivate: !isAuthenticated,
89
+ * });
90
+ * ```
91
+ */
92
+ export function buildReaderVisibilityConditions(
93
+ post: PostVisibilityColumns,
94
+ siteId: string,
95
+ options: ReaderVisibilityOptions = {},
96
+ ): SQL[] {
97
+ const effectiveVisibility = buildEffectiveVisibilityExpr(post, siteId);
98
+ const conditions: SQL[] = [];
99
+
100
+ if (options.status) {
101
+ conditions.push(sql`${post.status} = ${options.status}`);
102
+ }
103
+ if (options.visibility !== undefined) {
104
+ conditions.push(sql`${effectiveVisibility} = ${options.visibility}`);
105
+ }
106
+ if (options.excludePrivate) {
107
+ conditions.push(sql`${effectiveVisibility} != 'private'`);
108
+ }
109
+ if (options.excludeLatestHidden) {
110
+ conditions.push(sql`${effectiveVisibility} != 'latest_hidden'`);
111
+ }
112
+ if (options.lang) {
113
+ conditions.push(sql`${post.language} = ${options.lang}`);
114
+ }
115
+
116
+ return conditions;
117
+ }