@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * General Settings Component
3
3
  *
4
- * Main container for the General settings page. Saveable groups track dirty
5
- * state independently, while checkbox-only sections save immediately.
4
+ * Main container for the General settings page. Typed fields are grouped
5
+ * behind a Save button and track their dirty state independently; every
6
+ * control whose value is complete the moment it is set — checkbox, radio,
7
+ * select — saves on change.
6
8
  *
7
9
  * Light DOM only — BaseCoat and Tailwind classes apply directly.
8
10
  */
@@ -11,40 +13,55 @@ import { LitElement, html, nothing } from "lit";
11
13
  import type { Editor } from "@tiptap/core";
12
14
  import { MAX_SITE_NAME_LENGTH } from "../../types.js";
13
15
  import {
14
- getSupportedLocaleEntries,
15
- getOrBuildEntry,
16
- type LocaleEntry,
17
- } from "../../i18n/supported-locales.js";
16
+ discoverIntroRuns,
17
+ resolveDiscoverMode,
18
+ type DiscoverPageUrls,
19
+ } from "../../lib/discover.js";
18
20
  import type {
19
21
  SettingsInitialData,
20
22
  SettingsLabels,
21
23
  SettingsTimezone,
22
- SettingsCjkFont,
23
- SettingsDashboardLanguage,
24
24
  SettingsAboutPageStatus,
25
25
  } from "./settings-types.js";
26
- import { showToast } from "../toast.js";
26
+ import {
27
+ COPY_FIELD_BUTTON_CLASS,
28
+ COPY_FIELD_CLASS,
29
+ COPY_FIELD_CONTROL_CLASS,
30
+ COPY_FIELD_INPUT_CLASS,
31
+ } from "../../lib/copy-field.js";
27
32
  import {
28
33
  createSettingsEditor,
29
34
  jsonToMarkdown,
30
35
  } from "../tiptap/create-editor.js";
31
36
 
