@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
@@ -0,0 +1,1087 @@
1
+ /**
2
+ * Language Settings Component
3
+ *
4
+ * The single home for every language setting: the site's content language, the
5
+ * dashboard's UI language, and multilingual content.
6
+ *
7
+ * Unlike the General settings page, this component talks to the server itself
8
+ * rather than through the settings bridge. Its operations are not "save this
9
+ * form" — turning multilingual on stamps every existing post, removing a
10
+ * language can be refused, changing the primary language rewrites two settings
11
+ * at once — so each needs its own endpoint, its own confirmation, and its own
12
+ * error handling.
13
+ *
14
+ * Light DOM only — BaseCoat and Tailwind classes apply directly.
15
+ */
16
+
17
+ import { LitElement, html, nothing } from "lit";
18
+ import { showConfirmDialog } from "../confirm.js";
19
+ import { queueToastForNextPage, showToast } from "../toast.js";
20
+ import { getJsonString, readJsonObject } from "../json.js";
21
+ import { getJantDocsUrl } from "../../lib/jant-docs.js";
22
+ import {
23
+ LOCALE_PICKER_TRIGGER_CLASS,
24
+ type LocaleOption,
25
+ type LocalePickerLabels,
26
+ } from "./jant-locale-picker.js";
27
+
28
+ interface LanguageLabels {
29
+ siteSection: string;
30
+ dashboardSection: string;
31
+ contentLanguage: string;
32
+ contentLanguageHelp: string;
33
+ primaryLanguage: string;
34
+ primaryLanguageHelp: string;
35
+ languagesLabel: string;
36
+ primaryBadge: string;
37
+ makePrimary: string;
38
+ dashboardLanguage: string;
39
+ dashboardLanguageHelp: string;
40
+ followContent: string;
41
+ multilingual: string;
42
+ multilingualHelp: string;
43
+ multilingualDocs: string;
44
+ multilingualDocsHelp: string;
45
+ statusOn: string;
46
+ turnOn: string;
47
+ addMissingLanguage: string;
48
+ viewPosts: string;
49
+ otherLanguages: string;
50
+ addLanguage: string;
51
+ removeLanguage: string;
52
+ languageMenu: string;
53
+ enableTitle: string;
54
+ enableReassurance: string;
55
+ enableMarkTitle: string;
56
+ enableMarkWarning: string;
57
+ enableFixHint: string;
58
+ enableNeedsLanguage: string;
59
+ changePrimaryTitle: string;
60
+ changePrimaryBody: string;
61
+ changePrimaryConfirm: string;
62
+ disableTitle: string;
63
+ disableBody: string;
64
+ disableConfirm: string;
65
+ removeLastTitle: string;
66
+ removeLastBody: string;
67
+ removeLastConfirm: string;
68
+ cancel: string;
69
+ save: string;
70
+ saving: string;
71
+ searchPlaceholder: string;
72
+ noMatches: string;
73
+ }
74
+
75
+ interface LanguageInitialState {
76
+ contentLanguage: string;
77
+ dashboardLanguage: string;
78
+ multilingualEnabled: boolean;
79
+ additionalLanguages: string[];
80
+ unmarkedPostCount: number;
81
+ sitePathPrefix: string;
82
+ /**
83
+ * Docs link, resolved by the server. Vite's `define` does not reach modules
84
+ * the dev server hands the browser, so the environment's docs host is only
85
+ * known server-side.
86
+ */
87
+ multilingualDocsUrl: string;
88
+ }
89
+
90
+ /** Catalog locales the dashboard is translated into. */
91
+ const DASHBOARD_LOCALES = ["", "en", "zh-Hans", "zh-Hant"] as const;
92
+
93
+ /**
94
+ * The reference for everything this page cannot say in two lines: URL
95
+ * structure, per-language feeds, and how translation groups work. Same shape
96
+ * as the theming and API links elsewhere in settings. Used only if the server
97
+ * sent no link — it resolves the docs host for the deployed build, not for a
98
+ * dev server.
99
+ */
100
+ const MULTILINGUAL_DOCS_FALLBACK_URL = getJantDocsUrl("multilingual");
101
+
102
+ /**
103
+ * Fill the `{name}` slots the server deliberately left intact.
104
+ *
105
+ * Plurals and every other value are already resolved by Lingui server-side —
106
+ * only values that are not known until the author interacts (the language they
107
+ * picked, the prefix it maps to) reach here.
108
+ */
109
+ function interpolate(template: string, values: Record<string, string>): string {
110
+ // An unknown placeholder is left as-is rather than blanked: a copy change
111
+ // that renames a slot should read oddly, not silently drop a fact.
112
+ return template.replace(
113
+ /\{(\w+)\}/g,
114
+ (match, key: string) => values[key] ?? match,
115
+ );
116
+ }
117
+
118
+ export class JantSettingsLanguage extends LitElement {
119
+ static properties = {
120
+ labels: { type: Object },
121
+ locales: { type: Array },
122
+ _contentLanguage: { state: true },
123
+ _dashboardLanguage: { state: true },
124
+ _multilingualEnabled: { state: true },
125
+ _additional: { state: true },
126
+ _unmarkedPostCount: { state: true },
127
+ _sitePathPrefix: { state: true },
128
+ _savingSite: { state: true },
129
+ _savingDashboard: { state: true },
130
+ _removeError: { state: true },
131
+ _rowMenuOpen: { state: true },
132
+ _enableError: { state: true },
133
+ _enableOpen: { state: true },
134
+ _enableAdditional: { state: true },
135
+ _enablePrimary: { state: true },
136
+ _enableBusy: { state: true },
137
+ };
138
+
139
+ declare labels: LanguageLabels;
140
+ declare locales: LocaleOption[];
141
+ declare _contentLanguage: string;
142
+ declare _dashboardLanguage: string;
143
+ declare _multilingualEnabled: boolean;
144
+ declare _additional: string[];
145
+ declare _unmarkedPostCount: number;
146
+ declare _sitePathPrefix: string;
147
+ declare _savingSite: boolean;
148
+ declare _savingDashboard: boolean;
149
+ /**
150
+ * The refusal to remove a language, shown under that language's own row.
151
+ * Inline rather than a toast: the way out is a link to the posts, and a
152
+ * link belongs where the refusal happened.
153
+ */
154
+ declare _removeError: { tag: string; message: string } | null;
155
+ /** Language tag whose row menu (make primary / remove) is open. */
156
+ declare _rowMenuOpen: string | null;
157
+ /**
158
+ * Refusal from the enable dialog's save, shown inside the dialog. When the
159
+ * server names the language whose posts blocked the save, putting it back
160
+ * on the list is offered as one click.
161
+ */
162
+ declare _enableError: { message: string; language?: string } | null;
163
+ declare _enableOpen: boolean;
164
+ declare _enableAdditional: string[];
165
+ declare _enablePrimary: string;
166
+ declare _enableBusy: boolean;
167
+
168
+ /**
169
+ * Docs link for this page. Set from the server's initial state before the
170
+ * first render and never changed after, so it stays outside reactive state.
171
+ */
172
+ #multilingualDocsUrl = MULTILINGUAL_DOCS_FALLBACK_URL;
173
+
174
+ createRenderRoot() {
175
+ // Drop the server-rendered skeleton first. lit-html appends its parts
176
+ // rather than replacing existing children, so leaving it would stack the
177
+ // placeholder block above the real form.
178
+ this.innerHTML = "";
179
+ return this;
180
+ }
181
+
182
+ constructor() {
183
+ super();
184
+ this.labels = {} as LanguageLabels;
185
+ this.locales = [];
186
+ this._contentLanguage = "en";
187
+ this._dashboardLanguage = "";
188
+ this._multilingualEnabled = false;
189
+ this._additional = [];
190
+ this._unmarkedPostCount = 0;
191
+ this._sitePathPrefix = "";
192
+ this._savingSite = false;
193
+ this._savingDashboard = false;
194
+ this._removeError = null;
195
+ this._rowMenuOpen = null;
196
+ this._enableError = null;
197
+ this._enableOpen = false;
198
+ this._enableAdditional = [];
199
+ this._enablePrimary = "en";
200
+ this._enableBusy = false;
201
+ }
202
+
203
+ connectedCallback() {
204
+ super.connectedCallback();
205
+ this.#loadInitialState();
206
+ document.addEventListener("click", this.#onDocumentClick);
207
+ document.addEventListener("keydown", this.#onDocumentKeydown);
208
+ }
209
+
210
+ disconnectedCallback() {
211
+ super.disconnectedCallback();
212
+ document.removeEventListener("click", this.#onDocumentClick);
213
+ document.removeEventListener("keydown", this.#onDocumentKeydown);
214
+ }
215
+
216
+ #loadInitialState() {
217
+ const node = document.getElementById("language-settings-initial-data");
218
+ if (!node?.textContent) return;
219
+
220
+ let parsed: unknown;
221
+ try {
222
+ parsed = JSON.parse(node.textContent);
223
+ } catch {
224
+ return;
225
+ }
226
+ const state = parsed as Partial<LanguageInitialState>;
227
+
228
+ this._contentLanguage = state.contentLanguage ?? "en";
229
+ this._dashboardLanguage = state.dashboardLanguage ?? "";
230
+ this._multilingualEnabled = state.multilingualEnabled ?? false;
231
+ this._additional = Array.isArray(state.additionalLanguages)
232
+ ? [...state.additionalLanguages]
233
+ : [];
234
+ this._unmarkedPostCount = state.unmarkedPostCount ?? 0;
235
+ this._sitePathPrefix = state.sitePathPrefix ?? "";
236
+ this.#multilingualDocsUrl =
237
+ state.multilingualDocsUrl || MULTILINGUAL_DOCS_FALLBACK_URL;
238
+ }
239
+
240
+ // ── Presentation helpers ──────────────────────────────────────────
241
+
242
+ #entry(tag: string): LocaleOption {
243
+ return (
244
+ this.locales.find((locale) => locale.tag === tag) ?? {
245
+ tag,
246
+ native: tag,
247
+ english: tag,
248
+ coverage: 0,
249
+ }
250
+ );
251
+ }
252
+
253
+ #displayName(tag: string): string {
254
+ const entry = this.#entry(tag);
255
+ return entry.native || entry.tag;
256
+ }
257
+
258
+ #prefixFor(tag: string): string {
259
+ return `${this._sitePathPrefix}/${tag.toLowerCase()}`;
260
+ }
261
+
262
+ #endpoint(path: string): string {
263
+ return `${this._sitePathPrefix}/settings/language${path}`;
264
+ }
265
+
266
+ // ── Dismissal ─────────────────────────────────────────────────────
267
+
268
+ /** True while any locale picker on this page is showing its list. */
269
+ #pickerIsOpen(): boolean {
270
+ return this.querySelector("jant-locale-picker[open]") !== null;
271
+ }
272
+
273
+ #onDocumentClick = (event: Event) => {
274
+ if (!this._rowMenuOpen) return;
275
+ const target = event.target as globalThis.Element | null;
276
+ const inMenu =
277
+ target && this.contains(target) && target.closest?.("[data-row-menu]");
278
+ if (!inMenu) this._rowMenuOpen = null;
279
+ };
280
+
281
+ #onDocumentKeydown = (event: globalThis.KeyboardEvent) => {
282
+ if (event.key !== "Escape") return;
283
+ // An open picker handles its own Escape and stops the event there, so
284
+ // reaching this handler means no picker consumed it.
285
+ if (this._rowMenuOpen) {
286
+ event.stopPropagation();
287
+ this._rowMenuOpen = null;
288
+ return;
289
+ }
290
+ if (this._enableOpen) {
291
+ event.stopPropagation();
292
+ this.#closeEnableDialog();
293
+ }
294
+ };
295
+
296
+ #toggleRowMenu(tag: string) {
297
+ this._rowMenuOpen = this._rowMenuOpen === tag ? null : tag;
298
+ }
299
+
300
+ // ── Server calls ──────────────────────────────────────────────────
301
+
302
+ async #post(
303
+ path: string,
304
+ body: Record<string, unknown>,
305
+ ): Promise<{ ok: boolean; message?: string; language?: string }> {
306
+ try {
307
+ const res = await fetch(this.#endpoint(path), {
308
+ method: "POST",
309
+ headers: {
310
+ "Content-Type": "application/json",
311
+ Accept: "application/json",
312
+ },
313
+ body: JSON.stringify(body),
314
+ });
315
+ const json = await readJsonObject(res);
316
+ if (!res.ok) {
317
+ return {
318
+ ok: false,
319
+ message: getJsonString(json, "error") ?? `HTTP ${res.status}`,
320
+ // A language-in-use refusal names the language, so the dialog can
321
+ // offer putting it back as one click.
322
+ language: getJsonString(json, "language"),
323
+ };
324
+ }
325
+ return { ok: true, message: getJsonString(json, "toast") };
326
+ } catch {
327
+ return { ok: false, message: "Couldn't reach the server. Try again." };
328
+ }
329
+ }
330
+
331
+ async #selectPrimary(tag: string) {
332
+ this._removeError = null;
333
+ if (tag === this._contentLanguage) return;
334
+
335
+ // With multilingual off this is just "what language is this site in". With
336
+ // it on, the root URLs change hands, which subscribers notice — so say so.
337
+ if (this._multilingualEnabled) {
338
+ const previous = this._contentLanguage;
339
+ const confirmed = await showConfirmDialog({
340
+ title: this.labels.changePrimaryTitle,
341
+ message: interpolate(this.labels.changePrimaryBody, {
342
+ next: this.#displayName(tag),
343
+ previous: this.#displayName(previous),
344
+ prefix: this.#prefixFor(previous),
345
+ }),
346
+ confirmLabel: this.labels.changePrimaryConfirm,
347
+ cancelLabel: this.labels.cancel,
348
+ });
349
+ if (!confirmed) return;
350
+
351
+ this._savingSite = true;
352
+ const result = await this.#post("/primary", { language: tag });
353
+ this._savingSite = false;
354
+ if (!result.ok) {
355
+ showToast(result.message ?? "", "error");
356
+ return;
357
+ }
358
+ // The two lists swap in one server-side step; mirror that here so the
359
+ // page reads correctly without a reload.
360
+ this._additional = [
361
+ ...this._additional.filter((entry) => entry !== tag),
362
+ previous,
363
+ ];
364
+ this._contentLanguage = tag;
365
+ if (result.message) showToast(result.message);
366
+ return;
367
+ }
368
+
369
+ this._savingSite = true;
370
+ const previous = this._contentLanguage;
371
+ this._contentLanguage = tag;
372
+ const result = await this.#post("", { contentLanguage: tag });
373
+ this._savingSite = false;
374
+ if (!result.ok) {
375
+ this._contentLanguage = previous;
376
+ showToast(result.message ?? "", "error");
377
+ return;
378
+ }
379
+ if (result.message) showToast(result.message);
380
+ }
381
+
382
+ async #selectDashboardLanguage(value: string) {
383
+ const previous = this._dashboardLanguage;
384
+ this._dashboardLanguage = value;
385
+ this._savingDashboard = true;
386
+ const result = await this.#post("", { dashboardLanguage: value });
387
+ this._savingDashboard = false;
388
+ if (!result.ok) {
389
+ this._dashboardLanguage = previous;
390
+ showToast(result.message ?? "", "error");
391
+ return;
392
+ }
393
+ // The dashboard renders in the catalog that was active when the page
394
+ // loaded, so a reload is what actually applies the change.
395
+ window.location.reload();
396
+ }
397
+
398
+ async #addLanguage(tag: string) {
399
+ this._removeError = null;
400
+ const result = await this.#post("/add", { language: tag });
401
+ if (!result.ok) {
402
+ showToast(result.message ?? "", "error");
403
+ return;
404
+ }
405
+ this._additional = [...this._additional, tag];
406
+ if (result.message) showToast(result.message);
407
+ }
408
+
409
+ /**
410
+ * Remove a language, and with the last one the feature itself.
411
+ *
412
+ * Per-language views need a second language to mean anything, so the server
413
+ * turns multilingual content off when this empties the list. That is a bigger
414
+ * change than the menu item promises, hence the confirmation — asked before
415
+ * the request, which means it is also asked in the case the server then
416
+ * refuses (posts still written in that language). The refusal lands under the
417
+ * row as it always has; the alternative is a preflight round-trip on every
418
+ * removal to avoid one dialog in the rarer path.
419
+ */
420
+ async #removeLanguage(tag: string) {
421
+ this._removeError = null;
422
+ const isLast =
423
+ this._multilingualEnabled &&
424
+ this._additional.length === 1 &&
425
+ this._additional[0] === tag;
426
+
427
+ if (isLast) {
428
+ const confirmed = await showConfirmDialog({
429
+ title: this.labels.removeLastTitle,
430
+ message: interpolate(this.labels.removeLastBody, {
431
+ language: this.#displayName(tag),
432
+ prefix: this.#prefixFor(tag),
433
+ }),
434
+ confirmLabel: this.labels.removeLastConfirm,
435
+ cancelLabel: this.labels.cancel,
436
+ });
437
+ if (!confirmed) return;
438
+ }
439
+
440
+ const result = await this.#post("/remove", { language: tag });
441
+ if (!result.ok) {
442
+ this._removeError = { tag, message: result.message ?? "" };
443
+ return;
444
+ }
445
+ this._additional = this._additional.filter((entry) => entry !== tag);
446
+
447
+ if (isLast) {
448
+ this._multilingualEnabled = false;
449
+ // Same reload as turning it off directly: the header switcher and the
450
+ // composer's language control are server-rendered.
451
+ if (result.message) queueToastForNextPage(result.message);
452
+ window.location.reload();
453
+ return;
454
+ }
455
+ if (result.message) showToast(result.message);
456
+ }
457
+
458
+ async #turnOffMultilingual() {
459
+ this._removeError = null;
460
+ const confirmed = await showConfirmDialog({
461
+ title: this.labels.disableTitle,
462
+ message: interpolate(this.labels.disableBody, {
463
+ prefix: this.#prefixFor(this._additional[0] ?? "en"),
464
+ }),
465
+ confirmLabel: this.labels.disableConfirm,
466
+ cancelLabel: this.labels.cancel,
467
+ });
468
+ if (!confirmed) return;
469
+
470
+ const result = await this.#post("/disable", {});
471
+ if (!result.ok) {
472
+ showToast(result.message ?? "", "error");
473
+ return;
474
+ }
475
+ this._multilingualEnabled = false;
476
+ // Queued for the same reason as the enable path: the reload below happens
477
+ // before a toast raised here could be read.
478
+ if (result.message) queueToastForNextPage(result.message);
479
+ // The header's language switcher has to go away with the views.
480
+ window.location.reload();
481
+ }
482
+
483
+ // ── Enable dialog ─────────────────────────────────────────────────
484
+
485
+ #openEnableDialog() {
486
+ this._removeError = null;
487
+ this._enableError = null;
488
+ this._enablePrimary = this._contentLanguage;
489
+ this._enableAdditional = [...this._additional];
490
+ this._enableOpen = true;
491
+ void this.updateComplete.then(() => {
492
+ const dialog = this.querySelector<HTMLDialogElement>(
493
+ "[data-enable-dialog]",
494
+ );
495
+ if (dialog && !dialog.open) dialog.showModal();
496
+ dialog?.querySelector<HTMLElement>("[data-enable-panel]")?.focus();
497
+ });
498
+ }
499
+
500
+ #closeEnableDialog() {
501
+ const dialog = this.querySelector<HTMLDialogElement>(
502
+ "[data-enable-dialog]",
503
+ );
504
+ if (dialog?.open) dialog.close();
505
+ this._enableOpen = false;
506
+ }
507
+
508
+ async #confirmEnable() {
509
+ if (this._enableAdditional.length === 0 || this._enableBusy) return;
510
+
511
+ this._enableBusy = true;
512
+ this._enableError = null;
513
+ const result = await this.#post("/enable", {
514
+ primary: this._enablePrimary,
515
+ additional: this._enableAdditional,
516
+ });
517
+ this._enableBusy = false;
518
+ if (!result.ok) {
519
+ // Inside the dialog, where the refusal happened — a toast would sit
520
+ // beneath the modal's top layer and never be seen.
521
+ this._enableError = {
522
+ message: result.message ?? "",
523
+ ...(result.language ? { language: result.language } : {}),
524
+ };
525
+ return;
526
+ }
527
+
528
+ this._contentLanguage = this._enablePrimary;
529
+ this._additional = [...this._enableAdditional];
530
+ this._multilingualEnabled = true;
531
+ this._unmarkedPostCount = 0;
532
+ this.#closeEnableDialog();
533
+ // Queued rather than shown: the reload below would wipe a toast raised
534
+ // here, and this is the one confirmation that says how many posts were
535
+ // stamped — the number is gone from the page by the time it lands.
536
+ if (result.message) queueToastForNextPage(result.message);
537
+ // The page's own chrome changed with the setting — the language switcher
538
+ // exists now — and only the server can render it. Same pattern as the
539
+ // dashboard-language change above.
540
+ window.location.reload();
541
+ }
542
+
543
+ // ── Render ────────────────────────────────────────────────────────
544
+
545
+ /**
546
+ * Labels handed to every picker on the page.
547
+ *
548
+ * Built once: the picker takes them as a property, and a fresh object each
549
+ * render would make Lit see a changed property on every keystroke elsewhere
550
+ * in the form.
551
+ */
552
+ get #pickerLabels(): LocalePickerLabels {
553
+ this.#pickerLabelsCache ??= {
554
+ search: this.labels.searchPlaceholder,
555
+ empty: this.labels.noMatches,
556
+ };
557
+ return this.#pickerLabelsCache;
558
+ }
559
+
560
+ #pickerLabelsCache: LocalePickerLabels | null = null;
561
+
562
+ #renderLanguagePicker(options: {
563
+ labelId: string;
564
+ current?: string;
565
+ exclude: readonly string[];
566
+ triggerLabel?: string;
567
+ triggerClass: string;
568
+ onSelect: (tag: string) => void;
569
+ }) {
570
+ return html`
571
+ <jant-locale-picker
572
+ .locales=${this.locales}
573
+ .labels=${this.#pickerLabels}
574
+ .value=${options.current ?? ""}
575
+ .exclude=${[...options.exclude]}
576
+ .triggerLabel=${options.triggerLabel ?? ""}
577
+ .triggerClass=${options.triggerClass}
578
+ .labelledby=${options.labelId}
579
+ @locale-select=${(e: CustomEvent<{ tag: string }>) =>
580
+ options.onSelect(e.detail.tag)}
581
+ ></jant-locale-picker>
582
+ `;
583
+ }
584
+
585
+ /**
586
+ * The save refusal, rendered inside the dialog with its one-click fix.
587
+ *
588
+ * When the server names the language whose posts blocked the save, an
589
+ * "Add {language}" button puts it straight back on the list — the reader
590
+ * of the error should not have to find that language in a picker.
591
+ */
592
+ #renderEnableError(error: { message: string; language?: string }) {
593
+ return html`
594
+ <div class="flex flex-wrap items-center gap-2" role="alert">
595
+ <p class="text-sm text-destructive">${error.message}</p>
596
+ ${
597
+ error.language
598
+ ? html`
599
+ <button
600
+ type="button"
601
+ class="btn-sm-outline"
602
+ data-enable-add-back
603
+ @click=${() => this.#addBackLanguage(error.language ?? "")}
604
+ >
605
+ ${interpolate(this.labels.addMissingLanguage, {
606
+ language: this.#displayName(error.language),
607
+ })}
608
+ </button>
609
+ `
610
+ : nothing
611
+ }
612
+ </div>
613
+ `;
614
+ }
615
+
616
+ /** Put the language the refusal named back on the dialog's list. */
617
+ #addBackLanguage(tag: string) {
618
+ if (!tag) return;
619
+ this._enableError = null;
620
+ if (tag === this._enablePrimary || this._enableAdditional.includes(tag)) {
621
+ return;
622
+ }
623
+ this._enableAdditional = [...this._enableAdditional, tag];
624
+ }
625
+
626
+ /**
627
+ * The confirmation shown before multilingual content is switched on.
628
+ *
629
+ * BaseCoat's `.alert` is a two-column grid where only a title element and a
630
+ * `<section>` are placed in the readable column, so the warning uses that
631
+ * structure rather than bare paragraphs.
632
+ */
633
+ #renderEnableDialog() {
634
+ if (!this._enableOpen) return nothing;
635
+
636
+ // With nothing to stamp there is nothing to warn about — the alert only
637
+ // exists for the one-time marking of posts that predate the feature.
638
+ const warning =
639
+ this._unmarkedPostCount > 0
640
+ ? interpolate(this.labels.enableMarkWarning, {
641
+ language: this.#displayName(this._enablePrimary),
642
+ })
643
+ : null;
644
+
645
+ return html`
646
+ <dialog
647
+ class="dialog confirm-dialog"
648
+ data-enable-dialog
649
+ @cancel=${(e: Event) => {
650
+ e.preventDefault();
651
+ this.#closeEnableDialog();
652
+ }}
653
+ @click=${(e: Event) => {
654
+ if (e.target === e.currentTarget) this.#closeEnableDialog();
655
+ }}
656
+ @keydown=${(e: globalThis.KeyboardEvent) => {
657
+ if (e.key === "Enter" && !e.shiftKey && !this.#pickerIsOpen()) {
658
+ e.preventDefault();
659
+ void this.#confirmEnable();
660
+ }
661
+ }}
662
+ >
663
+ <div
664
+ class="confirm-dialog-panel"
665
+ data-enable-panel
666
+ tabindex="-1"
667
+ role="document"
668
+ aria-labelledby="language-enable-title"
669
+ >
670
+ <header class="confirm-dialog-header">
671
+ <h2 id="language-enable-title" class="confirm-dialog-title">
672
+ ${this.labels.enableTitle}
673
+ </h2>
674
+ </header>
675
+
676
+ <div class="flex flex-col gap-4 py-2">
677
+ <div class="field">
678
+ <span id="language-enable-primary-label" class="label"
679
+ >${this.labels.primaryLanguage}</span
680
+ >
681
+ ${this.#renderLanguagePicker({
682
+ labelId: "language-enable-primary-label",
683
+ current: this._enablePrimary,
684
+ exclude: this._enableAdditional,
685
+ triggerLabel: this.#displayName(this._enablePrimary),
686
+ triggerClass: LOCALE_PICKER_TRIGGER_CLASS,
687
+ onSelect: (tag) => {
688
+ this._enablePrimary = tag;
689
+ this._enableAdditional = this._enableAdditional.filter(
690
+ (entry) => entry !== tag,
691
+ );
692
+ },
693
+ })}
694
+ <p class="text-sm text-muted-foreground mt-1">
695
+ ${this.labels.primaryLanguageHelp}
696
+ </p>
697
+ </div>
698
+
699
+ <div class="field">
700
+ <span id="language-enable-add-label" class="label"
701
+ >${this.labels.otherLanguages}</span
702
+ >
703
+ <div class="flex flex-wrap items-center gap-2">
704
+ ${this._enableAdditional.map(
705
+ (tag) => html`
706
+ <span class="badge-secondary gap-1">
707
+ ${this.#displayName(tag)}
708
+ <button
709
+ type="button"
710
+ class="cursor-pointer opacity-70 hover:opacity-100"
711
+ aria-label=${interpolate(this.labels.removeLanguage, {
712
+ language: this.#displayName(tag),
713
+ })}
714
+ @click=${() => {
715
+ this._enableAdditional =
716
+ this._enableAdditional.filter(
717
+ (entry) => entry !== tag,
718
+ );
719
+ }}
720
+ >
721
+ ×
722
+ </button>
723
+ </span>
724
+ `,
725
+ )}
726
+ ${this.#renderLanguagePicker({
727
+ labelId: "language-enable-add-label",
728
+ exclude: [this._enablePrimary, ...this._enableAdditional],
729
+ triggerLabel: `+ ${this.labels.addLanguage}`,
730
+ triggerClass: "btn-sm-outline",
731
+ onSelect: (tag) => {
732
+ this._enableAdditional = [...this._enableAdditional, tag];
733
+ },
734
+ })}
735
+ </div>
736
+ </div>
737
+
738
+ ${
739
+ warning
740
+ ? html`
741
+ <div class="alert">
742
+ <svg
743
+ xmlns="http://www.w3.org/2000/svg"
744
+ viewBox="0 0 24 24"
745
+ fill="none"
746
+ stroke="currentColor"
747
+ stroke-width="2"
748
+ stroke-linecap="round"
749
+ stroke-linejoin="round"
750
+ aria-hidden="true"
751
+ >
752
+ <path
753
+ d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"
754
+ />
755
+ <path d="M12 9v4" />
756
+ <path d="M12 17h.01" />
757
+ </svg>
758
+ <strong>${this.labels.enableMarkTitle}</strong>
759
+ <section>
760
+ <p>${warning}</p>
761
+ <p>${this.labels.enableFixHint}</p>
762
+ </section>
763
+ </div>
764
+ `
765
+ : nothing
766
+ }
767
+
768
+ <p class="text-sm text-muted-foreground">
769
+ ${this.labels.enableReassurance}
770
+ </p>
771
+
772
+ ${
773
+ this._enableAdditional.length === 0
774
+ ? html`<p class="text-sm text-muted-foreground">
775
+ ${this.labels.enableNeedsLanguage}
776
+ </p>`
777
+ : nothing
778
+ }
779
+ ${
780
+ this._enableError
781
+ ? this.#renderEnableError(this._enableError)
782
+ : nothing
783
+ }
784
+ </div>
785
+
786
+ <footer class="confirm-dialog-actions">
787
+ <button
788
+ type="button"
789
+ class="btn-outline"
790
+ @click=${() => this.#closeEnableDialog()}
791
+ >
792
+ ${this.labels.cancel}
793
+ </button>
794
+ <button
795
+ type="button"
796
+ class="btn"
797
+ data-enable-confirm
798
+ ?disabled=${
799
+ this._enableAdditional.length === 0 || this._enableBusy
800
+ }
801
+ @click=${() => void this.#confirmEnable()}
802
+ >
803
+ ${this._enableBusy ? this.labels.saving : this.labels.save}
804
+ </button>
805
+ </footer>
806
+ </div>
807
+ </dialog>
808
+ `;
809
+ }
810
+
811
+ /**
812
+ * The folded actions of one non-primary language row.
813
+ *
814
+ * Making a language primary or removing it are once-a-year moves; a row of
815
+ * always-visible buttons would give them daily-driver prominence. A "⋯"
816
+ * menu keeps the list to what matters — language and address — and follows
817
+ * the dismissal rules every popover here follows.
818
+ */
819
+ #renderRowMenu(tag: string) {
820
+ const open = this._rowMenuOpen === tag;
821
+ const name = this.#displayName(tag);
822
+
823
+ return html`
824
+ <span class="relative ml-auto flex items-center" data-row-menu>
825
+ <button
826
+ type="button"
827
+ class="btn-sm-ghost text-muted-foreground"
828
+ aria-haspopup="menu"
829
+ aria-expanded=${open ? "true" : "false"}
830
+ aria-label=${interpolate(this.labels.languageMenu, {
831
+ language: name,
832
+ })}
833
+ data-language-menu
834
+ @click=${() => this.#toggleRowMenu(tag)}
835
+ >
836
+ ⋯
837
+ </button>
838
+ ${
839
+ open
840
+ ? html`
841
+ <div
842
+ role="menu"
843
+ class="absolute right-0 top-full z-20 mt-1 min-w-40 rounded-md border bg-popover py-1 text-popover-foreground shadow-md"
844
+ >
845
+ <button
846
+ type="button"
847
+ role="menuitem"
848
+ class="block w-full cursor-pointer px-3 py-2 text-left text-sm hover:bg-accent"
849
+ ?disabled=${this._savingSite}
850
+ @click=${() => {
851
+ this._rowMenuOpen = null;
852
+ void this.#selectPrimary(tag);
853
+ }}
854
+ >
855
+ ${this.labels.makePrimary}
856
+ </button>
857
+ <button
858
+ type="button"
859
+ role="menuitem"
860
+ class="block w-full cursor-pointer px-3 py-2 text-left text-sm text-destructive hover:bg-accent"
861
+ @click=${() => {
862
+ this._rowMenuOpen = null;
863
+ void this.#removeLanguage(tag);
864
+ }}
865
+ >
866
+ ${interpolate(this.labels.removeLanguage, {
867
+ language: name,
868
+ })}
869
+ </button>
870
+ </div>
871
+ `
872
+ : nothing
873
+ }
874
+ </span>
875
+ `;
876
+ }
877
+
878
+ /**
879
+ * The site's languages as one list, each with the address it is served at.
880
+ *
881
+ * One row per language keeps every fact and action in the same place —
882
+ * which language, which URL, which one owns the root, make another primary,
883
+ * remove one — instead of a picker, a chip row and a URL legend apart.
884
+ */
885
+ #renderLanguagesList() {
886
+ const languages = [this._contentLanguage, ...this._additional];
887
+
888
+ return html`
889
+ <div class="field">
890
+ <span id="language-list-label" class="label"
891
+ >${this.labels.languagesLabel}${
892
+ this._savingSite ? ` ${this.labels.saving}` : ""
893
+ }</span
894
+ >
895
+ <ul
896
+ class="flex flex-col rounded-md border divide-y"
897
+ aria-labelledby="language-list-label"
898
+ >
899
+ ${languages.map((tag) => {
900
+ const isPrimary = tag === this._contentLanguage;
901
+ const url = isPrimary
902
+ ? `${this._sitePathPrefix}/`
903
+ : this.#prefixFor(tag);
904
+ return html`
905
+ <li class="flex flex-col px-3 py-1.5 text-sm">
906
+ <div class="flex min-h-8 items-center gap-3">
907
+ <span class="min-w-0 truncate" lang=${tag}
908
+ >${this.#displayName(tag)}</span
909
+ >
910
+ <a
911
+ href=${url}
912
+ target="_blank"
913
+ rel="noopener noreferrer"
914
+ class="shrink-0"
915
+ >
916
+ <code
917
+ class="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
918
+ >${url}</code
919
+ >
920
+ </a>
921
+ ${
922
+ isPrimary
923
+ ? html`<span class="badge-secondary ml-auto"
924
+ >${this.labels.primaryBadge}</span
925
+ >`
926
+ : this.#renderRowMenu(tag)
927
+ }
928
+ </div>
929
+ ${
930
+ this._removeError?.tag === tag
931
+ ? html`
932
+ <p class="pb-1 text-sm text-destructive" role="alert">
933
+ ${this._removeError.message}${
934
+ this._multilingualEnabled
935
+ ? html`
936
+ <a
937
+ class="underline underline-offset-2"
938
+ href=${`${this.#prefixFor(tag)}/archive`}
939
+ target="_blank"
940
+ rel="noopener noreferrer"
941
+ >${this.labels.viewPosts}</a
942
+ >
943
+ `
944
+ : nothing
945
+ }
946
+ </p>
947
+ `
948
+ : nothing
949
+ }
950
+ </li>
951
+ `;
952
+ })}
953
+ </ul>
954
+ <div class="mt-2">
955
+ ${this.#renderLanguagePicker({
956
+ labelId: "language-list-label",
957
+ exclude: [this._contentLanguage, ...this._additional],
958
+ triggerLabel: `+ ${this.labels.addLanguage}`,
959
+ triggerClass: "btn-sm-outline",
960
+ onSelect: (tag) => void this.#addLanguage(tag),
961
+ })}
962
+ </div>
963
+ </div>
964
+ `;
965
+ }
966
+
967
+ render() {
968
+ if (!this.labels.contentLanguage) return nothing;
969
+
970
+ return html`
971
+ <div class="flex flex-col gap-8">
972
+ <section class="flex flex-col gap-4">
973
+ <h2 class="text-lg font-medium">${this.labels.siteSection}</h2>
974
+
975
+ ${
976
+ this._multilingualEnabled
977
+ ? this.#renderLanguagesList()
978
+ : html`
979
+ <div class="field">
980
+ <span id="language-primary-label" class="label"
981
+ >${this.labels.contentLanguage}</span
982
+ >
983
+ ${this.#renderLanguagePicker({
984
+ labelId: "language-primary-label",
985
+ current: this._contentLanguage,
986
+ exclude: [],
987
+ triggerLabel: this.#displayName(this._contentLanguage),
988
+ triggerClass: LOCALE_PICKER_TRIGGER_CLASS,
989
+ onSelect: (tag) => void this.#selectPrimary(tag),
990
+ })}
991
+ <p class="text-sm text-muted-foreground mt-1">
992
+ ${this.labels.contentLanguageHelp}${
993
+ this._savingSite ? ` ${this.labels.saving}` : ""
994
+ }
995
+ </p>
996
+ </div>
997
+ `
998
+ }
999
+ </section>
1000
+
1001
+ <section class="flex flex-col gap-3 border-t pt-8">
1002
+ <h2 class="text-lg font-medium">${this.labels.multilingual}</h2>
1003
+ <p class="text-sm text-muted-foreground">
1004
+ ${this.labels.multilingualHelp}
1005
+ </p>
1006
+ <p class="text-sm text-muted-foreground">
1007
+ <a
1008
+ href=${this.#multilingualDocsUrl}
1009
+ target="_blank"
1010
+ rel="noopener noreferrer"
1011
+ class="underline hover:text-foreground transition-colors"
1012
+ >${this.labels.multilingualDocs}</a
1013
+ >
1014
+ — ${this.labels.multilingualDocsHelp}
1015
+ </p>
1016
+ <div class="flex items-center gap-3">
1017
+ ${
1018
+ this._multilingualEnabled
1019
+ ? html`
1020
+ <span class="badge-secondary">${this.labels.statusOn}</span>
1021
+ <button
1022
+ type="button"
1023
+ class="btn-link h-auto p-0 text-sm text-muted-foreground"
1024
+ data-multilingual-off
1025
+ @click=${() => void this.#turnOffMultilingual()}
1026
+ >
1027
+ ${this.labels.disableConfirm}
1028
+ </button>
1029
+ `
1030
+ : html`
1031
+ <button
1032
+ type="button"
1033
+ class="btn-sm-outline"
1034
+ data-multilingual-setup
1035
+ @click=${() => this.#openEnableDialog()}
1036
+ >
1037
+ ${this.labels.turnOn}
1038
+ </button>
1039
+ `
1040
+ }
1041
+ </div>
1042
+ </section>
1043
+
1044
+ <section class="flex flex-col gap-4 border-t pt-8">
1045
+ <h2 class="text-lg font-medium">${this.labels.dashboardSection}</h2>
1046
+
1047
+ <div class="field">
1048
+ <label id="dashboard-language-label" class="label"
1049
+ >${this.labels.dashboardLanguage}</label
1050
+ >
1051
+ <select
1052
+ class="select"
1053
+ aria-labelledby="dashboard-language-label"
1054
+ ?disabled=${this._savingDashboard}
1055
+ @change=${(e: Event) =>
1056
+ void this.#selectDashboardLanguage(
1057
+ (e.target as HTMLSelectElement).value,
1058
+ )}
1059
+ >
1060
+ ${DASHBOARD_LOCALES.map(
1061
+ (tag) => html`
1062
+ <option
1063
+ value=${tag}
1064
+ ?selected=${this._dashboardLanguage === tag}
1065
+ >
1066
+ ${
1067
+ tag === ""
1068
+ ? this.labels.followContent
1069
+ : this.#displayName(tag)
1070
+ }
1071
+ </option>
1072
+ `,
1073
+ )}
1074
+ </select>
1075
+ <p class="text-sm text-muted-foreground mt-1">
1076
+ ${this.labels.dashboardLanguageHelp}
1077
+ </p>
1078
+ </div>
1079
+ </section>
1080
+ </div>
1081
+
1082
+ ${this.#renderEnableDialog()}
1083
+ `;
1084
+ }
1085
+ }
1086
+
1087
+ customElements.define("jant-settings-language", JantSettingsLanguage);