@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
@@ -26,15 +26,16 @@ import {
26
26
  setSortableDraggingState,
27
27
  } from "../sortable-list.js";
28
28
  import { showConfirmDialog } from "../confirm.js";
29
- import { publicPath } from "../runtime-paths.js";
29
+ import { publicPath, viewLang, viewPath } from "../runtime-paths.js";
30
30
  import { showToast, showToastWithAction } from "../toast.js";
31
- import { addCollectionToNavigation } from "../collection-navigation.js";
32
- import { consumeCollectionCreatedNotice } from "../collection-created-notice.js";
31
+ import {
32
+ addCollectionToNavigation,
33
+ addSmartCollectionToNavigation,
34
+ } from "../collection-navigation.js";
33
35
  import { getDividerCollectionGroup } from "../../lib/collection-groups.js";
34
36
  import {
35
- getCollectionEditPath,
37
+ getCollectionPagePath,
36
38
  getCollectionSelectionPath,
37
- getCollectionsDirectoryPath,
38
39
  } from "../../lib/collection-paths.js";
39
40
  import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
40
41
  import { render as renderMarkdown } from "../../lib/markdown.js";
@@ -47,7 +48,12 @@ import type {
47
48
  CollectionManagerItem,
48
49
  CollectionManagerLabels,
49
50
  ManagedCollection,
51
+ ManagedSmartCollection,
50
52
  } from "./collection-manager-types.js";
53
+ import { FUNNEL_ICON_SVG } from "../icons.js";
54
+ import { openCollectionDialog } from "../collection-dialog-host.js";
55
+ import { openSmartCollectionDialog } from "../smart-collection-dialog-host.js";
56
+ import { setCollectionVocabulary } from "./smart-collection-conditions.js";
51
57
 