37
+ /**
38
+ * Spacing every section shares, whatever its position.
39
+ *
40
+ * The container's `divide-y` draws the rule under each section but the last;
41
+ * this pads both sides of each rule and drops the padding at either end. The
42
+ * gap between a section's own children is left to the section.
43
+ */
44
+ const SECTION_CLASS = "flex flex-col py-8 first:pt-0 last:pb-0";
45
+
32
46
  export class JantSettingsGeneral extends LitElement {
33
47
  static properties = {
34
48
  labels: { type: Object },
35
49
  timezones: { type: Array },
36
- cjkFonts: { type: Array, attribute: "cjk-fonts" },
37
50
  siteNameFallback: { type: String, attribute: "sitename-fallback" },
38
51
  siteDescriptionFallback: {
39
52
  type: String,
40
53
  attribute: "sitedescription-fallback",
41
54
  },
42
- dashboardLanguages: { type: Array, attribute: "dashboard-languages" },
43
55
  demoMode: { type: Boolean, attribute: "demo-mode" },
56
+ discoverDefault: { type: String, attribute: "discover-default" },
57
+ discoverPages: { type: Object, attribute: "discover-pages" },
58
+ discoverStatus: { type: String, attribute: "discover-status" },
59
+ feedsEnabled: { type: Boolean, attribute: "feeds-enabled" },
44
60
  mainFeedUrl: { type: String, attribute: "main-feed-url" },
45
61
  latestFeedUrl: { type: String, attribute: "latest-feed-url" },
46
62
  featuredFeedUrl: { type: String, attribute: "featured-feed-url" },
47
63
  archiveFeedUrl: { type: String, attribute: "archive-feed-url" },
64
+ feedsDocsUrl: { type: String, attribute: "feeds-docs-url" },
48
65
  aboutPage: { type: Object, attribute: "about-page" },
49
66
  aboutEditUrl: { type: String, attribute: "about-edit-url" },
50
67
  aboutCreateUrl: {
@@ -60,12 +77,7 @@ export class JantSettingsGeneral extends LitElement {
60
77
  _siteDirty: { state: true },
61
78
  _siteLoading: { state: true },
62
79
 
63
- // Language, CJK & time group
64
- _siteLanguage: { state: true },
65
- _dashboardLanguage: { state: true },
66
- _localeOpen: { state: true },
67
- _localeQuery: { state: true },
68
- _cjkSerifFont: { state: true },
80
+ // Time group
69
81
  _timeZone: { state: true },
70
82
  _origLocale: { state: true },
71
83
  _localeDirty: { state: true },
@@ -74,7 +86,6 @@ export class JantSettingsGeneral extends LitElement {
74
86
  // Feed group
75
87
  _mainRssFeed: { state: true },
76
88
  _origMainRssFeed: { state: true },
77
- _feedDirty: { state: true },
78
89
  _feedLoading: { state: true },
79
90
 
80
91
  // Home auto-save
@@ -86,19 +97,40 @@ export class JantSettingsGeneral extends LitElement {
86
97
  _noindex: { state: true },
87
98
  _origNoindex: { state: true },
88
99
  _searchLoading: { state: true },
100
+
101
+ // Discover group
102
+ _discover: { state: true },
103
+ _origDiscover: { state: true },
104
+ _discoverLoading: { state: true },
89
105
  };
90
106
 
91
107
  declare labels: SettingsLabels;
92
108
  declare timezones: SettingsTimezone[];
93
- declare cjkFonts: SettingsCjkFont[];
94
- declare dashboardLanguages: SettingsDashboardLanguage[];
95
109
  declare siteNameFallback: string;
96
110
  declare siteDescriptionFallback: string;
97
111
  declare demoMode: boolean;
112
+ /**
113
+ * The deployment's own answer, unresolved: `""` when it has none.
114
+ *
115
+ * What the site actually declares is derived from this and from the controls
116
+ * around it — see `_effectiveDiscoverMode`.
117
+ */
118
+ declare discoverDefault: string;
119
+ /** The directory's own pages, or `null` when none is configured. */
120
+ declare discoverPages: DiscoverPageUrls | null;
121
+ /**
122
+ * JSON status block, already translated by the server.
123
+ *
124
+ * The sentences carry runtime numbers, so they are built where the values
125
+ * are. This component only decides where they go.
126
+ */
127
+ declare discoverStatus: string;
128
+ declare feedsEnabled: boolean;
98
129
  declare mainFeedUrl: string;
99
130
  declare latestFeedUrl: string;
100
131
  declare featuredFeedUrl: string;
101
132
  declare archiveFeedUrl: string;
133
+ declare feedsDocsUrl: string;
102
134
  declare aboutPage: SettingsAboutPageStatus;
103
135
  declare aboutEditUrl: string;
104
136
  declare aboutCreateUrl: string;
@@ -115,20 +147,10 @@ export class JantSettingsGeneral extends LitElement {
115
147
  declare _siteDirty: boolean;
116
148
  declare _siteLoading: boolean;
117
149
 
118
- // Language, CJK & time
119
- declare _siteLanguage: string;
120
- /** Admin dashboard UI locale (one of the translated catalog locales). */
121
- declare _dashboardLanguage: string;
122
- /** Whether the locale combobox dropdown is currently open. */
123
- declare _localeOpen: boolean;
124
- /** Search query inside the locale combobox. */
125
- declare _localeQuery: string;
126
- declare _cjkSerifFont: string;
150
+ // Time
151
+ /** IANA time zone name, such as `"America/New_York"`. */
127
152
  declare _timeZone: string;
128
153
  declare _origLocale: {
129
- siteLanguage: string;
130
- dashboardLanguage: string;
131
- cjkSerifFont: string;
132
154
  timeZone: string;
133
155
  };
134
156
  declare _localeDirty: boolean;
@@ -137,7 +159,6 @@ export class JantSettingsGeneral extends LitElement {
137
159
  // Feed
138
160
  declare _mainRssFeed: string;
139
161
  declare _origMainRssFeed: string;
140
- declare _feedDirty: boolean;
141
162
  declare _feedLoading: boolean;
142
163
 
143
164
  // Home
@@ -150,6 +171,12 @@ export class JantSettingsGeneral extends LitElement {
150
171
  declare _origNoindex: boolean;
151
172
  declare _searchLoading: boolean;
152
173
 
174
+ // Discover. "" means the owner has never used the control, which is not the
175
+ // same as "off" — an untouched site still follows the default.
176
+ declare _discover: string;
177
+ declare _origDiscover: string;
178
+ declare _discoverLoading: boolean;
179
+
153
180
  // TipTap editor instances
154
181
  private _descEditor: Editor | null = null;
155
182
  private _footerEditor: Editor | null = null;
@@ -163,8 +190,6 @@ export class JantSettingsGeneral extends LitElement {
163
190
  super();
164
191
  this.labels = {} as SettingsLabels;
165
192
  this.timezones = [];
166
- this.cjkFonts = [];
167
- this.dashboardLanguages = [];
168
193
  this.siteNameFallback = "";
169
194
  this.siteDescriptionFallback = "";
170
195
  this.demoMode = false;
@@ -172,6 +197,7 @@ export class JantSettingsGeneral extends LitElement {
172
197
  this.latestFeedUrl = "/latest/feed";
173
198
  this.featuredFeedUrl = "/featured/feed";
174
199
  this.archiveFeedUrl = "/archive/feed";
200
+ this.feedsDocsUrl = "";
175
201
  this.aboutPage = {
176
202
  state: "missing",
177
203
  path: "/about",
@@ -190,25 +216,14 @@ export class JantSettingsGeneral extends LitElement {
190
216
  this._siteDirty = false;
191
217
  this._siteLoading = false;
192
218
 
193
- this._siteLanguage = "en";
194
- this._dashboardLanguage = "en";
195
- this._localeOpen = false;
196
- this._localeQuery = "";
197
- this._cjkSerifFont = "off";
198
219
  this._timeZone = "UTC";
199
- this._origLocale = {
200
- siteLanguage: "en",
201
- dashboardLanguage: "en",
202
- cjkSerifFont: "off",
203
- timeZone: "UTC",
204
- };
220
+ this._origLocale = { timeZone: "UTC" };
205
221
 
206
222
  this._localeDirty = false;
207
223
  this._localeLoading = false;
208
224
 
209
225
  this._mainRssFeed = "featured";
210
226
  this._origMainRssFeed = "featured";
211
- this._feedDirty = false;
212
227
  this._feedLoading = false;
213
228
 
214
229
  this._noindex = false;
@@ -217,18 +232,22 @@ export class JantSettingsGeneral extends LitElement {
217
232
  this._origShowJantBrandingOnHome = false;
218
233
  this._homeLoading = false;
219
234
  this._searchLoading = false;
235
+
236
+ this.discoverDefault = "";
237
+ this.discoverPages = null;
238
+ this.discoverStatus = "";
239
+ this.feedsEnabled = false;
240
+ this._discover = "";
241
+ this._origDiscover = "";
242
+ this._discoverLoading = false;
220
243
  }
221
244
 
222
245
  connectedCallback() {
223
246
  super.connectedCallback();
224
- document.addEventListener("click", this._onLocalePickerDocumentClick);
225
- document.addEventListener("keydown", this._onLocalePickerKeydown);
226
247
  }
227
248
 
228
249
  disconnectedCallback() {
229
250
  super.disconnectedCallback();
230
- document.removeEventListener("click", this._onLocalePickerDocumentClick);
231
- document.removeEventListener("keydown", this._onLocalePickerKeydown);
232
251
  this._descEditor?.destroy();
233
252
  this._descEditor = null;
234
253
  this._footerEditor?.destroy();
@@ -241,16 +260,8 @@ export class JantSettingsGeneral extends LitElement {
241
260
  this._siteDescription = data.siteDescription;
242
261
  this._siteFooter = data.siteFooter;
243
262
 
244
- this._siteLanguage = data.siteLanguage;
245
- this._dashboardLanguage = data.dashboardLanguage;
246
- this._cjkSerifFont = data.cjkSerifFont;
247
263
  this._timeZone = data.timeZone;
248
- this._origLocale = {
249
- siteLanguage: data.siteLanguage,
250
- dashboardLanguage: data.dashboardLanguage,
251
- cjkSerifFont: data.cjkSerifFont,
252
- timeZone: data.timeZone,
253
- };
264
+ this._origLocale = { timeZone: data.timeZone };
254
265
 
255
266
  this._mainRssFeed = data.mainRssFeed;
256
267
  this._origMainRssFeed = data.mainRssFeed;
@@ -261,6 +272,9 @@ export class JantSettingsGeneral extends LitElement {
261
272
  this._noindex = data.noindex;
262
273
  this._origNoindex = data.noindex;
263
274
 
275
+ this._discover = data.discover;
276
+ this._origDiscover = data.discover;
277
+
264
278
  // Defer editor init to after Lit renders the containers
265
279
  this.updateComplete.then(() => {
266
280
  this._initEditors();
@@ -283,18 +297,12 @@ export class JantSettingsGeneral extends LitElement {
283
297
  };
284
298
  this._siteDirty = false;
285
299
  this._siteLoading = false;
286
- } else if (section === "language-time") {
287
- this._origLocale = {
288
- siteLanguage: this._siteLanguage,
289
- dashboardLanguage: this._dashboardLanguage,
290
- cjkSerifFont: this._cjkSerifFont,
291
- timeZone: this._timeZone,
292
- };
300
+ } else if (section === "time") {
301
+ this._origLocale = { timeZone: this._timeZone };
293
302
  this._localeDirty = false;
294
303
  this._localeLoading = false;
295
304
  } else if (section === "feeds") {
296
305
  this._origMainRssFeed = this._mainRssFeed;
297
- this._feedDirty = false;
298
306
  this._feedLoading = false;
299
307
  } else if (section === "home") {
300
308
  this._origShowJantBrandingOnHome = this._showJantBrandingOnHome;
@@ -302,6 +310,9 @@ export class JantSettingsGeneral extends LitElement {
302
310
  } else if (section === "search") {
303
311
  this._origNoindex = this._noindex;
304
312
  this._searchLoading = false;
313
+ } else if (section === "discover") {
314
+ this._origDiscover = this._discover;
315
+ this._discoverLoading = false;
305
316
  }
306
317
  }
307
318
 
@@ -309,9 +320,10 @@ export class JantSettingsGeneral extends LitElement {
309
320
  sectionError(section: string) {
310
321
  if (section === "site") {
311
322
  this._siteLoading = false;
312
- } else if (section === "language-time") {
323
+ } else if (section === "time") {
313
324
  this._localeLoading = false;
314
325
  } else if (section === "feeds") {
326
+ this._mainRssFeed = this._origMainRssFeed;
315
327
  this._feedLoading = false;
316
328
  } else if (section === "home") {
317
329
  this._showJantBrandingOnHome = this._origShowJantBrandingOnHome;
@@ -319,6 +331,9 @@ export class JantSettingsGeneral extends LitElement {
319
331
  } else if (section === "search") {
320
332
  this._noindex = this._origNoindex;
321
333
  this._searchLoading = false;
334
+ } else if (section === "discover") {
335
+ this._discover = this._origDiscover;
336
+ this._discoverLoading = false;
322
337
  }
323
338
  }
324
339
 
@@ -408,14 +423,10 @@ export class JantSettingsGeneral extends LitElement {
408
423
  );
409
424
  }
410
425
 
411
- // ── Language & time group helpers ────────────────────────────────
426
+ // ── Time group helpers ────────────────────────────────────────────
412
427
 
413
428
  private _syncLocaleDirty() {
414
- this._localeDirty =
415
- this._siteLanguage !== this._origLocale.siteLanguage ||
416
- this._dashboardLanguage !== this._origLocale.dashboardLanguage ||
417
- this._cjkSerifFont !== this._origLocale.cjkSerifFont ||
418
- this._timeZone !== this._origLocale.timeZone;
429
+ this._localeDirty = this._timeZone !== this._origLocale.timeZone;
419
430
  }
420
431
 
421
432
  private _saveLocale() {
@@ -425,157 +436,19 @@ export class JantSettingsGeneral extends LitElement {
425
436
  new CustomEvent("jant:settings-save", {
426
437
  bubbles: true,
427
438
  detail: {
428
- endpoint: "/settings/general/language-time",
429
- data: {
430
- siteLanguage: this._siteLanguage,
431
- dashboardLanguage: this._dashboardLanguage,
432
- cjkSerifFont: this._cjkSerifFont,
433
- timeZone: this._timeZone,
434
- },
435
- section: "language-time",
439
+ endpoint: "/settings/general/time",
440
+ data: { timeZone: this._timeZone },
441
+ section: "time",
436
442
  },
437
443
  }),
438
444
  );
439
445
  }
440
446
 
441
- // ── Locale combobox ────────────────────────────────────────────────
442
-
443
- private _filteredLocaleEntries(): LocaleEntry[] {
444
- const all = getSupportedLocaleEntries();
445
- const query = this._localeQuery.trim().toLowerCase();
446
- if (!query) return all;
447
- return all.filter(
448
- (e) =>
449
- e.tag.toLowerCase().includes(query) ||
450
- e.native.toLowerCase().includes(query) ||
451
- e.english.toLowerCase().includes(query),
452
- );
453
- }
454
-
455
- private _toggleLocalePicker = () => {
456
- this._localeOpen = !this._localeOpen;
457
- if (!this._localeOpen) {
458
- this._localeQuery = "";
459
- } else {
460
- // Focus the search input on next paint.
461
- this.updateComplete.then(() => {
462
- const input = this.querySelector<HTMLInputElement>(
463
- "[data-locale-search]",
464
- );
465
- input?.focus();
466
- });
467
- }
468
- };
469
-
470
- private _selectLocale(tag: string) {
471
- this._siteLanguage = tag;
472
- this._localeOpen = false;
473
- this._localeQuery = "";
474
- this._syncLocaleDirty();
475
- }
476
-
477
- private _onLocalePickerDocumentClick = (e: Event) => {
478
- if (!this._localeOpen) return;
479
- const target = e.target as Node | null;
480
- const picker = this.querySelector("[data-locale-picker]");
481
- if (picker && target && !picker.contains(target)) {
482
- this._localeOpen = false;
483
- }
484
- };
485
-
486
- private _onLocalePickerKeydown = (e: KeyboardEvent) => {
487
- if (e.isComposing || e.keyCode === 229) return;
488
- if (e.key === "Escape" && this._localeOpen) {
489
- this._localeOpen = false;
490
- this._localeQuery = "";
491
- }
492
- };
493
-
494
- private _renderLanguagePicker() {
495
- const current = getOrBuildEntry(this._siteLanguage || "en");
496
- const filtered = this._filteredLocaleEntries();
497
- const searchPlaceholder =
498
- this.labels.siteLanguageSearchPlaceholder || "Search…";
499
- const noMatches = this.labels.siteLanguageNoMatches || "No matches.";
500
-
501
- return html`
502
- <div class="relative w-fit max-w-full" data-locale-picker>
503
- <button
504
- type="button"
505
- class="flex h-9 w-full cursor-pointer items-center rounded-md border border-input bg-transparent bg-[image:var(--chevron-down-icon-50)] bg-position-[center_right_0.75rem] bg-size-[1rem] bg-no-repeat py-2 pl-3 pr-9 text-left text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
506
- aria-expanded=${this._localeOpen ? "true" : "false"}
507
- aria-haspopup="listbox"
508
- aria-labelledby="site-language-label"
509
- @click=${this._toggleLocalePicker}
510
- >
511
- <span class="min-w-0 truncate">${current.native}</span>
512
- </button>
513
-
514
- ${this._localeOpen
515
- ? html`
516
- <div
517
- class="absolute left-0 top-full z-10 mt-1 w-80 min-w-full max-w-[calc(100vw-2rem)] max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md"
518
- >
519
- <div class="border-b p-2">
520
- <input
521
- type="text"
522
- class="input w-full"
523
- data-locale-search
524
- placeholder=${searchPlaceholder}
525
- autocomplete="off"
526
- spellcheck="false"
527
- .value=${this._localeQuery}
528
- @input=${(e: Event) => {
529
- this._localeQuery = (e.target as HTMLInputElement).value;
530
- }}
531
- />
532
- </div>
533
- <div role="listbox" class="max-h-56 overflow-auto py-1">
534
- ${filtered.length === 0
535
- ? html`
536
- <div class="px-3 py-2 text-sm text-muted-foreground">
537
- ${noMatches}
538
- </div>
539
- `
540
- : filtered.map(
541
- (entry) => html`
542
- <button
543
- type="button"
544
- role="option"
545
- aria-selected=${entry.tag === this._siteLanguage
546
- ? "true"
547
- : "false"}
548
- class=${[
549
- "flex w-full flex-col px-3 py-2 text-left text-sm hover:bg-accent",
550
- entry.tag === this._siteLanguage
551
- ? "bg-accent/60"
552
- : "",
553
- ].join(" ")}
554
- @click=${() => this._selectLocale(entry.tag)}
555
- >
556
- <span>${entry.native}</span>
557
- <span class="text-xs text-muted-foreground">
558
- ${entry.english}
559
- </span>
560
- </button>
561
- `,
562
- )}
563
- </div>
564
- </div>
565
- `
566
- : nothing}
567
- </div>
568
- `;
569
- }
570
-
571
- // ── Feed group helpers ────────────────────────────────────────────
447
+ // ── Feed auto-save helpers ────────────────────────────────────────
572
448
 
573
- private _syncFeedDirty() {
574
- this._feedDirty = this._mainRssFeed !== this._origMainRssFeed;
575
- }
576
-
577
- private _saveFeeds() {
578
- if (this._feedLoading || !this._feedDirty) return;
449
+ private _saveMainRssFeed(value: string) {
450
+ if (this._feedLoading || value === this._mainRssFeed) return;
451
+ this._mainRssFeed = value;
579
452
  this._feedLoading = true;
580
453
  this.dispatchEvent(
581
454
  new CustomEvent("jant:settings-save", {
@@ -583,7 +456,7 @@ export class JantSettingsGeneral extends LitElement {
583
456
  detail: {
584
457
  endpoint: "/settings/general/feeds",
585
458
  data: {
586
- mainRssFeed: this._mainRssFeed,
459
+ mainRssFeed: value,
587
460
  },
588
461
  section: "feeds",
589
462
  },
@@ -631,6 +504,66 @@ export class JantSettingsGeneral extends LitElement {
631
504
  );
632
505
  }
633
506
 
507
+ // ── Discover helpers ──────────────────────────────────────────────
508
+
509
+ /**
510
+ * What this site's feeds declare, as of the controls on screen right now.
511
+ *
512
+ * The same `resolveDiscoverMode` the server runs, over the same inputs, so
513
+ * the checkbox cannot disagree with the feed. It has to be computed here
514
+ * rather than sent down resolved because one of its inputs, search indexing,
515
+ * is a control on this page: a value resolved on the server is correct until
516
+ * the first click and wrong from then until the next page load, which is
517
+ * exactly the state an owner reads it in. The feed switch is not — it lives
518
+ * in the Config Editor, so `feedsEnabled` holds for the life of the page.
519
+ */
520
+ private _effectiveDiscoverMode(overrides: { noindex?: boolean } = {}) {
521
+ return resolveDiscoverMode({
522
+ // "" is never chosen, which is what lets the rules below decide.
523
+ storedValue: this._discover || null,
524
+ defaultValue: this.discoverDefault || null,
525
+ demoMode: this.demoMode,
526
+ noindex: overrides.noindex ?? this._noindex,
527
+ rssFeedsEnabled: this.feedsEnabled,
528
+ });
529
+ }
530
+
531
+ /**
532
+ * On writes `latest`: the directory may read any public post, and decides
533
+ * itself which list each one reaches — featured posts on its home, links
534
+ * and quotes on their own lists.
535
+ *
536
+ * A site that stored `featured` under an older release still reads as
537
+ * ticked, and stays `featured` until its owner touches the box: widening
538
+ * what the directory may show is the owner's to do, not an upgrade's.
539
+ */
540
+ private _onDiscoverToggle(enabled: boolean) {
541
+ this._saveDiscover(enabled ? "latest" : "off");
542
+ }
543
+
544
+ /**
545
+ * Store the choice the owner just made.
546
+ *
547
+ * The control is disabled while a save is in flight, so a second call
548
+ * cannot arrive before the first has answered; the guard covers the event
549
+ * that is already queued when that happens.
550
+ */
551
+ private _saveDiscover(value: "latest" | "off") {
552
+ if (this._discoverLoading) return;
553
+ this._discover = value;
554
+ this._discoverLoading = true;
555
+ this.dispatchEvent(
556
+ new CustomEvent("jant:settings-save", {
557
+ bubbles: true,
558
+ detail: {
559
+ endpoint: "/settings/general/discover",
560
+ data: { discover: value },
561
+ section: "discover",
562
+ },
563
+ }),
564
+ );
565
+ }
566
+
634
567
  /** Submit on Enter from non-textarea fields */
635
568
  private _onKeydown(
636
569
  e: globalThis.KeyboardEvent,
@@ -666,21 +599,23 @@ export class JantSettingsGeneral extends LitElement {
666
599
  ?disabled=${loading || !dirty}
667
600
  @click=${onSave}
668
601
  >
669
- ${loading
670
- ? html`<svg
671
- class="animate-spin size-4"
672
- xmlns="http://www.w3.org/2000/svg"
673
- viewBox="0 0 24 24"
674
- fill="none"
675
- stroke="currentColor"
676
- stroke-width="2"
677
- stroke-linecap="round"
678
- stroke-linejoin="round"
679
- role="status"
680
- >
681
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
682
- </svg>`
683
- : nothing}
602
+ ${
603
+ loading
604
+ ? html`<svg
605
+ class="animate-spin size-4"
606
+ xmlns="http://www.w3.org/2000/svg"
607
+ viewBox="0 0 24 24"
608
+ fill="none"
609
+ stroke="currentColor"
610
+ stroke-width="2"
611
+ stroke-linecap="round"
612
+ stroke-linejoin="round"
613
+ role="status"
614
+ >
615
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
616
+ </svg>`
617
+ : nothing
618
+ }
684
619
  ${this.labels.save}
685
620
  </button>
686
621
  </div>
@@ -711,10 +646,8 @@ export class JantSettingsGeneral extends LitElement {
711
646
  value=${value}
712
647
  class="mt-1"
713
648
  .checked=${checked}
714
- @change=${() => {
715
- this._mainRssFeed = value;
716
- this._syncFeedDirty();
717
- }}
649
+ ?disabled=${this._feedLoading}
650
+ @change=${() => this._saveMainRssFeed(value)}
718
651
  />
719
652
  <div>
720
653
  <div class="font-medium">${title}</div>
@@ -724,47 +657,39 @@ export class JantSettingsGeneral extends LitElement {
724
657
  `;
725
658
  }
726
659
 
727
- private async _copyFeedUrl(value: string) {
728
- try {
729
- if (!globalThis.navigator.clipboard?.writeText) {
730
- throw new Error("Clipboard unavailable");
731
- }
732
-
733
- await globalThis.navigator.clipboard.writeText(value);
734
- showToast(this.labels.feedUrlCopied);
735
- } catch {
736
- showToast(this.labels.copyFailed, "error");
737
- }
738
- }
739
-
740
- private _renderFeedInfoRow(
741
- label: string,
742
- value: string,
743
- description?: string,
744
- ) {
660
+ /**
661
+ * One feed address with a copy button.
662
+ *
663
+ * The same field `ui/shared/CopyField.tsx` renders on the public subscribe
664
+ * page, driven by the same `client/copy-field.ts` enhancer — hence the shared
665
+ * classes. lit-html cannot interpolate attribute names, so the enhancer's
666
+ * hooks are spelled out; `jant-settings-general.test.ts` asserts they match.
667
+ * Unlike the public page the button is not rendered hidden: settings is
668
+ * behind auth, where the client bundle is always present.
669
+ */
670
+ private _renderFeedUrl(label: string, value: string, description?: string) {
745
671
  return html`
746
- <div class="flex min-w-0 flex-col gap-1">
672
+ <div class=${COPY_FIELD_CLASS} data-copy-field-root>
747
673
  <p class="text-sm font-medium">${label}</p>
748
- ${description
749
- ? html`<p class="text-sm text-muted-foreground">${description}</p>`
750
- : ""}
751
- <div class="relative">
674
+ ${
675
+ description
676
+ ? html`<p class="text-sm text-muted-foreground">${description}</p>`
677
+ : ""
678
+ }
679
+ <div class=${COPY_FIELD_CONTROL_CLASS}>
752
680
  <input
753
681
  type="text"
754
- class="input w-full pr-20 font-mono text-sm"
682
+ class=${COPY_FIELD_INPUT_CLASS}
755
683
  .value=${value}
756
684
  readonly
757
685
  aria-label=${label}
758
- @click=${(e: Event) =>
759
- (e.currentTarget as HTMLInputElement).select()}
760
- @focus=${(e: Event) =>
761
- (e.currentTarget as HTMLInputElement).select()}
686
+ data-copy-field-value
762
687
  />
763
688
  <button
764
689
  type="button"
765
- class="btn-sm-outline absolute right-2 top-1/2 h-7 -translate-y-1/2 px-2.5 text-xs"
766
- data-copy-feed-url
767
- @click=${() => void this._copyFeedUrl(value)}
690
+ class=${COPY_FIELD_BUTTON_CLASS}
691
+ data-copy-field=${this.labels.feedUrlCopied}
692
+ data-copy-field-failed=${this.labels.copyFailed}
768
693
  >
769
694
  ${this.labels.copy}
770
695
  </button>
@@ -779,319 +704,473 @@ export class JantSettingsGeneral extends LitElement {
779
704
  return html`
780
705
  <div class="mt-2 text-sm text-muted-foreground" data-about-page-row>
781
706
  ${this.labels.aboutPagePrompt}
782
- ${status.state === "ready"
783
- ? html`
784
- <a
785
- class="font-medium text-foreground underline-offset-4 hover:underline"
786
- href=${this.aboutEditUrl}
787
- >
788
- ${this.labels.editAboutPage}
789
- </a>
790
- `
791
- : status.state === "missing"
707
+ ${
708
+ status.state === "ready"
792
709
  ? html`
793
- <form
794
- class="inline"
795
- method="post"
796
- action=${this.aboutCreateUrl}
710
+ <a
711
+ class="font-medium text-foreground underline-offset-4 hover:underline"
712
+ href=${this.aboutEditUrl}
797
713
  >
798
- <button
799
- type="submit"
800
- class="inline cursor-pointer border-0 bg-transparent p-0 font-medium text-foreground underline-offset-4 hover:underline"
801
- >
802
- ${this.labels.createAboutPage}
803
- </button>
804
- </form>
714
+ ${this.labels.editAboutPage}
715
+ </a>
805
716
  `
806
- : html`
807
- <span class="text-destructive"
808
- >${this.labels.aboutPageConflict}</span
809
- >
810
- `}
717
+ : status.state === "missing"
718
+ ? html`
719
+ <form
720
+ class="inline"
721
+ method="post"
722
+ action=${this.aboutCreateUrl}
723
+ >
724
+ <button
725
+ type="submit"
726
+ class="inline cursor-pointer border-0 bg-transparent p-0 font-medium text-foreground underline-offset-4 hover:underline"
727
+ >
728
+ ${this.labels.createAboutPage}
729
+ </button>
730
+ </form>
731
+ `
732
+ : html`
733
+ <span class="text-destructive"
734
+ >${this.labels.aboutPageConflict}</span
735
+ >
736
+ `
737
+ }
811
738
  </div>
812
739
  `;
813
740
  }
814
741
 
815
- private _renderGeneralForm() {
742
+ // ── Sections, in the order `render()` lists them ──────────────────
743
+ //
744
+ // Each section carries only its own spacing (`SECTION_CLASS`); the rules
745
+ // between them are drawn by the container, so reordering is a change to
746
+ // `render()` alone.
747
+
748
+ private _renderSiteSection() {
816
749
  return html`
817
- <div class="flex flex-col gap-8">
818
- <div>
819
- <h2 class="text-lg font-semibold">${this.labels.general}</h2>
750
+ <section
751
+ class="${SECTION_CLASS} gap-4"
752
+ @keydown=${(e: globalThis.KeyboardEvent) =>
753
+ this._onKeydown(
754
+ e,
755
+ () => this._saveSite(),
756
+ this._siteDirty,
757
+ this._siteLoading,
758
+ )}
759
+ >
760
+ ${this._renderSectionTitle(this.labels.site)}
761
+ <div class="field">
762
+ <label class="label">${this.labels.siteName}</label>
763
+ <input
764
+ type="text"
765
+ class="input"
766
+ maxlength=${MAX_SITE_NAME_LENGTH}
767
+ .value=${this._siteName}
768
+ placeholder=${this.siteNameFallback}
769
+ @input=${(e: Event) => {
770
+ this._siteName = (e.target as HTMLInputElement).value;
771
+ this._syncSiteDirty();
772
+ }}
773
+ />
820
774
  </div>
821
775
 
822
- <section
823
- class="flex flex-col gap-4"
824
- @keydown=${(e: globalThis.KeyboardEvent) =>
825
- this._onKeydown(
826
- e,
827
- () => this._saveSite(),
828
- this._siteDirty,
829
- this._siteLoading,
830
- )}
831
- >
832
- ${this._renderSectionTitle(this.labels.site)}
833
- <div class="field">
834
- <label class="label">${this.labels.siteName}</label>
835
- <input
836
- type="text"
837
- class="input"
838
- maxlength=${MAX_SITE_NAME_LENGTH}
839
- .value=${this._siteName}
840
- placeholder=${this.siteNameFallback}
841
- @input=${(e: Event) => {
842
- this._siteName = (e.target as HTMLInputElement).value;
843
- this._syncSiteDirty();
844
- }}
845
- />
846
- </div>
776
+ <div class="field">
777
+ <label class="label">${this.labels.aboutBlog}</label>
778
+ <div class="settings-tiptap-editor" data-settings-desc-editor></div>
779
+ <p class="text-sm text-muted-foreground mt-1">
780
+ ${this.labels.aboutBlogHelp}
781
+ </p>
782
+ ${this._renderAboutPageRow()}
783
+ </div>
847
784
 
848
- <div class="field">
849
- <label class="label">${this.labels.aboutBlog}</label>
850
- <div class="settings-tiptap-editor" data-settings-desc-editor></div>
851
- <p class="text-sm text-muted-foreground mt-1">
852
- ${this.labels.aboutBlogHelp}
853
- </p>
854
- ${this._renderAboutPageRow()}
855
- </div>
785
+ <div class="field">
786
+ <label class="label">${this.labels.siteFooter}</label>
787
+ <div class="settings-tiptap-editor" data-settings-footer-editor></div>
788
+ <p class="text-sm text-muted-foreground mt-1">
789
+ ${this.labels.footerHelp}
790
+ </p>
791
+ </div>
856
792
 
857
- <div class="field">
858
- <label class="label">${this.labels.siteFooter}</label>
859
- <div
860
- class="settings-tiptap-editor"
861
- data-settings-footer-editor
862
- ></div>
863
- <p class="text-sm text-muted-foreground mt-1">
864
- ${this.labels.footerHelp}
865
- </p>
866
- </div>
793
+ ${this._renderSaveAction(this._siteLoading, this._siteDirty, () =>
794
+ this._saveSite(),
795
+ )}
796
+ </section>
797
+ `;
798
+ }
867
799
 
868
- ${this._renderSaveAction(this._siteLoading, this._siteDirty, () =>
869
- this._saveSite(),
870
- )}
871
- </section>
872
-
873
- <section
874
- class="flex flex-col gap-4 border-t pt-8"
875
- @keydown=${(e: globalThis.KeyboardEvent) =>
876
- this._onKeydown(
877
- e,
878
- () => this._saveLocale(),
879
- this._localeDirty,
880
- this._localeLoading,
881
- )}
882
- >
883
- ${this._renderSectionTitle(this.labels.languageAndTime)}
884
- <div class="field">
885
- <label id="site-language-label" class="label"
886
- >${this.labels.siteLanguage}</label
887
- >
888
- ${this._renderLanguagePicker()}
889
- <p class="text-sm text-muted-foreground mt-1">
890
- ${this.labels.siteLanguageHelp}
891
- </p>
892
- <p class="text-sm text-muted-foreground mt-1">
893
- ${this.labels.contentLanguagePreview}
894
- <code class="rounded bg-muted px-1.5 py-0.5 text-xs"
895
- >${`<html lang="${this._siteLanguage || "en"}">`}</code
896
- >
897
- </p>
898
- </div>
800
+ /**
801
+ * Search engine indexing and Jant Discover.
802
+ *
803
+ * The two groups sit further apart than a section's heading sits from its
804
+ * first control, so the Discover help text does not read as belonging to
805
+ * the indexing checkbox.
806
+ */
807
+ private _renderVisibilitySection() {
808
+ return html`
809
+ <section class="${SECTION_CLASS} gap-6">
810
+ ${this._renderSectionTitle(this.labels.siteVisibility)}
811
+ <div class="flex flex-col gap-2">
812
+ <label class="flex items-center gap-2 cursor-pointer">
813
+ <input
814
+ type="checkbox"
815
+ class="checkbox"
816
+ .checked=${!this._noindex}
817
+ ?disabled=${this.demoMode || this._searchLoading}
818
+ @change=${(e: Event) =>
819
+ this._saveSearchToggle((e.target as HTMLInputElement).checked)}
820
+ />
821
+ <span>${this.labels.allowIndexing}</span>
822
+ </label>
823
+ ${
824
+ this.demoMode
825
+ ? html`<p class="text-sm text-muted-foreground">
826
+ ${this.labels.demoSeoLocked}
827
+ </p>`
828
+ : nothing
829
+ }
830
+ </div>
831
+ ${this._renderDiscoverForm()}
832
+ </section>
833
+ `;
834
+ }
899
835
 
900
- <div class="field">
901
- <label id="dashboard-language-label" class="label"
902
- >${this.labels.dashboardLanguage}</label
903
- >
904
- <select
905
- class="select"
906
- aria-labelledby="dashboard-language-label"
907
- @change=${(e: Event) => {
908
- this._dashboardLanguage = (e.target as HTMLSelectElement).value;
909
- this._syncLocaleDirty();
910
- }}
911
- >
912
- ${this.dashboardLanguages.map(
913
- (lang) => html`
914
- <option
915
- value=${lang.value}
916
- ?selected=${this._dashboardLanguage === lang.value}
917
- >
918
- ${lang.label}
919
- </option>
920
- `,
921
- )}
922
- </select>
923
- <p class="text-sm text-muted-foreground mt-1">
924
- ${this.labels.dashboardLanguageHelp}
925
- </p>
836
+ private _renderFeedsSection() {
837
+ return html`
838
+ <section class="${SECTION_CLASS} gap-4">
839
+ ${this._renderSectionTitle(this.labels.feeds)}
840
+ <div class="field">
841
+ <p class="label">${this.labels.mainRssFeed}</p>
842
+ <p class="text-sm text-muted-foreground mt-1">
843
+ ${this.labels.mainRssFeedHelp}
844
+ </p>
845
+ <div class="mt-3 flex flex-col gap-2">
846
+ ${this._renderMainRssFeedOption(
847
+ "featured",
848
+ this.labels.featuredFeedOption,
849
+ this.labels.featuredFeedOptionDescription,
850
+ )}
851
+ ${this._renderMainRssFeedOption(
852
+ "latest",
853
+ this.labels.latestFeedOption,
854
+ this.labels.latestFeedOptionDescription,
855
+ )}
926
856
  </div>
857
+ <p class="text-sm text-muted-foreground mt-2">
858
+ ${this.labels.mainRssFeedWarning}
859
+ </p>
860
+ </div>
927
861
 
928
- <div class="field">
929
- <label class="label">${this.labels.cjkFont}</label>
930
- <select
931
- class="select"
932
- @change=${(e: Event) => {
933
- this._cjkSerifFont = (e.target as HTMLSelectElement).value;
934
- this._syncLocaleDirty();
935
- }}
936
- >
937
- ${this.cjkFonts.map(
938
- (font) => html`
939
- <option
940
- value=${font.value}
941
- ?selected=${this._cjkSerifFont === font.value}
942
- >
943
- ${font.label}
944
- </option>
945
- `,
946
- )}
947
- </select>
948
- <p class="text-sm text-muted-foreground mt-1">
949
- ${this.labels.cjkFontHelp}
950
- </p>
951
- </div>
862
+ <div class="rounded-xl border border-border/70 bg-muted/30 p-4">
863
+ <div class="flex flex-col gap-4">
864
+ <div class="flex flex-col gap-1">
865
+ <p class="text-sm font-medium">
866
+ ${this.labels.availableFeedUrls}
867
+ </p>
868
+ <p class="text-sm text-muted-foreground">
869
+ ${this.labels.availableFeedUrlsHelp}
870
+ ${
871
+ this.feedsDocsUrl
872
+ ? html`
873
+ <a
874
+ href=${this.feedsDocsUrl}
875
+ target="_blank"
876
+ rel="noopener noreferrer"
877
+ class="underline hover:text-foreground transition-colors"
878
+ >${this.labels.feedsDocs}</a
879
+ >
880
+ `
881
+ : ""
882
+ }
883
+ </p>
884
+ </div>
952
885
 
953
- <div class="field">
954
- <label class="label">${this.labels.timeZone}</label>
955
- <select
956
- class="select"
957
- @change=${(e: Event) => {
958
- this._timeZone = (e.target as HTMLSelectElement).value;
959
- this._syncLocaleDirty();
960
- }}
961
- >
962
- ${this.timezones.map(
963
- (tz) => html`
964
- <option
965
- value=${tz.value}
966
- ?selected=${this._timeZone === tz.value}
967
- >
968
- ${tz.label}
969
- </option>
970
- `,
971
- )}
972
- </select>
886
+ ${this._renderFeedUrl(this.labels.mainFeedUrl, this.mainFeedUrl)}
887
+ ${this._renderFeedUrl(
888
+ this.labels.latestFeedUrl,
889
+ this.latestFeedUrl,
890
+ )}
891
+ ${this._renderFeedUrl(
892
+ this.labels.featuredFeedUrl,
893
+ this.featuredFeedUrl,
894
+ )}
895
+ ${this._renderFeedUrl(
896
+ this.labels.archiveFeedUrl,
897
+ this.archiveFeedUrl,
898
+ this.labels.archiveFeedUrlHelp,
899
+ )}
973
900
  </div>
901
+ </div>
902
+ </section>
903
+ `;
904
+ }
974
905
 
975
- ${this._renderSaveAction(this._localeLoading, this._localeDirty, () =>
976
- this._saveLocale(),
906
+ private _renderTimeSection() {
907
+ return html`
908
+ <section
909
+ class="${SECTION_CLASS} gap-4"
910
+ @keydown=${(e: globalThis.KeyboardEvent) =>
911
+ this._onKeydown(
912
+ e,
913
+ () => this._saveLocale(),
914
+ this._localeDirty,
915
+ this._localeLoading,
977
916
  )}
978
- </section>
979
-
980
- <section
981
- class="flex flex-col gap-4 border-t pt-8"
982
- @keydown=${(e: globalThis.KeyboardEvent) =>
983
- this._onKeydown(
984
- e,
985
- () => this._saveFeeds(),
986
- this._feedDirty,
987
- this._feedLoading,
917
+ >
918
+ ${this._renderSectionTitle(this.labels.timeSection)}
919
+ <div class="field">
920
+ <label class="label">${this.labels.timeZone}</label>
921
+ <select
922
+ class="select"
923
+ @change=${(e: Event) => {
924
+ this._timeZone = (e.target as HTMLSelectElement).value;
925
+ this._syncLocaleDirty();
926
+ }}
927
+ >
928
+ ${this.timezones.map(
929
+ (tz) => html`
930
+ <option
931
+ value=${tz.value}
932
+ ?selected=${this._timeZone === tz.value}
933
+ >
934
+ ${tz.label}
935
+ </option>
936
+ `,
988
937
  )}
989
- >
990
- ${this._renderSectionTitle(this.labels.feeds)}
991
- <div class="field">
992
- <p class="label">${this.labels.mainRssFeed}</p>
993
- <p class="text-sm text-muted-foreground mt-1">
994
- ${this.labels.mainRssFeedHelp}
995
- </p>
996
- <div class="mt-3 flex flex-col gap-2">
997
- ${this._renderMainRssFeedOption(
998
- "featured",
999
- this.labels.featuredFeedOption,
1000
- this.labels.featuredFeedOptionDescription,
1001
- )}
1002
- ${this._renderMainRssFeedOption(
1003
- "latest",
1004
- this.labels.latestFeedOption,
1005
- this.labels.latestFeedOptionDescription,
1006
- )}
1007
- </div>
1008
- <p class="text-sm text-muted-foreground mt-2">
1009
- ${this.labels.mainRssFeedWarning}
1010
- </p>
1011
- </div>
938
+ </select>
939
+ </div>
1012
940
 
1013
- <div class="rounded-xl border border-border/70 bg-muted/30 p-4">
1014
- <div class="flex flex-col gap-4">
1015
- <div class="flex flex-col gap-1">
1016
- <p class="text-sm font-medium">
1017
- ${this.labels.availableFeedUrls}
1018
- </p>
1019
- <p class="text-sm text-muted-foreground">
1020
- ${this.labels.availableFeedUrlsHelp}
1021
- </p>
1022
- </div>
1023
-
1024
- ${this._renderFeedInfoRow(
1025
- this.labels.mainFeedUrl,
1026
- this.mainFeedUrl,
1027
- )}
1028
- ${this._renderFeedInfoRow(
1029
- this.labels.latestFeedUrl,
1030
- this.latestFeedUrl,
1031
- )}
1032
- ${this._renderFeedInfoRow(
1033
- this.labels.featuredFeedUrl,
1034
- this.featuredFeedUrl,
1035
- )}
1036
- ${this._renderFeedInfoRow(
1037
- this.labels.archiveFeedUrl,
1038
- this.archiveFeedUrl,
1039
- this.labels.archiveFeedUrlHelp,
1040
- )}
1041
- </div>
1042
- </div>
941
+ ${this._renderSaveAction(this._localeLoading, this._localeDirty, () =>
942
+ this._saveLocale(),
943
+ )}
944
+ </section>
945
+ `;
946
+ }
1043
947
 
1044
- ${this._renderSaveAction(this._feedLoading, this._feedDirty, () =>
1045
- this._saveFeeds(),
1046
- )}
1047
- </section>
948
+ private _renderHomeSection() {
949
+ return html`
950
+ <section class="${SECTION_CLASS} gap-4">
951
+ ${this._renderSectionTitle(this.labels.home)}
952
+ <label class="flex items-center gap-2 cursor-pointer">
953
+ <input
954
+ type="checkbox"
955
+ class="checkbox"
956
+ .checked=${this._showJantBrandingOnHome}
957
+ ?disabled=${this._homeLoading}
958
+ @change=${(e: Event) =>
959
+ this._saveHomeToggle((e.target as HTMLInputElement).checked)}
960
+ />
961
+ <span>${this.labels.showJantBrandingOnHome}</span>
962
+ </label>
963
+ </section>
964
+ `;
965
+ }
966
+
967
+ /**
968
+ * Jant Discover.
969
+ *
970
+ * Saves on change, like the indexing checkbox next to it: the box is a
971
+ * complete answer on its own, so there is nothing a Save button would be
972
+ * waiting for. One box and no sub-choice — which of a blog's posts reach
973
+ * which list is the directory's own rule, stated on the directory.
974
+ *
975
+ * Ticking the box is also what announces a self-hosted site to the
976
+ * directory.
977
+ */
978
+ private _renderDiscoverForm() {
979
+ const effective = this._effectiveDiscoverMode();
980
+ const enabled = effective !== "none";
981
+ const locked = this.demoMode || !this.feedsEnabled;
982
+ // The box unticks itself when search indexing goes off, and a control that
983
+ // moves on its own has to say why. Asked as a counterfactual rather than
984
+ // read off `noindex`, so the line appears only where it is the reason: a
985
+ // site the deployment would have listed, held back by that one setting.
986
+ // A site nobody would list either way is not being held back by anything,
987
+ // and an owner who ticked the box is not affected at all.
988
+ const heldBackBySearch =
989
+ !locked &&
990
+ !enabled &&
991
+ this._effectiveDiscoverMode({ noindex: false }) !== "none";
1048
992
 
1049
- <section class="flex flex-col gap-4 border-t pt-8 pb-6">
1050
- ${this._renderSectionTitle(this.labels.home)}
993
+ return html`
994
+ <div class="flex flex-col gap-3">
995
+ <div class="flex flex-col gap-2">
1051
996
  <label class="flex items-center gap-2 cursor-pointer">
1052
997
  <input
1053
998
  type="checkbox"
1054
999
  class="checkbox"
1055
- .checked=${this._showJantBrandingOnHome}
1056
- ?disabled=${this._homeLoading}
1000
+ .checked=${enabled}
1001
+ ?disabled=${locked || this._discoverLoading}
1057
1002
  @change=${(e: Event) =>
1058
- this._saveHomeToggle((e.target as HTMLInputElement).checked)}
1003
+ this._onDiscoverToggle((e.target as HTMLInputElement).checked)}
1059
1004
  />
1060
- <span>${this.labels.showJantBrandingOnHome}</span>
1005
+ <span>${this.labels.discoverEnabled}</span>
1061
1006
  </label>
1062
- </section>
1007
+ <p class="text-sm text-muted-foreground">
1008
+ ${this._renderDiscoverHelp(
1009
+ locked
1010
+ ? this.demoMode
1011
+ ? this.labels.discoverDemoLocked
1012
+ : this.labels.discoverFeedsOffLocked
1013
+ : this.labels.discoverIntro,
1014
+ )}
1015
+ </p>
1016
+ ${
1017
+ heldBackBySearch
1018
+ ? html`<p class="text-sm text-muted-foreground">
1019
+ ${this.labels.discoverSearchOff}
1020
+ </p>`
1021
+ : nothing
1022
+ }
1023
+ </div>
1024
+ ${locked ? nothing : this._renderDiscoverStatus()}
1063
1025
  </div>
1064
1026
  `;
1065
1027
  }
1066
1028
 
1067
- private _renderSearchForm() {
1029
+ /**
1030
+ * The help line under the checkbox, with each page it names linking to that
1031
+ * page.
1032
+ *
1033
+ * Whichever line is showing goes through here, the usual one or the notice
1034
+ * that replaces it while the box is locked, so a demo site's notice links the
1035
+ * directory's name too. Which words link where is `discoverIntroRuns`'s to
1036
+ * decide, so the setup screen, which shows the same opening, links the same
1037
+ * words.
1038
+ */
1039
+ private _renderDiscoverHelp(line: string) {
1040
+ const runs = discoverIntroRuns(
1041
+ line ?? "",
1042
+ {
1043
+ name: this.labels.discoverName ?? "",
1044
+ rules: this.labels.discoverRules ?? "",
1045
+ },
1046
+ this.discoverPages,
1047
+ );
1048
+
1049
+ return runs.map((run) =>
1050
+ run.href
1051
+ ? html`<a
1052
+ href=${run.href}
1053
+ target="_blank"
1054
+ rel="noopener noreferrer"
1055
+ class="underline hover:text-foreground transition-colors"
1056
+ >${run.text}</a
1057
+ >`
1058
+ : run.text,
1059
+ );
1060
+ }
1061
+
1062
+ /**
1063
+ * What is left to say once the controls above have spoken.
1064
+ *
1065
+ * Not a status report — a site whose Discover setting is doing exactly what
1066
+ * it says gets nothing here, and no heading announces a block that is
1067
+ * usually absent. The server sends a line only for a problem, a task, or an
1068
+ * answer still outstanding; deliberately all local evidence, because the
1069
+ * directory takes no status queries and cannot be asked whether a person has
1070
+ * moderated the site.
1071
+ *
1072
+ * The announce button appears when the directory has not heard from this
1073
+ * site; the manual form only when an announcement actually failed — beside a
1074
+ * working one it would read as a normal route in rather than the recovery it
1075
+ * is. The lines are what the server sent with the page, so ticking the box
1076
+ * does not rewrite them; the next load does.
1077
+ */
1078
+ private _renderDiscoverStatus() {
1079
+ const status = this._parsedDiscoverStatus();
1080
+ if (!status) return nothing;
1081
+ // The server wrote these for the mode the page loaded with. A blog that
1082
+ // has just switched itself off is not the blog they describe.
1083
+ if (this._effectiveDiscoverMode() === "none") return nothing;
1084
+ if (status.lines.length === 0 && !status.showAnnounce) return nothing;
1085
+
1068
1086
  return html`
1069
- <section class="flex flex-col gap-4 border-t pt-8">
1070
- ${this._renderSectionTitle(this.labels.search)}
1071
- <label class="flex items-center gap-2 cursor-pointer">
1072
- <input
1073
- type="checkbox"
1074
- class="checkbox"
1075
- .checked=${!this._noindex}
1076
- ?disabled=${this.demoMode || this._searchLoading}
1077
- @change=${(e: Event) =>
1078
- this._saveSearchToggle((e.target as HTMLInputElement).checked)}
1079
- />
1080
- <span>${this.labels.allowIndexing}</span>
1081
- </label>
1082
- ${this.demoMode
1083
- ? html`<p class="text-sm text-muted-foreground">
1084
- ${this.labels.demoSeoLocked}
1085
- </p>`
1086
- : nothing}
1087
- </section>
1087
+ <div class="flex flex-col gap-1 border-t pt-3">
1088
+ ${status.lines.map(
1089
+ (line) => html`<p class="text-sm text-muted-foreground">${line}</p>`,
1090
+ )}
1091
+ ${
1092
+ status.showAnnounce
1093
+ ? html`
1094
+ <div class="flex flex-wrap items-center gap-3 mt-1">
1095
+ <button
1096
+ type="button"
1097
+ class="btn-outline"
1098
+ ?disabled=${this._discoverLoading}
1099
+ @click=${() => this._announce()}
1100
+ >
1101
+ ${this.labels.discoverAnnounce}
1102
+ </button>
1103
+ ${
1104
+ status.submitUrl
1105
+ ? html`<a
1106
+ class="text-sm underline hover:text-foreground transition-colors"
1107
+ href=${status.submitUrl}
1108
+ target="_blank"
1109
+ rel="noopener noreferrer"
1110
+ >${this.labels.discoverAnnounceManual}</a
1111
+ >`
1112
+ : nothing
1113
+ }
1114
+ </div>
1115
+ `
1116
+ : nothing
1117
+ }
1118
+ </div>
1088
1119
  `;
1089
1120
  }
1090
1121
 
1122
+ private _parsedDiscoverStatus(): {
1123
+ lines: string[];
1124
+ showAnnounce: boolean;
1125
+ submitUrl: string | null;
1126
+ } | null {
1127
+ if (!this.discoverStatus) return null;
1128
+ try {
1129
+ const parsed: unknown = JSON.parse(this.discoverStatus);
1130
+ if (typeof parsed !== "object" || parsed === null) return null;
1131
+ const value = parsed as Record<string, unknown>;
1132
+ const lines = Array.isArray(value["lines"])
1133
+ ? value["lines"].filter(
1134
+ (line): line is string => typeof line === "string",
1135
+ )
1136
+ : [];
1137
+ return {
1138
+ lines,
1139
+ showAnnounce: value["showAnnounce"] === true,
1140
+ submitUrl:
1141
+ typeof value["submitUrl"] === "string" ? value["submitUrl"] : null,
1142
+ };
1143
+ } catch {
1144
+ // A settings page that renders without its status block is far better
1145
+ // than one that does not render.
1146
+ return null;
1147
+ }
1148
+ }
1149
+
1150
+ private _announce() {
1151
+ if (this._discoverLoading) return;
1152
+ this._discoverLoading = true;
1153
+ this.dispatchEvent(
1154
+ new CustomEvent("jant:settings-save", {
1155
+ bubbles: true,
1156
+ detail: {
1157
+ endpoint: "/settings/general/discover/announce",
1158
+ data: {},
1159
+ section: "discover",
1160
+ },
1161
+ }),
1162
+ );
1163
+ }
1164
+
1091
1165
  render() {
1092
1166
  return html`
1093
- <div class="flex flex-col">
1094
- ${this._renderGeneralForm()} ${this._renderSearchForm()}
1167
+ <div class="flex flex-col gap-8">
1168
+ <h2 class="text-lg font-semibold">${this.labels.general}</h2>
1169
+ <div class="flex flex-col divide-y">
1170
+ ${this._renderSiteSection()} ${this._renderVisibilitySection()}
1171
+ ${this._renderFeedsSection()} ${this._renderTimeSection()}
1172
+ ${this._renderHomeSection()}
1173
+ </div>
1095
1174
  </div>
1096
1175
  `;
1097
1176
  }