@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,402 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import type Database from "better-sqlite3";
3
+ import {
4
+ createTestDatabase,
5
+ DEFAULT_TEST_SITE_ID,
6
+ } from "../../__tests__/helpers/db.js";
7
+
8
+ /**
9
+ * The three tables a smart collection can be placed in, checked by inserting
10
+ * real rows.
11
+ *
12
+ * Reading a CHECK constraint and believing it is not the same as running it.
13
+ * The last time these constraints were written by hand, a test shaped exactly
14
+ * like this one found three bugs in a single pass: a branch that forgot to
15
+ * require its own foreign key, a branch that allowed two of them at once, and a
16
+ * unique index whose partial predicate never matched.
17
+ *
18
+ * Each case states the row it inserts and what the schema promises about it, so
19
+ * a failure names the rule rather than the constraint.
20
+ */
21
+
22
+ const SITE = DEFAULT_TEST_SITE_ID;
23
+ const NOW = 1_700_000_000;
24
+
25
+ function ids(prefix: string, n: number): string[] {
26
+ return Array.from(
27
+ { length: n },
28
+ (_, i) => `${prefix}_0000000000000000000000000${i}`,
29
+ );
30
+ }
31
+
32
+ describe("smart collection placement constraints", () => {
33
+ let sqlite: Database.Database;
34
+ let smartA: string;
35
+ let smartB: string;
36
+ let collectionA: string;
37
+ let postA: string;
38
+
39
+ beforeEach(() => {
40
+ ({ sqlite } = createTestDatabase());
41
+ [smartA, smartB] = ids("smc", 2) as [string, string];
42
+ [collectionA] = ids("col", 1) as [string];
43
+ [postA] = ids("pst", 1) as [string];
44
+
45
+ sqlite
46
+ .prepare(
47
+ `INSERT INTO collection (id, site_id, title, sort_order, created_at, updated_at)
48
+ VALUES (?, ?, 'Books', 'newest', ?, ?)`,
49
+ )
50
+ .run(collectionA, SITE, NOW, NOW);
51
+
52
+ for (const id of [smartA, smartB]) {
53
+ sqlite
54
+ .prepare(
55
+ `INSERT INTO smart_collection (id, site_id, title, sort, created_at, updated_at)
56
+ VALUES (?, ?, 'Quotes', 'newest', ?, ?)`,
57
+ )
58
+ .run(id, SITE, NOW, NOW);
59
+ }
60
+
61
+ sqlite
62
+ .prepare(
63
+ `INSERT INTO post (
64
+ id, site_id, thread_id, format, status, visibility,
65
+ body_html, body_text, created_at, updated_at
66
+ ) VALUES (?, ?, ?, 'note', 'published', 'public', '', '', ?, ?)`,
67
+ )
68
+ .run(postA, SITE, postA, NOW, NOW);
69
+ });
70
+
71
+ function insertPath(values: Record<string, unknown>) {
72
+ const row = {
73
+ id: `pth_${Math.random().toString(36).slice(2, 12)}`,
74
+ site_id: SITE,
75
+ path: `p-${Math.random().toString(36).slice(2, 8)}`,
76
+ kind: "slug",
77
+ post_id: null,
78
+ collection_id: null,
79
+ smart_collection_id: null,
80
+ redirect_to_path: null,
81
+ redirect_type: null,
82
+ archive_query: null,
83
+ created_at: NOW,
84
+ updated_at: NOW,
85
+ ...values,
86
+ };
87
+ const keys = Object.keys(row);
88
+ sqlite
89
+ .prepare(
90
+ `INSERT INTO path_registry (${keys.join(", ")})
91
+ VALUES (${keys.map(() => "?").join(", ")})`,
92
+ )
93
+ .run(...keys.map((key) => row[key]));
94
+ }
95
+
96
+ function insertDirectoryItem(values: Record<string, unknown>) {
97
+ const row = {
98
+ id: `cdi_${Math.random().toString(36).slice(2, 12)}`,
99
+ site_id: SITE,
100
+ type: "smart_collection",
101
+ collection_id: null,
102
+ smart_collection_id: null,
103
+ label: null,
104
+ url: null,
105
+ description: null,
106
+ position: `a${Math.random().toString(36).slice(2, 8)}`,
107
+ created_at: NOW,
108
+ updated_at: NOW,
109
+ ...values,
110
+ };
111
+ const keys = Object.keys(row);
112
+ sqlite
113
+ .prepare(
114
+ `INSERT INTO collection_directory_item (${keys.join(", ")})
115
+ VALUES (${keys.map(() => "?").join(", ")})`,
116
+ )
117
+ .run(...keys.map((key) => row[key]));
118
+ }
119
+
120
+ function insertNavItem(values: Record<string, unknown>) {
121
+ const row = {
122
+ id: `nav_${Math.random().toString(36).slice(2, 12)}`,
123
+ site_id: SITE,
124
+ type: "smart_collection",
125
+ system_key: null,
126
+ collection_id: null,
127
+ smart_collection_id: null,
128
+ post_id: null,
129
+ label: "",
130
+ url: "/quotes",
131
+ placement: "header",
132
+ position: `a${Math.random().toString(36).slice(2, 8)}`,
133
+ created_at: NOW,
134
+ updated_at: NOW,
135
+ ...values,
136
+ };
137
+ const keys = Object.keys(row);
138
+ sqlite
139
+ .prepare(
140
+ `INSERT INTO nav_item (${keys.join(", ")})
141
+ VALUES (${keys.map(() => "?").join(", ")})`,
142
+ )
143
+ .run(...keys.map((key) => row[key]));
144
+ }
145
+
146
+ describe("path_registry", () => {
147
+ it("accepts a slug pointing at a smart collection", () => {
148
+ expect(() =>
149
+ insertPath({ smart_collection_id: smartA, path: "quotes" }),
150
+ ).not.toThrow();
151
+ });
152
+
153
+ it("accepts an alias pointing at a smart collection", () => {
154
+ expect(() =>
155
+ insertPath({
156
+ kind: "alias",
157
+ smart_collection_id: smartA,
158
+ path: "old-quotes",
159
+ }),
160
+ ).not.toThrow();
161
+ });
162
+
163
+ it("refuses a slug that points at two things at once", () => {
164
+ expect(() =>
165
+ insertPath({ smart_collection_id: smartA, collection_id: collectionA }),
166
+ ).toThrow();
167
+ expect(() =>
168
+ insertPath({ smart_collection_id: smartA, post_id: postA }),
169
+ ).toThrow();
170
+ });
171
+
172
+ it("refuses a redirect or archive row that names a smart collection", () => {
173
+ expect(() =>
174
+ insertPath({
175
+ kind: "redirect",
176
+ smart_collection_id: smartA,
177
+ redirect_to_path: "quotes",
178
+ redirect_type: 301,
179
+ }),
180
+ ).toThrow();
181
+ expect(() =>
182
+ insertPath({
183
+ kind: "archive",
184
+ smart_collection_id: smartA,
185
+ archive_query: "format=quote",
186
+ }),
187
+ ).toThrow();
188
+ });
189
+
190
+ it("allows only one slug per smart collection", () => {
191
+ insertPath({ smart_collection_id: smartA, path: "quotes" });
192
+ expect(() =>
193
+ insertPath({ smart_collection_id: smartA, path: "quotes-2" }),
194
+ ).toThrow();
195
+
196
+ // An alias is not a second slug, so it is still allowed.
197
+ expect(() =>
198
+ insertPath({
199
+ kind: "alias",
200
+ smart_collection_id: smartA,
201
+ path: "old-quotes",
202
+ }),
203
+ ).not.toThrow();
204
+ });
205
+
206
+ it("keeps smart collections in the same address space as everything else", () => {
207
+ insertPath({ smart_collection_id: smartA, path: "quotes" });
208
+ expect(() =>
209
+ insertPath({ collection_id: collectionA, path: "quotes" }),
210
+ ).toThrow();
211
+ expect(() => insertPath({ post_id: postA, path: "quotes" })).toThrow();
212
+ });
213
+ });
214
+
215
+ describe("collection_directory_item", () => {
216
+ it("accepts a smart collection row", () => {
217
+ expect(() =>
218
+ insertDirectoryItem({ smart_collection_id: smartA }),
219
+ ).not.toThrow();
220
+ });
221
+
222
+ it("refuses a smart collection row with no target", () => {
223
+ expect(() => insertDirectoryItem({})).toThrow();
224
+ });
225
+
226
+ it("refuses a smart collection row carrying a label or url", () => {
227
+ expect(() =>
228
+ insertDirectoryItem({ smart_collection_id: smartA, label: "Quotes" }),
229
+ ).toThrow();
230
+ expect(() =>
231
+ insertDirectoryItem({ smart_collection_id: smartA, url: "/quotes" }),
232
+ ).toThrow();
233
+ });
234
+
235
+ it("refuses every other row type carrying a smart collection", () => {
236
+ expect(() =>
237
+ insertDirectoryItem({
238
+ type: "collection",
239
+ collection_id: collectionA,
240
+ smart_collection_id: smartA,
241
+ }),
242
+ ).toThrow();
243
+ expect(() =>
244
+ insertDirectoryItem({ type: "divider", smart_collection_id: smartA }),
245
+ ).toThrow();
246
+ expect(() =>
247
+ insertDirectoryItem({
248
+ type: "link",
249
+ label: "Elsewhere",
250
+ url: "https://example.com",
251
+ smart_collection_id: smartA,
252
+ }),
253
+ ).toThrow();
254
+ });
255
+
256
+ it("places a smart collection at most once", () => {
257
+ insertDirectoryItem({ smart_collection_id: smartA });
258
+ expect(() =>
259
+ insertDirectoryItem({ smart_collection_id: smartA }),
260
+ ).toThrow();
261
+ expect(() =>
262
+ insertDirectoryItem({ smart_collection_id: smartB }),
263
+ ).not.toThrow();
264
+ });
265
+ });
266
+
267
+ describe("nav_item", () => {
268
+ it("accepts a smart collection nav item", () => {
269
+ expect(() =>
270
+ insertNavItem({ smart_collection_id: smartA }),
271
+ ).not.toThrow();
272
+ });
273
+
274
+ it("refuses a smart collection nav item with no target", () => {
275
+ expect(() => insertNavItem({})).toThrow();
276
+ });
277
+
278
+ it("refuses every other nav type carrying a smart collection", () => {
279
+ expect(() =>
280
+ insertNavItem({
281
+ type: "link",
282
+ smart_collection_id: smartA,
283
+ label: "Elsewhere",
284
+ }),
285
+ ).toThrow();
286
+ expect(() =>
287
+ insertNavItem({
288
+ type: "system",
289
+ system_key: "latest",
290
+ smart_collection_id: smartA,
291
+ }),
292
+ ).toThrow();
293
+ expect(() =>
294
+ insertNavItem({
295
+ type: "collection",
296
+ collection_id: collectionA,
297
+ smart_collection_id: smartA,
298
+ }),
299
+ ).toThrow();
300
+ expect(() =>
301
+ insertNavItem({
302
+ type: "page",
303
+ post_id: postA,
304
+ smart_collection_id: smartA,
305
+ }),
306
+ ).toThrow();
307
+ });
308
+
309
+ it("adds a smart collection to navigation at most once", () => {
310
+ insertNavItem({ smart_collection_id: smartA });
311
+ expect(() => insertNavItem({ smart_collection_id: smartA })).toThrow();
312
+ expect(() =>
313
+ insertNavItem({ smart_collection_id: smartB }),
314
+ ).not.toThrow();
315
+ });
316
+ });
317
+
318
+ describe("smart_collection", () => {
319
+ it("refuses a private visibility condition", () => {
320
+ // The one value a published page can never name. Enforced in the column,
321
+ // not only in Zod, so no import path or backfill can slip one in.
322
+ expect(() =>
323
+ sqlite
324
+ .prepare(
325
+ `INSERT INTO smart_collection (id, site_id, title, visibility, sort, created_at, updated_at)
326
+ VALUES (?, ?, 'Private', 'private', 'newest', ?, ?)`,
327
+ )
328
+ .run(`smc_private0000000000000000000`, SITE, NOW, NOW),
329
+ ).toThrow();
330
+ });
331
+
332
+ it("refuses a sort or layout it cannot render", () => {
333
+ expect(() =>
334
+ sqlite
335
+ .prepare(
336
+ `INSERT INTO smart_collection (id, site_id, title, sort, created_at, updated_at)
337
+ VALUES (?, ?, 'Bad sort', 'rating_asc', ?, ?)`,
338
+ )
339
+ .run(`smc_badsort00000000000000000000`, SITE, NOW, NOW),
340
+ ).toThrow();
341
+ expect(() =>
342
+ sqlite
343
+ .prepare(
344
+ `INSERT INTO smart_collection (id, site_id, title, sort, layout, created_at, updated_at)
345
+ VALUES (?, ?, 'Bad layout', 'newest', 'carousel', ?, ?)`,
346
+ )
347
+ .run(`smc_badlayout00000000000000000`, SITE, NOW, NOW),
348
+ ).toThrow();
349
+ });
350
+
351
+ it("leaves a deleted collection's id in place rather than widening", () => {
352
+ sqlite
353
+ .prepare(`UPDATE smart_collection SET collection_id = ? WHERE id = ?`)
354
+ .run(collectionA, smartA);
355
+
356
+ // No foreign key on this column, alone in the schema. RESTRICT would fail
357
+ // anonymously and make every bulk delete an ordering rule; SET NULL would
358
+ // turn a curated page into the whole archive. The refusal an author sees
359
+ // is `smartCollections.assertCollectionUnused()`, and what is left if
360
+ // something reaches around it is a condition naming a collection that no
361
+ // longer exists — which matches nothing.
362
+ expect(() =>
363
+ sqlite.prepare(`DELETE FROM collection WHERE id = ?`).run(collectionA),
364
+ ).not.toThrow();
365
+
366
+ const row = sqlite
367
+ .prepare(`SELECT collection_id FROM smart_collection WHERE id = ?`)
368
+ .get(smartA) as { collection_id: string | null };
369
+ expect(row.collection_id).toBe(collectionA);
370
+ });
371
+
372
+ it("refuses a year no timestamp could carry", () => {
373
+ // `Date.UTC` returns NaN past year 275760, and a NaN bound is a
374
+ // comparison every row fails with nothing to report.
375
+ for (const year of [1970, 10000]) {
376
+ expect(() =>
377
+ sqlite
378
+ .prepare(
379
+ `INSERT INTO smart_collection (id, site_id, title, year, sort, created_at, updated_at)
380
+ VALUES (?, ?, 'Bad year', ?, 'newest', ?, ?)`,
381
+ )
382
+ .run(
383
+ `smc_year${year}000000000000000000`.slice(0, 30),
384
+ SITE,
385
+ year,
386
+ NOW,
387
+ NOW,
388
+ ),
389
+ ).toThrow();
390
+ }
391
+
392
+ expect(() =>
393
+ sqlite
394
+ .prepare(
395
+ `INSERT INTO smart_collection (id, site_id, title, year, sort, created_at, updated_at)
396
+ VALUES (?, ?, 'Edge years', 9999, 'newest', ?, ?)`,
397
+ )
398
+ .run(`smc_year9999000000000000000000`.slice(0, 30), SITE, NOW, NOW),
399
+ ).not.toThrow();
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,31 @@
1
+ -- Clear page and collection nav labels that merely mirror their target's title.
2
+ -- Previously these items stored a snapshot of the post or collection title taken
3
+ -- when the item was added, so renaming the target left the nav entry behind. Now
4
+ -- an empty label means "follow the target" and any non-empty value is a label the
5
+ -- author typed, which wins in every language view.
6
+ --
7
+ -- Rows whose stored label differs from the current title were customized and are
8
+ -- left alone. Idempotent: rerunning matches nothing once the labels are empty.
9
+ UPDATE "nav_item"
10
+ SET "label" = ''
11
+ WHERE "type" = 'page'
12
+ AND "post_id" IS NOT NULL
13
+ AND "label" <> ''
14
+ AND "label" = (
15
+ SELECT trim("post"."title")
16
+ FROM "post"
17
+ WHERE "post"."id" = "nav_item"."post_id"
18
+ AND "post"."site_id" = "nav_item"."site_id"
19
+ );
20
+
21
+ UPDATE "nav_item"
22
+ SET "label" = ''
23
+ WHERE "type" = 'collection'
24
+ AND "collection_id" IS NOT NULL
25
+ AND "label" <> ''
26
+ AND "label" = (
27
+ SELECT trim("collection"."title")
28
+ FROM "collection"
29
+ WHERE "collection"."id" = "nav_item"."collection_id"
30
+ AND "collection"."site_id" = "nav_item"."site_id"
31
+ );
@@ -0,0 +1,3 @@
1
+ ALTER TABLE `post` ADD `language` text;--> statement-breakpoint
2
+ ALTER TABLE `post` ADD `translation_group_id` text;--> statement-breakpoint
3
+ CREATE UNIQUE INDEX `uq_post_site_translation_group_language` ON `post` (`site_id`,`translation_group_id`,`language`) WHERE "post"."translation_group_id" IS NOT NULL;
@@ -0,0 +1,198 @@
1
+ CREATE TABLE `smart_collection` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `site_id` text NOT NULL,
4
+ `title` text NOT NULL,
5
+ `description` text,
6
+ `format` text,
7
+ `year` integer,
8
+ `collection_id` text,
9
+ `media` text,
10
+ `has_title` integer,
11
+ `has_replies` integer,
12
+ `visibility` text,
13
+ `sort` text DEFAULT 'newest' NOT NULL,
14
+ `layout` text,
15
+ `created_at` integer NOT NULL,
16
+ `updated_at` integer NOT NULL,
17
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
18
+ CONSTRAINT "chk_smart_collection_format" CHECK("smart_collection"."format" IS NULL OR "smart_collection"."format" IN ('note', 'link', 'quote')),
19
+ CONSTRAINT "chk_smart_collection_visibility" CHECK("smart_collection"."visibility" IS NULL OR "smart_collection"."visibility" IN ('public', 'featured', 'latest_hidden')),
20
+ CONSTRAINT "chk_smart_collection_sort" CHECK("smart_collection"."sort" IN ('newest', 'oldest', 'updated', 'rating_desc')),
21
+ CONSTRAINT "chk_smart_collection_layout" CHECK("smart_collection"."layout" IS NULL OR "smart_collection"."layout" IN ('list', 'grid')),
22
+ CONSTRAINT "chk_smart_collection_year" CHECK("smart_collection"."year" IS NULL OR "smart_collection"."year" BETWEEN 1971 AND 9999)
23
+ );
24
+ --> statement-breakpoint
25
+ CREATE INDEX `idx_smart_collection_site_created_at` ON `smart_collection` (`site_id`,`created_at`);--> statement-breakpoint
26
+ CREATE INDEX `idx_smart_collection_site_collection_id` ON `smart_collection` (`site_id`,`collection_id`);--> statement-breakpoint
27
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
28
+ CREATE TABLE `__new_collection_directory_item` (
29
+ `id` text PRIMARY KEY NOT NULL,
30
+ `site_id` text NOT NULL,
31
+ `type` text NOT NULL,
32
+ `collection_id` text,
33
+ `smart_collection_id` text,
34
+ `label` text,
35
+ `url` text,
36
+ `description` text,
37
+ `position` text DEFAULT 'a0' NOT NULL,
38
+ `created_at` integer NOT NULL,
39
+ `updated_at` integer NOT NULL,
40
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
41
+ FOREIGN KEY (`collection_id`) REFERENCES `collection`(`id`) ON UPDATE no action ON DELETE cascade,
42
+ FOREIGN KEY (`smart_collection_id`) REFERENCES `smart_collection`(`id`) ON UPDATE no action ON DELETE cascade,
43
+ CONSTRAINT "chk_collection_directory_item_type" CHECK("__new_collection_directory_item"."type" IN ('collection', 'smart_collection', 'divider', 'link')),
44
+ CONSTRAINT "chk_collection_directory_item_shape" CHECK((
45
+ "__new_collection_directory_item"."type" = 'collection'
46
+ AND "__new_collection_directory_item"."collection_id" IS NOT NULL
47
+ AND "__new_collection_directory_item"."smart_collection_id" IS NULL
48
+ AND "__new_collection_directory_item"."label" IS NULL
49
+ AND "__new_collection_directory_item"."url" IS NULL
50
+ ) OR (
51
+ "__new_collection_directory_item"."type" = 'smart_collection'
52
+ AND "__new_collection_directory_item"."collection_id" IS NULL
53
+ AND "__new_collection_directory_item"."smart_collection_id" IS NOT NULL
54
+ AND "__new_collection_directory_item"."label" IS NULL
55
+ AND "__new_collection_directory_item"."url" IS NULL
56
+ ) OR (
57
+ "__new_collection_directory_item"."type" = 'divider'
58
+ AND "__new_collection_directory_item"."collection_id" IS NULL
59
+ AND "__new_collection_directory_item"."smart_collection_id" IS NULL
60
+ AND "__new_collection_directory_item"."url" IS NULL
61
+ ) OR (
62
+ "__new_collection_directory_item"."type" = 'link'
63
+ AND "__new_collection_directory_item"."collection_id" IS NULL
64
+ AND "__new_collection_directory_item"."smart_collection_id" IS NULL
65
+ AND "__new_collection_directory_item"."label" IS NOT NULL
66
+ AND "__new_collection_directory_item"."url" IS NOT NULL
67
+ )),
68
+ CONSTRAINT "chk_collection_directory_item_label" CHECK("__new_collection_directory_item"."type" NOT IN ('collection', 'smart_collection') OR "__new_collection_directory_item"."label" IS NULL),
69
+ CONSTRAINT "chk_collection_directory_item_description" CHECK("__new_collection_directory_item"."type" = 'link' OR "__new_collection_directory_item"."description" IS NULL)
70
+ );
71
+ --> statement-breakpoint
72
+ INSERT INTO `__new_collection_directory_item`("id", "site_id", "type", "collection_id", "smart_collection_id", "label", "url", "description", "position", "created_at", "updated_at") SELECT "id", "site_id", "type", "collection_id", NULL, "label", "url", "description", "position", "created_at", "updated_at" FROM `collection_directory_item`;--> statement-breakpoint
73
+ DROP TABLE `collection_directory_item`;--> statement-breakpoint
74
+ ALTER TABLE `__new_collection_directory_item` RENAME TO `collection_directory_item`;--> statement-breakpoint
75
+ CREATE INDEX `idx_collection_directory_item_site_collection_id` ON `collection_directory_item` (`site_id`,`collection_id`);--> statement-breakpoint
76
+ CREATE INDEX `idx_collection_directory_item_site_smart_collection_id` ON `collection_directory_item` (`site_id`,`smart_collection_id`);--> statement-breakpoint
77
+ CREATE UNIQUE INDEX `uq_collection_directory_item_site_position` ON `collection_directory_item` (`site_id`,`position`);--> statement-breakpoint
78
+ CREATE UNIQUE INDEX `uq_collection_directory_item_site_collection_once` ON `collection_directory_item` (`site_id`,`collection_id`) WHERE "collection_directory_item"."type" = 'collection' AND "collection_directory_item"."collection_id" IS NOT NULL;--> statement-breakpoint
79
+ CREATE UNIQUE INDEX `uq_collection_directory_item_site_smart_collection_once` ON `collection_directory_item` (`site_id`,`smart_collection_id`) WHERE "collection_directory_item"."type" = 'smart_collection' AND "collection_directory_item"."smart_collection_id" IS NOT NULL;--> statement-breakpoint
80
+ CREATE TABLE `__new_nav_item` (
81
+ `id` text PRIMARY KEY NOT NULL,
82
+ `site_id` text NOT NULL,
83
+ `type` text DEFAULT 'link' NOT NULL,
84
+ `system_key` text,
85
+ `collection_id` text,
86
+ `smart_collection_id` text,
87
+ `post_id` text,
88
+ `label` text NOT NULL,
89
+ `url` text NOT NULL,
90
+ `placement` text DEFAULT 'header' NOT NULL,
91
+ `position` text DEFAULT 'a0' NOT NULL,
92
+ `created_at` integer NOT NULL,
93
+ `updated_at` integer NOT NULL,
94
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
95
+ FOREIGN KEY (`collection_id`) REFERENCES `collection`(`id`) ON UPDATE no action ON DELETE cascade,
96
+ FOREIGN KEY (`smart_collection_id`) REFERENCES `smart_collection`(`id`) ON UPDATE no action ON DELETE cascade,
97
+ FOREIGN KEY (`post_id`) REFERENCES `post`(`id`) ON UPDATE no action ON DELETE cascade,
98
+ CONSTRAINT "chk_nav_item_type" CHECK("__new_nav_item"."type" IN ('link', 'system', 'collection', 'smart_collection', 'page')),
99
+ CONSTRAINT "chk_nav_item_placement" CHECK("__new_nav_item"."placement" IN ('header', 'more')),
100
+ CONSTRAINT "chk_nav_item_shape" CHECK((
101
+ "__new_nav_item"."type" = 'link'
102
+ AND "__new_nav_item"."system_key" IS NULL
103
+ AND "__new_nav_item"."collection_id" IS NULL
104
+ AND "__new_nav_item"."smart_collection_id" IS NULL
105
+ AND "__new_nav_item"."post_id" IS NULL
106
+ ) OR (
107
+ "__new_nav_item"."type" = 'system'
108
+ AND "__new_nav_item"."system_key" IS NOT NULL
109
+ AND "__new_nav_item"."collection_id" IS NULL
110
+ AND "__new_nav_item"."smart_collection_id" IS NULL
111
+ AND "__new_nav_item"."post_id" IS NULL
112
+ ) OR (
113
+ "__new_nav_item"."type" = 'collection'
114
+ AND "__new_nav_item"."system_key" IS NULL
115
+ AND "__new_nav_item"."collection_id" IS NOT NULL
116
+ AND "__new_nav_item"."smart_collection_id" IS NULL
117
+ AND "__new_nav_item"."post_id" IS NULL
118
+ ) OR (
119
+ "__new_nav_item"."type" = 'smart_collection'
120
+ AND "__new_nav_item"."system_key" IS NULL
121
+ AND "__new_nav_item"."collection_id" IS NULL
122
+ AND "__new_nav_item"."smart_collection_id" IS NOT NULL
123
+ AND "__new_nav_item"."post_id" IS NULL
124
+ ) OR (
125
+ "__new_nav_item"."type" = 'page'
126
+ AND "__new_nav_item"."system_key" IS NULL
127
+ AND "__new_nav_item"."collection_id" IS NULL
128
+ AND "__new_nav_item"."smart_collection_id" IS NULL
129
+ AND "__new_nav_item"."post_id" IS NOT NULL
130
+ ))
131
+ );
132
+ --> statement-breakpoint
133
+ INSERT INTO `__new_nav_item`("id", "site_id", "type", "system_key", "collection_id", "smart_collection_id", "post_id", "label", "url", "placement", "position", "created_at", "updated_at") SELECT "id", "site_id", "type", "system_key", "collection_id", NULL, "post_id", "label", "url", "placement", "position", "created_at", "updated_at" FROM `nav_item`;--> statement-breakpoint
134
+ DROP TABLE `nav_item`;--> statement-breakpoint
135
+ ALTER TABLE `__new_nav_item` RENAME TO `nav_item`;--> statement-breakpoint
136
+ CREATE UNIQUE INDEX `uq_nav_item_site_position` ON `nav_item` (`site_id`,`position`);--> statement-breakpoint
137
+ CREATE UNIQUE INDEX `uq_nav_item_site_system_key` ON `nav_item` (`site_id`,`system_key`) WHERE "nav_item"."system_key" IS NOT NULL;--> statement-breakpoint
138
+ CREATE UNIQUE INDEX `uq_nav_item_site_collection_id` ON `nav_item` (`site_id`,`collection_id`) WHERE "nav_item"."collection_id" IS NOT NULL;--> statement-breakpoint
139
+ CREATE UNIQUE INDEX `uq_nav_item_site_smart_collection_id` ON `nav_item` (`site_id`,`smart_collection_id`) WHERE "nav_item"."smart_collection_id" IS NOT NULL;--> statement-breakpoint
140
+ CREATE UNIQUE INDEX `uq_nav_item_site_post_id` ON `nav_item` (`site_id`,`post_id`) WHERE "nav_item"."post_id" IS NOT NULL;--> statement-breakpoint
141
+ CREATE TABLE `__new_path_registry` (
142
+ `id` text PRIMARY KEY NOT NULL,
143
+ `site_id` text NOT NULL,
144
+ `path` text NOT NULL,
145
+ `kind` text NOT NULL,
146
+ `post_id` text,
147
+ `collection_id` text,
148
+ `smart_collection_id` text,
149
+ `redirect_to_path` text,
150
+ `redirect_type` integer,
151
+ `archive_query` text,
152
+ `created_at` integer NOT NULL,
153
+ `updated_at` integer NOT NULL,
154
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
155
+ FOREIGN KEY (`post_id`) REFERENCES `post`(`id`) ON UPDATE no action ON DELETE cascade,
156
+ FOREIGN KEY (`collection_id`) REFERENCES `collection`(`id`) ON UPDATE no action ON DELETE cascade,
157
+ FOREIGN KEY (`smart_collection_id`) REFERENCES `smart_collection`(`id`) ON UPDATE no action ON DELETE cascade,
158
+ CONSTRAINT "chk_path_registry_kind" CHECK("__new_path_registry"."kind" IN ('slug', 'alias', 'redirect', 'archive')),
159
+ CONSTRAINT "chk_path_registry_shape" CHECK((
160
+ "__new_path_registry"."kind" IN ('slug', 'alias')
161
+ AND (
162
+ ("__new_path_registry"."post_id" IS NOT NULL AND "__new_path_registry"."collection_id" IS NULL AND "__new_path_registry"."smart_collection_id" IS NULL)
163
+ OR ("__new_path_registry"."post_id" IS NULL AND "__new_path_registry"."collection_id" IS NOT NULL AND "__new_path_registry"."smart_collection_id" IS NULL)
164
+ OR ("__new_path_registry"."post_id" IS NULL AND "__new_path_registry"."collection_id" IS NULL AND "__new_path_registry"."smart_collection_id" IS NOT NULL)
165
+ )
166
+ AND "__new_path_registry"."redirect_to_path" IS NULL
167
+ AND "__new_path_registry"."redirect_type" IS NULL
168
+ AND "__new_path_registry"."archive_query" IS NULL
169
+ ) OR (
170
+ "__new_path_registry"."kind" = 'redirect'
171
+ AND "__new_path_registry"."post_id" IS NULL
172
+ AND "__new_path_registry"."collection_id" IS NULL
173
+ AND "__new_path_registry"."smart_collection_id" IS NULL
174
+ AND "__new_path_registry"."redirect_to_path" IS NOT NULL
175
+ AND "__new_path_registry"."redirect_type" IN (301, 302)
176
+ AND "__new_path_registry"."archive_query" IS NULL
177
+ ) OR (
178
+ "__new_path_registry"."kind" = 'archive'
179
+ AND "__new_path_registry"."post_id" IS NULL
180
+ AND "__new_path_registry"."collection_id" IS NULL
181
+ AND "__new_path_registry"."smart_collection_id" IS NULL
182
+ AND "__new_path_registry"."redirect_to_path" IS NULL
183
+ AND "__new_path_registry"."redirect_type" IS NULL
184
+ AND "__new_path_registry"."archive_query" IS NOT NULL
185
+ ))
186
+ );
187
+ --> statement-breakpoint
188
+ INSERT INTO `__new_path_registry`("id", "site_id", "path", "kind", "post_id", "collection_id", "smart_collection_id", "redirect_to_path", "redirect_type", "archive_query", "created_at", "updated_at") SELECT "id", "site_id", "path", "kind", "post_id", "collection_id", NULL, "redirect_to_path", "redirect_type", "archive_query", "created_at", "updated_at" FROM `path_registry`;--> statement-breakpoint
189
+ DROP TABLE `path_registry`;--> statement-breakpoint
190
+ ALTER TABLE `__new_path_registry` RENAME TO `path_registry`;--> statement-breakpoint
191
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
192
+ CREATE UNIQUE INDEX `uq_path_registry_site_path` ON `path_registry` (`site_id`,`path`);--> statement-breakpoint
193
+ CREATE UNIQUE INDEX `uq_path_registry_site_post_slug` ON `path_registry` (`site_id`,`post_id`) WHERE "path_registry"."kind" = 'slug' AND "path_registry"."post_id" IS NOT NULL;--> statement-breakpoint
194
+ CREATE UNIQUE INDEX `uq_path_registry_site_collection_slug` ON `path_registry` (`site_id`,`collection_id`) WHERE "path_registry"."kind" = 'slug' AND "path_registry"."collection_id" IS NOT NULL;--> statement-breakpoint
195
+ CREATE UNIQUE INDEX `uq_path_registry_site_smart_collection_slug` ON `path_registry` (`site_id`,`smart_collection_id`) WHERE "path_registry"."kind" = 'slug' AND "path_registry"."smart_collection_id" IS NOT NULL;--> statement-breakpoint
196
+ CREATE INDEX `idx_path_registry_site_post_id` ON `path_registry` (`site_id`,`post_id`);--> statement-breakpoint
197
+ CREATE INDEX `idx_path_registry_site_collection_id` ON `path_registry` (`site_id`,`collection_id`);--> statement-breakpoint
198
+ CREATE INDEX `idx_path_registry_site_smart_collection_id` ON `path_registry` (`site_id`,`smart_collection_id`);
@@ -0,0 +1,35 @@
1
+ -- `updated` was a fourth smart collection order until `newest` came to mean the
2
+ -- same thing. The rebuild below narrows the CHECK to three, so any row still
3
+ -- naming it has to be moved first or the copy fails on its own constraint.
4
+ UPDATE `smart_collection` SET `sort` = 'newest' WHERE `sort` = 'updated';--> statement-breakpoint
5
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
6
+ CREATE TABLE `__new_smart_collection` (
7
+ `id` text PRIMARY KEY NOT NULL,
8
+ `site_id` text NOT NULL,
9
+ `title` text NOT NULL,
10
+ `description` text,
11
+ `format` text,
12
+ `year` integer,
13
+ `collection_id` text,
14
+ `media` text,
15
+ `has_title` integer,
16
+ `has_replies` integer,
17
+ `visibility` text,
18
+ `sort` text DEFAULT 'newest' NOT NULL,
19
+ `layout` text,
20
+ `created_at` integer NOT NULL,
21
+ `updated_at` integer NOT NULL,
22
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
23
+ CONSTRAINT "chk_smart_collection_format" CHECK("__new_smart_collection"."format" IS NULL OR "__new_smart_collection"."format" IN ('note', 'link', 'quote')),
24
+ CONSTRAINT "chk_smart_collection_visibility" CHECK("__new_smart_collection"."visibility" IS NULL OR "__new_smart_collection"."visibility" IN ('public', 'featured', 'latest_hidden')),
25
+ CONSTRAINT "chk_smart_collection_sort" CHECK("__new_smart_collection"."sort" IN ('newest', 'oldest', 'rating_desc')),
26
+ CONSTRAINT "chk_smart_collection_layout" CHECK("__new_smart_collection"."layout" IS NULL OR "__new_smart_collection"."layout" IN ('list', 'grid')),
27
+ CONSTRAINT "chk_smart_collection_year" CHECK("__new_smart_collection"."year" IS NULL OR "__new_smart_collection"."year" BETWEEN 1971 AND 9999)
28
+ );
29
+ --> statement-breakpoint
30
+ INSERT INTO `__new_smart_collection`("id", "site_id", "title", "description", "format", "year", "collection_id", "media", "has_title", "has_replies", "visibility", "sort", "layout", "created_at", "updated_at") SELECT "id", "site_id", "title", "description", "format", "year", "collection_id", "media", "has_title", "has_replies", "visibility", "sort", "layout", "created_at", "updated_at" FROM `smart_collection`;--> statement-breakpoint
31
+ DROP TABLE `smart_collection`;--> statement-breakpoint
32
+ ALTER TABLE `__new_smart_collection` RENAME TO `smart_collection`;--> statement-breakpoint
33
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
34
+ CREATE INDEX `idx_smart_collection_site_created_at` ON `smart_collection` (`site_id`,`created_at`);--> statement-breakpoint
35
+ CREATE INDEX `idx_smart_collection_site_collection_id` ON `smart_collection` (`site_id`,`collection_id`);