@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Smart Collection Page
3
+ *
4
+ * The same shape as a collection page — title, description, thread count, feed
5
+ * link, timeline — minus the archive's chip bar. A collection page has no
6
+ * filter bar either, and this one is not a place to filter: the conditions are
7
+ * edited in a dialog.
8
+ *
9
+ * What marks it as smart is the funnel after the title, the same marker the
10
+ * directory puts on a smart row, and the same tooltip. The conditions
11
+ * themselves stand in for a description the owner has not written; once one is
12
+ * written, it takes the slot and the marker carries the signal alone.
13
+ */
14
+
15
+ import { msg } from "@lingui/core/macro";
16
+ import type { FC } from "hono/jsx";
17
+ import { useLingui } from "../../i18n/context.js";
18
+ import type { SmartCollectionPageProps } from "../../types.js";
19
+ import { getCollectionsDirectoryPath } from "../../lib/collection-paths.js";
20
+ import { render as renderMarkdown } from "../../lib/markdown.js";
21
+ import { formatPageLabel } from "../../lib/pagination.js";
22
+ import { toPublicPath } from "../../lib/url.js";
23
+ import { TimelineFeed } from "../feed/TimelineFeed.js";
24
+ import { getCollectionMutationLabels } from "../shared/collection-management-labels.js";
25
+ import {
26
+ getSmartCollectionDialogLabels,
27
+ getSmartCollectionLabels,
28
+ } from "../shared/smart-collection-labels.js";
29
+ import { getIconSvg } from "../../lib/icons.js";
30
+ import { Icon } from "../shared/Icon.js";
31
+ import { NAVIGATION_SETTINGS_PATH } from "../../lib/settings-paths.js";
32
+
33
+ const escapeJson = (data: unknown) =>
34
+ JSON.stringify(data).replace(/</g, "\\u003c");
35
+
36
+ export const SmartCollectionPage: FC<SmartCollectionPageProps> = ({
37
+ smartCollection,
38
+ items,
39
+ totalThreadCount,
40
+ currentPage,
41
+ totalPages,
42
+ pagePath,
43
+ baseUrl,
44
+ currentSort,
45
+ defaultSort,
46
+ showRatingSort,
47
+ conditionSummary,
48
+ conditionHref,
49
+ isAuthenticated,
50
+ isInNavigation = false,
51
+ sitePathPrefix = "",
52
+ basePath = sitePathPrefix,
53
+ feedHref,
54
+ }) => {
55
+ const { i18n } = useLingui();
56
+ const labels = getSmartCollectionLabels(i18n);
57
+ const mutationLabels = getCollectionMutationLabels(i18n);
58
+ const pageUrl = toPublicPath(pagePath, basePath);
59
+ const navigationSettingsUrl = toPublicPath(
60
+ NAVIGATION_SETTINGS_PATH,
61
+ sitePathPrefix,
62
+ );
63
+ const sortTriggerId = `smart-collection-sort-trigger-${smartCollection.id}`;
64
+ const sortPopoverId = `smart-collection-sort-popover-${smartCollection.id}`;
65
+ const pageLabel =
66
+ currentPage > 1 ? formatPageLabel(currentPage, totalPages) : null;
67
+
68
+ const sortOptions = [
69
+ {
70
+ value: "newest",
71
+ label: i18n._(
72
+ msg({
73
+ message: "Newest first",
74
+ comment: "@context: Collection sort order option",
75
+ }),
76
+ ),
77
+ },
78
+ {
79
+ value: "oldest",
80
+ label: i18n._(
81
+ msg({
82
+ message: "Oldest first",
83
+ comment: "@context: Collection sort order option",
84
+ }),
85
+ ),
86
+ },
87
+ ...(showRatingSort
88
+ ? [
89
+ {
90
+ value: "rating_desc",
91
+ label: i18n._(
92
+ msg({
93
+ message: "Highest rated",
94
+ comment: "@context: Collection sort order option",
95
+ }),
96
+ ),
97
+ },
98
+ ]
99
+ : []),
100
+ ] as const;
101
+ const currentSortLabel =
102
+ sortOptions.find((option) => option.value === currentSort)?.label ??
103
+ sortOptions[0].label;
104
+
105
+ return (
106
+ <div
107
+ class="py-6"
108
+ // The same page kind a theme already styles; the second attribute is the
109
+ // hook for anything that needs to tell the two apart.
110
+ data-page="collection"
111
+ data-smart-collection={smartCollection.slug}
112
+ data-collection-mode="smart"
113
+ >
114
+ <header class="collection-page-header">
115
+ <div class="collection-page-topbar">
116
+ {/* Only the return trip is left. A breadcrumb's last crumb named this
117
+ page, which the title one line below already does, word for
118
+ word — so the trail was two thirds repetition. */}
119
+ <a
120
+ class="collection-page-back"
121
+ href={toPublicPath(getCollectionsDirectoryPath(), basePath)}
122
+ >
123
+ <Icon name="arrow-left" class="collection-page-back-icon" />
124
+ {i18n._(
125
+ msg({
126
+ message: "Collections",
127
+ comment:
128
+ "@context: Link back to the collections directory from a collection page",
129
+ }),
130
+ )}
131
+ </a>
132
+ </div>
133
+
134
+ <div class="collection-page-title-block">
135
+ <h1 class="collection-page-title">
136
+ <span>
137
+ {smartCollection.title}
138
+ {/* The marker every reader gets, in the same slot and the same
139
+ words the directory uses: which kind of collection this is
140
+ changes how an absence reads. Unlike the directory's — which
141
+ sits inside the row's link and so cannot be one itself — this
142
+ one carries the way to the archive, so a description written
143
+ over the condition line does not take it away. */}
144
+ <a
145
+ href={toPublicPath(conditionHref, basePath)}
146
+ class="collection-page-smart-icon"
147
+ title={conditionSummary}
148
+ aria-label={conditionSummary}
149
+ dangerouslySetInnerHTML={{
150
+ __html: getIconSvg("funnel", "icon-fine") ?? "",
151
+ }}
152
+ />
153
+ </span>
154
+ </h1>
155
+ {smartCollection.description ? (
156
+ <div
157
+ class="collection-page-description prose"
158
+ dangerouslySetInnerHTML={{
159
+ __html: renderMarkdown(smartCollection.description, {
160
+ namespace: smartCollection.id,
161
+ }),
162
+ }}
163
+ />
164
+ ) : (
165
+ /* Nothing written, so the conditions describe the page — and the
166
+ link is there for a reader who wants to narrow further, since
167
+ the archive already reads this vocabulary. */
168
+ <p class="collection-page-description smart-collection-conditions">
169
+ <a href={toPublicPath(conditionHref, basePath)}>
170
+ {conditionSummary}
171
+ </a>
172
+ </p>
173
+ )}
174
+ </div>
175
+
176
+ <div class="collection-page-subhead">
177
+ <div class="collection-page-meta-row">
178
+ <p class="collection-page-meta">
179
+ {totalThreadCount}{" "}
180
+ {totalThreadCount === 1
181
+ ? i18n._(
182
+ msg({
183
+ message: "thread",
184
+ comment:
185
+ "@context: Singular thread count label on collection detail page",
186
+ }),
187
+ )
188
+ : i18n._(
189
+ msg({
190
+ message: "threads",
191
+ comment:
192
+ "@context: Plural thread count label on collection detail page",
193
+ }),
194
+ )}
195
+ {pageLabel ? <span> / {pageLabel}</span> : null}
196
+ {feedHref ? (
197
+ <>
198
+ {" "}
199
+ <a
200
+ href={toPublicPath(feedHref, basePath)}
201
+ class="feed-link"
202
+ title={i18n._(
203
+ msg({
204
+ message: "RSS feed",
205
+ comment:
206
+ "@context: Tooltip for the RSS feed link on a collection page",
207
+ }),
208
+ )}
209
+ rel="noopener noreferrer"
210
+ >
211
+ <span
212
+ dangerouslySetInnerHTML={{
213
+ __html: getIconSvg("rss") ?? "",
214
+ }}
215
+ />
216
+ </a>
217
+ </>
218
+ ) : null}
219
+ </p>
220
+ <span class="collection-page-meta-divider" aria-hidden="true">
221
+ &middot;
222
+ </span>
223
+ <div class="dropdown-menu collection-sort-menu">
224
+ <button
225
+ type="button"
226
+ id={sortTriggerId}
227
+ class="collection-sort-trigger"
228
+ aria-haspopup="menu"
229
+ aria-controls={sortPopoverId}
230
+ aria-expanded="false"
231
+ >
232
+ <span class="sr-only">
233
+ {i18n._(
234
+ msg({
235
+ message: "Sort",
236
+ comment:
237
+ "@context: Sort menu label on collection detail page",
238
+ }),
239
+ )}
240
+ :{" "}
241
+ </span>
242
+ <span>{currentSortLabel}</span>
243
+ <Icon name="chevron-down" size={14} />
244
+ </button>
245
+ <div
246
+ id={sortPopoverId}
247
+ class="collection-sort-popover"
248
+ data-popover
249
+ data-align="end"
250
+ aria-hidden="true"
251
+ >
252
+ <div
253
+ class="collection-sort-options"
254
+ role="menu"
255
+ aria-labelledby={sortTriggerId}
256
+ data-collection-sort-options
257
+ >
258
+ {sortOptions.map((option) => (
259
+ <a
260
+ key={option.value}
261
+ href={
262
+ option.value === defaultSort
263
+ ? pageUrl
264
+ : `${pageUrl}?sort=${option.value}`
265
+ }
266
+ role="menuitem"
267
+ class={`collection-sort-option ${
268
+ option.value === currentSort
269
+ ? "collection-sort-option-active"
270
+ : ""
271
+ }`}
272
+ aria-current={
273
+ option.value === currentSort ? "true" : undefined
274
+ }
275
+ >
276
+ <span class="collection-sort-option-leading">
277
+ <span>{option.label}</span>
278
+ </span>
279
+ </a>
280
+ ))}
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+
286
+ {isAuthenticated ? (
287
+ <div class="collection-page-owner-tools">
288
+ {/* The dialog is a Lit component and cannot reach the i18n
289
+ catalogs, so the page that can open it carries its strings. */}
290
+ <div
291
+ hidden
292
+ data-smart-collection-dialog-labels={escapeJson(
293
+ getSmartCollectionDialogLabels(i18n),
294
+ )}
295
+ />
296
+ <div
297
+ class="collection-page-manage"
298
+ data-smart-collection-page-actions
299
+ data-smart-collection-id={smartCollection.id}
300
+ data-collection-page-labels={escapeJson({
301
+ ...mutationLabels,
302
+ confirmDelete: labels.confirmDelete,
303
+ deleteCollection: labels.deleteSmartCollection,
304
+ deleted: i18n._(
305
+ msg({
306
+ message: "Smart collection deleted.",
307
+ comment:
308
+ "@context: Confirmation after deleting a smart collection",
309
+ }),
310
+ ),
311
+ })}
312
+ data-collection-in-navigation={String(isInNavigation)}
313
+ data-collection-page-redirect-url={toPublicPath(
314
+ getCollectionsDirectoryPath(),
315
+ basePath,
316
+ )}
317
+ >
318
+ <button
319
+ type="button"
320
+ class="collection-page-manage-trigger"
321
+ aria-label={mutationLabels.moreActions}
322
+ aria-expanded="false"
323
+ aria-haspopup="menu"
324
+ data-collection-page-action="toggle-menu"
325
+ >
326
+ <svg
327
+ xmlns="http://www.w3.org/2000/svg"
328
+ width="16"
329
+ height="16"
330
+ viewBox="0 0 24 24"
331
+ fill="currentColor"
332
+ >
333
+ <circle cx="5" cy="12" r="2" />
334
+ <circle cx="12" cy="12" r="2" />
335
+ <circle cx="19" cy="12" r="2" />
336
+ </svg>
337
+ </button>
338
+
339
+ <div
340
+ class="collections-page-menu"
341
+ role="menu"
342
+ data-collection-page-menu
343
+ hidden
344
+ >
345
+ {/* Opens the dialog rather than navigating: there is no
346
+ editor page, so creating and editing are one surface. */}
347
+ <button
348
+ type="button"
349
+ class="collections-page-menu-item"
350
+ role="menuitem"
351
+ data-collection-page-action="edit"
352
+ >
353
+ <span
354
+ class="collections-page-menu-item-icon"
355
+ aria-hidden="true"
356
+ >
357
+ <Icon name="pencil" />
358
+ </span>
359
+ <span class="collections-page-menu-item-label">
360
+ {mutationLabels.edit}
361
+ </span>
362
+ </button>
363
+ <button
364
+ type="button"
365
+ class="collections-page-menu-item"
366
+ role="menuitem"
367
+ data-collection-page-action="add-to-navigation"
368
+ hidden={isInNavigation}
369
+ >
370
+ <span
371
+ class="collections-page-menu-item-icon"
372
+ aria-hidden="true"
373
+ >
374
+ <Icon name="plus" />
375
+ </span>
376
+ <span class="collections-page-menu-item-label">
377
+ {mutationLabels.addToNavigation}
378
+ </span>
379
+ </button>
380
+ <a
381
+ href={navigationSettingsUrl}
382
+ class="collections-page-menu-item"
383
+ role="menuitem"
384
+ data-collection-page-edit-navigation
385
+ hidden={!isInNavigation}
386
+ >
387
+ <span
388
+ class="collections-page-menu-item-icon"
389
+ aria-hidden="true"
390
+ >
391
+ <Icon name="list" />
392
+ </span>
393
+ <span class="collections-page-menu-item-label">
394
+ {mutationLabels.editNavigation}
395
+ </span>
396
+ </a>
397
+ <button
398
+ type="button"
399
+ class="collections-page-menu-item collections-page-menu-item-danger"
400
+ role="menuitem"
401
+ data-collection-page-action="delete"
402
+ >
403
+ <span
404
+ class="collections-page-menu-item-icon"
405
+ aria-hidden="true"
406
+ >
407
+ <Icon name="trash-2" />
408
+ </span>
409
+ <span class="collections-page-menu-item-label">
410
+ {labels.deleteSmartCollection}
411
+ </span>
412
+ </button>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ ) : null}
417
+ </div>
418
+ </header>
419
+
420
+ <main>
421
+ {items.length === 0 ? (
422
+ <p class="text-muted-foreground">{labels.emptyMatches}</p>
423
+ ) : (
424
+ <TimelineFeed
425
+ items={items}
426
+ baseUrl={baseUrl}
427
+ currentPage={currentPage}
428
+ totalPages={totalPages}
429
+ />
430
+ )}
431
+ </main>
432
+ </div>
433
+ );
434
+ };
@@ -0,0 +1,236 @@
1
+ import type { FC } from "hono/jsx";
2
+ import { msg } from "@lingui/core/macro";
3
+ import { useLingui } from "../../i18n/context.js";
4
+ import { getIconSvg } from "../../lib/icons.js";
5
+ import { CopyField } from "../shared/CopyField.js";
6
+ import { PaginatedPageHeader } from "../shared/PaginatedPageHeader.js";
7
+
8
+ /** One feed offered on this page. */
9
+ export interface SubscribeFeed {
10
+ /** What to call it, as the reader would ask for it. */
11
+ label: string;
12
+ /** Absolute address, for pasting into a reader. */
13
+ url: string;
14
+ /** One line on what this feed carries that the others do not. */
15
+ description: string;
16
+ /**
17
+ * The page whose posts this feed carries, linked from the label.
18
+ *
19
+ * Feeds live at `{page}/feed`, so each one has a page a reader can open to
20
+ * see exactly what they would be subscribing to. That page is the only
21
+ * honest preview — describing a feed in a sentence can only approximate it.
22
+ */
23
+ pageUrl: string;
24
+ }
25
+
26
+ export interface SubscribePageProps {
27
+ /**
28
+ * The feed to lead with — whichever `/feed` currently returns. Presented on
29
+ * its own because the page's job is to answer "which one do I use", and a
30
+ * menu of equals hands that question back to the reader.
31
+ */
32
+ mainFeed: SubscribeFeed;
33
+ /**
34
+ * The other two, at lower visual weight: the opposite end of the same list
35
+ * from the main feed, and the complete record.
36
+ */
37
+ otherFeeds: readonly SubscribeFeed[];
38
+ }
39
+
40
+ /**
41
+ * The page's measure.
42
+ *
43
+ * The addresses are copy fields — a read-only input with the button laid over
44
+ * its right edge — and the site's content column runs to 1088px. Left at full
45
+ * width the button sits some 700px from the address it copies, and three of
46
+ * them stacked read as a settings form. 576px holds a long site URL with a
47
+ * language prefix whole while keeping the button next to its address.
48
+ */
49
+ const COLUMN_CLASS = "max-w-xl";
50
+
51
+ /**
52
+ * How to follow this site.
53
+ *
54
+ * Every address here is rendered by the server. This page is its addresses —
55
+ * there is nothing else on it — so none of them may wait for JavaScript.
56
+ *
57
+ * @param props - The main feed and the two shown beneath it
58
+ * @returns The subscribe page content
59
+ */
60
+ export const SubscribePage: FC<SubscribePageProps> = ({
61
+ mainFeed,
62
+ otherFeeds,
63
+ }) => {
64
+ const { i18n } = useLingui();
65
+
66
+ const copyLabel = i18n._(
67
+ msg({
68
+ message: "Copy",
69
+ comment: "@context: Button that copies a feed address to the clipboard",
70
+ }),
71
+ );
72
+ const copiedMessage = i18n._(
73
+ msg({
74
+ message: "Feed URL copied.",
75
+ comment: "@context: Toast after copying a feed URL to the clipboard",
76
+ }),
77
+ );
78
+ // One string for all three rows: it explains the affordance, and the row's
79
+ // own description already names which page it opens.
80
+ const pageLinkTitle = i18n._(
81
+ msg({
82
+ message: "Open the page this feed carries",
83
+ comment:
84
+ "@context: Tooltip on the subscribe page feed names, which link to the page whose posts that feed carries",
85
+ }),
86
+ );
87
+ const failedMessage = i18n._(
88
+ msg({
89
+ message: "Could not copy. Select the address and copy it.",
90
+ comment:
91
+ "@context: Toast when the clipboard is unavailable, pointing at selecting the address by hand",
92
+ }),
93
+ );
94
+
95
+ return (
96
+ <div data-page="subscribe" class={COLUMN_CLASS}>
97
+ <PaginatedPageHeader
98
+ title={i18n._(
99
+ msg({
100
+ message: "Subscribe",
101
+ comment: "@context: Page title for the feed subscription page",
102
+ }),
103
+ )}
104
+ // The page is about feeds and carried no feed mark at all. It also
105
+ // ends by telling the reader to look for this glyph elsewhere, which
106
+ // only works once they have seen it here.
107
+ iconHtml={
108
+ getIconSvg("rss", "size-6 text-muted-foreground") ?? undefined
109
+ }
110
+ description={i18n._(
111
+ msg({
112
+ message:
113
+ "This site publishes Atom feeds. Copy one of these addresses into a feed reader.",
114
+ comment:
115
+ "@context: Subscribe page introduction, above the feed addresses. States the format the site serves, then the one action to take.",
116
+ }),
117
+ )}
118
+ />
119
+
120
+ <div class="card p-5">
121
+ <CopyField
122
+ label={mainFeed.label}
123
+ labelHref={mainFeed.pageUrl}
124
+ labelTitle={pageLinkTitle}
125
+ value={mainFeed.url}
126
+ description={mainFeed.description}
127
+ copyLabel={copyLabel}
128
+ copiedMessage={copiedMessage}
129
+ failedMessage={failedMessage}
130
+ />
131
+ </div>
132
+
133
+ <div class="mt-8 flex flex-col gap-5">
134
+ {otherFeeds.map((feed) => (
135
+ <CopyField
136
+ key={feed.url}
137
+ label={feed.label}
138
+ labelHref={feed.pageUrl}
139
+ labelTitle={pageLinkTitle}
140
+ value={feed.url}
141
+ description={feed.description}
142
+ copyLabel={copyLabel}
143
+ copiedMessage={copiedMessage}
144
+ failedMessage={failedMessage}
145
+ />
146
+ ))}
147
+ </div>
148
+
149
+ <p class="mt-8 flex items-start gap-2 text-sm text-muted-foreground">
150
+ {/* The specimen the sentence points at. Decorative — the sentence
151
+ names it, so a screen reader announcing the glyph twice would only
152
+ get in the way. */}
153
+ <span
154
+ class="mt-0.5 shrink-0 opacity-70"
155
+ aria-hidden="true"
156
+ dangerouslySetInnerHTML={{
157
+ __html: getIconSvg("rss", "size-3.5") ?? "",
158
+ }}
159
+ />
160
+ <span>
161
+ {i18n._(
162
+ msg({
163
+ message:
164
+ "Collections and filtered archive views have their own feeds. Look for this icon on those pages.",
165
+ comment:
166
+ "@context: Subscribe page closing note, next to a small feed icon standing in for the ones on collection and archive pages",
167
+ }),
168
+ )}
169
+ </span>
170
+ </p>
171
+
172
+ {/* Last, under a rule: a reader who already keeps a feed reader never
173
+ has to read it, and one who does not can find it by its heading.
174
+ Naming specific readers was considered and dropped — the list would
175
+ be identical on every Jant site, which makes the software the one
176
+ endorsing them, and it would rot as readers shut down or change
177
+ hands. */}
178
+ <section class="mt-10 border-t pt-6">
179
+ <h2 class="text-base font-medium">
180
+ {i18n._(
181
+ msg({
182
+ message: "What a feed reader does",
183
+ comment:
184
+ "@context: Subscribe page heading over the explanation of feed readers, for readers who have not used one",
185
+ }),
186
+ )}
187
+ </h2>
188
+ <p class="mt-2 text-sm text-muted-foreground">
189
+ {i18n._(
190
+ msg({
191
+ message:
192
+ "A feed reader checks these addresses for new posts and collects them in one place. Most are an app on your phone or computer; some are a website you sign in to.",
193
+ comment:
194
+ "@context: Subscribe page explanation of what a feed reader is and where it runs",
195
+ }),
196
+ )}
197
+ </p>
198
+ <p class="mt-2 text-sm text-muted-foreground">
199
+ {i18n._(
200
+ msg({
201
+ message:
202
+ "Subscribing creates no account here. To stop, delete the address from your reader.",
203
+ comment:
204
+ "@context: Subscribe page note that subscribing is one-sided and how to undo it",
205
+ }),
206
+ )}
207
+ </p>
208
+ {/* The whole sentence is the link, so its accessible name describes
209
+ where it goes and no fragment has to be assembled around an anchor.
210
+ aboutfeeds.com is the one widely used vendor-neutral explainer
211
+ written for exactly this reader — someone who just met a feed
212
+ address. It does name a few readers, which is the point: a third
213
+ party can recommend them without every Jant site appearing to. If
214
+ it ever goes dark the two paragraphs above still stand on their
215
+ own. */}
216
+ <p class="mt-4 text-sm">
217
+ <a
218
+ href="https://aboutfeeds.com/"
219
+ class="underline decoration-1 underline-offset-4"
220
+ target="_blank"
221
+ rel="noopener noreferrer"
222
+ >
223
+ {i18n._(
224
+ msg({
225
+ message:
226
+ "A longer introduction to feeds and readers, at aboutfeeds.com",
227
+ comment:
228
+ "@context: Subscribe page link to the independent aboutfeeds.com explainer. The whole sentence is the link text.",
229
+ }),
230
+ )}
231
+ </a>
232
+ </p>
233
+ </section>
234
+ </div>
235
+ );
236
+ };