52
58
  interface CollectionsResponse {
53
59
  collections?: Array<{
@@ -59,10 +65,22 @@ interface CollectionsResponse {
59
65
  threadCount: number;
60
66
  recentActivityAt: number;
61
67
  }>;
68
+ smartCollections?: Array<{
69
+ id: string;
70
+ slug: string;
71
+ title: string;
72
+ description: string | null;
73
+ selection: Record<string, unknown>;
74
+ sort: string;
75
+ layout: string | null;
76
+ threadCount: number;
77
+ recentActivityAt: number;
78
+ }>;
62
79
  directoryItems?: Array<{
63
80
  id: string;
64
- type: "collection" | "divider" | "link";
81
+ type: "collection" | "smart_collection" | "divider" | "link";
65
82
  collectionId: string | null;
83
+ smartCollectionId: string | null;
66
84
  label: string | null;
67
85
  url: string | null;
68
86
  description: string | null;
@@ -105,6 +123,7 @@ export class JantCollectionsManager extends LitElement {
105
123
 
106
124
  declare items: CollectionManagerItem[];
107
125
  declare labels: CollectionManagerLabels;
126
+ /** Collections and smart collections already in the site navigation. */
108
127
  declare navigationCollectionIds: string[];
109
128
 
110
129
  declare _items: CollectionManagerItem[];
@@ -186,6 +205,17 @@ export class JantCollectionsManager extends LitElement {
186
205
  case "link":
187
206
  this.#openLinkForm();
188
207
  break;
208
+ case "collection":
209
+ void this.#createCollection();
210
+ break;
211
+ case "smart-collection":
212
+ // Beside Add link and Add divider rather than a second `+` button:
213
+ // the `+` is where a collection is created, and two plus signs would
214
+ // immediately raise "which plus".
215
+ void openSmartCollectionDialog({}).then((changed) => {
216
+ if (changed) void this.#refreshList();
217
+ });
218
+ break;
189
219
  default:
190
220
  break;
191
221
  }
@@ -199,12 +229,33 @@ export class JantCollectionsManager extends LitElement {
199
229
  connectedCallback() {
200
230
  super.connectedCallback();
201
231
  this.#bindManagerRoot();
202
- const createdCollectionId = consumeCollectionCreatedNotice();
203
- if (createdCollectionId) {
204
- this._createdCollectionId = createdCollectionId;
232
+ }
233
+
234
+ /**
235
+ * Create a collection, then point at the row it became.
236
+ *
237
+ * The inline notice offering navigation is the reason the new collection's
238
+ * id is carried back out of the dialog: without it the author would have to
239
+ * find the row themselves to do the one thing they are most likely to want
240
+ * next.
241
+ */
242
+ async #createCollection() {
243
+ const result = await openCollectionDialog();
244
+ if (!result.changed) return;
245
+ await this.#refreshList();
246
+ if (result.collection) {
247
+ this._createdCollectionId = result.collection.id;
205
248
  }
206
249
  }
207
250
 
251
+ /** Edit a collection from its directory row, then re-read the row. */
252
+ async #editCollection(collectionId: string) {
253
+ this._showItemMenuId = null;
254
+ document.removeEventListener("click", this.#closeItemMenu);
255
+ const { changed } = await openCollectionDialog({ collectionId });
256
+ if (changed) await this.#refreshList();
257
+ }
258
+
208
259
  constructor() {
209
260
  super();
210
261
  this.items = [];
@@ -257,6 +308,7 @@ export class JantCollectionsManager extends LitElement {
257
308
  return this._items.some(
258
309
  (item) =>
259
310
  (item.type === "collection" && item.collection) ||
311
+ (item.type === "smart_collection" && item.smartCollection) ||
260
312
  (item.type === "link" && item.label && item.url),
261
313
  );
262
314
  }
@@ -327,6 +379,30 @@ export class JantCollectionsManager extends LitElement {
327
379
  }
328
380
  }
329
381
 
382
+ /**
383
+ * Keep the shared collection vocabulary current.
384
+ *
385
+ * The upgrade check on a `/archive?collection=…` link has to resolve that
386
+ * slug, and this component already holds every collection on the site, so
387
+ * doing it here costs nothing — and means the menu item never disappears
388
+ * merely because a lookup had not happened yet.
389
+ */
390
+ #syncCollectionVocabulary() {
391
+ setCollectionVocabulary(
392
+ this._items.flatMap((item) =>
393
+ item.collection
394
+ ? [
395
+ {
396
+ id: item.collection.id,
397
+ slug: item.collection.slug,
398
+ title: item.collection.title,
399
+ },
400
+ ]
401
+ : [],
402
+ ),
403
+ );
404
+ }
405
+
330
406
  #toItems(json: CollectionsResponse): CollectionManagerItem[] {
331
407
  const collections = json.collections ?? [];
332
408
  const directoryItems = json.directoryItems ?? [];
@@ -344,7 +420,24 @@ export class JantCollectionsManager extends LitElement {
344
420
  });
345
421
  }
346
422
 
423
+ const smartCollections = json.smartCollections ?? [];
424
+ const smartCollectionMap = new Map<string, ManagedSmartCollection>();
425
+ for (const smartCollection of smartCollections) {
426
+ smartCollectionMap.set(smartCollection.id, {
427
+ id: smartCollection.id,
428
+ slug: smartCollection.slug,
429
+ title: smartCollection.title,
430
+ description: smartCollection.description,
431
+ selection: smartCollection.selection ?? {},
432
+ sort: smartCollection.sort,
433
+ layout: smartCollection.layout,
434
+ threadCount: smartCollection.threadCount ?? 0,
435
+ recentActivityAt: smartCollection.recentActivityAt,
436
+ });
437
+ }
438
+
347
439
  const seenCollections = new Set<string>();
440
+ const seenSmartCollections = new Set<string>();
348
441
  const orderedItems: CollectionManagerItem[] = [];
349
442
 
350
443
  for (const item of directoryItems) {
@@ -352,31 +445,48 @@ export class JantCollectionsManager extends LitElement {
352
445
  item.collectionId != null
353
446
  ? collectionMap.get(item.collectionId)
354
447
  : undefined;
448
+ const smartCollection =
449
+ item.smartCollectionId != null
450
+ ? smartCollectionMap.get(item.smartCollectionId)
451
+ : undefined;
355
452
 
356
453
  if (item.type === "collection" && !collection) {
357
454
  continue;
358
455
  }
456
+ if (item.type === "smart_collection" && !smartCollection) {
457
+ continue;
458
+ }
359
459
 
360
460
  if (collection) {
361
461
  seenCollections.add(collection.id);
362
462
  }
463
+ if (smartCollection) {
464
+ seenSmartCollections.add(smartCollection.id);
465
+ }
363
466
 
364
467
  orderedItems.push({
365
468
  id: item.id,
366
469
  type: item.type,
367
470
  collectionId: item.collectionId,
471
+ smartCollectionId: item.smartCollectionId,
368
472
  label: item.label,
369
473
  url: item.url,
370
474
  description: item.description,
371
475
  position: item.position,
372
476
  collection,
477
+ smartCollection,
373
478
  });
374
479
  }
375
480
 
481
+ // Anything with no directory row is appended, both kinds alike. That is
482
+ // what makes "a collection missing from /collections" an impossible state.
483
+ // Such a row carries its own id, exactly as the server renders it, because
484
+ // there is no row id to carry — the move endpoint reads that as "place this
485
+ // first, then move it".
376
486
  for (const collection of collections) {
377
487
  if (seenCollections.has(collection.id)) continue;
378
488
  orderedItems.push({
379
- id: `collection-${collection.id}`,
489
+ id: collection.id,
380
490
  type: "collection",
381
491
  collectionId: collection.id,
382
492
  label: null,
@@ -386,12 +496,33 @@ export class JantCollectionsManager extends LitElement {
386
496
  });
387
497
  }
388
498
 
499
+ for (const smartCollection of smartCollections) {
500
+ if (seenSmartCollections.has(smartCollection.id)) continue;
501
+ orderedItems.push({
502
+ id: smartCollection.id,
503
+ type: "smart_collection",
504
+ smartCollectionId: smartCollection.id,
505
+ label: null,
506
+ url: null,
507
+ position: "",
508
+ smartCollection: smartCollectionMap.get(smartCollection.id),
509
+ });
510
+ }
511
+
389
512
  return orderedItems;
390
513
  }
391
514
 
392
515
  async #refreshList() {
393
516
  try {
394
- const res = await fetch("/api/collections");
517
+ // The counts are narrowed to this view's language, exactly as the
518
+ // server rendered them — without this a reorder would make every number
519
+ // jump to its site-wide value.
520
+ const lang = viewLang();
521
+ const res = await fetch(
522
+ lang
523
+ ? `/api/collections?lang=${encodeURIComponent(lang)}`
524
+ : "/api/collections",
525
+ );
395
526
  if (!res.ok) return;
396
527
  const json = (await res.json()) as CollectionsResponse;
397
528
  this._items = this.#toItems(json);
@@ -481,6 +612,7 @@ export class JantCollectionsManager extends LitElement {
481
612
  ): void {
482
613
  this.#bindManagerRoot();
483
614
  this.#syncHeaderState();
615
+ this.#syncCollectionVocabulary();
484
616
 
485
617
  if (this._reorderMode) {
486
618
  this.#initSortable();
@@ -682,18 +814,29 @@ export class JantCollectionsManager extends LitElement {
682
814
  }
683
815
  }
684
816
 
685
- async #addCollectionToNavigation(collectionId: string) {
686
- if (!collectionId || this._addingToNavigationId) return;
687
-
688
- const usesInlineNotice = this._createdCollectionId === collectionId;
817
+ /**
818
+ * Put a directory entry in the site navigation. Collections and smart
819
+ * collections differ only in which endpoint payload they send, so they share
820
+ * the pending state, the id list and the confirmation.
821
+ *
822
+ * @param targetId - TypeID of the collection or smart collection
823
+ * @param add - Helper that creates the navigation item for that kind
824
+ */
825
+ async #addToNavigation(
826
+ targetId: string,
827
+ add: (id: string) => Promise<string | undefined>,
828
+ ) {
829
+ if (!targetId || this._addingToNavigationId) return;
830
+
831
+ const usesInlineNotice = this._createdCollectionId === targetId;
689
832
  this._showItemMenuId = null;
690
- this._addingToNavigationId = collectionId;
833
+ this._addingToNavigationId = targetId;
691
834
  document.removeEventListener("click", this.#closeItemMenu);
692
835
 
693
836
  try {
694
- await addCollectionToNavigation(collectionId);
837
+ await add(targetId);
695
838
  this.navigationCollectionIds = [
696
- ...new Set([...this.navigationCollectionIds, collectionId]),
839
+ ...new Set([...this.navigationCollectionIds, targetId]),
697
840
  ];
698
841
  if (!usesInlineNotice) {
699
842
  showToastWithAction(this.labels.addedToNavigation, {
@@ -804,6 +947,7 @@ export class JantCollectionsManager extends LitElement {
804
947
  #computeSequenceLabels(): string[] {
805
948
  const isContentItem = (item: CollectionManagerItem) =>
806
949
  (item.type === "collection" && item.collection) ||
950
+ (item.type === "smart_collection" && item.smartCollection) ||
807
951
  (item.type === "link" && item.label && item.url);
808
952
 
809
953
  const groupSizes: number[] = [];
@@ -864,7 +1008,9 @@ export class JantCollectionsManager extends LitElement {
864
1008
  const collection = item.collection;
865
1009
  if (!collection) return nothing;
866
1010
 
867
- const collectionHref = publicPath(
1011
+ // A collection page is served once per language, so this link has to stay
1012
+ // in the view the reader is already in.
1013
+ const collectionHref = viewPath(
868
1014
  getCollectionSelectionPath(collection.slug),
869
1015
  );
870
1016
 
@@ -878,17 +1024,19 @@ export class JantCollectionsManager extends LitElement {
878
1024
  <span class="collection-directory-title">${collection.title}</span>
879
1025
  </a>
880
1026
  </div>
881
- ${collection.description
882
- ? html`
883
- <div class="collection-directory-description prose">
884
- ${unsafeHTML(
885
- renderMarkdown(collection.description, {
886
- namespace: collection.id,
887
- }),
888
- )}
889
- </div>
890
- `
891
- : nothing}
1027
+ ${
1028
+ collection.description
1029
+ ? html`
1030
+ <div class="collection-directory-description prose">
1031
+ ${unsafeHTML(
1032
+ renderMarkdown(collection.description, {
1033
+ namespace: collection.id,
1034
+ }),
1035
+ )}
1036
+ </div>
1037
+ `
1038
+ : nothing
1039
+ }
892
1040
  <p class="collection-directory-summary">
893
1041
  <span class="collection-directory-meta"
894
1042
  >${this.#countLabel(collection.threadCount)}</span
@@ -956,6 +1104,141 @@ export class JantCollectionsManager extends LitElement {
956
1104
  `;
957
1105
  }
958
1106
 
1107
+ /**
1108
+ * One smart collection row.
1109
+ *
1110
+ * The same row shape as a collection — count, then last activity — with the
1111
+ * `funnel` marker in the slot the link rows already use. Only the marker
1112
+ * says the membership came from conditions; the two measures beside it are
1113
+ * the same two, and are meant to be read against the rows above and below.
1114
+ */
1115
+ #renderSmartCollectionItem(item: CollectionManagerItem, sequence: string) {
1116
+ const smartCollection = item.smartCollection;
1117
+ if (!smartCollection) return nothing;
1118
+
1119
+ const href = viewPath(getCollectionPagePath(smartCollection.slug));
1120
+ const body = html`
1121
+ <div class="collection-directory-main">
1122
+ <span class="collection-directory-sequence" aria-hidden="true"
1123
+ >${sequence}</span
1124
+ >
1125
+ <div class="collection-directory-title-row">
1126
+ <a href=${publicPath(href)} class="collection-directory-title-link">
1127
+ <span class="collection-directory-title"
1128
+ >${smartCollection.title}
1129
+ <span
1130
+ class="collection-directory-smart-icon"
1131
+ role="img"
1132
+ aria-label=${this.labels.smartCollectionNoun}
1133
+ title=${this.labels.smartCollectionNoun}
1134
+ >${unsafeHTML(FUNNEL_ICON_SVG)}</span
1135
+ ></span
1136
+ >
1137
+ </a>
1138
+ </div>
1139
+ ${
1140
+ smartCollection.description
1141
+ ? html`
1142
+ <div class="collection-directory-description prose">
1143
+ ${unsafeHTML(
1144
+ renderMarkdown(smartCollection.description, {
1145
+ namespace: smartCollection.id,
1146
+ }),
1147
+ )}
1148
+ </div>
1149
+ `
1150
+ : nothing
1151
+ }
1152
+ <p class="collection-directory-summary">
1153
+ <span class="collection-directory-meta"
1154
+ >${this.#countLabel(smartCollection.threadCount)}</span
1155
+ >
1156
+ <span class="collection-directory-meta-separator" aria-hidden="true"
1157
+ >/</span
1158
+ >
1159
+ <time
1160
+ class="collection-directory-updated"
1161
+ datetime=${toISOString(smartCollection.recentActivityAt)}
1162
+ >
1163
+ ${formatRelativeAge(smartCollection.recentActivityAt)}
1164
+ </time>
1165
+ </p>
1166
+ </div>
1167
+ `;
1168
+
1169
+ if (this._reorderMode) {
1170
+ return html`
1171
+ <div
1172
+ data-directory-item=${item.id}
1173
+ class="collection-directory-item collection-directory-item-reorder"
1174
+ >
1175
+ <div class="collection-directory-handle" data-drag-handle>
1176
+ <svg
1177
+ xmlns="http://www.w3.org/2000/svg"
1178
+ width="16"
1179
+ height="16"
1180
+ viewBox="0 0 24 24"
1181
+ fill="none"
1182
+ stroke="currentColor"
1183
+ stroke-width="2"
1184
+ stroke-linecap="round"
1185
+ stroke-linejoin="round"
1186
+ >
1187
+ <circle cx="9" cy="12" r="1" />
1188
+ <circle cx="9" cy="5" r="1" />
1189
+ <circle cx="9" cy="19" r="1" />
1190
+ <circle cx="15" cy="12" r="1" />
1191
+ <circle cx="15" cy="5" r="1" />
1192
+ <circle cx="15" cy="19" r="1" />
1193
+ </svg>
1194
+ </div>
1195
+ <div class="collection-directory-reorder-main">${body}</div>
1196
+ </div>
1197
+ `;
1198
+ }
1199
+
1200
+ return html`
1201
+ <div
1202
+ class=${classMap({
1203
+ "group relative": true,
1204
+ "collection-directory-managed-row": true,
1205
+ "z-50": this._showItemMenuId === item.id,
1206
+ })}
1207
+ >
1208
+ <div
1209
+ class="collection-directory-item collection-directory-item-manageable"
1210
+ >
1211
+ ${body}
1212
+ </div>
1213
+ ${this.#renderItemMenu(item)}
1214
+ </div>
1215
+ `;
1216
+ }
1217
+
1218
+ async #deleteSmartCollection(smartCollection: ManagedSmartCollection) {
1219
+ this._showItemMenuId = null;
1220
+ document.removeEventListener("click", this.#closeItemMenu);
1221
+
1222
+ const confirmed = await showConfirmDialog({
1223
+ message: this.labels.confirmDeleteSmartCollection,
1224
+ confirmLabel: this.labels.deleteSmartCollection,
1225
+ cancelLabel: this.labels.cancel,
1226
+ tone: "danger",
1227
+ });
1228
+ if (!confirmed) return;
1229
+
1230
+ try {
1231
+ const res = await fetch(`/api/smart-collections/${smartCollection.id}`, {
1232
+ method: "DELETE",
1233
+ });
1234
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1235
+ showToast(this.labels.smartCollectionDeleted);
1236
+ await this.#refreshList();
1237
+ } catch {
1238
+ showToast(this.labels.saveFailed, "error");
1239
+ }
1240
+ }
1241
+
959
1242
  #renderLinkEditPanel(item: CollectionManagerItem) {
960
1243
  if (this._editingLinkId !== item.id) return nothing;
961
1244
 
@@ -1016,9 +1299,7 @@ export class JantCollectionsManager extends LitElement {
1016
1299
  >
1017
1300
  ${this.labels.cancel}
1018
1301
  </button>
1019
- <button type="submit" class="btn-sm">
1020
- ${this.labels.formLabels.submitLabel}
1021
- </button>
1302
+ <button type="submit" class="btn-sm">${this.labels.save}</button>
1022
1303
  </div>
1023
1304
  </form>
1024
1305
  </div>
@@ -1072,21 +1353,23 @@ export class JantCollectionsManager extends LitElement {
1072
1353
  </span>
1073
1354
  </a>
1074
1355
  </div>
1075
- ${item.description
1076
- ? html`
1077
- <div class="collection-directory-description prose">
1078
- ${unsafeHTML(
1079
- renderMarkdown(item.description, { namespace: item.id }),
1080
- )}
1081
- </div>
1082
- `
1083
- : html`
1084
- <p class="collection-directory-summary">
1085
- <span class="collection-directory-meta"
1086
- >${this.labels.linkDescriptionPlaceholder}</span
1087
- >
1088
- </p>
1089
- `}
1356
+ ${
1357
+ item.description
1358
+ ? html`
1359
+ <div class="collection-directory-description prose">
1360
+ ${unsafeHTML(
1361
+ renderMarkdown(item.description, { namespace: item.id }),
1362
+ )}
1363
+ </div>
1364
+ `
1365
+ : html`
1366
+ <p class="collection-directory-summary">
1367
+ <span class="collection-directory-meta"
1368
+ >${this.labels.linkDescriptionPlaceholder}</span
1369
+ >
1370
+ </p>
1371
+ `
1372
+ }
1090
1373
  </div>
1091
1374
  `;
1092
1375
 
@@ -1142,8 +1425,9 @@ export class JantCollectionsManager extends LitElement {
1142
1425
 
1143
1426
  #renderItemMenu(item: CollectionManagerItem) {
1144
1427
  const collection = item.collection;
1428
+ const smartCollection = item.smartCollection;
1145
1429
  const isLink = item.type === "link" && !!item.label && !!item.url;
1146
- if (!collection && !isLink) return nothing;
1430
+ if (!collection && !smartCollection && !isLink) return nothing;
1147
1431
 
1148
1432
  const isOpen = this._showItemMenuId === item.id;
1149
1433
 
@@ -1179,85 +1463,165 @@ export class JantCollectionsManager extends LitElement {
1179
1463
  <circle cx="19" cy="12" r="2" />
1180
1464
  </svg>
1181
1465
  </button>
1182
- ${isOpen
1183
- ? html`
1184
- <div
1185
- class="collections-page-menu"
1186
- @click=${(e: Event) => e.stopPropagation()}
1187
- >
1188
- ${collection
1189
- ? html`
1190
- <a
1191
- href=${publicPath(
1192
- `${getCollectionEditPath(
1193
- collection.slug,
1194
- )}?returnTo=${encodeURIComponent(
1195
- publicPath(getCollectionsDirectoryPath()),
1196
- )}`,
1197
- )}
1198
- class="collections-page-menu-item"
1199
- >
1200
- ${this.labels.edit}
1201
- </a>
1202
- ${this.navigationCollectionIds.includes(collection.id)
1466
+ ${
1467
+ isOpen
1468
+ ? html`
1469
+ <div
1470
+ class="collections-page-menu"
1471
+ @click=${(e: Event) => e.stopPropagation()}
1472
+ >
1473
+ ${
1474
+ smartCollection
1475
+ ? html`
1476
+ <!-- Opens the dialog, not an editor page: creating and
1477
+ editing a smart collection are one surface. There is
1478
+ deliberately no "Remove from Collections" — a
1479
+ directory row is only a position, and deleting it
1480
+ would move the entry to the end rather than hide it,
1481
+ so the menu item would not do what it says. -->
1482
+ <button
1483
+ type="button"
1484
+ class="collections-page-menu-item"
1485
+ @click=${() => {
1486
+ this._showItemMenuId = null;
1487
+ document.removeEventListener(
1488
+ "click",
1489
+ this.#closeItemMenu,
1490
+ );
1491
+ void openSmartCollectionDialog({
1492
+ smartCollectionId: smartCollection.id,
1493
+ }).then((changed) => {
1494
+ if (changed) void this.#refreshList();
1495
+ });
1496
+ }}
1497
+ >
1498
+ ${this.labels.edit}
1499
+ </button>
1500
+ ${
1501
+ this.navigationCollectionIds.includes(
1502
+ smartCollection.id,
1503
+ )
1504
+ ? html`
1505
+ <a
1506
+ href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1507
+ class="collections-page-menu-item"
1508
+ >
1509
+ ${this.labels.editNavigation}
1510
+ </a>
1511
+ `
1512
+ : html`
1513
+ <button
1514
+ type="button"
1515
+ class="collections-page-menu-item"
1516
+ ?disabled=${
1517
+ this._addingToNavigationId ===
1518
+ smartCollection.id
1519
+ }
1520
+ @click=${() =>
1521
+ void this.#addToNavigation(
1522
+ smartCollection.id,
1523
+ addSmartCollectionToNavigation,
1524
+ )}
1525
+ >
1526
+ ${
1527
+ this._addingToNavigationId ===
1528
+ smartCollection.id
1529
+ ? this.labels.addingToNavigation
1530
+ : this.labels.addToNavigation
1531
+ }
1532
+ </button>
1533
+ `
1534
+ }
1535
+ <button
1536
+ type="button"
1537
+ class="collections-page-menu-item collections-page-menu-item-danger"
1538
+ @click=${() =>
1539
+ void this.#deleteSmartCollection(smartCollection)}
1540
+ >
1541
+ ${this.labels.deleteSmartCollection}
1542
+ </button>
1543
+ `
1544
+ : collection
1203
1545
  ? html`
1204
- <a
1205
- href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1546
+ <button
1547
+ type="button"
1206
1548
  class="collections-page-menu-item"
1549
+ @click=${() =>
1550
+ void this.#editCollection(collection.id)}
1207
1551
  >
1208
- ${this.labels.editNavigation}
1209
- </a>
1552
+ ${this.labels.edit}
1553
+ </button>
1554
+ ${
1555
+ this.navigationCollectionIds.includes(
1556
+ collection.id,
1557
+ )
1558
+ ? html`
1559
+ <a
1560
+ href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1561
+ class="collections-page-menu-item"
1562
+ >
1563
+ ${this.labels.editNavigation}
1564
+ </a>
1565
+ `
1566
+ : html`
1567
+ <button
1568
+ type="button"
1569
+ class="collections-page-menu-item"
1570
+ ?disabled=${
1571
+ this._addingToNavigationId ===
1572
+ collection.id
1573
+ }
1574
+ @click=${() =>
1575
+ void this.#addToNavigation(
1576
+ collection.id,
1577
+ addCollectionToNavigation,
1578
+ )}
1579
+ >
1580
+ ${
1581
+ this._addingToNavigationId ===
1582
+ collection.id
1583
+ ? this.labels.addingToNavigation
1584
+ : this.labels.addToNavigation
1585
+ }
1586
+ </button>
1587
+ `
1588
+ }
1589
+ <button
1590
+ type="button"
1591
+ class="collections-page-menu-item collections-page-menu-item-danger"
1592
+ @click=${() => this.#deleteCollection(collection)}
1593
+ >
1594
+ ${this.labels.deleteCollection}
1595
+ </button>
1210
1596
  `
1211
1597
  : html`
1212
1598
  <button
1213
1599
  type="button"
1214
1600
  class="collections-page-menu-item"
1215
- ?disabled=${this._addingToNavigationId ===
1216
- collection.id}
1217
- @click=${() =>
1218
- void this.#addCollectionToNavigation(
1219
- collection.id,
1220
- )}
1601
+ @click=${() => {
1602
+ this._showItemMenuId = null;
1603
+ document.removeEventListener(
1604
+ "click",
1605
+ this.#closeItemMenu,
1606
+ );
1607
+ this.#toggleLinkEdit(item);
1608
+ }}
1221
1609
  >
1222
- ${this._addingToNavigationId === collection.id
1223
- ? this.labels.addingToNavigation
1224
- : this.labels.addToNavigation}
1610
+ ${this.labels.edit}
1225
1611
  </button>
1226
- `}
1227
- <button
1228
- type="button"
1229
- class="collections-page-menu-item collections-page-menu-item-danger"
1230
- @click=${() => this.#deleteCollection(collection)}
1231
- >
1232
- ${this.labels.deleteCollection}
1233
- </button>
1234
- `
1235
- : html`
1236
- <button
1237
- type="button"
1238
- class="collections-page-menu-item"
1239
- @click=${() => {
1240
- this._showItemMenuId = null;
1241
- document.removeEventListener(
1242
- "click",
1243
- this.#closeItemMenu,
1244
- );
1245
- this.#toggleLinkEdit(item);
1246
- }}
1247
- >
1248
- ${this.labels.edit}
1249
- </button>
1250
- <button
1251
- type="button"
1252
- class="collections-page-menu-item collections-page-menu-item-danger"
1253
- @click=${() => this.#deleteLink(item)}
1254
- >
1255
- ${this.labels.deleteLink}
1256
- </button>
1257
- `}
1258
- </div>
1259
- `
1260
- : nothing}
1612
+ <button
1613
+ type="button"
1614
+ class="collections-page-menu-item collections-page-menu-item-danger"
1615
+ @click=${() => this.#deleteLink(item)}
1616
+ >
1617
+ ${this.labels.deleteLink}
1618
+ </button>
1619
+ `
1620
+ }
1621
+ </div>
1622
+ `
1623
+ : nothing
1624
+ }
1261
1625
  </div>
1262
1626
  `;
1263
1627
  }
@@ -1351,27 +1715,31 @@ export class JantCollectionsManager extends LitElement {
1351
1715
  class="collection-directory-divider-row"
1352
1716
  aria-hidden=${hasLabel ? nothing : "true"}
1353
1717
  >
1354
- ${hasLabel
1355
- ? html`
1356
- ${group
1357
- ? html`
1358
- <a
1359
- href=${publicPath(
1360
- getCollectionSelectionPath(group.slugExpression),
1361
- )}
1362
- class="collection-directory-divider-link collection-directory-divider-text"
1363
- >
1364
- ${item.label}
1365
- </a>
1366
- `
1367
- : html`
1368
- <span class="collection-directory-divider-text">
1369
- ${item.label}
1370
- </span>
1371
- `}
1372
- <hr class="collection-directory-divider-line" />
1373
- `
1374
- : html`<hr class="collection-directory-divider-line" />`}
1718
+ ${
1719
+ hasLabel
1720
+ ? html`
1721
+ ${
1722
+ group
1723
+ ? html`
1724
+ <a
1725
+ href=${viewPath(
1726
+ getCollectionSelectionPath(group.slugExpression),
1727
+ )}
1728
+ class="collection-directory-divider-link collection-directory-divider-text"
1729
+ >
1730
+ ${item.label}
1731
+ </a>
1732
+ `
1733
+ : html`
1734
+ <span class="collection-directory-divider-text">
1735
+ ${item.label}
1736
+ </span>
1737
+ `
1738
+ }
1739
+ <hr class="collection-directory-divider-line" />
1740
+ `
1741
+ : html`<hr class="collection-directory-divider-line" />`
1742
+ }
1375
1743
  </div>
1376
1744
  </div>
1377
1745
  `;
@@ -1476,33 +1844,42 @@ export class JantCollectionsManager extends LitElement {
1476
1844
  id="collection-created-notice-title"
1477
1845
  class="collection-created-notice-message"
1478
1846
  >
1479
- ${isInNavigation
1480
- ? this.labels.addedToNavigation
1481
- : this.labels.formLabels.createdLabel}
1847
+ ${
1848
+ isInNavigation
1849
+ ? this.labels.addedToNavigation
1850
+ : this.labels.formLabels.createdLabel
1851
+ }
1482
1852
  </p>
1483
1853
  <div class="collection-created-notice-actions">
1484
- ${isInNavigation
1485
- ? html`
1486
- <a
1487
- href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1488
- class="collection-created-notice-action collection-created-notice-action-primary"
1489
- >
1490
- ${this.labels.editNavigation}
1491
- </a>
1492
- `
1493
- : html`
1494
- <button
1495
- type="button"
1496
- class="collection-created-notice-action collection-created-notice-action-primary"
1497
- ?disabled=${this._addingToNavigationId === collection.id}
1498
- @click=${() =>
1499
- void this.#addCollectionToNavigation(collection.id)}
1500
- >
1501
- ${this._addingToNavigationId === collection.id
1502
- ? this.labels.addingToNavigation
1503
- : this.labels.addToNavigation}
1504
- </button>
1505
- `}
1854
+ ${
1855
+ isInNavigation
1856
+ ? html`
1857
+ <a
1858
+ href=${publicPath(NAVIGATION_SETTINGS_PATH)}
1859
+ class="collection-created-notice-action collection-created-notice-action-primary"
1860
+ >
1861
+ ${this.labels.editNavigation}
1862
+ </a>
1863
+ `
1864
+ : html`
1865
+ <button
1866
+ type="button"
1867
+ class="collection-created-notice-action collection-created-notice-action-primary"
1868
+ ?disabled=${this._addingToNavigationId === collection.id}
1869
+ @click=${() =>
1870
+ void this.#addToNavigation(
1871
+ collection.id,
1872
+ addCollectionToNavigation,
1873
+ )}
1874
+ >
1875
+ ${
1876
+ this._addingToNavigationId === collection.id
1877
+ ? this.labels.addingToNavigation
1878
+ : this.labels.addToNavigation
1879
+ }
1880
+ </button>
1881
+ `
1882
+ }
1506
1883
  <button
1507
1884
  type="button"
1508
1885
  class="collection-created-notice-dismiss"
@@ -1536,24 +1913,32 @@ export class JantCollectionsManager extends LitElement {
1536
1913
  render() {
1537
1914
  return html`
1538
1915
  ${this.#renderCreatedCollectionNotice()} ${this.#renderCreateLinkForm()}
1539
- ${this.#hasDirectoryContent()
1540
- ? html`
1541
- <div id="collections-manager-list" class="collection-directory">
1542
- ${(() => {
1543
- const labels = this.#computeSequenceLabels();
1544
- return this._items.map((item, index) => {
1545
- if (item.type === "collection") {
1546
- return this.#renderCollectionItem(item, labels[index]);
1547
- }
1548
- if (item.type === "link") {
1549
- return this.#renderLinkItem(item, labels[index]);
1550
- }
1551
- return this.#renderDividerItem(item, index);
1552
- });
1553
- })()}
1554
- </div>
1555
- `
1556
- : html`<p class="text-muted-foreground">${this.labels.emptyState}</p>`}
1916
+ ${
1917
+ this.#hasDirectoryContent()
1918
+ ? html`
1919
+ <div id="collections-manager-list" class="collection-directory">
1920
+ ${(() => {
1921
+ const labels = this.#computeSequenceLabels();
1922
+ return this._items.map((item, index) => {
1923
+ if (item.type === "collection") {
1924
+ return this.#renderCollectionItem(item, labels[index]);
1925
+ }
1926
+ if (item.type === "smart_collection") {
1927
+ return this.#renderSmartCollectionItem(
1928
+ item,
1929
+ labels[index],
1930
+ );
1931
+ }
1932
+ if (item.type === "link") {
1933
+ return this.#renderLinkItem(item, labels[index]);
1934
+ }
1935
+ return this.#renderDividerItem(item, index);
1936
+ });
1937
+ })()}
1938
+ </div>
1939
+ `
1940
+ : html`<p class="text-muted-foreground">${this.labels.emptyState}</p>`
1941
+ }
1557
1942
  `;
1558
1943
  }
1559
1944
  }