@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
@@ -310,47 +310,51 @@ export class JantTextPreview extends LitElement {
310
310
  </svg>
311
311
  </button>
312
312
  <div class="text-preview-toolbar-actions">
313
- ${this.#shareHref
314
- ? html`<button
315
- type="button"
316
- class="text-preview-btn"
317
- @click=${() => this.#copyLink()}
318
- ?disabled=${this._loading}
319
- title="Copy link"
320
- aria-label="Copy shareable link"
321
- >
322
- ${this._linkCopied
323
- ? html`<svg
324
- width="16"
325
- height="16"
326
- viewBox="0 0 24 24"
327
- fill="none"
328
- stroke="currentColor"
329
- stroke-width="2"
330
- stroke-linecap="round"
331
- stroke-linejoin="round"
332
- >
333
- <path d="M20 6 9 17l-5-5" />
334
- </svg>`
335
- : html`<svg
336
- width="16"
337
- height="16"
338
- viewBox="0 0 24 24"
339
- fill="none"
340
- stroke="currentColor"
341
- stroke-width="2"
342
- stroke-linecap="round"
343
- stroke-linejoin="round"
344
- >
345
- <path
346
- d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
347
- />
348
- <path
349
- d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
350
- />
351
- </svg>`}
352
- </button>`
353
- : nothing}
313
+ ${
314
+ this.#shareHref
315
+ ? html`<button
316
+ type="button"
317
+ class="text-preview-btn"
318
+ @click=${() => this.#copyLink()}
319
+ ?disabled=${this._loading}
320
+ title="Copy link"
321
+ aria-label="Copy shareable link"
322
+ >
323
+ ${
324
+ this._linkCopied
325
+ ? html`<svg
326
+ width="16"
327
+ height="16"
328
+ viewBox="0 0 24 24"
329
+ fill="none"
330
+ stroke="currentColor"
331
+ stroke-width="2"
332
+ stroke-linecap="round"
333
+ stroke-linejoin="round"
334
+ >
335
+ <path d="M20 6 9 17l-5-5" />
336
+ </svg>`
337
+ : html`<svg
338
+ width="16"
339
+ height="16"
340
+ viewBox="0 0 24 24"
341
+ fill="none"
342
+ stroke="currentColor"
343
+ stroke-width="2"
344
+ stroke-linecap="round"
345
+ stroke-linejoin="round"
346
+ >
347
+ <path
348
+ d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
349
+ />
350
+ <path
351
+ d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
352
+ />
353
+ </svg>`
354
+ }
355
+ </button>`
356
+ : nothing
357
+ }
354
358
  <button
355
359
  type="button"
356
360
  class="text-preview-btn"
@@ -359,54 +363,58 @@ export class JantTextPreview extends LitElement {
359
363
  title="Copy text"
360
364
  aria-label="Copy text content"
361
365
  >
362
- ${this._copied
363
- ? html`<svg
364
- width="16"
365
- height="16"
366
- viewBox="0 0 24 24"
367
- fill="none"
368
- stroke="currentColor"
369
- stroke-width="2"
370
- stroke-linecap="round"
371
- stroke-linejoin="round"
372
- >
373
- <path d="M20 6 9 17l-5-5" />
374
- </svg>`
375
- : html`<svg
376
- width="16"
377
- height="16"
378
- viewBox="0 0 24 24"
379
- fill="none"
380
- stroke="currentColor"
381
- stroke-width="2"
382
- stroke-linecap="round"
383
- stroke-linejoin="round"
384
- >
385
- <rect width="14" height="14" x="8" y="8" rx="2" />
386
- <path
387
- d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
388
- />
389
- </svg>`}
366
+ ${
367
+ this._copied
368
+ ? html`<svg
369
+ width="16"
370
+ height="16"
371
+ viewBox="0 0 24 24"
372
+ fill="none"
373
+ stroke="currentColor"
374
+ stroke-width="2"
375
+ stroke-linecap="round"
376
+ stroke-linejoin="round"
377
+ >
378
+ <path d="M20 6 9 17l-5-5" />
379
+ </svg>`
380
+ : html`<svg
381
+ width="16"
382
+ height="16"
383
+ viewBox="0 0 24 24"
384
+ fill="none"
385
+ stroke="currentColor"
386
+ stroke-width="2"
387
+ stroke-linecap="round"
388
+ stroke-linejoin="round"
389
+ >
390
+ <rect width="14" height="14" x="8" y="8" rx="2" />
391
+ <path
392
+ d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
393
+ />
394
+ </svg>`
395
+ }
390
396
  </button>
391
397
  </div>
392
398
  </div>
393
- ${this._loading
394
- ? html`<div class="text-preview-loading">
395
- <svg
396
- class="animate-spin size-5"
397
- viewBox="0 0 24 24"
398
- fill="none"
399
- stroke="currentColor"
400
- stroke-width="2"
401
- stroke-linecap="round"
402
- stroke-linejoin="round"
403
- >
404
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
405
- </svg>
406
- </div>`
407
- : html`<div class="text-preview-body prose">
408
- ${unsafeHTML(this._html)}
409
- </div>`}
399
+ ${
400
+ this._loading
401
+ ? html`<div class="text-preview-loading">
402
+ <svg
403
+ class="animate-spin size-5"
404
+ viewBox="0 0 24 24"
405
+ fill="none"
406
+ stroke="currentColor"
407
+ stroke-width="2"
408
+ stroke-linecap="round"
409
+ stroke-linejoin="round"
410
+ >
411
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
412
+ </svg>
413
+ </div>`
414
+ : html`<div class="text-preview-body prose">
415
+ ${unsafeHTML(this._html)}
416
+ </div>`
417
+ }
410
418
  </div>
411
419
  </dialog>
412
420
  `;
@@ -7,12 +7,17 @@ import type { CollectionFormLabels } from "./collection-types.js";
7
7
 
8
8
  export interface NavManagerItem {
9
9
  id: string;
10
- type: "link" | "system" | "collection" | "page";
10
+ type: "link" | "system" | "collection" | "smart_collection" | "page";
11
11
  systemKey?: SystemNavKey;
12
12
  collectionId?: string;
13
+ smartCollectionId?: string;
13
14
  postId?: string;
15
+ /** Author's override, or `""` when the item follows its target's title */
14
16
  label: string;
17
+ /** Resolved label from the server-rendered list */
15
18
  displayLabel?: string;
19
+ /** Target's current title, present on mutation responses */
20
+ targetTitle?: string;
16
21
  url: string;
17
22
  placement?: "header" | "more";
18
23
  }
@@ -24,6 +29,36 @@ export interface NavManagerPage {
24
29
  updatedAt: number;
25
30
  }
26
31
 
32
+ /**
33
+ * What the server made of an address the author pasted into the page picker:
34
+ * the item it could become, or why it cannot become one.
35
+ *
36
+ * `address` is what to show back and what to prefill the link form with — the
37
+ * public path for anything on this site, the pasted URL for anything else.
38
+ */
39
+ export type NavAddressResolution =
40
+ | { kind: "page"; address: string; page: NavManagerPage }
41
+ | {
42
+ kind: "collection";
43
+ address: string;
44
+ collection: { id: string; title: string; slug: string };
45
+ }
46
+ | {
47
+ kind: "smart_collection";
48
+ address: string;
49
+ smartCollection: { id: string; title: string; slug: string };
50
+ }
51
+ | {
52
+ kind:
53
+ | "external"
54
+ | "link_only"
55
+ | "untitled"
56
+ | "unpublished"
57
+ | "private"
58
+ | "not_found";
59
+ address: string;
60
+ };
61
+
27
62
  export interface SystemNavConfig {
28
63
  key: SystemNavKey;
29
64
  label: string;
@@ -37,6 +72,14 @@ export interface NavManagerCollection {
37
72
  slug: string;
38
73
  /** Group label from directory divider, if this collection belongs to one */
39
74
  group?: string | null;
75
+ /**
76
+ * True for a collection whose members come from conditions.
77
+ *
78
+ * Both kinds belong in navigation — a reader following either lands on a
79
+ * page of posts — so the picker lists them together and marks which is
80
+ * which, rather than keeping two lists that would need two empty states.
81
+ */
82
+ isSmart?: boolean;
40
83
  }
41
84
 
42
85
  export interface NavManagerSuggestedLink {
@@ -84,7 +127,17 @@ export interface NavManagerLabels {
84
127
  addPageDescription: string;
85
128
  addPage: string;
86
129
  searchPages: string;
130
+ searchPagesHint: string;
87
131
  recentPages: string;
132
+ addressMatch: string;
133
+ addressAlreadyAdded: string;
134
+ addressNotFound: string;
135
+ addressUnpublished: string;
136
+ addressPrivate: string;
137
+ addressUntitled: string;
138
+ addressExternal: string;
139
+ addressLinkOnly: string;
140
+ addressAddAsLink: string;
88
141
  searchingPages: string;
89
142
  noMatchingPages: string;
90
143
  noPages: string;
@@ -111,6 +164,8 @@ export interface NavManagerLabels {
111
164
  back: string;
112
165
  collection: string;
113
166
  addCollection: string;
167
+ /** Tooltip on the icon marking a smart collection in the picker. */
168
+ smartCollectionLabel?: string;
114
169
  addCollectionToNavigation: string;
115
170
  addCollectionDescription: string;
116
171
  allCollectionsAdded: string;
@@ -126,6 +181,12 @@ export interface NavManagerLabels {
126
181
  confirmDeleteCollection: string;
127
182
  headerSection: string;
128
183
  moreSection: string;
184
+ /**
185
+ * Label on the preview's overflow button. Mirrors the real site header, so it
186
+ * comes from the public nav catalog (untranslated) rather than `moreSection`,
187
+ * which is the translated settings section heading.
188
+ */
189
+ previewMore: string;
129
190
  moreEmptyHint: string;
130
191
  placementSaved: string;
131
192
  cancel: string;
@@ -23,9 +23,9 @@ export interface SettingsLabels {
23
23
  aboutPageConflict: string;
24
24
  createAboutPage: string;
25
25
  editAboutPage: string;
26
- languageAndTime: string;
26
+ timeSection: string;
27
27
  home: string;
28
- search: string;
28
+ siteVisibility: string;
29
29
  siteName: string;
30
30
  aboutBlog: string;
31
31
  aboutBlogHelp: string;
@@ -37,6 +37,7 @@ export interface SettingsLabels {
37
37
  mainRssFeedWarning: string;
38
38
  availableFeedUrls: string;
39
39
  availableFeedUrlsHelp: string;
40
+ feedsDocs: string;
40
41
  mainFeedUrl: string;
41
42
  latestFeedUrl: string;
42
43
  featuredFeedUrl: string;
@@ -48,23 +49,20 @@ export interface SettingsLabels {
48
49
  featuredFeedOptionDescription: string;
49
50
  showJantBrandingOnHome: string;
50
51
  markdownSupported: string;
51
- siteLanguage: string;
52
- siteLanguageHelp: string;
53
- /** Placeholder shown inside the locale combobox search field. */
54
- siteLanguageSearchPlaceholder: string;
55
- /** Empty-state message when the search filters out every option. */
56
- siteLanguageNoMatches: string;
57
- /** Lead text before the live `<html lang>` preview. */
58
- contentLanguagePreview: string;
59
- dashboardLanguage: string;
60
- dashboardLanguageHelp: string;
61
- cjkFont: string;
62
- cjkFontHelp: string;
63
52
  timeZone: string;
64
53
 
65
54
  // Search
66
55
  allowIndexing: string;
67
56
  demoSeoLocked: string;
57
+ discoverName: string;
58
+ discoverRules: string;
59
+ discoverEnabled: string;
60
+ discoverIntro: string;
61
+ discoverDemoLocked: string;
62
+ discoverFeedsOffLocked: string;
63
+ discoverSearchOff: string;
64
+ discoverAnnounce: string;
65
+ discoverAnnounceManual: string;
68
66
 
69
67
  // Actions
70
68
  save: string;
@@ -80,29 +78,16 @@ export interface SettingsTimezone {
80
78
  label: string;
81
79
  }
82
80
 
83
- /** CJK font option for the select dropdown */
84
- export interface SettingsCjkFont {
85
- value: string;
86
- label: string;
87
- }
88
-
89
- /** Dashboard UI language option for the select dropdown */
90
- export interface SettingsDashboardLanguage {
91
- value: string;
92
- label: string;
93
- }
94
-
95
81
  export interface SettingsInitialData {
96
82
  siteName: string;
97
83
  siteDescription: string;
98
- siteLanguage: string;
99
- dashboardLanguage: string;
100
- cjkSerifFont: string;
101
84
  mainRssFeed: string;
102
85
  timeZone: string;
103
86
  siteFooter: string;
104
87
  showJantBrandingOnHome: boolean;
105
88
  noindex: boolean;
89
+ /** Stored Discover choice; "" when the owner has never used the control. */
90
+ discover: string;
106
91
  }
107
92
 
108
93
  export type SettingsAboutPageStatus =
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Condition controls for the smart collection dialog.
3
+ *
4
+ * Each control renders one dimension and produces a string in that dimension's
5
+ * **URL spelling** — the same one a query string carries. That is the whole
6
+ * trick: the dialog never encodes a value itself, so it cannot invent a second
7
+ * vocabulary beside the registry's, and adding a dimension that reuses an
8
+ * existing control shape adds nothing here at all.
9
+ *
10
+ * There is no "unset" option in any control. A row exists or it does not, and
11
+ * that is already how "unset" is said; a second way to say it would be two
12
+ * switches for one state.
13
+ */
14
+
15
+ import { html, type TemplateResult } from "lit";
16
+ import {
17
+ FILTER_DIMENSION_KEYS,
18
+ FILTER_DIMENSIONS,
19
+ parsePostFilterSelection,
20
+ serializePostFilterSelection,
21
+ type PostFilterSelection,
22
+ } from "../../lib/filter-dimensions.js";
23
+ import { FORMATS, MEDIA_KINDS } from "../../types/constants.js";
24
+ import type {
25
+ SmartCollectionDialogLabels,
26
+ SmartConditionRow,
27
+ } from "./smart-collection-dialog-types.js";
28
+
29
+ export type DimensionKey = (typeof FILTER_DIMENSION_KEYS)[number];
30
+
31
+ type Emit = (value: string) => void;
32
+
33
+ interface DimensionControl {
34
+ /** The value a freshly added row starts on. */
35
+ defaultValue: string;
36
+ render(
37
+ value: string,
38
+ emit: Emit,
39
+ labels: SmartCollectionDialogLabels,
40
+ ): TemplateResult;
41
+ }
42
+
43
+ /** A plain `<select>` over a fixed list of URL values. */
44
+ function selectControl(
45
+ dimension: DimensionKey,
46
+ values: readonly string[],
47
+ ): DimensionControl["render"] {
48
+ return (value, emit, labels) => html`
49
+ <select
50
+ class="select smart-condition-select"
51
+ .value=${value}
52
+ @change=${(event: Event) =>
53
+ emit((event.target as HTMLSelectElement).value)}
54
+ >
55
+ ${values.map(
56
+ (option) => html`
57
+ <option value=${option} ?selected=${option === value}>
58
+ ${labels.values[`${dimension}.${option}`] ?? option}
59
+ </option>
60
+ `,
61
+ )}
62
+ </select>
63
+ `;
64
+ }
65
+
66
+ /** `any` / `none`, the two states a presence dimension has. */
67
+ function presenceControl(dimension: DimensionKey): DimensionControl["render"] {
68
+ return selectControl(dimension, ["any", "none"]);
69
+ }
70
+
71
+ /**
72
+ * The media control, over the one folded vocabulary.
73
+ *
74
+ * `any`, `none`, or a set of kinds — one value, because a presence flag beside
75
+ * a kind list would allow "no media, of kinds image and video", which means
76
+ * nothing. Ticking a kind while `any` or `none` is selected replaces it.
77
+ */
78
+ const mediaControl: DimensionControl["render"] = (value, emit, labels) => {
79
+ const kinds = value === "any" || value === "none" ? [] : value.split(",");
80
+
81
+ const toggleKind = (kind: string, checked: boolean) => {
82
+ const next = checked
83
+ ? [...kinds, kind]
84
+ : kinds.filter((entry) => entry !== kind);
85
+ // An empty set is not a third meaning — it falls back to "has media".
86
+ emit(next.length > 0 ? next.join(",") : "any");
87
+ };
88
+
89
+ return html`
90
+ <div class="smart-condition-media">
91
+ <select
92
+ class="select smart-condition-select"
93
+ .value=${value === "none" ? "none" : "any"}
94
+ @change=${(event: Event) =>
95
+ emit((event.target as HTMLSelectElement).value)}
96
+ >
97
+ <option value="any" ?selected=${value !== "none"}>
98
+ ${labels.values["media.any"] ?? "any"}
99
+ </option>
100
+ <option value="none" ?selected=${value === "none"}>
101
+ ${labels.values["media.none"] ?? "none"}
102
+ </option>
103
+ </select>
104
+ ${
105
+ value === "none"
106
+ ? null
107
+ : html`
108
+ <div class="smart-condition-media-kinds">
109
+ ${MEDIA_KINDS.map(
110
+ (kind) => html`
111
+ <label class="smart-condition-kind">
112
+ <input
113
+ class="input"
114
+ type="checkbox"
115
+ .checked=${kinds.includes(kind)}
116
+ @change=${(event: Event) =>
117
+ toggleKind(
118
+ kind,
119
+ (event.target as HTMLInputElement).checked,
120
+ )}
121
+ />
122
+ <span>${labels.values[`media.${kind}`] ?? kind}</span>
123
+ </label>
124
+ `,
125
+ )}
126
+ </div>
127
+ `
128
+ }
129
+ </div>
130
+ `;
131
+ };
132
+
133
+ /** A year, typed. Bounded below by the first year a Unix timestamp can name. */
134
+ const yearControl: DimensionControl["render"] = (value, emit) => html`
135
+ <input
136
+ class="input smart-condition-year"
137
+ type="number"
138
+ min="1971"
139
+ step="1"
140
+ .value=${value}
141
+ @input=${(event: Event) => emit((event.target as HTMLInputElement).value)}
142
+ />
143
+ `;
144
+
145
+ /**
146
+ * A collection, chosen from the ones this site has.
147
+ *
148
+ * The options are filled in by the dialog host, which knows the site's
149
+ * collections; the control only spells the chosen slug.
150
+ */
151
+ const collectionControl: DimensionControl["render"] = (value, emit, labels) => {
152
+ const options = Object.entries(labels.values)
153
+ .filter(([key]) => key.startsWith("collection."))
154
+ .map(([key, label]) => [key.slice("collection.".length), label] as const);
155
+
156
+ return html`
157
+ <select
158
+ class="select smart-condition-select"
159
+ .value=${value}
160
+ @change=${(event: Event) =>
161
+ emit((event.target as HTMLSelectElement).value)}
162
+ >
163
+ ${options.map(
164
+ ([slug, label]) => html`
165
+ <option value=${slug} ?selected=${slug === value}>${label}</option>
166
+ `,
167
+ )}
168
+ </select>
169
+ `;
170
+ };
171
+
172
+ /** One control per dimension. Adding a dimension that reuses a shape adds none. */
173
+ export const DIMENSION_CONTROLS: Record<DimensionKey, DimensionControl> = {
174
+ collection: { defaultValue: "", render: collectionControl },
175
+ format: {
176
+ defaultValue: FORMATS[0],
177
+ render: selectControl("format", FORMATS),
178
+ },
179
+ title: { defaultValue: "any", render: presenceControl("title") },
180
+ year: {
181
+ defaultValue: String(new Date().getUTCFullYear()),
182
+ render: yearControl,
183
+ },
184
+ media: { defaultValue: "any", render: mediaControl },
185
+ replies: { defaultValue: "any", render: presenceControl("replies") },
186
+ visibility: {
187
+ defaultValue: "public",
188
+ // `private` is absent on purpose: a smart collection is a published page,
189
+ // so it can never name the one set only its author can see.
190
+ render: selectControl("visibility", ["public", "featured", "hidden"]),
191
+ },
192
+ };
193
+
194
+ /** The empty selection — no conditions, which means every post. */
195
+ export function emptySelection(): PostFilterSelection {
196
+ return {};
197
+ }
198
+
199
+ /**
200
+ * Read a stored selection into condition rows.
201
+ *
202
+ * Goes through the registry's serializer so every row holds the same URL
203
+ * spelling the controls produce.
204
+ *
205
+ * @param selection - A stored selection, as the API returns it
206
+ * @returns One row per set dimension, in registry order
207
+ * @example
208
+ * selectionToRows({ format: "quote" }); // [{ key: "format", value: "quote" }]
209
+ */
210
+ export function selectionToRows(
211
+ selection: Record<string, unknown>,
212
+ ): SmartConditionRow[] {
213
+ const params = serializePostFilterSelection(
214
+ selection as PostFilterSelection,
215
+ { collections: collectionVocabulary() },
216
+ );
217
+
218
+ const rows: SmartConditionRow[] = [];
219
+ for (const key of FILTER_DIMENSION_KEYS) {
220
+ const value = params.get(FILTER_DIMENSIONS[key].url.param);
221
+ if (value !== null) rows.push({ key, value });
222
+ }
223
+ return rows;
224
+ }
225
+
226
+ /**
227
+ * Turn condition rows back into a selection the API accepts.
228
+ *
229
+ * Runs the rows through the registry's own parser rather than mapping them by
230
+ * hand — the values are already in the URL spelling, so this is exactly the
231
+ * read the archive performs.
232
+ *
233
+ * @param rows - The dialog's condition rows
234
+ * @returns The selection, with any unreadable row dropped
235
+ * @example
236
+ * rowsToSelection([{ key: "format", value: "quote" }]); // { format: "quote" }
237
+ */
238
+ export function rowsToSelection(
239
+ rows: readonly SmartConditionRow[],
240
+ ): PostFilterSelection {
241
+ const params = new URLSearchParams();
242
+ for (const row of rows) {
243
+ if (!row.value) continue;
244
+ const dimension = FILTER_DIMENSIONS[row.key];
245
+ if (dimension) params.set(dimension.url.param, row.value);
246
+ }
247
+
248
+ const { selection } = parsePostFilterSelection(
249
+ (key) => params.get(key) ?? undefined,
250
+ { collections: collectionVocabulary() },
251
+ );
252
+ return selection;
253
+ }
254
+
255
+ /**
256
+ * The site's collections, as slug↔id lookups.
257
+ *
258
+ * Filled in by the dialog host before the dialog opens. Kept module-level
259
+ * because both conversions above need it and neither is called from a place
260
+ * that has it to hand.
261
+ */
262
+ let vocabulary = {
263
+ idBySlug: new Map<string, string>(),
264
+ slugById: new Map<string, string>(),
265
+ titleById: new Map<string, string>(),
266
+ };
267
+
268
+ export function setCollectionVocabulary(
269
+ collections: readonly { id: string; slug: string; title: string }[],
270
+ ): void {
271
+ const idBySlug = new Map<string, string>();
272
+ const slugById = new Map<string, string>();
273
+ const titleById = new Map<string, string>();
274
+ for (const collection of collections) {
275
+ idBySlug.set(collection.slug, collection.id);
276
+ slugById.set(collection.id, collection.slug);
277
+ titleById.set(collection.id, collection.title);
278
+ }
279
+ vocabulary = { idBySlug, slugById, titleById };
280
+ }
281
+
282
+ /** The current lookups. Exported so the upgrade check can resolve a slug. */
283
+ export function collectionVocabulary() {
284
+ return vocabulary;
285
+ }